Chromium Code Reviews| Index: components/ntp_snippets/ntp_snippets_service.cc |
| diff --git a/components/ntp_snippets/ntp_snippets_service.cc b/components/ntp_snippets/ntp_snippets_service.cc |
| index 99546b7f2e9ad352b9b6e6c091d5804633cb1c11..1f0909290671e5b99efe52cb9d8630e2a48f1b50 100644 |
| --- a/components/ntp_snippets/ntp_snippets_service.cc |
| +++ b/components/ntp_snippets/ntp_snippets_service.cc |
| @@ -202,9 +202,10 @@ NTPSnippetsService::NTPSnippetsService( |
| std::unique_ptr<NTPSnippetsStatusService> status_service) |
| : ContentSuggestionsProvider(observer, category_factory), |
| state_(State::NOT_INITED), |
| - category_status_(CategoryStatus::INITIALIZING), |
| pref_service_(pref_service), |
| suggestions_service_(suggestions_service), |
| + articles_category_( |
| + category_factory->FromKnownCategory(KnownCategories::ARTICLES)), |
| application_language_code_(application_language_code), |
| scheduler_(scheduler), |
| history_service_observer_(this), |
| @@ -215,14 +216,16 @@ NTPSnippetsService::NTPSnippetsService( |
| snippets_status_service_(std::move(status_service)), |
| fetch_after_load_(false), |
| nuke_after_load_(false), |
| - provided_category_( |
| - category_factory->FromKnownCategory(KnownCategories::ARTICLES)), |
| thumbnail_requests_throttler_( |
| pref_service, |
| RequestThrottler::RequestType::CONTENT_SUGGESTION_THUMBNAIL) { |
| - observer->OnCategoryStatusChanged(this, provided_category_, category_status_); |
| + // Articles category always exists; others will be added as needed. |
| + categories_[articles_category_] = CategoryContent(); |
| + observer->OnCategoryStatusChanged(this, articles_category_, |
| + categories_[articles_category_].status); |
| if (database_->IsErrorState()) { |
| - EnterState(State::ERROR_OCCURRED, CategoryStatus::LOADING_ERROR); |
| + EnterState(State::ERROR_OCCURRED); |
| + UpdateAllCategoryStatus(CategoryStatus::LOADING_ERROR); |
| return; |
| } |
| @@ -262,12 +265,19 @@ void NTPSnippetsService::FetchSnippetsFromHosts( |
| if (!ready()) |
| return; |
| - if (snippets_.empty()) |
| - UpdateCategoryStatus(CategoryStatus::AVAILABLE_LOADING); |
| + // Empty categories are marked as loading; others are unchanged. |
| + for (const auto& item : categories_) { |
| + Category category = item.first; |
| + const CategoryContent& content = item.second; |
| + if (content.snippets.empty()) |
| + UpdateCategoryStatus(category, CategoryStatus::AVAILABLE_LOADING); |
| + } |
| std::set<std::string> excluded_ids; |
| - for (const auto& snippet : dismissed_snippets_) { |
| - excluded_ids.insert(snippet->id()); |
| + for (const auto& item : categories_) { |
| + const CategoryContent& content = item.second; |
| + for (const auto& snippet : content.dismissed) |
| + excluded_ids.insert(snippet->id()); |
| } |
| snippets_fetcher_->FetchSnippetsFromHosts(hosts, application_language_code_, |
| excluded_ids, kMaxSnippetCount, |
| @@ -290,11 +300,13 @@ void NTPSnippetsService::RescheduleFetching() { |
| } |
| CategoryStatus NTPSnippetsService::GetCategoryStatus(Category category) { |
| - DCHECK(category.IsKnownCategory(KnownCategories::ARTICLES)); |
| - return category_status_; |
| + DCHECK(categories_.find(category) != categories_.end()); |
| + return categories_[category].status; |
| } |
| CategoryInfo NTPSnippetsService::GetCategoryInfo(Category category) { |
| + DCHECK(categories_.find(category) != categories_.end()); |
| + // TODO(sfiera): pass back titles for server categories. |
| return CategoryInfo( |
| l10n_util::GetStringUTF16(IDS_NTP_ARTICLE_SUGGESTIONS_SECTION_HEADER), |
| ContentSuggestionsCardLayout::FULL_CARD, |
| @@ -306,23 +318,27 @@ void NTPSnippetsService::DismissSuggestion(const std::string& suggestion_id) { |
| if (!ready()) |
| return; |
| + Category category = GetCategoryFromUniqueID(suggestion_id); |
| std::string snippet_id = GetWithinCategoryIDFromUniqueID(suggestion_id); |
| + DCHECK(categories_.find(category) != categories_.end()); |
| + |
| + CategoryContent* content = &categories_[category]; |
| auto it = |
| - std::find_if(snippets_.begin(), snippets_.end(), |
| + std::find_if(content->snippets.begin(), content->snippets.end(), |
| [&snippet_id](const std::unique_ptr<NTPSnippet>& snippet) { |
| return snippet->id() == snippet_id; |
| }); |
| - if (it == snippets_.end()) |
| + if (it == content->snippets.end()) |
| return; |
| (*it)->set_dismissed(true); |
| database_->SaveSnippet(**it); |
| - database_->DeleteImage((*it)->id()); |
| + database_->DeleteImage(snippet_id); |
| - dismissed_snippets_.push_back(std::move(*it)); |
| - snippets_.erase(it); |
| + content->dismissed.push_back(std::move(*it)); |
| + content->snippets.erase(it); |
| } |
| void NTPSnippetsService::FetchSuggestionImage( |
| @@ -332,19 +348,22 @@ void NTPSnippetsService::FetchSuggestionImage( |
| database_->LoadImage( |
| snippet_id, |
| base::Bind(&NTPSnippetsService::OnSnippetImageFetchedFromDatabase, |
| - base::Unretained(this), callback, snippet_id)); |
| + base::Unretained(this), callback, suggestion_id)); |
| } |
| void NTPSnippetsService::ClearCachedSuggestions(Category category) { |
| - DCHECK_EQ(category, provided_category_); |
| if (!initialized()) |
| return; |
| - if (snippets_.empty()) |
| + if (categories_.find(category) == categories_.end()) |
| + return; |
| + CategoryContent* content = &categories_[category]; |
| + if (content->snippets.empty()) |
| return; |
| - database_->DeleteSnippets(snippets_); |
| - snippets_.clear(); |
| + if (category == articles_category_) |
| + database_->DeleteSnippets(content->snippets); |
| + content->snippets.clear(); |
| NotifyNewSuggestions(); |
| } |
| @@ -352,14 +371,15 @@ void NTPSnippetsService::ClearCachedSuggestions(Category category) { |
| void NTPSnippetsService::GetDismissedSuggestionsForDebugging( |
| Category category, |
| const DismissedSuggestionsCallback& callback) { |
| - DCHECK_EQ(category, provided_category_); |
| + DCHECK(categories_.find(category) != categories_.end()); |
| + |
| std::vector<ContentSuggestion> result; |
| - for (const std::unique_ptr<NTPSnippet>& snippet : dismissed_snippets_) { |
| + const CategoryContent& content = categories_[category]; |
| + for (const std::unique_ptr<NTPSnippet>& snippet : content.dismissed) { |
| if (!snippet->is_complete()) |
| continue; |
| - ContentSuggestion suggestion( |
| - MakeUniqueID(provided_category_, snippet->id()), |
| - snippet->best_source().url); |
| + ContentSuggestion suggestion(MakeUniqueID(category, snippet->id()), |
| + snippet->best_source().url); |
| suggestion.set_amp_url(snippet->best_source().amp_url); |
| suggestion.set_title(base::UTF8ToUTF16(snippet->title())); |
| suggestion.set_snippet_text(base::UTF8ToUTF16(snippet->snippet())); |
| @@ -374,15 +394,18 @@ void NTPSnippetsService::GetDismissedSuggestionsForDebugging( |
| void NTPSnippetsService::ClearDismissedSuggestionsForDebugging( |
| Category category) { |
| - DCHECK_EQ(category, provided_category_); |
| + DCHECK(categories_.find(category) != categories_.end()); |
| + |
| if (!initialized()) |
| return; |
| - if (dismissed_snippets_.empty()) |
| + CategoryContent* content = &categories_[category]; |
| + if (content->dismissed.empty()) |
| return; |
| - database_->DeleteSnippets(dismissed_snippets_); |
| - dismissed_snippets_.clear(); |
| + if (category == articles_category_) |
| + database_->DeleteSnippets(content->dismissed); |
| + content->dismissed.clear(); |
| } |
| std::set<std::string> NTPSnippetsService::GetSuggestionsHosts() const { |
| @@ -426,18 +449,27 @@ void NTPSnippetsService::HistoryServiceBeingDeleted( |
| } |
| // image_fetcher::ImageFetcherDelegate implementation. |
| -void NTPSnippetsService::OnImageDataFetched(const std::string& snippet_id, |
| +void NTPSnippetsService::OnImageDataFetched(const std::string& suggestion_id, |
| const std::string& image_data) { |
| if (image_data.empty()) |
| return; |
| + Category category = GetCategoryFromUniqueID(suggestion_id); |
| + std::string snippet_id = GetWithinCategoryIDFromUniqueID(suggestion_id); |
| + |
| + auto category_it = categories_.find(category); |
| + if (category_it == categories_.end()) |
| + return; |
| + |
| + const CategoryContent& content = category_it->second; |
| + |
| // Only save the image if the corresponding snippet still exists. |
| auto it = |
| - std::find_if(snippets_.begin(), snippets_.end(), |
| + std::find_if(content.snippets.begin(), content.snippets.end(), |
| [&snippet_id](const std::unique_ptr<NTPSnippet>& snippet) { |
| return snippet->id() == snippet_id; |
| }); |
| - if (it == snippets_.end()) |
| + if (it == content.snippets.end()) |
| return; |
| database_->SaveImage(snippet_id, image_data); |
| @@ -447,15 +479,19 @@ void NTPSnippetsService::OnDatabaseLoaded(NTPSnippet::PtrVector snippets) { |
| if (state_ == State::ERROR_OCCURRED) |
| return; |
| DCHECK(state_ == State::NOT_INITED); |
| - DCHECK(snippets_.empty()); |
| - DCHECK(dismissed_snippets_.empty()); |
| + DCHECK(categories_.size() == 1); // Only articles category, so far. |
| + DCHECK(categories_.find(articles_category_) != categories_.end()); |
| + |
| + // TODO(sfiera): support non-article categories in database. |
| + CategoryContent* content = &categories_[articles_category_]; |
| for (std::unique_ptr<NTPSnippet>& snippet : snippets) { |
| if (snippet->is_dismissed()) |
| - dismissed_snippets_.emplace_back(std::move(snippet)); |
| + content->dismissed.emplace_back(std::move(snippet)); |
| else |
| - snippets_.emplace_back(std::move(snippet)); |
| + content->snippets.emplace_back(std::move(snippet)); |
| } |
| - std::sort(snippets_.begin(), snippets_.end(), |
| + |
| + std::sort(content->snippets.begin(), content->snippets.end(), |
| [](const std::unique_ptr<NTPSnippet>& lhs, |
| const std::unique_ptr<NTPSnippet>& rhs) { |
| return lhs->score() > rhs->score(); |
| @@ -466,7 +502,8 @@ void NTPSnippetsService::OnDatabaseLoaded(NTPSnippet::PtrVector snippets) { |
| } |
| void NTPSnippetsService::OnDatabaseError() { |
| - EnterState(State::ERROR_OCCURRED, CategoryStatus::LOADING_ERROR); |
| + EnterState(State::ERROR_OCCURRED); |
| + UpdateAllCategoryStatus(CategoryStatus::LOADING_ERROR); |
| } |
| // TODO(dgn): name clash between content suggestions and suggestions hosts. |
| @@ -480,15 +517,21 @@ void NTPSnippetsService::OnSuggestionsChanged( |
| return; |
| // Remove existing snippets that aren't in the suggestions anymore. |
| + // |
| // TODO(treib,maybelle): If there is another source with an allowed host, |
| // then we should fall back to that. |
| + // |
| + // TODO(sfiera): determine when non-article categories should restrict hosts, |
| + // and apply the same logic to them here. Maybe never? |
| + // |
| // First, move them over into |to_delete|. |
| + CategoryContent* content = &categories_[articles_category_]; |
| NTPSnippet::PtrVector to_delete; |
| - for (std::unique_ptr<NTPSnippet>& snippet : snippets_) { |
| + for (std::unique_ptr<NTPSnippet>& snippet : content->snippets) { |
| if (!hosts.count(snippet->best_source().url.host())) |
| to_delete.emplace_back(std::move(snippet)); |
| } |
| - Compact(&snippets_); |
| + Compact(&content->snippets); |
| // Then delete the removed snippets from the database. |
| database_->DeleteSnippets(to_delete); |
| @@ -507,48 +550,76 @@ void NTPSnippetsService::OnFetchFinished( |
| if (!ready()) |
| return; |
| - DCHECK(category_status_ == CategoryStatus::AVAILABLE || |
| - category_status_ == CategoryStatus::AVAILABLE_LOADING); |
| - |
| - // TODO(sfiera): support more than just the provided_category_ ARTICLES. |
| - if (snippets && (snippets->find(provided_category_) != snippets->end())) { |
| - // Sparse histogram used because the number of snippets is small (bound by |
| - // kMaxSnippetCount). |
| - DCHECK_LE(snippets->size(), static_cast<size_t>(kMaxSnippetCount)); |
| - UMA_HISTOGRAM_SPARSE_SLOWLY("NewTabPage.Snippets.NumArticlesFetched", |
| - (*snippets)[provided_category_].size()); |
| - MergeSnippets(std::move((*snippets)[provided_category_])); |
| + for (auto& item : categories_) { |
| + CategoryContent* content = &item.second; |
| + content->provided_by_server = false; |
| } |
| + // If snippets were fetched successfully, update our |categories_| from each |
| + // category provided by the server. |
| + if (snippets) { |
| + for (std::pair<const Category, NTPSnippet::PtrVector>& item : *snippets) { |
| + Category category = item.first; |
| + NTPSnippet::PtrVector& new_snippets = item.second; |
| + |
| + DCHECK_LE(snippets->size(), static_cast<size_t>(kMaxSnippetCount)); |
| + // TODO(sfiera): histograms for server categories. |
| + // Sparse histogram used because the number of snippets is small (bound by |
| + // kMaxSnippetCount). |
| + if (category == articles_category_) { |
| + UMA_HISTOGRAM_SPARSE_SLOWLY("NewTabPage.Snippets.NumArticlesFetched", |
| + new_snippets.size()); |
| + } |
| + |
| + MergeSnippets(category, std::move(new_snippets)); |
| + |
| + // If there are more snippets than we want to show, delete the extra ones. |
| + CategoryContent* content = &categories_[category]; |
| + content->provided_by_server = true; |
| + if (content->snippets.size() > kMaxSnippetCount) { |
| + NTPSnippet::PtrVector to_delete( |
| + std::make_move_iterator(content->snippets.begin() + |
| + kMaxSnippetCount), |
| + std::make_move_iterator(content->snippets.end())); |
| + content->snippets.resize(kMaxSnippetCount); |
| + if (category == articles_category_) |
| + database_->DeleteSnippets(to_delete); |
| + } |
| + } |
| + } |
| + |
| + // Trigger expiration. This probably won't expire any current snippets (old |
| + // ones should have already been expired by the timer, and new ones shouldn't |
| + // have expired yet), but it will update the timer for the next run. |
| ClearExpiredSnippets(); |
| - // If there are more snippets than we want to show, delete the extra ones. |
| - if (snippets_.size() > kMaxSnippetCount) { |
| - NTPSnippet::PtrVector to_delete( |
| - std::make_move_iterator(snippets_.begin() + kMaxSnippetCount), |
| - std::make_move_iterator(snippets_.end())); |
| - snippets_.resize(kMaxSnippetCount); |
| - database_->DeleteSnippets(to_delete); |
| + for (const auto& item : categories_) { |
| + Category category = item.first; |
| + UpdateCategoryStatus(category, CategoryStatus::AVAILABLE); |
| } |
| + // TODO(sfiera): equivalent metrics for non-articles. |
| + const CategoryContent& content = categories_[articles_category_]; |
| UMA_HISTOGRAM_SPARSE_SLOWLY("NewTabPage.Snippets.NumArticles", |
| - snippets_.size()); |
| - if (snippets_.empty() && !dismissed_snippets_.empty()) { |
| + content.snippets.size()); |
| + if (content.snippets.empty() && !content.dismissed.empty()) { |
| UMA_HISTOGRAM_COUNTS("NewTabPage.Snippets.NumArticlesZeroDueToDiscarded", |
| - dismissed_snippets_.size()); |
| + content.dismissed.size()); |
| } |
| - UpdateCategoryStatus(CategoryStatus::AVAILABLE); |
| + // TODO(sfiera): notify only when a category changed above. |
| NotifyNewSuggestions(); |
| } |
| -void NTPSnippetsService::MergeSnippets(NTPSnippet::PtrVector new_snippets) { |
| +void NTPSnippetsService::MergeSnippets(Category category, |
| + NTPSnippet::PtrVector new_snippets) { |
| DCHECK(ready()); |
| + CategoryContent* content = &categories_[category]; |
| // Remove new snippets that we already have, or that have been dismissed. |
| std::set<std::string> old_snippet_ids; |
| - InsertAllIDs(dismissed_snippets_, &old_snippet_ids); |
| - InsertAllIDs(snippets_, &old_snippet_ids); |
| + InsertAllIDs(content->dismissed, &old_snippet_ids); |
| + InsertAllIDs(content->snippets, &old_snippet_ids); |
| new_snippets.erase( |
| std::remove_if( |
| new_snippets.begin(), new_snippets.end(), |
| @@ -596,13 +667,15 @@ void NTPSnippetsService::MergeSnippets(NTPSnippet::PtrVector new_snippets) { |
| } |
| } |
| - // Save the new snippets to the DB. |
| - database_->SaveSnippets(new_snippets); |
| + // Save new articles to the DB. |
| + // TODO(sfiera): save non-articles to DB too. |
| + if (category == articles_category_) |
| + database_->SaveSnippets(new_snippets); |
| // Insert the new snippets at the front. |
| - snippets_.insert(snippets_.begin(), |
| - std::make_move_iterator(new_snippets.begin()), |
| - std::make_move_iterator(new_snippets.end())); |
| + content->snippets.insert(content->snippets.begin(), |
| + std::make_move_iterator(new_snippets.begin()), |
| + std::make_move_iterator(new_snippets.end())); |
| } |
| std::set<std::string> NTPSnippetsService::GetSnippetHostsFromPrefs() const { |
| @@ -626,106 +699,156 @@ void NTPSnippetsService::StoreSnippetHostsToPrefs( |
| } |
| void NTPSnippetsService::ClearExpiredSnippets() { |
| - base::Time expiry = base::Time::Now(); |
| - |
| - // Move expired snippets over into |to_delete|. |
| - NTPSnippet::PtrVector to_delete; |
| - for (std::unique_ptr<NTPSnippet>& snippet : snippets_) { |
| - if (snippet->expiry_date() <= expiry) |
| - to_delete.emplace_back(std::move(snippet)); |
| - } |
| - Compact(&snippets_); |
| - |
| - // Move expired dismissed snippets over into |to_delete| as well. |
| - for (std::unique_ptr<NTPSnippet>& snippet : dismissed_snippets_) { |
| - if (snippet->expiry_date() <= expiry) |
| - to_delete.emplace_back(std::move(snippet)); |
| - } |
| - Compact(&dismissed_snippets_); |
| - |
| - // Finally, actually delete the removed snippets from the DB. |
| - database_->DeleteSnippets(to_delete); |
| - |
| - // If there are any snippets left, schedule a timer for the next expiry. |
| - if (snippets_.empty() && dismissed_snippets_.empty()) |
| - return; |
| + std::vector<Category> categories_to_erase; |
| + const base::Time expiry = base::Time::Now(); |
| base::Time next_expiry = base::Time::Max(); |
| - for (const auto& snippet : snippets_) { |
| - if (snippet->expiry_date() < next_expiry) |
| - next_expiry = snippet->expiry_date(); |
| + |
| + for (auto& item : categories_) { |
| + Category category = item.first; |
| + CategoryContent* content = &item.second; |
| + |
| + // Move expired snippets over into |to_delete|. |
| + NTPSnippet::PtrVector to_delete; |
| + for (std::unique_ptr<NTPSnippet>& snippet : content->snippets) { |
| + if (snippet->expiry_date() <= expiry) |
| + to_delete.emplace_back(std::move(snippet)); |
| + } |
| + Compact(&content->snippets); |
| + |
| + // Move expired dismissed snippets over into |to_delete| as well. |
| + for (std::unique_ptr<NTPSnippet>& snippet : content->dismissed) { |
| + if (snippet->expiry_date() <= expiry) |
| + to_delete.emplace_back(std::move(snippet)); |
| + } |
| + Compact(&content->dismissed); |
| + |
| + // Finally, actually delete the removed snippets from the DB. |
| + if (category == articles_category_) |
| + database_->DeleteSnippets(to_delete); |
| + |
| + if (content->snippets.empty() && content->dismissed.empty()) { |
| + if ((category != articles_category_) && !content->provided_by_server) |
| + categories_to_erase.push_back(category); |
| + continue; |
| + } |
| + |
| + for (const auto& snippet : content->snippets) { |
| + if (snippet->expiry_date() < next_expiry) |
| + next_expiry = snippet->expiry_date(); |
| + } |
| + for (const auto& snippet : content->dismissed) { |
| + if (snippet->expiry_date() < next_expiry) |
| + next_expiry = snippet->expiry_date(); |
| + } |
| } |
| - for (const auto& snippet : dismissed_snippets_) { |
| - if (snippet->expiry_date() < next_expiry) |
| - next_expiry = snippet->expiry_date(); |
| + |
| + for (Category category : categories_to_erase) { |
| + UpdateCategoryStatus(category, CategoryStatus::NOT_PROVIDED); |
| + categories_.erase(category); |
| } |
| + |
| + // Unless there are no snippets left, schedule a timer for the next expiry. |
| DCHECK_GT(next_expiry, expiry); |
| - expiry_timer_.Start(FROM_HERE, next_expiry - expiry, |
| - base::Bind(&NTPSnippetsService::ClearExpiredSnippets, |
| - base::Unretained(this))); |
| + if (next_expiry < base::Time::Max()) { |
| + expiry_timer_.Start(FROM_HERE, next_expiry - expiry, |
|
tschumann
2016/08/25 16:15:21
i wonder if we should add some leeway here for the
|
| + base::Bind(&NTPSnippetsService::ClearExpiredSnippets, |
| + base::Unretained(this))); |
| + } |
| } |
| void NTPSnippetsService::NukeAllSnippets() { |
| - ClearCachedSuggestions(provided_category_); |
| - ClearDismissedSuggestionsForDebugging(provided_category_); |
| - |
| - // Temporarily enter an "explicitly disabled" state, so that any open UIs |
| - // will clear the suggestions too. |
| - if (category_status_ != CategoryStatus::CATEGORY_EXPLICITLY_DISABLED) { |
| - CategoryStatus old_category_status = category_status_; |
| - UpdateCategoryStatus(CategoryStatus::CATEGORY_EXPLICITLY_DISABLED); |
| - UpdateCategoryStatus(old_category_status); |
| + std::vector<Category> categories_to_erase; |
| + |
| + // Empty the ARTICLES category and remove all others, since they may or may |
| + // not be personalized. |
| + for (const auto& item : categories_) { |
| + Category category = item.first; |
| + |
| + ClearCachedSuggestions(category); |
| + ClearDismissedSuggestionsForDebugging(category); |
| + |
| + if (category == articles_category_) { |
| + // Temporarily enter an "explicitly disabled" state, so that any open UIs |
| + // will clear the suggestions too. |
| + CategoryContent& content = categories_[category]; |
| + if (content.status != CategoryStatus::CATEGORY_EXPLICITLY_DISABLED) { |
| + CategoryStatus old_category_status = content.status; |
| + UpdateCategoryStatus(category, |
| + CategoryStatus::CATEGORY_EXPLICITLY_DISABLED); |
| + UpdateCategoryStatus(category, old_category_status); |
| + } |
| + } else { |
| + // Remove other categories entirely; they may or may not reappear. |
| + UpdateCategoryStatus(category, CategoryStatus::NOT_PROVIDED); |
| + categories_to_erase.push_back(category); |
| + } |
| + } |
| + |
| + for (Category category : categories_to_erase) { |
| + categories_.erase(category); |
| } |
| } |
| void NTPSnippetsService::OnSnippetImageFetchedFromDatabase( |
| const ImageFetchedCallback& callback, |
| - const std::string& snippet_id, |
| + const std::string& suggestion_id, |
| std::string data) { |
| // |image_decoder_| is null in tests. |
| if (image_decoder_ && !data.empty()) { |
| image_decoder_->DecodeImage( |
| std::move(data), |
| base::Bind(&NTPSnippetsService::OnSnippetImageDecodedFromDatabase, |
| - base::Unretained(this), callback, snippet_id)); |
| + base::Unretained(this), callback, suggestion_id)); |
| return; |
| } |
| // Fetching from the DB failed; start a network fetch. |
| - FetchSnippetImageFromNetwork(snippet_id, callback); |
| + FetchSnippetImageFromNetwork(suggestion_id, callback); |
| } |
| void NTPSnippetsService::OnSnippetImageDecodedFromDatabase( |
| const ImageFetchedCallback& callback, |
| - const std::string& snippet_id, |
| + const std::string& suggestion_id, |
| const gfx::Image& image) { |
| if (!image.IsEmpty()) { |
| - callback.Run(MakeUniqueID(provided_category_, snippet_id), image); |
| + callback.Run(suggestion_id, image); |
| return; |
| } |
| // If decoding the image failed, delete the DB entry. |
| + std::string snippet_id = GetWithinCategoryIDFromUniqueID(suggestion_id); |
| database_->DeleteImage(snippet_id); |
| - FetchSnippetImageFromNetwork(snippet_id, callback); |
| + FetchSnippetImageFromNetwork(suggestion_id, callback); |
| } |
| void NTPSnippetsService::FetchSnippetImageFromNetwork( |
| - const std::string& snippet_id, |
| + const std::string& suggestion_id, |
| const ImageFetchedCallback& callback) { |
| + Category category = GetCategoryFromUniqueID(suggestion_id); |
| + std::string snippet_id = GetWithinCategoryIDFromUniqueID(suggestion_id); |
| + |
| + auto category_it = categories_.find(category); |
| + if (category_it == categories_.end()) { |
| + OnSnippetImageDecodedFromNetwork(callback, suggestion_id, gfx::Image()); |
| + return; |
| + } |
| + |
| + const CategoryContent& content = category_it->second; |
| auto it = |
| - std::find_if(snippets_.begin(), snippets_.end(), |
| + std::find_if(content.snippets.begin(), content.snippets.end(), |
| [&snippet_id](const std::unique_ptr<NTPSnippet>& snippet) { |
| return snippet->id() == snippet_id; |
| }); |
| - if (it == snippets_.end() || |
| + if (it == content.snippets.end() || |
| !thumbnail_requests_throttler_.DemandQuotaForRequest( |
| /*interactive_request=*/true)) { |
| // Return an empty image. Directly, this is never synchronous with the |
| // original FetchSuggestionImage() call - an asynchronous database query has |
| // happened in the meantime. |
| - OnSnippetImageDecodedFromNetwork(callback, snippet_id, gfx::Image()); |
| + OnSnippetImageDecodedFromNetwork(callback, suggestion_id, gfx::Image()); |
| return; |
| } |
| @@ -738,16 +861,16 @@ void NTPSnippetsService::FetchSnippetImageFromNetwork( |
| // is an ImageFetcherDelegate) and then also |
| // OnSnippetImageDecodedFromNetwork() after the raw data gets decoded. |
| image_fetcher_->StartOrQueueNetworkRequest( |
| - snippet.id(), snippet.salient_image_url(), |
| + suggestion_id, snippet.salient_image_url(), |
| base::Bind(&NTPSnippetsService::OnSnippetImageDecodedFromNetwork, |
| base::Unretained(this), callback)); |
| } |
| void NTPSnippetsService::OnSnippetImageDecodedFromNetwork( |
| const ImageFetchedCallback& callback, |
| - const std::string& snippet_id, |
| + const std::string& suggestion_id, |
| const gfx::Image& image) { |
| - callback.Run(MakeUniqueID(provided_category_, snippet_id), image); |
| + callback.Run(suggestion_id, image); |
| } |
| void NTPSnippetsService::EnterStateEnabled(bool fetch_snippets) { |
| @@ -756,8 +879,10 @@ void NTPSnippetsService::EnterStateEnabled(bool fetch_snippets) { |
| // FetchSnippets should set the status to |AVAILABLE_LOADING| if relevant, |
| // otherwise we transition to |AVAILABLE| here. |
| - if (category_status_ != CategoryStatus::AVAILABLE_LOADING) |
| - UpdateCategoryStatus(CategoryStatus::AVAILABLE); |
| + if (categories_[articles_category_].status != |
| + CategoryStatus::AVAILABLE_LOADING) { |
| + UpdateCategoryStatus(articles_category_, CategoryStatus::AVAILABLE); |
| + } |
| // If host restrictions are enabled, register for host list updates. |
| // |suggestions_service_| can be null in tests. |
| @@ -771,8 +896,23 @@ void NTPSnippetsService::EnterStateEnabled(bool fetch_snippets) { |
| } |
| void NTPSnippetsService::EnterStateDisabled() { |
| - ClearCachedSuggestions(provided_category_); |
| - ClearDismissedSuggestionsForDebugging(provided_category_); |
| + std::vector<Category> categories_to_erase; |
| + |
| + // Empty the ARTICLES category and remove all others, since they may or may |
| + // not be personalized. |
| + for (const auto& item : categories_) { |
| + Category category = item.first; |
| + ClearCachedSuggestions(category); |
| + ClearDismissedSuggestionsForDebugging(category); |
| + if (category != articles_category_) { |
| + UpdateCategoryStatus(category, CategoryStatus::NOT_PROVIDED); |
| + categories_to_erase.push_back(category); |
| + } |
| + } |
| + |
| + for (Category category : categories_to_erase) { |
| + categories_.erase(category); |
| + } |
| expiry_timer_.Stop(); |
| suggestions_service_subscription_.reset(); |
| @@ -817,22 +957,22 @@ void NTPSnippetsService::OnDisabledReasonChanged( |
| switch (disabled_reason) { |
| case DisabledReason::NONE: |
| // Do not change the status. That will be done in EnterStateEnabled() |
| - EnterState(State::READY, category_status_); |
| + EnterState(State::READY); |
| break; |
| case DisabledReason::EXPLICITLY_DISABLED: |
| - EnterState(State::DISABLED, CategoryStatus::CATEGORY_EXPLICITLY_DISABLED); |
| + EnterState(State::DISABLED); |
| + UpdateAllCategoryStatus(CategoryStatus::CATEGORY_EXPLICITLY_DISABLED); |
| break; |
| case DisabledReason::SIGNED_OUT: |
| - EnterState(State::DISABLED, CategoryStatus::SIGNED_OUT); |
| + EnterState(State::DISABLED); |
| + UpdateAllCategoryStatus(CategoryStatus::SIGNED_OUT); |
| break; |
| } |
| } |
| -void NTPSnippetsService::EnterState(State state, CategoryStatus status) { |
| - UpdateCategoryStatus(status); |
| - |
| +void NTPSnippetsService::EnterState(State state) { |
| if (state == state_) |
| return; |
| @@ -845,7 +985,8 @@ void NTPSnippetsService::EnterState(State state, CategoryStatus status) { |
| case State::READY: { |
| DCHECK(state_ == State::NOT_INITED || state_ == State::DISABLED); |
| - bool fetch_snippets = snippets_.empty() || fetch_after_load_; |
| + bool fetch_snippets = |
| + categories_[articles_category_].snippets.empty() || fetch_after_load_; |
| DVLOG(1) << "Entering state: READY"; |
| state_ = State::READY; |
| fetch_after_load_ = false; |
| @@ -870,32 +1011,61 @@ void NTPSnippetsService::EnterState(State state, CategoryStatus status) { |
| } |
| void NTPSnippetsService::NotifyNewSuggestions() { |
| - std::vector<ContentSuggestion> result; |
| - for (const std::unique_ptr<NTPSnippet>& snippet : snippets_) { |
| - if (!snippet->is_complete()) |
| - continue; |
| - ContentSuggestion suggestion( |
| - MakeUniqueID(provided_category_, snippet->id()), |
| - snippet->best_source().url); |
| - suggestion.set_amp_url(snippet->best_source().amp_url); |
| - suggestion.set_title(base::UTF8ToUTF16(snippet->title())); |
| - suggestion.set_snippet_text(base::UTF8ToUTF16(snippet->snippet())); |
| - suggestion.set_publish_date(snippet->publish_date()); |
| - suggestion.set_publisher_name( |
| - base::UTF8ToUTF16(snippet->best_source().publisher_name)); |
| - suggestion.set_score(snippet->score()); |
| - result.emplace_back(std::move(suggestion)); |
| + for (const auto& item : categories_) { |
| + Category category = item.first; |
| + const CategoryContent& content = item.second; |
| + |
| + std::vector<ContentSuggestion> result; |
| + for (const std::unique_ptr<NTPSnippet>& snippet : content.snippets) { |
| + // TODO(sfiera): if a snippet is not going to be displayed, move it |
| + // directly to content.dismissed on fetch. Otherwise, we might prune |
| + // other snippets to get down to kMaxSnippetCount, only to hide one of the |
| + // incomplete ones we kept. |
| + if (!snippet->is_complete()) |
| + continue; |
| + ContentSuggestion suggestion(MakeUniqueID(category, snippet->id()), |
| + snippet->best_source().url); |
| + suggestion.set_amp_url(snippet->best_source().amp_url); |
| + suggestion.set_title(base::UTF8ToUTF16(snippet->title())); |
| + suggestion.set_snippet_text(base::UTF8ToUTF16(snippet->snippet())); |
| + suggestion.set_publish_date(snippet->publish_date()); |
| + suggestion.set_publisher_name( |
| + base::UTF8ToUTF16(snippet->best_source().publisher_name)); |
| + suggestion.set_score(snippet->score()); |
| + result.emplace_back(std::move(suggestion)); |
| + } |
| + |
| + DVLOG(1) << "NotifyNewSuggestions(): " << result.size() |
| + << " items in category " << category; |
| + observer()->OnNewSuggestions(this, category, std::move(result)); |
| } |
| - observer()->OnNewSuggestions(this, provided_category_, std::move(result)); |
| } |
| -void NTPSnippetsService::UpdateCategoryStatus(CategoryStatus status) { |
| - if (status == category_status_) |
| +void NTPSnippetsService::UpdateCategoryStatus(Category category, |
| + CategoryStatus status) { |
| + DCHECK(categories_.find(category) != categories_.end()); |
| + CategoryContent& content = categories_[category]; |
| + if (status == content.status) |
| return; |
| - category_status_ = status; |
| - observer()->OnCategoryStatusChanged(this, provided_category_, |
| - category_status_); |
| + DVLOG(1) << "UpdateCategoryStatus(): " << category.id() << ": " |
| + << static_cast<int>(content.status) << " -> " |
| + << static_cast<int>(status); |
| + content.status = status; |
| + observer()->OnCategoryStatusChanged(this, category, content.status); |
| } |
| +void NTPSnippetsService::UpdateAllCategoryStatus(CategoryStatus status) { |
| + for (const auto& category : categories_) { |
| + UpdateCategoryStatus(category.first, status); |
| + } |
| +} |
| + |
| +NTPSnippetsService::CategoryContent::CategoryContent() = default; |
| +NTPSnippetsService::CategoryContent::CategoryContent(CategoryContent&&) = |
| + default; |
| +NTPSnippetsService::CategoryContent::~CategoryContent() = default; |
| +NTPSnippetsService::CategoryContent& NTPSnippetsService::CategoryContent:: |
| +operator=(CategoryContent&&) = default; |
| + |
| } // namespace ntp_snippets |