blob: 82b3aeadf1bad84c6968c33b2c41837332980fc7 [file] [log] [blame]
// Copyright 2015 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/ui/webui/downloads/downloads_list_tracker.h"
#include <iterator>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/i18n/rtl.h"
#include "base/i18n/unicodestring.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/download/download_crx_util.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_query.h"
#include "chrome/browser/extensions/api/downloads/downloads_api.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/downloads/downloads.mojom.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_item.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/download_manager.h"
#include "extensions/browser/extension_registry.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/filename_util.h"
#include "third_party/icu/source/i18n/unicode/datefmt.h"
#include "ui/base/l10n/time_format.h"
using content::BrowserContext;
using download::DownloadItem;
using content::DownloadManager;
using DownloadVector = DownloadManager::DownloadVector;
namespace {
// Max URL length to be sent to the download page.
const int kMaxURLLength = 2 * 1024 * 1024;
// Returns a string constant to be used as the |danger_type| value in
// CreateDownloadData(). This can be the empty string, if the danger type is not
// relevant for the UI.
const char* GetDangerTypeString(download::DownloadDangerType danger_type) {
switch (danger_type) {
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
return "DANGEROUS_FILE";
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
return "DANGEROUS_URL";
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
return "DANGEROUS_CONTENT";
case download::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
return "UNCOMMON_CONTENT";
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
return "DANGEROUS_HOST";
case download::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED:
return "POTENTIALLY_UNWANTED";
case download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING:
return "ASYNC_SCANNING";
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_PASSWORD_PROTECTED:
return "BLOCKED_PASSWORD_PROTECTED";
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_TOO_LARGE:
return "BLOCKED_TOO_LARGE";
case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING:
return "SENSITIVE_CONTENT_WARNING";
case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK:
return "SENSITIVE_CONTENT_BLOCK";
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE:
return "DEEP_SCANNED_SAFE";
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS:
return "DEEP_SCANNED_OPENED_DANGEROUS";
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_UNSUPPORTED_FILETYPE:
return "BLOCKED_UNSUPPORTED_FILE_TYPE";
case download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING:
case download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS:
case download::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
case download::DOWNLOAD_DANGER_TYPE_USER_VALIDATED:
case download::DOWNLOAD_DANGER_TYPE_ALLOWLISTED_BY_POLICY:
case download::DOWNLOAD_DANGER_TYPE_MAX:
break;
}
// Don't return a danger type string if it is NOT_DANGEROUS,
// MAYBE_DANGEROUS_CONTENT, or USER_VALIDATED, or ALLOWLISTED_BY_POLICY.
return "";
}
// TODO(dbeam): if useful elsewhere, move to base/i18n/time_formatting.h?
std::string TimeFormatLongDate(const base::Time& time) {
std::unique_ptr<icu::DateFormat> formatter(
icu::DateFormat::createDateInstance(icu::DateFormat::kLong));
icu::UnicodeString date_string;
formatter->format(static_cast<UDate>(time.ToDoubleT() * 1000), date_string);
return base::UTF16ToUTF8(base::i18n::UnicodeStringToString16(date_string));
}
} // namespace
DownloadsListTracker::DownloadsListTracker(
DownloadManager* download_manager,
mojo::PendingRemote<downloads::mojom::Page> page)
: main_notifier_(download_manager, this),
page_(std::move(page)),
should_show_(base::BindRepeating(&DownloadsListTracker::ShouldShow,
base::Unretained(this))) {
Init();
}
DownloadsListTracker::~DownloadsListTracker() {}
void DownloadsListTracker::Reset() {
if (sending_updates_)
page_->ClearAll();
sent_to_page_ = 0u;
}
bool DownloadsListTracker::SetSearchTerms(
const std::vector<std::string>& search_terms) {
std::vector<base::string16> new_terms;
new_terms.resize(search_terms.size());
for (const auto& t : search_terms)
new_terms.push_back(base::UTF8ToUTF16(t));
if (new_terms == search_terms_)
return false;
search_terms_.swap(new_terms);
RebuildSortedItems();
return true;
}
void DownloadsListTracker::StartAndSendChunk() {
sending_updates_ = true;
CHECK_LE(sent_to_page_, sorted_items_.size());
auto it = sorted_items_.begin();
std::advance(it, sent_to_page_);
std::vector<downloads::mojom::DataPtr> list;
while (it != sorted_items_.end() && list.size() < chunk_size_) {
list.push_back(CreateDownloadData(*it));
++it;
}
size_t list_size = list.size();
page_->InsertItems(static_cast<int>(sent_to_page_), std::move(list));
sent_to_page_ += list_size;
}
void DownloadsListTracker::Stop() {
sending_updates_ = false;
}
DownloadManager* DownloadsListTracker::GetMainNotifierManager() const {
return main_notifier_.GetManager();
}
DownloadManager* DownloadsListTracker::GetOriginalNotifierManager() const {
return original_notifier_ ? original_notifier_->GetManager() : nullptr;
}
void DownloadsListTracker::OnDownloadCreated(DownloadManager* manager,
DownloadItem* download_item) {
DCHECK_EQ(0u, sorted_items_.count(download_item));
if (should_show_.Run(*download_item))
InsertItem(sorted_items_.insert(download_item).first);
}
void DownloadsListTracker::OnDownloadUpdated(DownloadManager* manager,
DownloadItem* download_item) {
auto current_position = sorted_items_.find(download_item);
bool is_showing = current_position != sorted_items_.end();
bool should_show = should_show_.Run(*download_item);
if (!is_showing && should_show)
InsertItem(sorted_items_.insert(download_item).first);
else if (is_showing && !should_show)
RemoveItem(current_position);
else if (is_showing)
UpdateItem(current_position);
}
void DownloadsListTracker::OnDownloadRemoved(DownloadManager* manager,
DownloadItem* download_item) {
auto current_position = sorted_items_.find(download_item);
if (current_position != sorted_items_.end())
RemoveItem(current_position);
}
DownloadsListTracker::DownloadsListTracker(
DownloadManager* download_manager,
mojo::PendingRemote<downloads::mojom::Page> page,
base::Callback<bool(const DownloadItem&)> should_show)
: main_notifier_(download_manager, this),
page_(std::move(page)),
should_show_(should_show) {
DCHECK(page_);
Init();
}
downloads::mojom::DataPtr DownloadsListTracker::CreateDownloadData(
download::DownloadItem* download_item) const {
// TODO(asanka): Move towards using download_model here for getting status and
// progress. The difference currently only matters to Drive downloads and
// those don't show up on the downloads page, but should.
DownloadItemModel download_model(download_item);
auto file_value = downloads::mojom::Data::New();
file_value->started =
static_cast<int>(download_item->GetStartTime().ToTimeT());
file_value->since_string = base::UTF16ToUTF8(
ui::TimeFormat::RelativeDate(download_item->GetStartTime(), NULL));
file_value->date_string = TimeFormatLongDate(download_item->GetStartTime());
file_value->id = base::NumberToString(download_item->GetId());
base::FilePath download_path(download_item->GetTargetFilePath());
file_value->file_path = download_path.AsUTF8Unsafe();
file_value->file_url = net::FilePathToFileURL(download_path).spec();
extensions::DownloadedByExtension* by_ext =
extensions::DownloadedByExtension::Get(download_item);
std::string by_ext_id;
std::string by_ext_name;
if (by_ext) {
by_ext_id = by_ext->id();
// TODO(dbeam): why doesn't DownloadsByExtension::name() return a string16?
by_ext_name = by_ext->name();
// Lookup the extension's current name() in case the user changed their
// language. This won't work if the extension was uninstalled, so the name
// might be the wrong language.
auto* profile = Profile::FromBrowserContext(
content::DownloadItemUtils::GetBrowserContext(download_item));
auto* registry = extensions::ExtensionRegistry::Get(profile);
const extensions::Extension* extension = registry->GetExtensionById(
by_ext->id(), extensions::ExtensionRegistry::EVERYTHING);
if (extension)
by_ext_name = extension->name();
}
file_value->by_ext_id = by_ext_id;
file_value->by_ext_name = by_ext_name;
// Keep file names as LTR. TODO(dbeam): why?
base::string16 file_name =
download_item->GetFileNameToReportUser().LossyDisplayName();
file_name = base::i18n::GetDisplayStringInLTRDirectionality(file_name);
file_value->file_name = base::UTF16ToUTF8(file_name);
file_value->url = download_item->GetURL().spec();
// If URL is too long, truncate it.
if (file_value->url.size() > kMaxURLLength)
file_value->url.resize(kMaxURLLength);
file_value->total = static_cast<int>(download_item->GetTotalBytes());
file_value->file_externally_removed =
download_item->GetFileExternallyRemoved();
file_value->resume = download_item->CanResume();
file_value->otr = IsIncognito(*download_item);
const char* danger_type = GetDangerTypeString(download_item->GetDangerType());
base::string16 last_reason_text;
// -2 is invalid, -1 means indeterminate, and 0-100 are in-progress.
int percent = -2;
base::string16 progress_status_text;
bool retry = false;
const char* state = nullptr;
switch (download_item->GetState()) {
case download::DownloadItem::IN_PROGRESS: {
if (download_item->IsDangerous()) {
state = "DANGEROUS";
} else if (download_item->IsMixedContent()) {
state = "MIXED_CONTENT";
} else if (download_item->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING) {
state = "ASYNC_SCANNING";
} else if (download_item->IsPaused()) {
state = "PAUSED";
} else {
state = "IN_PROGRESS";
}
progress_status_text = download_model.GetTabProgressStatusText();
percent = download_item->PercentComplete();
break;
}
case download::DownloadItem::INTERRUPTED:
state = "INTERRUPTED";
progress_status_text = download_model.GetTabProgressStatusText();
if (download_item->CanResume())
percent = download_item->PercentComplete();
// TODO(asanka): last_reason_text should be set via
// download_model.GetInterruptReasonText(). But we are using
// GetStatusText() as a temporary measure until the layout is fixed to
// accommodate the longer string. http://crbug.com/609255
last_reason_text = download_model.GetStatusText();
if (download::DOWNLOAD_INTERRUPT_REASON_CRASH ==
download_item->GetLastReason() &&
!download_item->CanResume()) {
retry = true;
}
break;
case download::DownloadItem::CANCELLED:
state = "CANCELLED";
retry = true;
break;
case download::DownloadItem::COMPLETE:
DCHECK(!download_item->IsDangerous());
state = "COMPLETE";
break;
case download::DownloadItem::MAX_DOWNLOAD_STATE:
NOTREACHED();
}
DCHECK(state);
file_value->danger_type = danger_type;
file_value->is_dangerous = download_item->IsDangerous();
file_value->is_mixed_content = download_item->IsMixedContent();
file_value->last_reason_text = base::UTF16ToUTF8(last_reason_text);
file_value->percent = percent;
file_value->progress_status_text = base::UTF16ToUTF8(progress_status_text);
file_value->retry = retry;
file_value->state = state;
return file_value;
}
bool DownloadsListTracker::IsIncognito(const DownloadItem& item) const {
return GetOriginalNotifierManager() && GetMainNotifierManager() &&
GetMainNotifierManager()->GetDownload(item.GetId()) == &item;
}
const DownloadItem* DownloadsListTracker::GetItemForTesting(size_t index)
const {
if (index >= sorted_items_.size())
return nullptr;
auto it = sorted_items_.begin();
std::advance(it, index);
return *it;
}
void DownloadsListTracker::SetChunkSizeForTesting(size_t chunk_size) {
CHECK_EQ(0u, sent_to_page_);
chunk_size_ = chunk_size;
}
bool DownloadsListTracker::ShouldShow(const DownloadItem& item) const {
return !download_crx_util::IsTrustedExtensionDownload(
Profile::FromBrowserContext(
GetMainNotifierManager()->GetBrowserContext()),
item) &&
!item.IsTemporary() && !item.IsTransient() &&
!item.GetFileNameToReportUser().empty() &&
!item.GetTargetFilePath().empty() && !item.GetURL().is_empty() &&
DownloadItemModel(const_cast<DownloadItem*>(&item))
.ShouldShowInShelf() &&
DownloadQuery::MatchesQuery(search_terms_, item);
}
bool DownloadsListTracker::StartTimeComparator::operator()(
const download::DownloadItem* a,
const download::DownloadItem* b) const {
return a->GetStartTime() > b->GetStartTime();
}
void DownloadsListTracker::Init() {
Profile* profile = Profile::FromBrowserContext(
GetMainNotifierManager()->GetBrowserContext());
if (profile->IsOffTheRecord()) {
Profile* original_profile = profile->GetOriginalProfile();
original_notifier_ = std::make_unique<download::AllDownloadItemNotifier>(
BrowserContext::GetDownloadManager(original_profile), this);
}
RebuildSortedItems();
}
void DownloadsListTracker::RebuildSortedItems() {
DownloadVector all_items, visible_items;
GetMainNotifierManager()->GetAllDownloads(&all_items);
if (GetOriginalNotifierManager())
GetOriginalNotifierManager()->GetAllDownloads(&all_items);
DownloadQuery query;
query.AddFilter(should_show_);
query.Search(all_items.begin(), all_items.end(), &visible_items);
SortedSet sorted_items(visible_items.begin(), visible_items.end());
sorted_items_.swap(sorted_items);
}
void DownloadsListTracker::InsertItem(const SortedSet::iterator& insert) {
if (!sending_updates_)
return;
size_t index = GetIndex(insert);
if (index >= chunk_size_ && index >= sent_to_page_)
return;
std::vector<downloads::mojom::DataPtr> list;
list.push_back(CreateDownloadData(*insert));
page_->InsertItems(static_cast<int>(index), std::move(list));
sent_to_page_++;
}
void DownloadsListTracker::UpdateItem(const SortedSet::iterator& update) {
if (!sending_updates_ || GetIndex(update) >= sent_to_page_)
return;
page_->UpdateItem(static_cast<int>(GetIndex(update)),
CreateDownloadData(*update));
}
size_t DownloadsListTracker::GetIndex(const SortedSet::iterator& item) const {
// TODO(dbeam): this could be log(N) if |item| was random access.
return std::distance(sorted_items_.begin(), item);
}
void DownloadsListTracker::RemoveItem(const SortedSet::iterator& remove) {
if (sending_updates_) {
size_t index = GetIndex(remove);
if (index < sent_to_page_) {
page_->RemoveItem(static_cast<int>(index));
sent_to_page_--;
}
}
sorted_items_.erase(remove);
}