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

Side by Side 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, 2 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 unified diff | Download patch
OLDNEW
(Empty)
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
3 // found in the LICENSE file.
4
5 #include "components/ntp_snippets/downloads/download_suggestions_provider.h"
6
7 #include <algorithm>
8 #include <utility>
9
10 #include "base/bind.h"
11 #include "base/guid.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/threading/thread_task_runner_handle.h"
16 #include "components/ntp_snippets/pref_names.h"
17 #include "components/ntp_snippets/pref_util.h"
18 #include "components/offline_pages/client_namespace_constants.h"
19 #include "components/prefs/pref_registry_simple.h"
20 #include "components/prefs/pref_service.h"
21 #include "grit/components_strings.h"
22 #include "net/base/filename_util.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/gfx/image/image.h"
25
26 using content::DownloadItem;
27 using content::DownloadManager;
28 using offline_pages::OfflinePageItem;
29
30 namespace ntp_snippets {
31
32 namespace {
33
34 const int kMaxSuggestionsCount = 5;
35 const char kDownloadsPrefix[] = "D";
36 const char kOfflinePagesPrefix[] = "O";
37
38 std::string GetOfflinePagePerCategoryID(int64_t raw_offline_page_id) {
39 // Raw ID is prefixed in order to avoid conflicts with Downloads.
40 return base::JoinString(
41 {kOfflinePagesPrefix, base::IntToString(raw_offline_page_id)}, "");
42 }
43
44 std::string GetAssetDownloadPerCategoryID(uint32_t raw_download_id) {
45 // Raw ID is prefixed in order to avoid conflicts with OfflinePages.
46 return base::JoinString(
47 {kDownloadsPrefix, base::UintToString(raw_download_id)}, "");
48 }
49
50 bool IsOfflinePageID(const std::string& within_category_id) {
51 if (!within_category_id.empty()) {
52 if (within_category_id[0] == kOfflinePagesPrefix[0])
53 return true;
54 if (within_category_id[0] == kDownloadsPrefix[0])
55 return false;
56 }
57 NOTREACHED() << "Unknown within_category_id " << within_category_id;
58 return false;
59 }
60
61 struct OrderOfflinePagesByMostRecentlyVisitedFirst {
62 bool operator()(const OfflinePageItem* left,
63 const OfflinePageItem* right) const {
64 return left->last_access_time > right->last_access_time;
65 }
66 };
67
68 bool IsOfflinePageDownload(const offline_pages::ClientId& client_id) {
69 return (client_id.name_space == offline_pages::kAsyncNamespace ||
70 client_id.name_space == offline_pages::kDownloadNamespace ||
71 client_id.name_space == offline_pages::kNTPSuggestionsNamespace) &&
72 base::IsValidGUID(client_id.id);
73 }
74
75 bool IsDownloadCompleted(const DownloadItem* item) {
76 return item->GetState() == content::DownloadItem::DownloadState::COMPLETE &&
77 !item->GetFileExternallyRemoved();
78 }
79
80 struct OrderDownloadsMostRecentlyDownloadedCompletedFirst {
81 bool operator()(const DownloadItem* left, const DownloadItem* right) const {
82 if (IsDownloadCompleted(left) != IsDownloadCompleted(right))
83 return IsDownloadCompleted(left);
84 return left->GetEndTime() > right->GetEndTime();
85 }
86 };
87
88 struct SuggestionItemWrapper {
89 base::Time time;
90 bool is_offline_page;
91 int index;
92 bool operator<(const SuggestionItemWrapper& other) const {
93 return time > other.time;
94 }
95 };
96
97 } // namespace
98
99 DownloadSuggestionsProvider::DownloadSuggestionsProvider(
100 ContentSuggestionsProvider::Observer* observer,
101 CategoryFactory* category_factory,
102 const scoped_refptr<OfflinePageProxy>& offline_page_proxy,
103 content::DownloadManager* download_manager,
104 PrefService* pref_service,
105 bool download_manager_ui_enabled)
106 : ContentSuggestionsProvider(observer, category_factory),
107 category_status_(CategoryStatus::AVAILABLE_LOADING),
108 provided_category_(
109 category_factory->FromKnownCategory(KnownCategories::DOWNLOADS)),
110 offline_page_proxy_(offline_page_proxy),
111 download_manager_notifier_(download_manager, this),
112 pref_service_(pref_service),
113 download_manager_ui_enabled_(download_manager_ui_enabled),
114 weak_ptr_factory_(this) {
115 observer->OnCategoryStatusChanged(this, provided_category_, category_status_);
116 offline_page_proxy_->AddObserver(this);
117 FetchAllDownloadsAndSubmitSuggestions();
118 }
119
120 DownloadSuggestionsProvider::~DownloadSuggestionsProvider() {
121 offline_page_proxy_->RemoveObserver(this);
122 }
123
124 CategoryStatus DownloadSuggestionsProvider::GetCategoryStatus(
125 Category category) {
126 if (category == provided_category_)
127 return category_status_;
128 NOTREACHED() << "Unknown category " << category.id();
129 return CategoryStatus::NOT_PROVIDED;
130 }
131
132 CategoryInfo DownloadSuggestionsProvider::GetCategoryInfo(Category category) {
133 if (category == provided_category_) {
134 return CategoryInfo(
135 l10n_util::GetStringUTF16(IDS_NTP_DOWNLOAD_SUGGESTIONS_SECTION_HEADER),
136 ContentSuggestionsCardLayout::MINIMAL_CARD,
137 /*has_more_button=*/download_manager_ui_enabled_,
138 /*show_if_empty=*/false);
139 }
140 NOTREACHED() << "Unknown category " << category.id();
141 return CategoryInfo(base::string16(),
142 ContentSuggestionsCardLayout::MINIMAL_CARD,
143 /*has_more_button=*/false,
144 /*show_if_empty=*/false);
145 }
146
147 void DownloadSuggestionsProvider::DismissSuggestion(
148 const std::string& suggestion_id) {
149 DCHECK_EQ(provided_category_, GetCategoryFromUniqueID(suggestion_id));
150 std::string within_category_id =
151 GetWithinCategoryIDFromUniqueID(suggestion_id);
152 std::set<std::string> dismissed_ids =
153 (IsOfflinePageID(within_category_id)
154 ? ReadOfflinePageDismissedIDsFromPrefs()
155 : ReadAssetDismissedIDsFromPrefs());
156
157 dismissed_ids.insert(within_category_id);
158 if (IsOfflinePageID(within_category_id))
159 StoreOfflinePageDismissedIDsToPrefs(dismissed_ids);
160 else
161 StoreAssetDismissedIDsToPrefs(dismissed_ids);
162 }
163
164 void DownloadSuggestionsProvider::FetchSuggestionImage(
165 const std::string& suggestion_id,
166 const ImageFetchedCallback& callback) {
167 // TODO(vitaliii): Fetch proper thumbnail from OfflinePageModel once it's
168 // available there.
169 // TODO(vitaliii): Provide site's favicon for assets downloads.
170 base::ThreadTaskRunnerHandle::Get()->PostTask(
171 FROM_HERE, base::Bind(callback, gfx::Image()));
172 }
173
174 void DownloadSuggestionsProvider::ClearHistory(
175 base::Time begin,
176 base::Time end,
177 const base::Callback<bool(const GURL& url)>& filter) {
178 ClearDismissedSuggestionsForDebugging(provided_category_);
179 cached_offline_page_downloads_.clear();
180 cached_asset_downloads_.clear();
181 FetchAllDownloadsAndSubmitSuggestions();
182 }
183
184 void DownloadSuggestionsProvider::ClearCachedSuggestions(Category category) {
185 // Ignored.
186 }
187
188 void DownloadSuggestionsProvider::GetDismissedSuggestionsForDebugging(
189 Category category,
190 const DismissedSuggestionsCallback& callback) {
191 DCHECK_EQ(provided_category_, category);
192 // TODO(vitaliii): Implement.
193 callback.Run(std::vector<ContentSuggestion>());
194 }
195
196 void DownloadSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
197 Category category) {
198 DCHECK_EQ(provided_category_, category);
199 StoreAssetDismissedIDsToPrefs(std::set<std::string>());
200 StoreOfflinePageDismissedIDsToPrefs(std::set<std::string>());
201 FetchAllDownloadsAndSubmitSuggestions();
202 }
203
204 // static
205 void DownloadSuggestionsProvider::RegisterProfilePrefs(
206 PrefRegistrySimple* registry) {
207 registry->RegisterListPref(prefs::kDismissedAssetDownloadSuggestions);
208 registry->RegisterListPref(prefs::kDismissedOfflinePageDownloadSuggestions);
209 }
210
211 ////////////////////////////////////////////////////////////////////////////////
212 // Private methods
213
214 void DownloadSuggestionsProvider::OfflinePageModelChanged(
215 const std::vector<offline_pages::OfflinePageItem>& offline_pages) {
216 NotifyStatusChanged(CategoryStatus::AVAILABLE);
217
218 std::set<std::string> old_dismissed_ids =
219 ReadOfflinePageDismissedIDsFromPrefs();
220 std::set<std::string> new_dismissed_ids;
221 std::vector<const OfflinePageItem*> items;
222 for (const OfflinePageItem& item : offline_pages) {
223 std::string per_category_id = GetOfflinePagePerCategoryID(item.offline_id);
224 if (!IsOfflinePageDownload(item.client_id))
225 return;
226
227 if (!old_dismissed_ids.count(per_category_id))
228 items.push_back(&item);
229 else
230 new_dismissed_ids.insert(per_category_id);
231 }
232
233 if (static_cast<int>(items.size()) > kMaxSuggestionsCount) {
234 std::nth_element(items.begin(), items.begin() + kMaxSuggestionsCount,
235 items.end(),
236 OrderOfflinePagesByMostRecentlyVisitedFirst());
237 items.resize(kMaxSuggestionsCount);
238 }
239
240 cached_offline_page_downloads_.clear();
241 for (const OfflinePageItem* item : items) {
242 cached_offline_page_downloads_.push_back(*item);
243 }
244
245 if (old_dismissed_ids.size() != new_dismissed_ids.size()) {
246 StoreOfflinePageDismissedIDsToPrefs(new_dismissed_ids);
247 }
248
249 SubmitContentSuggestions();
250 }
251
252 void DownloadSuggestionsProvider::OfflinePageDeleted(
253 int64_t offline_id,
254 const offline_pages::ClientId& client_id) {
255 // Because we never switch to NOT_PROVIDED dynamically, there can be no open
256 // UI containing an invalidated suggestion unless the status is something
257 // other than NOT_PROVIDED, so only notify invalidation in that case.
258 if (category_status_ != CategoryStatus::NOT_PROVIDED &&
259 IsOfflinePageDownload(client_id)) {
260 InvalidateSuggestion(GetOfflinePagePerCategoryID(offline_id));
261 }
262 }
263
264 void DownloadSuggestionsProvider::OnDownloadCreated(
265 DownloadManager* manager,
266 content::DownloadItem* item) {
267 if (!IsDownloadCompleted(item))
268 return;
269 // TODO(vitaliii): Rewrite to not retrieve all downloads.
270 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.
271 SubmitContentSuggestions();
272 }
273
274 void DownloadSuggestionsProvider::OnDownloadUpdated(
275 DownloadManager* manager,
276 content::DownloadItem* item) {
277 if (!IsDownloadCompleted(item))
278 return;
279 // TODO(vitaliii): Rewrite to not retrieve all downloads.
280 FetchAssetsDownloads();
281 SubmitContentSuggestions();
282 }
283 void DownloadSuggestionsProvider::OnDownloadOpened(
284 DownloadManager* manager,
285 content::DownloadItem* item) {
286 // Ignored.
287 }
288 void DownloadSuggestionsProvider::OnDownloadRemoved(
289 DownloadManager* manager,
290 content::DownloadItem* item) {
291 if (!IsDownloadCompleted(item))
292 return;
293 // TODO(vitaliii): Implement a better way to clean up dismissed IDs.
294 InvalidateSuggestion(GetAssetDownloadPerCategoryID(item->GetId()));
295 }
296
297 void DownloadSuggestionsProvider::NotifyStatusChanged(
298 CategoryStatus new_status) {
299 DCHECK_NE(CategoryStatus::NOT_PROVIDED, category_status_);
300 if (category_status_ == new_status)
301 return;
302 category_status_ = new_status;
303 observer()->OnCategoryStatusChanged(this, provided_category_, new_status);
304 }
305
306 void DownloadSuggestionsProvider::
307 FetchOfflinePagesDownloadsAndSubmitSuggestions() {
308 // TODO(vitaliii): When something other than |GetAllPages| is used here, the
309 // dismissed IDs cleanup in |OfflinePageModelChanged| needs to be changed to
310 // avoid accidentally undismissing suggestions.
311 offline_page_proxy_->GetAllPages(
312 base::Bind(&DownloadSuggestionsProvider::OfflinePageModelChanged,
313 weak_ptr_factory_.GetWeakPtr()));
314 }
315
316 void DownloadSuggestionsProvider::FetchAssetsDownloads() {
317 DownloadManager* manager = download_manager_notifier_.GetManager();
318 if (!manager) {
319 // The manager has gone down.
320 return;
321 }
322
323 NotifyStatusChanged(CategoryStatus::AVAILABLE);
324
325 std::vector<DownloadItem*> downloads;
326 manager->GetAllDownloads(&downloads);
327
328 std::set<std::string> dismissed_ids = ReadAssetDismissedIDsFromPrefs();
329 std::vector<DownloadItem*> not_dismissed_downloads;
330 for (DownloadItem* item : downloads) {
331 std::string within_category_id =
332 GetAssetDownloadPerCategoryID(item->GetId());
333 if (!dismissed_ids.count(within_category_id)) {
334 not_dismissed_downloads.push_back(item);
335 }
336
337 downloads = not_dismissed_downloads;
338
339 if (static_cast<int>(downloads.size()) > kMaxSuggestionsCount) {
340 std::nth_element(downloads.begin(),
341 downloads.begin() + kMaxSuggestionsCount,
342 downloads.end(),
343 OrderDownloadsMostRecentlyDownloadedCompletedFirst());
344 downloads.resize(kMaxSuggestionsCount);
345 }
346
347 cached_asset_downloads_.clear();
348 for (DownloadItem* item : downloads) {
349 if (IsDownloadCompleted(item))
350 cached_asset_downloads_.push_back(item);
351 }
352 }
353 }
354
355 void DownloadSuggestionsProvider::FetchAllDownloadsAndSubmitSuggestions() {
356 FetchAssetsDownloads();
357 FetchOfflinePagesDownloadsAndSubmitSuggestions();
358 }
359
360 void DownloadSuggestionsProvider::SubmitContentSuggestions() {
361 NotifyStatusChanged(CategoryStatus::AVAILABLE);
362
363 std::vector<SuggestionItemWrapper> suggestion_items;
364 for (int i = 0; i < static_cast<int>(cached_offline_page_downloads_.size());
365 ++i) {
366 SuggestionItemWrapper wrapped_item;
367 wrapped_item.is_offline_page = true;
368 wrapped_item.index = i;
369 wrapped_item.time = cached_offline_page_downloads_[i].last_access_time;
370 suggestion_items.push_back(wrapped_item);
371 }
372
373 for (int i = 0; i < static_cast<int>(cached_asset_downloads_.size()); ++i) {
374 SuggestionItemWrapper wrapped_item;
375 wrapped_item.is_offline_page = false;
376 wrapped_item.index = i;
377 wrapped_item.time = cached_asset_downloads_[i]->GetEndTime();
378 suggestion_items.push_back(wrapped_item);
379 }
380
381 std::sort(suggestion_items.begin(), suggestion_items.end());
382
383 std::vector<ContentSuggestion> suggestions;
384 for (const SuggestionItemWrapper& wrapped_item : suggestion_items) {
385 if (suggestions.size() >= kMaxSuggestionsCount)
386 break;
387
388 if (wrapped_item.is_offline_page) {
389 suggestions.push_back(ConvertOfflinePage(
390 cached_offline_page_downloads_[wrapped_item.index]));
391 } else {
392 suggestions.push_back(
393 ConvertDownloadItem(cached_asset_downloads_[wrapped_item.index]));
394 }
395 }
396
397 observer()->OnNewSuggestions(this, provided_category_,
398 std::move(suggestions));
399 }
400
401 ContentSuggestion DownloadSuggestionsProvider::ConvertOfflinePage(
402 const OfflinePageItem& offline_page) const {
403 // TODO(vitaliii): Make sure the URL is actually opened as an offline URL
404 // and not just as a downloaded file.
405 ContentSuggestion suggestion(
406 MakeUniqueID(provided_category_,
407 GetOfflinePagePerCategoryID(offline_page.offline_id)),
408 offline_page.GetOfflineURL());
409
410 if (offline_page.title.empty()) {
411 // TODO(vitaliii): Remove this fallback once the OfflinePageModel provides
412 // titles for all (relevant) OfflinePageItems.
413 suggestion.set_title(base::UTF8ToUTF16(offline_page.url.spec()));
414 } else {
415 suggestion.set_title(offline_page.title);
416 }
417 suggestion.set_publish_date(offline_page.creation_time);
418 suggestion.set_publisher_name(base::UTF8ToUTF16(offline_page.url.host()));
419 return suggestion;
420 }
421
422 ContentSuggestion DownloadSuggestionsProvider::ConvertDownloadItem(
423 const content::DownloadItem* download_item) const {
424 std::string per_category_id =
425 MakeUniqueID(provided_category_,
426 GetAssetDownloadPerCategoryID(download_item->GetId()));
427 // TODO(vitaliii): Ensure that files are opened in browser, but not
428 // downloaded
429 // again.
430 ContentSuggestion suggestion(
431 per_category_id,
432 net::FilePathToFileURL(download_item->GetTargetFilePath()));
433 // TODO(vitaliii): Set proper title.
434 suggestion.set_title(
435 base::UTF8ToUTF16(download_item->GetTargetFilePath().BaseName().value()));
436 suggestion.set_publish_date(download_item->GetEndTime());
437 suggestion.set_publisher_name(
438 base::UTF8ToUTF16(download_item->GetURL().host()));
439 // TODO(vitaliii): Set suggestion icon.
440 return suggestion;
441 }
442
443 void DownloadSuggestionsProvider::InvalidateSuggestion(
444 const std::string& per_category_id) {
445 observer()->OnSuggestionInvalidated(
446 this, provided_category_,
447 MakeUniqueID(provided_category_, per_category_id));
448
449 std::set<std::string> dismissed_ids =
450 (IsOfflinePageID(per_category_id) ? ReadOfflinePageDismissedIDsFromPrefs()
451 : ReadAssetDismissedIDsFromPrefs());
452 auto it = dismissed_ids.find(per_category_id);
453 if (it != dismissed_ids.end()) {
454 dismissed_ids.erase(it);
455 if (IsOfflinePageID(per_category_id))
456 StoreOfflinePageDismissedIDsToPrefs(dismissed_ids);
457 else
458 StoreAssetDismissedIDsToPrefs(dismissed_ids);
459 }
460 }
461
462 std::set<std::string>
463 DownloadSuggestionsProvider::ReadAssetDismissedIDsFromPrefs() const {
464 return prefs::ReadDismissedIDsFromPrefs(
465 *pref_service_, prefs::kDismissedAssetDownloadSuggestions);
466 }
467
468 void DownloadSuggestionsProvider::StoreAssetDismissedIDsToPrefs(
469 const std::set<std::string>& dismissed_ids) {
470 prefs::StoreDismissedIDsToPrefs(
471 pref_service_, prefs::kDismissedAssetDownloadSuggestions, dismissed_ids);
472 }
473
474 std::set<std::string>
475 DownloadSuggestionsProvider::ReadOfflinePageDismissedIDsFromPrefs() const {
476 return prefs::ReadDismissedIDsFromPrefs(
477 *pref_service_, prefs::kDismissedOfflinePageDownloadSuggestions);
478 }
479
480 void DownloadSuggestionsProvider::StoreOfflinePageDismissedIDsToPrefs(
481 const std::set<std::string>& dismissed_ids) {
482 prefs::StoreDismissedIDsToPrefs(
483 pref_service_, prefs::kDismissedOfflinePageDownloadSuggestions,
484 dismissed_ids);
485 }
486
487 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698