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

Unified Diff: components/ntp_snippets/downloads/download_suggestions_provider.cc

Issue 2360263002: [NTPSnippets] Show all downloads on the NTP (3/3): Downloads provider. (Closed)
Patch Set: Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: components/ntp_snippets/downloads/download_suggestions_provider.cc
diff --git a/components/ntp_snippets/downloads/download_suggestions_provider.cc b/components/ntp_snippets/downloads/download_suggestions_provider.cc
new file mode 100644
index 0000000000000000000000000000000000000000..3649b4937e66a066025e53ecdb3cf3cb6664b99b
--- /dev/null
+++ b/components/ntp_snippets/downloads/download_suggestions_provider.cc
@@ -0,0 +1,487 @@
+// 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 "components/ntp_snippets/downloads/download_suggestions_provider.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/guid.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 "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 "grit/components_strings.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 offline_pages::OfflinePageItem;
+
+namespace ntp_snippets {
+
+namespace {
+
+const int kMaxSuggestionsCount = 5;
+const char kDownloadsPrefix[] = "D";
+const char kOfflinePagesPrefix[] = "O";
+
+std::string GetOfflinePagePerCategoryID(int64_t raw_offline_page_id) {
+ // Raw ID is prefixed in order to avoid conflicts with Downloads.
+ return base::JoinString(
+ {kOfflinePagesPrefix, base::IntToString(raw_offline_page_id)}, "");
+}
+
+std::string GetAssetDownloadPerCategoryID(uint32_t raw_download_id) {
+ // Raw ID is prefixed in order to avoid conflicts with OfflinePages.
+ return base::JoinString(
+ {kDownloadsPrefix, base::UintToString(raw_download_id)}, "");
+}
+
+bool IsOfflinePageID(const std::string& within_category_id) {
+ if (!within_category_id.empty()) {
+ if (within_category_id[0] == kOfflinePagesPrefix[0])
+ return true;
+ if (within_category_id[0] == kDownloadsPrefix[0])
+ return false;
+ }
+ NOTREACHED() << "Unknown within_category_id " << within_category_id;
+ return false;
+}
+
+struct OrderOfflinePagesByMostRecentlyVisitedFirst {
+ bool operator()(const OfflinePageItem* left,
+ const OfflinePageItem* right) const {
+ return left->last_access_time > right->last_access_time;
+ }
+};
+
+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) &&
+ base::IsValidGUID(client_id.id);
+}
+
+bool IsDownloadCompleted(const DownloadItem* item) {
+ return item->GetState() == content::DownloadItem::DownloadState::COMPLETE &&
+ !item->GetFileExternallyRemoved();
+}
+
+struct OrderDownloadsMostRecentlyDownloadedCompletedFirst {
+ bool operator()(const DownloadItem* left, const DownloadItem* right) const {
+ if (IsDownloadCompleted(left) != IsDownloadCompleted(right))
+ return IsDownloadCompleted(left);
+ return left->GetEndTime() > right->GetEndTime();
+ }
+};
+
+struct SuggestionItemWrapper {
+ base::Time time;
+ bool is_offline_page;
+ int index;
+ bool operator<(const SuggestionItemWrapper& other) const {
+ return time > other.time;
+ }
+};
+
+} // namespace
+
+DownloadSuggestionsProvider::DownloadSuggestionsProvider(
+ ContentSuggestionsProvider::Observer* observer,
+ CategoryFactory* category_factory,
+ const scoped_refptr<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(KnownCategories::DOWNLOADS)),
+ offline_page_proxy_(offline_page_proxy),
+ download_manager_notifier_(download_manager, this),
+ 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);
+ FetchAllDownloadsAndSubmitSuggestions();
+}
+
+DownloadSuggestionsProvider::~DownloadSuggestionsProvider() {
+ offline_page_proxy_->RemoveObserver(this);
+}
+
+CategoryStatus DownloadSuggestionsProvider::GetCategoryStatus(
+ Category category) {
+ if (category == provided_category_)
+ return category_status_;
+ NOTREACHED() << "Unknown category " << category.id();
+ return CategoryStatus::NOT_PROVIDED;
+}
+
+CategoryInfo DownloadSuggestionsProvider::GetCategoryInfo(Category category) {
+ if (category == provided_category_) {
+ return CategoryInfo(
+ l10n_util::GetStringUTF16(IDS_NTP_DOWNLOAD_SUGGESTIONS_SECTION_HEADER),
+ ContentSuggestionsCardLayout::MINIMAL_CARD,
+ /*has_more_button=*/download_manager_ui_enabled_,
+ /*show_if_empty=*/false);
+ }
+ NOTREACHED() << "Unknown category " << category.id();
+ return CategoryInfo(base::string16(),
+ ContentSuggestionsCardLayout::MINIMAL_CARD,
+ /*has_more_button=*/false,
+ /*show_if_empty=*/false);
+}
+
+void DownloadSuggestionsProvider::DismissSuggestion(
+ const std::string& suggestion_id) {
+ DCHECK_EQ(provided_category_, GetCategoryFromUniqueID(suggestion_id));
+ std::string within_category_id =
+ GetWithinCategoryIDFromUniqueID(suggestion_id);
+ std::set<std::string> dismissed_ids =
+ (IsOfflinePageID(within_category_id)
+ ? ReadOfflinePageDismissedIDsFromPrefs()
+ : ReadAssetDismissedIDsFromPrefs());
+
+ dismissed_ids.insert(within_category_id);
+ if (IsOfflinePageID(within_category_id))
+ StoreOfflinePageDismissedIDsToPrefs(dismissed_ids);
+ else
+ StoreAssetDismissedIDsToPrefs(dismissed_ids);
+}
+
+void DownloadSuggestionsProvider::FetchSuggestionImage(
+ const std::string& suggestion_id,
+ const ImageFetchedCallback& callback) {
+ // TODO(vitaliii): Fetch proper thumbnail from OfflinePageModel once it's
+ // available there.
+ // TODO(vitaliii): Provide site's favicon for assets downloads.
+ 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) {
+ ClearDismissedSuggestionsForDebugging(provided_category_);
+ cached_offline_page_downloads_.clear();
+ cached_asset_downloads_.clear();
+ FetchAllDownloadsAndSubmitSuggestions();
+}
+
+void DownloadSuggestionsProvider::ClearCachedSuggestions(Category category) {
+ // Ignored.
+}
+
+void DownloadSuggestionsProvider::GetDismissedSuggestionsForDebugging(
+ Category category,
+ const DismissedSuggestionsCallback& callback) {
+ DCHECK_EQ(provided_category_, category);
+ // TODO(vitaliii): Implement.
+ callback.Run(std::vector<ContentSuggestion>());
+}
+
+void DownloadSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
+ Category category) {
+ DCHECK_EQ(provided_category_, category);
+ StoreAssetDismissedIDsToPrefs(std::set<std::string>());
+ StoreOfflinePageDismissedIDsToPrefs(std::set<std::string>());
+ FetchAllDownloadsAndSubmitSuggestions();
+}
+
+// static
+void DownloadSuggestionsProvider::RegisterProfilePrefs(
+ PrefRegistrySimple* registry) {
+ registry->RegisterListPref(prefs::kDismissedAssetDownloadSuggestions);
+ registry->RegisterListPref(prefs::kDismissedOfflinePageDownloadSuggestions);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Private methods
+
+void DownloadSuggestionsProvider::OfflinePageModelChanged(
+ const std::vector<offline_pages::OfflinePageItem>& offline_pages) {
+ NotifyStatusChanged(CategoryStatus::AVAILABLE);
+
+ std::set<std::string> old_dismissed_ids =
+ ReadOfflinePageDismissedIDsFromPrefs();
+ std::set<std::string> new_dismissed_ids;
+ std::vector<const OfflinePageItem*> items;
+ for (const OfflinePageItem& item : offline_pages) {
+ std::string per_category_id = GetOfflinePagePerCategoryID(item.offline_id);
+ if (!IsOfflinePageDownload(item.client_id))
+ return;
+
+ if (!old_dismissed_ids.count(per_category_id))
+ items.push_back(&item);
+ else
+ new_dismissed_ids.insert(per_category_id);
+ }
+
+ if (static_cast<int>(items.size()) > kMaxSuggestionsCount) {
+ std::nth_element(items.begin(), items.begin() + kMaxSuggestionsCount,
+ items.end(),
+ OrderOfflinePagesByMostRecentlyVisitedFirst());
+ 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() != new_dismissed_ids.size()) {
+ StoreOfflinePageDismissedIDsToPrefs(new_dismissed_ids);
+ }
+
+ SubmitContentSuggestions();
+}
+
+void DownloadSuggestionsProvider::OfflinePageDeleted(
+ int64_t offline_id,
+ const offline_pages::ClientId& client_id) {
+ // Because we never switch to NOT_PROVIDED dynamically, there can be no open
+ // UI containing an invalidated suggestion unless the status is something
+ // other than NOT_PROVIDED, so only notify invalidation in that case.
+ if (category_status_ != CategoryStatus::NOT_PROVIDED &&
+ IsOfflinePageDownload(client_id)) {
+ InvalidateSuggestion(GetOfflinePagePerCategoryID(offline_id));
+ }
+}
+
+void DownloadSuggestionsProvider::OnDownloadCreated(
+ DownloadManager* manager,
+ content::DownloadItem* item) {
+ if (!IsDownloadCompleted(item))
+ return;
+ // TODO(vitaliii): Rewrite to not retrieve all downloads.
+ FetchAssetsDownloads();
vitaliii 2016/09/22 13:43:17 Since this is called when each item is read, the o
vitaliii 2016/10/11 08:15:56 I rewrote this to avoid fetching all items.
+ SubmitContentSuggestions();
+}
+
+void DownloadSuggestionsProvider::OnDownloadUpdated(
+ DownloadManager* manager,
+ content::DownloadItem* item) {
+ if (!IsDownloadCompleted(item))
+ return;
+ // TODO(vitaliii): Rewrite to not retrieve all downloads.
+ FetchAssetsDownloads();
+ SubmitContentSuggestions();
+}
+void DownloadSuggestionsProvider::OnDownloadOpened(
+ DownloadManager* manager,
+ content::DownloadItem* item) {
+ // Ignored.
+}
+void DownloadSuggestionsProvider::OnDownloadRemoved(
+ DownloadManager* manager,
+ content::DownloadItem* item) {
+ if (!IsDownloadCompleted(item))
+ return;
+ // TODO(vitaliii): Implement a better way to clean up dismissed IDs.
+ InvalidateSuggestion(GetAssetDownloadPerCategoryID(item->GetId()));
+}
+
+void DownloadSuggestionsProvider::NotifyStatusChanged(
+ CategoryStatus new_status) {
+ DCHECK_NE(CategoryStatus::NOT_PROVIDED, category_status_);
+ if (category_status_ == new_status)
+ return;
+ category_status_ = new_status;
+ observer()->OnCategoryStatusChanged(this, provided_category_, new_status);
+}
+
+void DownloadSuggestionsProvider::
+ FetchOfflinePagesDownloadsAndSubmitSuggestions() {
+ // TODO(vitaliii): When something other than |GetAllPages| is used here, the
+ // dismissed IDs cleanup in |OfflinePageModelChanged| needs to be changed to
+ // avoid accidentally undismissing suggestions.
+ offline_page_proxy_->GetAllPages(
+ base::Bind(&DownloadSuggestionsProvider::OfflinePageModelChanged,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void DownloadSuggestionsProvider::FetchAssetsDownloads() {
+ DownloadManager* manager = download_manager_notifier_.GetManager();
+ if (!manager) {
+ // The manager has gone down.
+ return;
+ }
+
+ NotifyStatusChanged(CategoryStatus::AVAILABLE);
+
+ std::vector<DownloadItem*> downloads;
+ manager->GetAllDownloads(&downloads);
+
+ std::set<std::string> dismissed_ids = ReadAssetDismissedIDsFromPrefs();
+ std::vector<DownloadItem*> not_dismissed_downloads;
+ for (DownloadItem* item : downloads) {
+ std::string within_category_id =
+ GetAssetDownloadPerCategoryID(item->GetId());
+ if (!dismissed_ids.count(within_category_id)) {
+ not_dismissed_downloads.push_back(item);
+ }
+
+ downloads = not_dismissed_downloads;
+
+ if (static_cast<int>(downloads.size()) > kMaxSuggestionsCount) {
+ std::nth_element(downloads.begin(),
+ downloads.begin() + kMaxSuggestionsCount,
+ downloads.end(),
+ OrderDownloadsMostRecentlyDownloadedCompletedFirst());
+ downloads.resize(kMaxSuggestionsCount);
+ }
+
+ cached_asset_downloads_.clear();
+ for (DownloadItem* item : downloads) {
+ if (IsDownloadCompleted(item))
+ cached_asset_downloads_.push_back(item);
+ }
+ }
+}
+
+void DownloadSuggestionsProvider::FetchAllDownloadsAndSubmitSuggestions() {
+ FetchAssetsDownloads();
+ FetchOfflinePagesDownloadsAndSubmitSuggestions();
+}
+
+void DownloadSuggestionsProvider::SubmitContentSuggestions() {
+ NotifyStatusChanged(CategoryStatus::AVAILABLE);
+
+ std::vector<SuggestionItemWrapper> suggestion_items;
+ for (int i = 0; i < static_cast<int>(cached_offline_page_downloads_.size());
+ ++i) {
+ SuggestionItemWrapper wrapped_item;
+ wrapped_item.is_offline_page = true;
+ wrapped_item.index = i;
+ wrapped_item.time = cached_offline_page_downloads_[i].last_access_time;
+ suggestion_items.push_back(wrapped_item);
+ }
+
+ for (int i = 0; i < static_cast<int>(cached_asset_downloads_.size()); ++i) {
+ SuggestionItemWrapper wrapped_item;
+ wrapped_item.is_offline_page = false;
+ wrapped_item.index = i;
+ wrapped_item.time = cached_asset_downloads_[i]->GetEndTime();
+ suggestion_items.push_back(wrapped_item);
+ }
+
+ std::sort(suggestion_items.begin(), suggestion_items.end());
+
+ std::vector<ContentSuggestion> suggestions;
+ for (const SuggestionItemWrapper& wrapped_item : suggestion_items) {
+ if (suggestions.size() >= kMaxSuggestionsCount)
+ break;
+
+ if (wrapped_item.is_offline_page) {
+ suggestions.push_back(ConvertOfflinePage(
+ cached_offline_page_downloads_[wrapped_item.index]));
+ } else {
+ suggestions.push_back(
+ ConvertDownloadItem(cached_asset_downloads_[wrapped_item.index]));
+ }
+ }
+
+ 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
+ // and not just as a downloaded file.
+ ContentSuggestion suggestion(
+ MakeUniqueID(provided_category_,
+ GetOfflinePagePerCategoryID(offline_page.offline_id)),
+ offline_page.GetOfflineURL());
+
+ 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 content::DownloadItem* download_item) const {
+ std::string per_category_id =
+ MakeUniqueID(provided_category_,
+ GetAssetDownloadPerCategoryID(download_item->GetId()));
+ // TODO(vitaliii): Ensure that files are opened in browser, but not
+ // downloaded
+ // again.
+ ContentSuggestion suggestion(
+ per_category_id,
+ net::FilePathToFileURL(download_item->GetTargetFilePath()));
+ // TODO(vitaliii): Set proper title.
+ suggestion.set_title(
+ base::UTF8ToUTF16(download_item->GetTargetFilePath().BaseName().value()));
+ suggestion.set_publish_date(download_item->GetEndTime());
+ suggestion.set_publisher_name(
+ base::UTF8ToUTF16(download_item->GetURL().host()));
+ // TODO(vitaliii): Set suggestion icon.
+ return suggestion;
+}
+
+void DownloadSuggestionsProvider::InvalidateSuggestion(
+ const std::string& per_category_id) {
+ observer()->OnSuggestionInvalidated(
+ this, provided_category_,
+ MakeUniqueID(provided_category_, per_category_id));
+
+ std::set<std::string> dismissed_ids =
+ (IsOfflinePageID(per_category_id) ? ReadOfflinePageDismissedIDsFromPrefs()
+ : ReadAssetDismissedIDsFromPrefs());
+ auto it = dismissed_ids.find(per_category_id);
+ if (it != dismissed_ids.end()) {
+ dismissed_ids.erase(it);
+ if (IsOfflinePageID(per_category_id))
+ StoreOfflinePageDismissedIDsToPrefs(dismissed_ids);
+ else
+ StoreAssetDismissedIDsToPrefs(dismissed_ids);
+ }
+}
+
+std::set<std::string>
+DownloadSuggestionsProvider::ReadAssetDismissedIDsFromPrefs() const {
+ return prefs::ReadDismissedIDsFromPrefs(
+ *pref_service_, prefs::kDismissedAssetDownloadSuggestions);
+}
+
+void DownloadSuggestionsProvider::StoreAssetDismissedIDsToPrefs(
+ const std::set<std::string>& dismissed_ids) {
+ prefs::StoreDismissedIDsToPrefs(
+ pref_service_, prefs::kDismissedAssetDownloadSuggestions, dismissed_ids);
+}
+
+std::set<std::string>
+DownloadSuggestionsProvider::ReadOfflinePageDismissedIDsFromPrefs() const {
+ return prefs::ReadDismissedIDsFromPrefs(
+ *pref_service_, prefs::kDismissedOfflinePageDownloadSuggestions);
+}
+
+void DownloadSuggestionsProvider::StoreOfflinePageDismissedIDsToPrefs(
+ const std::set<std::string>& dismissed_ids) {
+ prefs::StoreDismissedIDsToPrefs(
+ pref_service_, prefs::kDismissedOfflinePageDownloadSuggestions,
+ dismissed_ids);
+}
+
+} // namespace ntp_snippets

Powered by Google App Engine
This is Rietveld 408576698