| 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 RestoreDismissedCategory(category_provider_pair.first); |
| 166 category_provider_pair.first); | |
| 167 } | 179 } |
| 180 StoreDismissedCategoriesToPrefs(); |
| 168 DCHECK(dismissed_providers_by_category_.empty()); | 181 DCHECK(dismissed_providers_by_category_.empty()); |
| 169 } | 182 } |
| 170 | 183 |
| 171 void ContentSuggestionsService::AddObserver(Observer* observer) { | 184 void ContentSuggestionsService::AddObserver(Observer* observer) { |
| 172 observers_.AddObserver(observer); | 185 observers_.AddObserver(observer); |
| 173 } | 186 } |
| 174 | 187 |
| 175 void ContentSuggestionsService::RemoveObserver(Observer* observer) { | 188 void ContentSuggestionsService::RemoveObserver(Observer* observer) { |
| 176 observers_.RemoveObserver(observer); | 189 observers_.RemoveObserver(observer); |
| 177 } | 190 } |
| 178 | 191 |
| 179 void ContentSuggestionsService::RegisterProvider( | 192 void ContentSuggestionsService::RegisterProvider( |
| 180 std::unique_ptr<ContentSuggestionsProvider> provider) { | 193 std::unique_ptr<ContentSuggestionsProvider> provider) { |
| 181 DCHECK(state_ == State::ENABLED); | 194 DCHECK(state_ == State::ENABLED); |
| 182 providers_.push_back(std::move(provider)); | 195 providers_.push_back(std::move(provider)); |
| 183 } | 196 } |
| 184 | 197 |
| 185 //////////////////////////////////////////////////////////////////////////////// | 198 //////////////////////////////////////////////////////////////////////////////// |
| 186 // Private methods | 199 // Private methods |
| 187 | 200 |
| 188 void ContentSuggestionsService::OnNewSuggestions( | 201 void ContentSuggestionsService::OnNewSuggestions( |
| 189 ContentSuggestionsProvider* provider, | 202 ContentSuggestionsProvider* provider, |
| 190 Category category, | 203 Category category, |
| 191 std::vector<ContentSuggestion> suggestions) { | 204 std::vector<ContentSuggestion> suggestions) { |
| 192 if (RegisterCategoryIfRequired(provider, category)) | 205 if (TryRegisterProviderForCategory(provider, category)) { |
| 193 NotifyCategoryStatusChanged(category); | 206 NotifyCategoryStatusChanged(category); |
| 207 } else if (IsCategoryDismissed(category)) { |
| 208 // The category has been registered as a dismissed one. We need to |
| 209 // check if the dismissal can be cleared now that we received new data. |
| 210 if (suggestions.empty()) |
| 211 return; |
| 212 |
| 213 RestoreDismissedCategory(category); |
| 214 StoreDismissedCategoriesToPrefs(); |
| 215 |
| 216 NotifyCategoryStatusChanged(category); |
| 217 } |
| 194 | 218 |
| 195 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) { | 219 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) { |
| 196 // A provider shouldn't send us suggestions while it's not available. | 220 // A provider shouldn't send us suggestions while it's not available. |
| 197 DCHECK(suggestions.empty()); | 221 DCHECK(suggestions.empty()); |
| 198 return; | 222 return; |
| 199 } | 223 } |
| 200 | 224 |
| 201 suggestions_by_category_[category] = std::move(suggestions); | 225 suggestions_by_category_[category] = std::move(suggestions); |
| 202 | 226 |
| 203 // The positioning of the bookmarks category depends on whether it's empty. | 227 // The positioning of the bookmarks category depends on whether it's empty. |
| 204 // TODO(treib): Remove this temporary hack, crbug.com/640568. | 228 // TODO(treib): Remove this temporary hack, crbug.com/640568. |
| 205 if (category.IsKnownCategory(KnownCategories::BOOKMARKS)) | 229 if (category.IsKnownCategory(KnownCategories::BOOKMARKS)) |
| 206 SortCategories(); | 230 SortCategories(); |
| 207 | 231 |
| 208 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category)); | 232 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category)); |
| 209 } | 233 } |
| 210 | 234 |
| 211 void ContentSuggestionsService::OnCategoryStatusChanged( | 235 void ContentSuggestionsService::OnCategoryStatusChanged( |
| 212 ContentSuggestionsProvider* provider, | 236 ContentSuggestionsProvider* provider, |
| 213 Category category, | 237 Category category, |
| 214 CategoryStatus new_status) { | 238 CategoryStatus new_status) { |
| 215 if (!IsCategoryStatusAvailable(new_status)) { | |
| 216 suggestions_by_category_.erase(category); | |
| 217 } | |
| 218 if (new_status == CategoryStatus::NOT_PROVIDED) { | 239 if (new_status == CategoryStatus::NOT_PROVIDED) { |
| 219 DCHECK(providers_by_category_.find(category) != | 240 UnregisterCategory(category, provider); |
| 220 providers_by_category_.end()); | |
| 221 DCHECK_EQ(provider, providers_by_category_.find(category)->second); | |
| 222 DismissCategory(category); | |
| 223 } else { | 241 } else { |
| 224 RegisterCategoryIfRequired(provider, category); | 242 if (!IsCategoryStatusAvailable(new_status)) |
| 243 suggestions_by_category_.erase(category); |
| 244 TryRegisterProviderForCategory(provider, category); |
| 225 DCHECK_EQ(new_status, provider->GetCategoryStatus(category)); | 245 DCHECK_EQ(new_status, provider->GetCategoryStatus(category)); |
| 226 } | 246 } |
| 227 NotifyCategoryStatusChanged(category); | 247 |
| 248 if (!IsCategoryDismissed(category)) |
| 249 NotifyCategoryStatusChanged(category); |
| 228 } | 250 } |
| 229 | 251 |
| 230 void ContentSuggestionsService::OnSuggestionInvalidated( | 252 void ContentSuggestionsService::OnSuggestionInvalidated( |
| 231 ContentSuggestionsProvider* provider, | 253 ContentSuggestionsProvider* provider, |
| 232 const ContentSuggestion::ID& suggestion_id) { | 254 const ContentSuggestion::ID& suggestion_id) { |
| 233 RemoveSuggestionByID(suggestion_id); | 255 RemoveSuggestionByID(suggestion_id); |
| 234 FOR_EACH_OBSERVER(Observer, observers_, | 256 FOR_EACH_OBSERVER(Observer, observers_, |
| 235 OnSuggestionInvalidated(suggestion_id)); | 257 OnSuggestionInvalidated(suggestion_id)); |
| 236 } | 258 } |
| 237 | 259 |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 274 deleted_urls); | 296 deleted_urls); |
| 275 ClearHistory(begin, end, filter); | 297 ClearHistory(begin, end, filter); |
| 276 } | 298 } |
| 277 } | 299 } |
| 278 | 300 |
| 279 void ContentSuggestionsService::HistoryServiceBeingDeleted( | 301 void ContentSuggestionsService::HistoryServiceBeingDeleted( |
| 280 history::HistoryService* history_service) { | 302 history::HistoryService* history_service) { |
| 281 history_service_observer_.RemoveAll(); | 303 history_service_observer_.RemoveAll(); |
| 282 } | 304 } |
| 283 | 305 |
| 284 bool ContentSuggestionsService::RegisterCategoryIfRequired( | 306 bool ContentSuggestionsService::TryRegisterProviderForCategory( |
| 285 ContentSuggestionsProvider* provider, | 307 ContentSuggestionsProvider* provider, |
| 286 Category category) { | 308 Category category) { |
| 287 auto it = providers_by_category_.find(category); | 309 auto it = providers_by_category_.find(category); |
| 288 if (it != providers_by_category_.end()) { | 310 if (it != providers_by_category_.end()) { |
| 289 DCHECK_EQ(it->second, provider); | 311 DCHECK_EQ(it->second, provider); |
| 290 return false; | 312 return false; |
| 291 } | 313 } |
| 292 | 314 |
| 293 auto dismissed_it = dismissed_providers_by_category_.find(category); | 315 auto dismissed_it = dismissed_providers_by_category_.find(category); |
| 294 if (dismissed_it != dismissed_providers_by_category_.end()) { | 316 if (dismissed_it != dismissed_providers_by_category_.end()) { |
| 295 DCHECK_EQ(dismissed_it->second, provider); | 317 // The initialisation of dismissed categories registers them with |nullptr| |
| 296 dismissed_providers_by_category_.erase(dismissed_it); | 318 // for providers, we need to check for that to see if the provider is |
| 319 // already registered or not. |
| 320 if (!dismissed_it->second) { |
| 321 dismissed_it->second = provider; |
| 322 } else { |
| 323 DCHECK_EQ(dismissed_it->second, provider); |
| 324 } |
| 325 return false; |
| 297 } | 326 } |
| 298 | 327 |
| 328 RegisterCategory(category, provider); |
| 329 return true; |
| 330 } |
| 331 |
| 332 void ContentSuggestionsService::RegisterCategory( |
| 333 Category category, |
| 334 ContentSuggestionsProvider* provider) { |
| 335 DCHECK(!base::ContainsKey(providers_by_category_, category)); |
| 336 DCHECK(!IsCategoryDismissed(category)); |
| 337 |
| 299 providers_by_category_[category] = provider; | 338 providers_by_category_[category] = provider; |
| 300 categories_.push_back(category); | 339 categories_.push_back(category); |
| 301 SortCategories(); | 340 SortCategories(); |
| 302 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) { | 341 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) { |
| 303 suggestions_by_category_.insert( | 342 suggestions_by_category_.insert( |
| 304 std::make_pair(category, std::vector<ContentSuggestion>())); | 343 std::make_pair(category, std::vector<ContentSuggestion>())); |
| 305 } | 344 } |
| 306 return true; | 345 } |
| 346 |
| 347 void ContentSuggestionsService::UnregisterCategory( |
| 348 Category category, |
| 349 ContentSuggestionsProvider* provider) { |
| 350 auto providers_it = providers_by_category_.find(category); |
| 351 if (providers_it == providers_by_category_.end()) { |
| 352 DCHECK(IsCategoryDismissed(category)); |
| 353 return; |
| 354 } |
| 355 |
| 356 DCHECK_EQ(provider, providers_it->second); |
| 357 providers_by_category_.erase(providers_it); |
| 358 categories_.erase( |
| 359 std::find(categories_.begin(), categories_.end(), category)); |
| 360 suggestions_by_category_.erase(category); |
| 307 } | 361 } |
| 308 | 362 |
| 309 bool ContentSuggestionsService::RemoveSuggestionByID( | 363 bool ContentSuggestionsService::RemoveSuggestionByID( |
| 310 const ContentSuggestion::ID& suggestion_id) { | 364 const ContentSuggestion::ID& suggestion_id) { |
| 311 std::vector<ContentSuggestion>* suggestions = | 365 std::vector<ContentSuggestion>* suggestions = |
| 312 &suggestions_by_category_[suggestion_id.category()]; | 366 &suggestions_by_category_[suggestion_id.category()]; |
| 313 auto position = | 367 auto position = |
| 314 std::find_if(suggestions->begin(), suggestions->end(), | 368 std::find_if(suggestions->begin(), suggestions->end(), |
| 315 [&suggestion_id](const ContentSuggestion& suggestion) { | 369 [&suggestion_id](const ContentSuggestion& suggestion) { |
| 316 return suggestion_id == suggestion.id(); | 370 return suggestion_id == suggestion.id(); |
| (...skipping 29 matching lines...) Expand all Loading... |
| 346 if (bookmarks_empty) { | 400 if (bookmarks_empty) { |
| 347 if (left.IsKnownCategory(KnownCategories::BOOKMARKS)) | 401 if (left.IsKnownCategory(KnownCategories::BOOKMARKS)) |
| 348 return false; | 402 return false; |
| 349 if (right.IsKnownCategory(KnownCategories::BOOKMARKS)) | 403 if (right.IsKnownCategory(KnownCategories::BOOKMARKS)) |
| 350 return true; | 404 return true; |
| 351 } | 405 } |
| 352 return category_factory_.CompareCategories(left, right); | 406 return category_factory_.CompareCategories(left, right); |
| 353 }); | 407 }); |
| 354 } | 408 } |
| 355 | 409 |
| 410 bool ContentSuggestionsService::IsCategoryDismissed(Category category) const { |
| 411 return base::ContainsKey(dismissed_providers_by_category_, category); |
| 412 } |
| 413 |
| 414 void ContentSuggestionsService::RestoreDismissedCategory(Category category) { |
| 415 auto dismissed_it = dismissed_providers_by_category_.find(category); |
| 416 DCHECK(base::ContainsKey(dismissed_providers_by_category_, category)); |
| 417 |
| 418 // Keep the reference to the provider and remove it from the dismissed ones, |
| 419 // because the category registration enforces that it's not dismissed. |
| 420 ContentSuggestionsProvider* provider = dismissed_it->second; |
| 421 dismissed_providers_by_category_.erase(dismissed_it); |
| 422 |
| 423 if (provider) |
| 424 RegisterCategory(category, provider); |
| 425 } |
| 426 |
| 427 void ContentSuggestionsService::RestoreDismissedCategoriesFromPrefs() { |
| 428 // This must only be called at startup. |
| 429 DCHECK(dismissed_providers_by_category_.empty()); |
| 430 DCHECK(providers_by_category_.empty()); |
| 431 |
| 432 const base::ListValue* list = |
| 433 pref_service_->GetList(prefs::kDismissedCategories); |
| 434 for (const std::unique_ptr<base::Value>& entry : *list) { |
| 435 int id = 0; |
| 436 if (!entry->GetAsInteger(&id)) { |
| 437 DLOG(WARNING) << "Invalid category pref value: " << *entry; |
| 438 continue; |
| 439 } |
| 440 |
| 441 // When the provider is registered, it will be stored in this map. |
| 442 dismissed_providers_by_category_[category_factory()->FromIDValue(id)] = |
| 443 nullptr; |
| 444 } |
| 445 } |
| 446 |
| 447 void ContentSuggestionsService::StoreDismissedCategoriesToPrefs() { |
| 448 base::ListValue list; |
| 449 for (const auto& category_provider_pair : dismissed_providers_by_category_) { |
| 450 list.AppendInteger(category_provider_pair.first.id()); |
| 451 } |
| 452 |
| 453 pref_service_->Set(prefs::kDismissedCategories, list); |
| 454 } |
| 455 |
| 356 } // namespace ntp_snippets | 456 } // namespace ntp_snippets |
| OLD | NEW |