| Index: chrome/browser/ntp_snippets/download_suggestions_provider.cc
|
| diff --git a/chrome/browser/ntp_snippets/download_suggestions_provider.cc b/chrome/browser/ntp_snippets/download_suggestions_provider.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..23e14af664846b03c6d8c4fe3180b693fc2f5f9a
|
| --- /dev/null
|
| +++ b/chrome/browser/ntp_snippets/download_suggestions_provider.cc
|
| @@ -0,0 +1,629 @@
|
| +// Copyright 2016 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +#include "chrome/browser/ntp_snippets/download_suggestions_provider.h"
|
| +
|
| +#include <algorithm>
|
| +
|
| +#include "base/bind.h"
|
| +#include "base/feature_list.h"
|
| +#include "base/guid.h"
|
| +#include "base/stl_util.h"
|
| +#include "base/strings/string_number_conversions.h"
|
| +#include "base/strings/string_util.h"
|
| +#include "base/strings/utf_string_conversions.h"
|
| +#include "base/threading/thread_task_runner_handle.h"
|
| +#include "base/time/time.h"
|
| +#include "chrome/grit/generated_resources.h"
|
| +#include "components/ntp_snippets/pref_names.h"
|
| +#include "components/ntp_snippets/pref_util.h"
|
| +#include "components/offline_pages/client_namespace_constants.h"
|
| +#include "components/prefs/pref_registry_simple.h"
|
| +#include "components/prefs/pref_service.h"
|
| +#include "net/base/filename_util.h"
|
| +#include "ui/base/l10n/l10n_util.h"
|
| +#include "ui/gfx/image/image.h"
|
| +
|
| +using content::DownloadItem;
|
| +using content::DownloadManager;
|
| +using ntp_snippets::Category;
|
| +using ntp_snippets::CategoryInfo;
|
| +using ntp_snippets::CategoryStatus;
|
| +using ntp_snippets::ContentSuggestion;
|
| +using ntp_snippets::prefs::kDismissedAssetDownloadSuggestions;
|
| +using ntp_snippets::prefs::kDismissedOfflinePageDownloadSuggestions;
|
| +using offline_pages::OfflinePageItem;
|
| +
|
| +namespace {
|
| +
|
| +// TODO(vitaliii): Make this configurable via a variation param. See
|
| +// crbug.com/654800.
|
| +const int kMaxSuggestionsCount = 5;
|
| +const char kAssetDownloadsPrefix = 'D';
|
| +const char kOfflinePageDownloadsPrefix = 'O';
|
| +
|
| +std::string GetOfflinePagePerCategoryID(int64_t raw_offline_page_id) {
|
| + // Raw ID is prefixed in order to avoid conflicts with asset downloads.
|
| + return std::string(1, kOfflinePageDownloadsPrefix) +
|
| + base::IntToString(raw_offline_page_id);
|
| +}
|
| +
|
| +std::string GetAssetDownloadPerCategoryID(uint32_t raw_download_id) {
|
| + // Raw ID is prefixed in order to avoid conflicts with offline page downloads.
|
| + return std::string(1, kAssetDownloadsPrefix) +
|
| + base::UintToString(raw_download_id);
|
| +}
|
| +
|
| +// Determines whether |suggestion_id| corresponds to offline page suggestion or
|
| +// asset download based on |id_within_category| prefix.
|
| +bool CorrespondsToOfflinePage(const ContentSuggestion::ID& suggestion_id) {
|
| + const std::string& id_within_category = suggestion_id.id_within_category();
|
| + if (!id_within_category.empty()) {
|
| + if (id_within_category[0] == kOfflinePageDownloadsPrefix)
|
| + return true;
|
| + if (id_within_category[0] == kAssetDownloadsPrefix)
|
| + return false;
|
| + }
|
| + NOTREACHED() << "Unknown id_within_category " << id_within_category;
|
| + return false;
|
| +}
|
| +
|
| +bool IsOfflinePageDownload(const offline_pages::ClientId& client_id) {
|
| + return client_id.name_space == offline_pages::kAsyncNamespace ||
|
| + client_id.name_space == offline_pages::kDownloadNamespace ||
|
| + client_id.name_space == offline_pages::kNTPSuggestionsNamespace;
|
| +}
|
| +
|
| +bool IsDownloadCompleted(const DownloadItem& item) {
|
| + return item.GetState() == DownloadItem::DownloadState::COMPLETE &&
|
| + !item.GetFileExternallyRemoved();
|
| +}
|
| +
|
| +struct OrderDownloadsMostRecentlyDownloadedFirst {
|
| + bool operator()(const DownloadItem* left, const DownloadItem* right) const {
|
| + return left->GetEndTime() > right->GetEndTime();
|
| + }
|
| +};
|
| +
|
| +} // namespace
|
| +
|
| +DownloadSuggestionsProvider::DownloadSuggestionsProvider(
|
| + ContentSuggestionsProvider::Observer* observer,
|
| + ntp_snippets::CategoryFactory* category_factory,
|
| + scoped_refptr<ntp_snippets::OfflinePageProxy> offline_page_proxy,
|
| + content::DownloadManager* download_manager,
|
| + PrefService* pref_service,
|
| + bool download_manager_ui_enabled)
|
| + : ContentSuggestionsProvider(observer, category_factory),
|
| + category_status_(CategoryStatus::AVAILABLE_LOADING),
|
| + provided_category_(category_factory->FromKnownCategory(
|
| + ntp_snippets::KnownCategories::DOWNLOADS)),
|
| + offline_page_proxy_(std::move(offline_page_proxy)),
|
| + download_manager_(download_manager),
|
| + pref_service_(pref_service),
|
| + download_manager_ui_enabled_(download_manager_ui_enabled),
|
| + weak_ptr_factory_(this) {
|
| + observer->OnCategoryStatusChanged(this, provided_category_, category_status_);
|
| + offline_page_proxy_->AddObserver(this);
|
| + if (download_manager_)
|
| + download_manager_->AddObserver(this);
|
| + // No need to explicitly fetch the asset downloads, since for each of them
|
| + // |OnDownloadCreated| is fired.
|
| + AsynchronouslyFetchOfflinePagesDownloads(/*notify=*/true);
|
| +}
|
| +
|
| +DownloadSuggestionsProvider::~DownloadSuggestionsProvider() {
|
| + offline_page_proxy_->RemoveObserver(this);
|
| + if (download_manager_) {
|
| + download_manager_->RemoveObserver(this);
|
| + UnregisterDownloadItemObservers();
|
| + }
|
| +}
|
| +
|
| +CategoryStatus DownloadSuggestionsProvider::GetCategoryStatus(
|
| + Category category) {
|
| + DCHECK_EQ(provided_category_, category);
|
| + return category_status_;
|
| +}
|
| +
|
| +CategoryInfo DownloadSuggestionsProvider::GetCategoryInfo(Category category) {
|
| + DCHECK_EQ(provided_category_, category);
|
| + // TODO(vitaliii): Do not show "More" button when there is no downloads UI.
|
| + // See crbug.com/660030.
|
| + return CategoryInfo(
|
| + l10n_util::GetStringUTF16(IDS_NTP_DOWNLOAD_SUGGESTIONS_SECTION_HEADER),
|
| + ntp_snippets::ContentSuggestionsCardLayout::MINIMAL_CARD,
|
| + /*has_more_button=*/download_manager_ui_enabled_,
|
| + /*show_if_empty=*/false,
|
| + l10n_util::GetStringUTF16(IDS_NTP_DOWNLOADS_SUGGESTIONS_SECTION_EMPTY));
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::DismissSuggestion(
|
| + const ContentSuggestion::ID& suggestion_id) {
|
| + DCHECK_EQ(provided_category_, suggestion_id.category());
|
| + std::set<std::string> dismissed_ids =
|
| + ReadDismissedIDsFromPrefs(CorrespondsToOfflinePage(suggestion_id));
|
| + dismissed_ids.insert(suggestion_id.id_within_category());
|
| + StoreDismissedIDsToPrefs(CorrespondsToOfflinePage(suggestion_id),
|
| + dismissed_ids);
|
| +
|
| + RemoveSuggestionFromCacheAndRetrieveMoreIfNeeded(suggestion_id);
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::FetchSuggestionImage(
|
| + const ContentSuggestion::ID& suggestion_id,
|
| + const ImageFetchedCallback& callback) {
|
| + // TODO(vitaliii): Fetch proper thumbnail from OfflinePageModel once it is
|
| + // available there.
|
| + // TODO(vitaliii): Provide site's favicon for assets downloads. See
|
| + // crbug.com/631447.
|
| + base::ThreadTaskRunnerHandle::Get()->PostTask(
|
| + FROM_HERE, base::Bind(callback, gfx::Image()));
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::ClearHistory(
|
| + base::Time begin,
|
| + base::Time end,
|
| + const base::Callback<bool(const GURL& url)>& filter) {
|
| + cached_offline_page_downloads_.clear();
|
| + cached_asset_downloads_.clear();
|
| + // This will trigger an asynchronous re-fetch.
|
| + ClearDismissedSuggestionsForDebugging(provided_category_);
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::ClearCachedSuggestions(Category category) {
|
| + DCHECK_EQ(provided_category_, category);
|
| + // Ignored. The internal caches are not stored on disk and they are just
|
| + // partial copies of the data stored at OfflinePage model and DownloadManager.
|
| + // If it is cleared there, it will be cleared in these caches as well.
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::GetDismissedSuggestionsForDebugging(
|
| + Category category,
|
| + const DismissedSuggestionsCallback& callback) {
|
| + DCHECK_EQ(provided_category_, category);
|
| +
|
| + offline_page_proxy_->GetAllPages(
|
| + base::Bind(&DownloadSuggestionsProvider::
|
| + GetAllPagesCallbackForGetDismissedSuggestions,
|
| + weak_ptr_factory_.GetWeakPtr(), callback));
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
|
| + Category category) {
|
| + DCHECK_EQ(provided_category_, category);
|
| + StoreAssetDismissedIDsToPrefs(std::set<std::string>());
|
| + StoreOfflinePageDismissedIDsToPrefs(std::set<std::string>());
|
| + AsynchronouslyFetchAllDownloadsAndSubmitSuggestions();
|
| +}
|
| +
|
| +// static
|
| +void DownloadSuggestionsProvider::RegisterProfilePrefs(
|
| + PrefRegistrySimple* registry) {
|
| + registry->RegisterListPref(kDismissedAssetDownloadSuggestions);
|
| + registry->RegisterListPref(kDismissedOfflinePageDownloadSuggestions);
|
| +}
|
| +
|
| +////////////////////////////////////////////////////////////////////////////////
|
| +// Private methods
|
| +
|
| +void DownloadSuggestionsProvider::GetAllPagesCallbackForGetDismissedSuggestions(
|
| + const DismissedSuggestionsCallback& callback,
|
| + const std::vector<OfflinePageItem>& offline_pages) const {
|
| + std::set<std::string> dismissed_ids = ReadOfflinePageDismissedIDsFromPrefs();
|
| + std::vector<ContentSuggestion> suggestions;
|
| + for (const OfflinePageItem& item : offline_pages) {
|
| + if (dismissed_ids.count(GetOfflinePagePerCategoryID(item.offline_id)))
|
| + suggestions.push_back(ConvertOfflinePage(item));
|
| + }
|
| +
|
| + if (download_manager_) {
|
| + std::vector<DownloadItem*> all_downloads;
|
| + download_manager_->GetAllDownloads(&all_downloads);
|
| +
|
| + dismissed_ids = ReadAssetDismissedIDsFromPrefs();
|
| +
|
| + for (const DownloadItem* item : all_downloads) {
|
| + if (dismissed_ids.count(GetAssetDownloadPerCategoryID(item->GetId())))
|
| + suggestions.push_back(ConvertDownloadItem(*item));
|
| + }
|
| + }
|
| +
|
| + callback.Run(std::move(suggestions));
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::OfflinePageModelChanged(
|
| + const std::vector<offline_pages::OfflinePageItem>& offline_pages) {
|
| + UpdateOfflinePagesCache(/*notify=*/true, offline_pages);
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::OfflinePageDeleted(
|
| + int64_t offline_id,
|
| + const offline_pages::ClientId& client_id) {
|
| + if (IsOfflinePageDownload(client_id))
|
| + InvalidateSuggestion(GetOfflinePagePerCategoryID(offline_id));
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::OnDownloadCreated(DownloadManager* manager,
|
| + DownloadItem* item) {
|
| + DCHECK_EQ(download_manager_, manager);
|
| + // This is called when new downloads are started and on startup for existing
|
| + // ones. We listen to each item to know when it is destroyed.
|
| + item->AddObserver(this);
|
| + if (CacheAssetDownloadIfNeeded(item))
|
| + SubmitContentSuggestions();
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::ManagerGoingDown(DownloadManager* manager) {
|
| + DCHECK_EQ(download_manager_, manager);
|
| + UnregisterDownloadItemObservers();
|
| + download_manager_ = nullptr;
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::OnDownloadUpdated(DownloadItem* item) {
|
| + if (base::ContainsValue(cached_asset_downloads_, item)) {
|
| + if (item->GetFileExternallyRemoved()) {
|
| + InvalidateSuggestion(GetAssetDownloadPerCategoryID(item->GetId()));
|
| + } else {
|
| + // The download may have changed.
|
| + SubmitContentSuggestions();
|
| + }
|
| + } else {
|
| + // Unfinished downloads may become completed.
|
| + if (CacheAssetDownloadIfNeeded(item))
|
| + SubmitContentSuggestions();
|
| + }
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::OnDownloadOpened(DownloadItem* item) {
|
| + // Ignored.
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::OnDownloadRemoved(DownloadItem* item) {
|
| + // Ignored. We listen to |OnDownloadDestroyed| instead. The reason is that
|
| + // we may need to retrieve all downloads, but |OnDownloadRemoved| is called
|
| + // before the download is removed from the list.
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::OnDownloadDestroyed(
|
| + content::DownloadItem* item) {
|
| + item->RemoveObserver(this);
|
| +
|
| + if (!IsDownloadCompleted(*item))
|
| + return;
|
| + // TODO(vitaliii): Implement a better way to clean up dismissed IDs (in case
|
| + // some calls are missed).
|
| + InvalidateSuggestion(GetAssetDownloadPerCategoryID(item->GetId()));
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::NotifyStatusChanged(
|
| + CategoryStatus new_status) {
|
| + DCHECK_NE(CategoryStatus::NOT_PROVIDED, category_status_);
|
| + DCHECK_NE(CategoryStatus::NOT_PROVIDED, new_status);
|
| + if (category_status_ == new_status)
|
| + return;
|
| + category_status_ = new_status;
|
| + observer()->OnCategoryStatusChanged(this, provided_category_,
|
| + category_status_);
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::AsynchronouslyFetchOfflinePagesDownloads(
|
| + bool notify) {
|
| + offline_page_proxy_->GetAllPages(
|
| + base::Bind(&DownloadSuggestionsProvider::UpdateOfflinePagesCache,
|
| + weak_ptr_factory_.GetWeakPtr(), notify));
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::FetchAssetsDownloads() {
|
| + if (!download_manager_) {
|
| + // The manager has gone down.
|
| + return;
|
| + }
|
| +
|
| + std::vector<DownloadItem*> all_downloads;
|
| + download_manager_->GetAllDownloads(&all_downloads);
|
| + std::set<std::string> old_dismissed_ids = ReadAssetDismissedIDsFromPrefs();
|
| + std::set<std::string> retained_dismissed_ids;
|
| + cached_asset_downloads_.clear();
|
| + for (const DownloadItem* item : all_downloads) {
|
| + std::string within_category_id =
|
| + GetAssetDownloadPerCategoryID(item->GetId());
|
| + if (!old_dismissed_ids.count(within_category_id)) {
|
| + if (IsDownloadCompleted(*item))
|
| + cached_asset_downloads_.push_back(item);
|
| + } else {
|
| + retained_dismissed_ids.insert(within_category_id);
|
| + }
|
| + }
|
| +
|
| + if (old_dismissed_ids.size() != retained_dismissed_ids.size())
|
| + StoreAssetDismissedIDsToPrefs(retained_dismissed_ids);
|
| +
|
| + if (static_cast<int>(cached_asset_downloads_.size()) > kMaxSuggestionsCount) {
|
| + // Partially sorts |downloads| such that:
|
| + // 1) The element at the index |kMaxSuggestionsCount| is changed to the
|
| + // element which would occur on this position if |downloads| was sorted;
|
| + // 2) All of the elements before index |kMaxSuggestionsCount| are less than
|
| + // or equal to the elements after it.
|
| + std::nth_element(cached_asset_downloads_.begin(),
|
| + cached_asset_downloads_.begin() + kMaxSuggestionsCount,
|
| + cached_asset_downloads_.end(),
|
| + OrderDownloadsMostRecentlyDownloadedFirst());
|
| + cached_asset_downloads_.resize(kMaxSuggestionsCount);
|
| + }
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::
|
| + AsynchronouslyFetchAllDownloadsAndSubmitSuggestions() {
|
| + FetchAssetsDownloads();
|
| + AsynchronouslyFetchOfflinePagesDownloads(/*notify=*/true);
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::SubmitContentSuggestions() {
|
| + NotifyStatusChanged(CategoryStatus::AVAILABLE);
|
| +
|
| + std::vector<ContentSuggestion> suggestions;
|
| + for (const OfflinePageItem& item : cached_offline_page_downloads_)
|
| + suggestions.push_back(ConvertOfflinePage(item));
|
| +
|
| + for (const DownloadItem* item : cached_asset_downloads_)
|
| + suggestions.push_back(ConvertDownloadItem(*item));
|
| +
|
| + std::sort(suggestions.begin(), suggestions.end(),
|
| + [](const ContentSuggestion& left, const ContentSuggestion& right) {
|
| + return left.publish_date() > right.publish_date();
|
| + });
|
| +
|
| + // TODO(vitaliii): Use resize() here. In order to do so, mark
|
| + // ContentSuggestion move constructor noexcept.
|
| + while (suggestions.size() > kMaxSuggestionsCount)
|
| + suggestions.pop_back();
|
| +
|
| + observer()->OnNewSuggestions(this, provided_category_,
|
| + std::move(suggestions));
|
| +}
|
| +
|
| +ContentSuggestion DownloadSuggestionsProvider::ConvertOfflinePage(
|
| + const OfflinePageItem& offline_page) const {
|
| + // TODO(vitaliii): Make sure the URL is actually opened as an offline URL even
|
| + // when the user is online. See crbug.com/641568.
|
| + ContentSuggestion suggestion(
|
| + ContentSuggestion::ID(provided_category_, GetOfflinePagePerCategoryID(
|
| + offline_page.offline_id)),
|
| + offline_page.url);
|
| +
|
| + if (offline_page.title.empty()) {
|
| + // TODO(vitaliii): Remove this fallback once the OfflinePageModel provides
|
| + // titles for all (relevant) OfflinePageItems.
|
| + suggestion.set_title(base::UTF8ToUTF16(offline_page.url.spec()));
|
| + } else {
|
| + suggestion.set_title(offline_page.title);
|
| + }
|
| + suggestion.set_publish_date(offline_page.creation_time);
|
| + suggestion.set_publisher_name(base::UTF8ToUTF16(offline_page.url.host()));
|
| + return suggestion;
|
| +}
|
| +
|
| +ContentSuggestion DownloadSuggestionsProvider::ConvertDownloadItem(
|
| + const DownloadItem& download_item) const {
|
| + // TODO(vitaliii): Ensure that files are opened in browser, but not downloaded
|
| + // again. See crbug.com/641568.
|
| + ContentSuggestion suggestion(
|
| + ContentSuggestion::ID(provided_category_, GetAssetDownloadPerCategoryID(
|
| + download_item.GetId())),
|
| + net::FilePathToFileURL(download_item.GetTargetFilePath()));
|
| + // TODO(vitaliii): Set proper title.
|
| + suggestion.set_title(
|
| + download_item.GetTargetFilePath().BaseName().LossyDisplayName());
|
| + suggestion.set_publish_date(download_item.GetEndTime());
|
| + suggestion.set_publisher_name(
|
| + base::UTF8ToUTF16(download_item.GetURL().host()));
|
| + // TODO(vitaliii): Set suggestion icon.
|
| + return suggestion;
|
| +}
|
| +
|
| +bool DownloadSuggestionsProvider::CacheAssetDownloadIfNeeded(
|
| + const content::DownloadItem* item) {
|
| + if (!IsDownloadCompleted(*item))
|
| + return false;
|
| +
|
| + if (base::ContainsValue(cached_asset_downloads_, item))
|
| + return false;
|
| +
|
| + std::set<std::string> dismissed_ids = ReadAssetDismissedIDsFromPrefs();
|
| + if (dismissed_ids.count(GetAssetDownloadPerCategoryID(item->GetId())))
|
| + return false;
|
| +
|
| + DCHECK_LE(static_cast<int>(cached_asset_downloads_.size()),
|
| + kMaxSuggestionsCount);
|
| + if (cached_asset_downloads_.size() == kMaxSuggestionsCount) {
|
| + auto oldest = std::max_element(cached_asset_downloads_.begin(),
|
| + cached_asset_downloads_.end(),
|
| + OrderDownloadsMostRecentlyDownloadedFirst());
|
| + if (item->GetEndTime() <= (*oldest)->GetEndTime())
|
| + return false;
|
| +
|
| + *oldest = item;
|
| + } else {
|
| + cached_asset_downloads_.push_back(item);
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool DownloadSuggestionsProvider::RemoveSuggestionFromCacheIfPresent(
|
| + const ContentSuggestion::ID& suggestion_id) {
|
| + DCHECK_EQ(provided_category_, suggestion_id.category());
|
| + if (CorrespondsToOfflinePage(suggestion_id)) {
|
| + auto matching =
|
| + std::find_if(cached_offline_page_downloads_.begin(),
|
| + cached_offline_page_downloads_.end(),
|
| + [&suggestion_id](const OfflinePageItem& item) {
|
| + return GetOfflinePagePerCategoryID(item.offline_id) ==
|
| + suggestion_id.id_within_category();
|
| + });
|
| + if (matching != cached_offline_page_downloads_.end()) {
|
| + cached_offline_page_downloads_.erase(matching);
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + auto matching = std::find_if(
|
| + cached_asset_downloads_.begin(), cached_asset_downloads_.end(),
|
| + [&suggestion_id](const DownloadItem* item) {
|
| + return GetAssetDownloadPerCategoryID(item->GetId()) ==
|
| + suggestion_id.id_within_category();
|
| + });
|
| + if (matching != cached_asset_downloads_.end()) {
|
| + cached_asset_downloads_.erase(matching);
|
| + return true;
|
| + }
|
| + return false;
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::
|
| + RemoveSuggestionFromCacheAndRetrieveMoreIfNeeded(
|
| + const ContentSuggestion::ID& suggestion_id) {
|
| + DCHECK_EQ(provided_category_, suggestion_id.category());
|
| + if (!RemoveSuggestionFromCacheIfPresent(suggestion_id))
|
| + return;
|
| +
|
| + if (CorrespondsToOfflinePage(suggestion_id)) {
|
| + if (cached_offline_page_downloads_.size() == kMaxSuggestionsCount - 1) {
|
| + // Previously there were |kMaxSuggestionsCount| cached suggestion,
|
| + // therefore, overall there may be more than |kMaxSuggestionsCount|
|
| + // suggestions in the model and now one of them may be cached instead of
|
| + // the removed one. Even though, the suggestions are not immediately
|
| + // used the cache has to be kept up to date, because it may be used when
|
| + // other data source is updated.
|
| + AsynchronouslyFetchOfflinePagesDownloads(/*notify=*/false);
|
| + }
|
| + } else {
|
| + if (cached_asset_downloads_.size() == kMaxSuggestionsCount - 1) {
|
| + // The same as the case above.
|
| + FetchAssetsDownloads();
|
| + }
|
| + }
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::UpdateOfflinePagesCache(
|
| + bool notify,
|
| + const std::vector<offline_pages::OfflinePageItem>& all_offline_pages) {
|
| + std::set<std::string> old_dismissed_ids =
|
| + ReadOfflinePageDismissedIDsFromPrefs();
|
| + std::set<std::string> retained_dismissed_ids;
|
| + std::vector<const OfflinePageItem*> items;
|
| + // Filtering out dismissed items and pruning dismissed IDs.
|
| + for (const OfflinePageItem& item : all_offline_pages) {
|
| + if (!IsOfflinePageDownload(item.client_id))
|
| + continue;
|
| +
|
| + std::string id_within_category =
|
| + GetOfflinePagePerCategoryID(item.offline_id);
|
| + if (!old_dismissed_ids.count(id_within_category))
|
| + items.push_back(&item);
|
| + else
|
| + retained_dismissed_ids.insert(id_within_category);
|
| + }
|
| +
|
| + if (static_cast<int>(items.size()) > kMaxSuggestionsCount) {
|
| + // Partially sorts |items| such that:
|
| + // 1) The element at the index |kMaxSuggestionsCount| is changed to the
|
| + // element which would occur on this position if |items| was sorted;
|
| + // 2) All of the elements before index |kMaxSuggestionsCount| are less than
|
| + // or equal to the elements after it.
|
| + std::nth_element(
|
| + items.begin(), items.begin() + kMaxSuggestionsCount, items.end(),
|
| + [](const OfflinePageItem* left, const OfflinePageItem* right) {
|
| + return left->creation_time > right->creation_time;
|
| + });
|
| + items.resize(kMaxSuggestionsCount);
|
| + }
|
| +
|
| + cached_offline_page_downloads_.clear();
|
| + for (const OfflinePageItem* item : items)
|
| + cached_offline_page_downloads_.push_back(*item);
|
| +
|
| + if (old_dismissed_ids.size() != retained_dismissed_ids.size())
|
| + StoreOfflinePageDismissedIDsToPrefs(retained_dismissed_ids);
|
| +
|
| + if (notify)
|
| + SubmitContentSuggestions();
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::InvalidateSuggestion(
|
| + const std::string& id_within_category) {
|
| + ContentSuggestion::ID suggestion_id(provided_category_, id_within_category);
|
| + observer()->OnSuggestionInvalidated(this, suggestion_id);
|
| +
|
| + std::set<std::string> dismissed_ids =
|
| + ReadDismissedIDsFromPrefs(CorrespondsToOfflinePage(suggestion_id));
|
| + auto it = dismissed_ids.find(id_within_category);
|
| + if (it != dismissed_ids.end()) {
|
| + dismissed_ids.erase(it);
|
| + StoreDismissedIDsToPrefs(CorrespondsToOfflinePage(suggestion_id),
|
| + dismissed_ids);
|
| + }
|
| +
|
| + RemoveSuggestionFromCacheAndRetrieveMoreIfNeeded(suggestion_id);
|
| +}
|
| +
|
| +std::set<std::string>
|
| +DownloadSuggestionsProvider::ReadAssetDismissedIDsFromPrefs() const {
|
| + return ntp_snippets::prefs::ReadDismissedIDsFromPrefs(
|
| + *pref_service_, kDismissedAssetDownloadSuggestions);
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::StoreAssetDismissedIDsToPrefs(
|
| + const std::set<std::string>& dismissed_ids) {
|
| + DCHECK(std::all_of(
|
| + dismissed_ids.begin(), dismissed_ids.end(),
|
| + [](const std::string& id) { return id[0] == kAssetDownloadsPrefix; }));
|
| + ntp_snippets::prefs::StoreDismissedIDsToPrefs(
|
| + pref_service_, kDismissedAssetDownloadSuggestions, dismissed_ids);
|
| +}
|
| +
|
| +std::set<std::string>
|
| +DownloadSuggestionsProvider::ReadOfflinePageDismissedIDsFromPrefs() const {
|
| + return ntp_snippets::prefs::ReadDismissedIDsFromPrefs(
|
| + *pref_service_, kDismissedOfflinePageDownloadSuggestions);
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::StoreOfflinePageDismissedIDsToPrefs(
|
| + const std::set<std::string>& dismissed_ids) {
|
| + DCHECK(std::all_of(dismissed_ids.begin(), dismissed_ids.end(),
|
| + [](const std::string& id) {
|
| + return id[0] == kOfflinePageDownloadsPrefix;
|
| + }));
|
| + ntp_snippets::prefs::StoreDismissedIDsToPrefs(
|
| + pref_service_, kDismissedOfflinePageDownloadSuggestions, dismissed_ids);
|
| +}
|
| +
|
| +std::set<std::string> DownloadSuggestionsProvider::ReadDismissedIDsFromPrefs(
|
| + bool for_offline_page_downloads) const {
|
| + if (for_offline_page_downloads)
|
| + return ReadOfflinePageDismissedIDsFromPrefs();
|
| + return ReadAssetDismissedIDsFromPrefs();
|
| +}
|
| +
|
| +// TODO(vitaliii): Store one set instead of two. See crbug.com/656024.
|
| +void DownloadSuggestionsProvider::StoreDismissedIDsToPrefs(
|
| + bool for_offline_page_downloads,
|
| + const std::set<std::string>& dismissed_ids) {
|
| + if (for_offline_page_downloads)
|
| + StoreOfflinePageDismissedIDsToPrefs(dismissed_ids);
|
| + else
|
| + StoreAssetDismissedIDsToPrefs(dismissed_ids);
|
| +}
|
| +
|
| +void DownloadSuggestionsProvider::UnregisterDownloadItemObservers() {
|
| + DCHECK_NE(download_manager_, nullptr);
|
| +
|
| + std::vector<DownloadItem*> all_downloads;
|
| + download_manager_->GetAllDownloads(&all_downloads);
|
| +
|
| + for (DownloadItem* item : all_downloads)
|
| + item->RemoveObserver(this);
|
| +}
|
|
|