// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/download/notification/download_item_notification.h"
#include <stddef.h>
#include <stdint.h>
#include <memory>
#include "ash/public/cpp/notification_utils.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/escape.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/download_commands.h"
#include "chrome/browser/download/download_crx_util.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_stats.h"
#include "chrome/browser/download/notification/download_notification_manager.h"
#include "chrome/browser/enterprise/connectors/common.h"
#include "chrome/browser/enterprise/connectors/connectors_service.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/notifications/notification_handler.h"
#include "chrome/browser/safe_browsing/advanced_protection_status_manager.h"
#include "chrome/browser/safe_browsing/advanced_protection_status_manager_factory.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_interrupt_reasons.h"
#include "components/download/public/common/download_item.h"
#include "components/download/public/common/download_utils.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/elide_url.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/web_contents.h"
#include "net/base/mime_util.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkImage.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/text/bytes_formatting.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/image.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/constants/notifier_catalogs.h"
#include "chrome/browser/apps/app_service/policy_util.h"
#include "chrome/browser/ash/file_manager/file_tasks.h"
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/startup/browser_params_proxy.h"
#endif
using base::UserMetricsAction;
using offline_items_collection::FailState;
namespace {
const char kDownloadNotificationNotifierId[] =
"chrome://downloads/notification/id-notifier";
const char kDownloadNotificationOrigin[] = "chrome://downloads";
// Background color of the preview images
const SkColor kImageBackgroundColor = SK_ColorWHITE;
// Maximum size of preview image. If the image exceeds this size, don't show the
// preview image.
const int64_t kMaxImagePreviewSize = 10 * 1024 * 1024; // 10 MB
std::string ReadNotificationImage(const base::FilePath& file_path) {
std::string data;
bool ret = base::ReadFileToString(file_path, &data);
if (!ret)
return std::string();
DCHECK_LE(data.size(), static_cast<size_t>(kMaxImagePreviewSize));
return data;
}
SkBitmap CropImage(const SkBitmap& original_bitmap) {
DCHECK_NE(0, original_bitmap.width());
DCHECK_NE(0, original_bitmap.height());
const SkSize container_size =
SkSize::Make(message_center::kNotificationPreferredImageWidth,
message_center::kNotificationPreferredImageHeight);
const float container_aspect_ratio =
static_cast<float>(message_center::kNotificationPreferredImageWidth) /
message_center::kNotificationPreferredImageHeight;
const float image_aspect_ratio =
static_cast<float>(original_bitmap.width()) / original_bitmap.height();
SkRect source_rect;
if (image_aspect_ratio > container_aspect_ratio) {
float width = original_bitmap.height() * container_aspect_ratio;
source_rect = SkRect::MakeXYWH((original_bitmap.width() - width) / 2, 0,
width, original_bitmap.height());
} else {
float height = original_bitmap.width() / container_aspect_ratio;
source_rect = SkRect::MakeXYWH(0, (original_bitmap.height() - height) / 2,
original_bitmap.width(), height);
}
SkBitmap container_bitmap;
container_bitmap.allocN32Pixels(container_size.width(),
container_size.height());
SkSamplingOptions sampling({1.0f / 3, 1.0f / 3});
SkCanvas container_image(container_bitmap);
container_image.drawColor(kImageBackgroundColor);
container_image.drawImageRect(original_bitmap.asImage(), source_rect,
SkRect::MakeSize(container_size), sampling,
nullptr, SkCanvas::kStrict_SrcRectConstraint);
return container_bitmap;
}
void RecordButtonClickAction(DownloadCommands::Command command) {
switch (command) {
case DownloadCommands::SHOW_IN_FOLDER:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_ShowInFolder"));
break;
case DownloadCommands::OPEN_WHEN_COMPLETE:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_OpenWhenComplete"));
break;
case DownloadCommands::CANCEL:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_Cancel"));
break;
case DownloadCommands::DISCARD:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_Discard"));
break;
case DownloadCommands::KEEP:
base::RecordAction(UserMetricsAction("DownloadNotification.Button_Keep"));
break;
case DownloadCommands::LEARN_MORE_SCANNING:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_LearnScanning"));
break;
case DownloadCommands::LEARN_MORE_INSECURE_DOWNLOAD:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_LearnMixedContent"));
break;
case DownloadCommands::PAUSE:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_Pause"));
break;
case DownloadCommands::RESUME:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_Resume"));
break;
case DownloadCommands::COPY_TO_CLIPBOARD:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_CopyToClipboard"));
break;
case DownloadCommands::DEEP_SCAN:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_DeepScan"));
break;
case DownloadCommands::REVIEW:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_Review"));
break;
case DownloadCommands::OPEN_WITH_MEDIA_APP:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_OpenWithMediaApp"));
return;
case DownloadCommands::EDIT_WITH_MEDIA_APP:
base::RecordAction(
UserMetricsAction("DownloadNotification.Button_EditWithMediaApp"));
return;
// Not actually displayed in notification, so should never be reached.
case DownloadCommands::ALWAYS_OPEN_TYPE:
case DownloadCommands::PLATFORM_OPEN:
case DownloadCommands::LEARN_MORE_INTERRUPTED:
case DownloadCommands::LEARN_MORE_DOWNLOAD_BLOCKED:
case DownloadCommands::OPEN_SAFE_BROWSING_SETTING:
case DownloadCommands::BYPASS_DEEP_SCANNING:
case DownloadCommands::BYPASS_DEEP_SCANNING_AND_OPEN:
case DownloadCommands::CANCEL_DEEP_SCAN:
case DownloadCommands::RETRY:
NOTREACHED_IN_MIGRATION();
break;
}
}
bool IsExtensionDownload(DownloadUIModel* item) {
return item->GetDownloadItem() &&
download_crx_util::IsExtensionDownload(*item->GetDownloadItem());
}
} // namespace
DownloadItemNotification::DownloadItemNotification(
Profile* profile,
DownloadUIModel::DownloadUIModelPtr item)
: profile_(profile), item_(std::move(item)) {
item_->SetDelegate(this);
// Creates the notification instance. |title|, |body| and |icon| will be
// overridden by UpdateNotificationData() below.
message_center::RichNotificationData rich_notification_data;
rich_notification_data.should_make_spoken_feedback_for_popup_updates = false;
rich_notification_data.vector_small_image =
&vector_icons::kNotificationDownloadIcon;
notification_ = std::make_unique<message_center::Notification>(
message_center::NOTIFICATION_TYPE_PROGRESS, GetNotificationId(),
std::u16string(), // title
std::u16string(), // body
ui::ImageModel(), // icon
l10n_util::GetStringUTF16(
IDS_DOWNLOAD_NOTIFICATION_DISPLAY_SOURCE), // display_source
GURL(kDownloadNotificationOrigin), // origin_url
#if BUILDFLAG(IS_CHROMEOS_ASH)
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kDownloadNotificationNotifierId,
ash::NotificationCatalogName::kDownloadNotification),
#else
message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
kDownloadNotificationNotifierId),
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
rich_notification_data,
base::MakeRefCounted<message_center::ThunkNotificationDelegate>(
weak_factory_.GetWeakPtr()));
notification_->set_progress(0);
notification_->set_fullscreen_visibility(
message_center::FullscreenVisibility::OVER_USER);
Update();
}
DownloadItemNotification::~DownloadItemNotification() {
if (image_decode_status_ == IN_PROGRESS)
ImageDecoder::Cancel(this);
}
void DownloadItemNotification::SetObserver(Observer* observer) {
observer_ = observer;
}
DownloadUIModel* DownloadItemNotification::GetDownload() {
return item_.get();
}
void DownloadItemNotification::OnDownloadUpdated() {
Update();
}
void DownloadItemNotification::OnDownloadDestroyed(const ContentId& id) {
item_.reset();
NotificationDisplayServiceFactory::GetForProfile(profile())->Close(
NotificationHandler::Type::TRANSIENT, id.id);
// |this| will be deleted before there's a chance for Close() to be called
// through the delegate, so preemptively call it now.
Close(false);
// This object may get deleted after this call.
observer_->OnDownloadDestroyed(id);
}
void DownloadItemNotification::DisablePopup() {
if (notification_->priority() == message_center::LOW_PRIORITY)
return;
// Hides a notification from popup notifications if it's a pop-up, by
// decreasing its priority and reshowing itself. Low-priority notifications
// doesn't pop-up itself so this logic works as disabling pop-up.
CloseNotification();
notification_->set_priority(message_center::LOW_PRIORITY);
closed_ = false;
NotificationDisplayServiceFactory::GetForProfile(profile())->Display(
NotificationHandler::Type::TRANSIENT, *notification_,
/*metadata=*/nullptr);
}
void DownloadItemNotification::Close(bool by_user) {
closed_ = true;
if (item_ && item_->IsDangerous() && !item_->IsDone()) {
base::RecordAction(
UserMetricsAction("DownloadNotification.Close_Dangerous"));
item_->Cancel(by_user);
return;
}
if (item_ && item_->IsInsecure() && !item_->IsDone()) {
item_->Cancel(by_user);
return;
}
if (image_decode_status_ == IN_PROGRESS) {
image_decode_status_ = NOT_STARTED;
ImageDecoder::Cancel(this);
}
}
void DownloadItemNotification::Click(
const std::optional<int>& button_index,
const std::optional<std::u16string>& reply) {
if (!item_)
return;
if (button_index) {
if (*button_index < 0 ||
static_cast<size_t>(*button_index) >= button_actions_->size()) {
// Out of boundary.
NOTREACHED_IN_MIGRATION();
return;
}
DownloadCommands::Command command = button_actions_->at(*button_index);
RecordButtonClickAction(command);
// Completing Safe Browsing scan early if requested to open.
if (IsScanning() && AllowedToOpenWhileScanning() &&
command == DownloadCommands::OPEN_WHEN_COMPLETE) {
item_->CompleteSafeBrowsingScan();
}
DownloadCommands(item_->GetWeakPtr()).ExecuteCommand(command);
// After DISCARD, `this` has been destroyed.
if (command == DownloadCommands::DISCARD) {
return;
}
// ExecuteCommand() might cause |item_| to be destroyed.
if (item_ && command != DownloadCommands::PAUSE &&
command != DownloadCommands::RESUME &&
command != DownloadCommands::REVIEW) {
CloseNotification();
}
// Shows the notification again after clicking "Keep" on dangerous download.
if (command == DownloadCommands::KEEP) {
show_next_ = true;
Update();
}
if (command == DownloadCommands::REVIEW) {
content::WebContents* contents =
GetBrowser()->tab_strip_model()->GetActiveWebContents();
// If there is no currently active web contents, just show the user the
// downloads page so they get more context on the warned download needing
// to be reviewed.
// TODO(b/285119059): Expand this solution by having the review dialog
// also open immediately after the download page is available.
if (!contents) {
chrome::ShowDownloads(GetBrowser());
return;
}
item_->ReviewScanningVerdict(contents);
in_review_ = true;
Update();
}
return;
}
// Handle a click on the notification's body.
if (item_->IsDangerous()) {
base::RecordAction(
UserMetricsAction("DownloadNotification.Click_Dangerous"));
// Do nothing.
return;
}
// Handle a click on the notification's body.
if (item_->IsInsecure()) {
chrome::ShowDownloads(GetBrowser());
return;
}
// Handle a click on the notification's body while scanning.
if (IsScanning() && AllowedToOpenWhileScanning()) {
item_->CompleteSafeBrowsingScan();
item_->OpenDownload();
return;
}
switch (item_->GetState()) {
case download::DownloadItem::IN_PROGRESS:
base::RecordAction(
UserMetricsAction("DownloadNotification.Click_InProgress"));
item_->SetOpenWhenComplete(!item_->GetOpenWhenComplete()); // Toggle
break;
case download::DownloadItem::CANCELLED:
case download::DownloadItem::INTERRUPTED:
base::RecordAction(
UserMetricsAction("DownloadNotification.Click_Stopped"));
chrome::ShowDownloads(GetBrowser());
CloseNotification();
break;
case download::DownloadItem::COMPLETE:
base::RecordAction(
UserMetricsAction("DownloadNotification.Click_Completed"));
item_->OpenDownload();
CloseNotification();
break;
case download::DownloadItem::MAX_DOWNLOAD_STATE:
NOTREACHED_IN_MIGRATION();
}
}
std::string DownloadItemNotification::GetNotificationId() const {
return item_->GetContentId().id;
}
void DownloadItemNotification::CloseNotification() {
if (closed_)
return;
NotificationDisplayServiceFactory::GetForProfile(profile())->Close(
NotificationHandler::Type::TRANSIENT, GetNotificationId());
}
void DownloadItemNotification::Update() {
if (!item_)
return;
auto download_state = item_->GetState();
// When the download is just completed, interrupted or transitions to
// dangerous, make sure it pops up again.
bool pop_up =
((item_->IsDangerous() && !previous_dangerous_state_) ||
(item_->IsInsecure() && !previous_insecure_state_) ||
(download_state == download::DownloadItem::COMPLETE &&
previous_download_state_ != download::DownloadItem::COMPLETE) ||
(download_state == download::DownloadItem::INTERRUPTED &&
previous_download_state_ != download::DownloadItem::INTERRUPTED));
UpdateNotificationData(!closed_ || show_next_ || pop_up, pop_up);
show_next_ = false;
previous_download_state_ = item_->GetState();
previous_dangerous_state_ = item_->IsDangerous();
previous_insecure_state_ = item_->IsInsecure();
}
void DownloadItemNotification::UpdateNotificationData(bool display,
bool force_pop_up) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (item_->GetState() == download::DownloadItem::CANCELLED) {
// Confirms that a download is cancelled by user action.
DCHECK(item_->GetLastFailState() == FailState::USER_CANCELED ||
item_->GetLastFailState() == FailState::USER_SHUTDOWN);
CloseNotification();
return;
}
DownloadCommands command(item_->GetWeakPtr());
notification_->set_title(GetTitle());
notification_->set_message(GetSubStatusString());
notification_->set_progress_status(GetStatusString());
if (item_->IsDangerous()) {
notification_->set_type(message_center::NOTIFICATION_TYPE_SIMPLE);
MaybeRecordDangerousDownloadWarningShown(*item_);
if (!item_->MightBeMalicious() &&
item_->GetDangerType() !=
download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING) {
notification_->set_priority(message_center::HIGH_PRIORITY);
} else {
notification_->set_priority(message_center::DEFAULT_PRIORITY);
}
} else if (item_->IsInsecure()) {
notification_->set_type(message_center::NOTIFICATION_TYPE_SIMPLE);
switch (item_->GetInsecureDownloadStatus()) {
case download::DownloadItem::InsecureDownloadStatus::BLOCK:
notification_->set_priority(message_center::HIGH_PRIORITY);
break;
case download::DownloadItem::InsecureDownloadStatus::WARN:
notification_->set_priority(message_center::DEFAULT_PRIORITY);
break;
case download::DownloadItem::InsecureDownloadStatus::UNKNOWN:
case download::DownloadItem::InsecureDownloadStatus::SAFE:
case download::DownloadItem::InsecureDownloadStatus::VALIDATED:
case download::DownloadItem::InsecureDownloadStatus::SILENT_BLOCK:
NOTREACHED_IN_MIGRATION();
break;
}
} else {
switch (item_->GetState()) {
case download::DownloadItem::IN_PROGRESS: {
int percent_complete = item_->PercentComplete();
// Show "running" progress when percent is unknown or during cloud scan.
if (percent_complete >= 0 && !IsScanning()) {
notification_->set_progress(percent_complete);
} else {
// Negative progress value shows an indeterminate progress bar.
notification_->set_progress(-1);
}
notification_->set_type(message_center::NOTIFICATION_TYPE_PROGRESS);
break;
}
case download::DownloadItem::COMPLETE:
DCHECK(item_->IsDone());
notification_->set_priority(message_center::DEFAULT_PRIORITY);
notification_->set_type(message_center::NOTIFICATION_TYPE_SIMPLE);
notification_->set_progress(100);
break;
case download::DownloadItem::CANCELLED:
// Handled above.
NOTREACHED_IN_MIGRATION();
return;
case download::DownloadItem::INTERRUPTED:
// Shows a notifiation as progress type once so the visible content will
// be updated. (same as the case of type = COMPLETE)
notification_->set_type(message_center::NOTIFICATION_TYPE_SIMPLE);
notification_->set_progress(0);
notification_->set_priority(message_center::DEFAULT_PRIORITY);
break;
case download::DownloadItem::MAX_DOWNLOAD_STATE: // sentinel
NOTREACHED_IN_MIGRATION();
}
}
SkColor notification_color = GetNotificationIconColor();
ui::ColorId color_id = cros_tokens::kCrosSysPrimary;
switch (notification_color) {
case ash::kSystemNotificationColorNormal:
color_id = cros_tokens::kCrosSysPrimary;
break;
case ash::kSystemNotificationColorWarning:
color_id = cros_tokens::kCrosSysWarning;
break;
case ash::kSystemNotificationColorCriticalWarning:
color_id = cros_tokens::kCrosSysError;
break;
}
notification_->set_accent_color_id(color_id);
std::vector<message_center::ButtonInfo> notification_actions;
std::unique_ptr<std::vector<DownloadCommands::Command>> actions(
GetExtraActions());
button_actions_ = std::make_unique<std::vector<DownloadCommands::Command>>();
for (auto it = actions->begin(); it != actions->end(); it++) {
button_actions_->push_back(*it);
message_center::ButtonInfo button_info =
message_center::ButtonInfo(GetCommandLabel(*it));
notification_actions.push_back(button_info);
}
notification_->set_buttons(notification_actions);
notification_->set_renotify(force_pop_up);
if (display) {
closed_ = false;
NotificationDisplayServiceFactory::GetForProfile(profile())->Display(
NotificationHandler::Type::TRANSIENT, *notification_,
/*metadata=*/nullptr);
}
if (item_->IsDone() && image_decode_status_ == NOT_STARTED) {
// TODO(yoshiki): Add an UMA to collect statistics of image file sizes.
if (item_->GetCompletedBytes() > kMaxImagePreviewSize)
return;
DCHECK(notification_->image().IsEmpty());
image_decode_status_ = IN_PROGRESS;
if (item_->HasSupportedImageMimeType()) {
base::FilePath file_path = item_->GetFullPath();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&ReadNotificationImage, file_path),
base::BindOnce(&DownloadItemNotification::OnImageLoaded,
weak_factory_.GetWeakPtr()));
}
}
}
SkColor DownloadItemNotification::GetNotificationIconColor() {
if (item_->IsDangerous()) {
return (item_->MightBeMalicious() &&
item_->GetDangerType() !=
download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING)
? ash::kSystemNotificationColorCriticalWarning
: ash::kSystemNotificationColorWarning;
}
if (item_->IsInsecure()) {
switch (item_->GetInsecureDownloadStatus()) {
case download::DownloadItem::InsecureDownloadStatus::BLOCK:
return ash::kSystemNotificationColorCriticalWarning;
case download::DownloadItem::InsecureDownloadStatus::WARN:
return ash::kSystemNotificationColorWarning;
case download::DownloadItem::InsecureDownloadStatus::UNKNOWN:
case download::DownloadItem::InsecureDownloadStatus::SAFE:
case download::DownloadItem::InsecureDownloadStatus::VALIDATED:
case download::DownloadItem::InsecureDownloadStatus::SILENT_BLOCK:
NOTREACHED_IN_MIGRATION();
break;
}
}
switch (item_->GetState()) {
case download::DownloadItem::IN_PROGRESS:
case download::DownloadItem::COMPLETE:
return ash::kSystemNotificationColorNormal;
case download::DownloadItem::INTERRUPTED:
return ash::kSystemNotificationColorCriticalWarning;
case download::DownloadItem::CANCELLED:
break;
case download::DownloadItem::MAX_DOWNLOAD_STATE:
NOTREACHED_IN_MIGRATION();
break;
}
return gfx::kPlaceholderColor;
}
void DownloadItemNotification::OnImageLoaded(std::string image_data) {
if (image_data.empty())
return;
// TODO(yoshiki): Set option to reduce the image size to supress memory usage.
ImageDecoder::Start(this, std::move(image_data));
}
void DownloadItemNotification::OnImageDecoded(const SkBitmap& decoded_bitmap) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (decoded_bitmap.drawsNothing()) {
OnDecodeImageFailed();
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&CropImage, decoded_bitmap),
base::BindOnce(&DownloadItemNotification::OnImageCropped,
weak_factory_.GetWeakPtr()));
}
void DownloadItemNotification::OnImageCropped(const SkBitmap& bitmap) {
gfx::Image image = gfx::Image::CreateFrom1xBitmap(bitmap);
notification_->SetImage(image);
// Provide the file path that backs the image to facilitate notification drag.
#if BUILDFLAG(IS_CHROMEOS)
notification_->set_image_path(item_->GetFullPath());
#endif
image_decode_status_ = DONE;
UpdateNotificationData(!closed_, false);
}
void DownloadItemNotification::OnDecodeImageFailed() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(notification_->image().IsEmpty());
image_decode_status_ = FAILED;
UpdateNotificationData(!closed_, false);
}
std::unique_ptr<std::vector<DownloadCommands::Command>>
DownloadItemNotification::GetExtraActions() const {
std::unique_ptr<std::vector<DownloadCommands::Command>> actions(
new std::vector<DownloadCommands::Command>());
if (item_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING) {
actions->push_back(DownloadCommands::DEEP_SCAN);
actions->push_back(DownloadCommands::KEEP);
return actions;
}
if (item_->IsDangerous()) {
if (item_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING) {
actions->push_back(DownloadCommands::LEARN_MORE_SCANNING);
} else if (item_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT ||
item_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE) {
actions->push_back(DownloadCommands::DISCARD);
actions->push_back(DownloadCommands::KEEP);
} else if (item_->GetDangerType() !=
download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING) {
actions->push_back(DownloadCommands::DISCARD);
} else {
actions->push_back(DownloadCommands::DISCARD);
// Only include a keep/review button if there isn't an extra review dialog
// opened already.
if (!in_review_) {
if (enterprise_connectors::ShouldPromptReviewForDownload(
profile(), item_->GetDownloadItem())) {
actions->push_back(DownloadCommands::REVIEW);
} else {
actions->push_back(DownloadCommands::KEEP);
}
}
}
return actions;
}
if (item_->IsInsecure()) {
switch (item_->GetInsecureDownloadStatus()) {
case download::DownloadItem::InsecureDownloadStatus::BLOCK:
actions->push_back(DownloadCommands::DISCARD);
break;
case download::DownloadItem::InsecureDownloadStatus::WARN:
actions->push_back(DownloadCommands::KEEP);
break;
case download::DownloadItem::InsecureDownloadStatus::UNKNOWN:
case download::DownloadItem::InsecureDownloadStatus::SAFE:
case download::DownloadItem::InsecureDownloadStatus::VALIDATED:
case download::DownloadItem::InsecureDownloadStatus::SILENT_BLOCK:
NOTREACHED_IN_MIGRATION();
break;
}
actions->push_back(DownloadCommands::LEARN_MORE_INSECURE_DOWNLOAD);
return actions;
}
switch (item_->GetState()) {
case download::DownloadItem::IN_PROGRESS:
if (item_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING) {
if (AllowedToOpenWhileScanning())
actions->push_back(DownloadCommands::OPEN_WHEN_COMPLETE);
} else if (!item_->IsPaused()) {
actions->push_back(DownloadCommands::PAUSE);
} else {
actions->push_back(DownloadCommands::RESUME);
}
actions->push_back(DownloadCommands::CANCEL);
break;
case download::DownloadItem::CANCELLED:
case download::DownloadItem::INTERRUPTED:
if (item_->CanResume())
actions->push_back(DownloadCommands::RESUME);
break;
case download::DownloadItem::COMPLETE: {
#if BUILDFLAG(IS_CHROMEOS_ASH)
std::optional<DownloadCommands::Command> command =
item_->MaybeGetMediaAppAction();
if (command) {
actions->push_back(*command);
}
#endif
actions->push_back(DownloadCommands::SHOW_IN_FOLDER);
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
// We disable this functionality for now as the usage is very low, the
// feature gets re-written at this time and there is currently no secure
// way to determine the caller on the Ash side as the dialog is still
// active when |seat::SetSelection| is reached.
if (!notification_->image().IsEmpty())
actions->push_back(DownloadCommands::COPY_TO_CLIPBOARD);
#endif
break;
}
case download::DownloadItem::MAX_DOWNLOAD_STATE:
NOTREACHED_IN_MIGRATION();
}
return actions;
}
std::u16string DownloadItemNotification::GetTitle() const {
std::u16string title_text;
if (item_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING) {
return l10n_util::GetStringUTF16(
IDS_PROMPT_SEND_TO_SAFEBROWSING_DOWNLOAD_TITLE);
}
if (item_->IsDangerous()) {
if (item_->MightBeMalicious() &&
item_->GetDangerType() !=
download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING) {
return l10n_util::GetStringUTF16(
IDS_PROMPT_BLOCKED_MALICIOUS_DOWNLOAD_TITLE);
} else {
return l10n_util::GetStringUTF16(
IDS_CONFIRM_KEEP_DANGEROUS_DOWNLOAD_TITLE);
}
}
if (item_->IsInsecure()) {
return l10n_util::GetStringUTF16(
IDS_PROMPT_BLOCKED_INSECURE_DOWNLOAD_TITLE);
}
std::u16string file_name =
item_->GetFileNameToReportUser().LossyDisplayName();
if (IsScanning()) {
return l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_SCAN_TITLE,
file_name);
}
switch (item_->GetState()) {
case download::DownloadItem::IN_PROGRESS:
if (!item_->IsPaused()) {
title_text = l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_IN_PROGRESS_TITLE, file_name);
} else {
title_text = l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_PAUSED_TITLE, file_name);
}
break;
case download::DownloadItem::COMPLETE:
title_text =
l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_COMPLETE_TITLE);
break;
case download::DownloadItem::INTERRUPTED:
title_text = l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_DOWNLOAD_FAILED_TITLE, file_name);
break;
case download::DownloadItem::CANCELLED:
title_text = l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_DOWNLOAD_FAILED_TITLE, file_name);
break;
case download::DownloadItem::MAX_DOWNLOAD_STATE:
NOTREACHED_IN_MIGRATION();
}
return title_text;
}
std::u16string DownloadItemNotification::GetCommandLabel(
DownloadCommands::Command command) const {
int id = -1;
switch (command) {
case DownloadCommands::OPEN_WHEN_COMPLETE:
if (item_ && !item_->IsDone() &&
item_->GetDangerType() !=
download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING)
id = IDS_DOWNLOAD_NOTIFICATION_LABEL_OPEN_WHEN_COMPLETE;
else
id = IDS_DOWNLOAD_NOTIFICATION_LABEL_OPEN;
break;
case DownloadCommands::PAUSE:
// Only for non menu.
id = IDS_DOWNLOAD_LINK_PAUSE;
break;
case DownloadCommands::RESUME:
// Only for non menu.
id = IDS_DOWNLOAD_LINK_RESUME;
break;
case DownloadCommands::SHOW_IN_FOLDER:
return item_->GetShowInFolderText();
case DownloadCommands::DISCARD:
id = IDS_DISCARD_DOWNLOAD;
break;
case DownloadCommands::KEEP:
id = IDS_CONFIRM_DOWNLOAD;
break;
case DownloadCommands::CANCEL:
id = IDS_DOWNLOAD_LINK_CANCEL;
break;
case DownloadCommands::LEARN_MORE_SCANNING:
id = IDS_LEARN_MORE;
break;
case DownloadCommands::COPY_TO_CLIPBOARD:
id = IDS_DOWNLOAD_NOTIFICATION_COPY_TO_CLIPBOARD;
break;
case DownloadCommands::LEARN_MORE_INSECURE_DOWNLOAD:
id = IDS_LEARN_MORE;
break;
case DownloadCommands::DEEP_SCAN:
id = IDS_SCAN_DOWNLOAD;
break;
case DownloadCommands::REVIEW:
id = IDS_REVIEW_DOWNLOAD;
break;
#if BUILDFLAG(IS_CHROMEOS_ASH)
case DownloadCommands::OPEN_WITH_MEDIA_APP:
id = IDS_DOWNLOAD_NOTIFICATION_LABEL_OPEN;
break;
case DownloadCommands::EDIT_WITH_MEDIA_APP:
id = IDS_DOWNLOAD_NOTIFICATION_LABEL_OPEN_AND_EDIT;
break;
#else
case DownloadCommands::OPEN_WITH_MEDIA_APP:
case DownloadCommands::EDIT_WITH_MEDIA_APP:
NOTREACHED_IN_MIGRATION();
return std::u16string();
#endif
case DownloadCommands::ALWAYS_OPEN_TYPE:
case DownloadCommands::PLATFORM_OPEN:
case DownloadCommands::LEARN_MORE_INTERRUPTED:
case DownloadCommands::LEARN_MORE_DOWNLOAD_BLOCKED:
case DownloadCommands::OPEN_SAFE_BROWSING_SETTING:
case DownloadCommands::BYPASS_DEEP_SCANNING:
case DownloadCommands::BYPASS_DEEP_SCANNING_AND_OPEN:
case DownloadCommands::CANCEL_DEEP_SCAN:
case DownloadCommands::RETRY:
// Only for menu.
NOTREACHED_IN_MIGRATION();
return std::u16string();
}
CHECK_NE(id, -1);
return l10n_util::GetStringUTF16(id);
}
std::u16string DownloadItemNotification::GetWarningStatusString() const {
// Should only be called if IsDangerous() or IsInsecure().
DCHECK(item_->IsDangerous() || item_->IsInsecure());
std::u16string elided_filename =
item_->GetFileNameToReportUser().LossyDisplayName();
// If insecure, that warning is shown first.
if (item_->IsInsecure()) {
return l10n_util::GetStringFUTF16(IDS_PROMPT_DOWNLOAD_INSECURE_BLOCKED,
elided_filename);
}
switch (item_->GetDangerType()) {
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL: {
return l10n_util::GetStringUTF16(IDS_PROMPT_MALICIOUS_DOWNLOAD_URL);
}
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE: {
if (IsExtensionDownload(item_.get())) {
return l10n_util::GetStringUTF16(
IDS_PROMPT_DANGEROUS_DOWNLOAD_EXTENSION);
} else {
return l10n_util::GetStringFUTF16(IDS_PROMPT_DANGEROUS_DOWNLOAD,
elided_filename);
}
}
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_ACCOUNT_COMPROMISE: {
return l10n_util::GetStringFUTF16(IDS_PROMPT_MALICIOUS_DOWNLOAD_CONTENT,
elided_filename);
}
case download::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT: {
bool requests_ap_verdicts =
safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile(
profile())
->IsUnderAdvancedProtection();
return l10n_util::GetStringFUTF16(
requests_ap_verdicts
? IDS_PROMPT_UNCOMMON_DOWNLOAD_CONTENT_IN_ADVANCED_PROTECTION
: IDS_PROMPT_UNCOMMON_DOWNLOAD_CONTENT,
elided_filename);
}
case download::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED: {
return l10n_util::GetStringFUTF16(IDS_PROMPT_DOWNLOAD_CHANGES_SETTINGS,
elided_filename);
}
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_TOO_LARGE: {
return l10n_util::GetStringFUTF16(IDS_PROMPT_DOWNLOAD_BLOCKED_TOO_LARGE,
elided_filename);
}
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_PASSWORD_PROTECTED: {
return l10n_util::GetStringFUTF16(
IDS_PROMPT_DOWNLOAD_BLOCKED_PASSWORD_PROTECTED, elided_filename);
}
case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING: {
return l10n_util::GetStringUTF16(
IDS_PROMPT_DOWNLOAD_SENSITIVE_CONTENT_WARNING);
}
case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK: {
return l10n_util::GetStringUTF16(
IDS_PROMPT_DOWNLOAD_SENSITIVE_CONTENT_BLOCKED);
}
case download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING: {
return l10n_util::GetStringFUTF16(IDS_PROMPT_DEEP_SCANNING,
elided_filename);
}
case download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_LOCAL_PASSWORD_SCANNING:
case download::DOWNLOAD_DANGER_TYPE_ASYNC_LOCAL_PASSWORD_SCANNING: {
// TODO(crbug.com/40074456): Implement UX for this danger type.
DUMP_WILL_BE_NOTREACHED();
break;
}
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_SCAN_FAILED: {
return l10n_util::GetStringUTF16(IDS_PROMPT_DOWNLOAD_BLOCKED_SCAN_FAILED);
}
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_FAILED:
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE:
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS:
case download::DOWNLOAD_DANGER_TYPE_ASYNC_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;
}
}
NOTREACHED_IN_MIGRATION();
return std::u16string();
}
std::u16string DownloadItemNotification::GetInProgressSubStatusString() const {
// "Paused"
if (item_->IsPaused())
return l10n_util::GetStringUTF16(IDS_DOWNLOAD_PROGRESS_PAUSED);
// "In progress" (scanning)
if (IsScanning())
return l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_IN_PROGRESS_SHORT);
base::TimeDelta time_remaining;
// time_remaining is only known if the download isn't paused.
bool time_remaining_known =
(!item_->IsPaused() && item_->TimeRemaining(&time_remaining));
// A download scheduled to be opened when complete.
if (item_->GetOpenWhenComplete()) {
// "Opening when complete"
if (!time_remaining_known)
return l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_OPEN_WHEN_COMPLETE);
// "Opening in 10 secs"
return l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_OPEN_IN,
ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_DURATION,
ui::TimeFormat::LENGTH_SHORT, time_remaining));
}
// In progress download with known time left: "10 secs left"
if (time_remaining_known) {
return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_REMAINING,
ui::TimeFormat::LENGTH_SHORT, time_remaining);
}
// "In progress"
if (item_->GetCompletedBytes() > 0)
return l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_IN_PROGRESS_SHORT);
// "Starting..."
return l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING);
}
std::u16string DownloadItemNotification::GetSubStatusString() const {
if (item_->IsInsecure() || item_->IsDangerous())
return GetWarningStatusString();
if (item_->GetDangerType() ==
download::DownloadDangerType::
DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS) {
return l10n_util::GetStringUTF16(
IDS_PROMPT_DOWNLOAD_DEEP_SCANNED_OPENED_DANGEROUS);
}
switch (item_->GetState()) {
case download::DownloadItem::IN_PROGRESS:
// The download is a CRX (app, extension, theme, ...) and it is being
// unpacked and validated.
if (item_->AllDataSaved() && IsExtensionDownload(item_.get())) {
return l10n_util::GetStringUTF16(
IDS_DOWNLOAD_STATUS_CRX_INSTALL_RUNNING);
} else {
return GetInProgressSubStatusString();
}
case download::DownloadItem::COMPLETE: {
if (item_->GetFileExternallyRemoved()) {
// If the file has been removed: "Removed"
return l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_REMOVED);
} else {
std::u16string file_name =
item_->GetFileNameToReportUser().LossyDisplayName();
base::i18n::AdjustStringForLocaleDirection(&file_name);
return file_name;
}
}
case download::DownloadItem::INTERRUPTED: {
FailState fail_state = item_->GetLastFailState();
if (fail_state != FailState::USER_CANCELED) {
const auto interrupt_text = item_->GetInterruptDescription();
DCHECK(!interrupt_text.empty());
return interrupt_text;
}
[[fallthrough]]; // Same as download::DownloadItem::CANCELLED.
}
case download::DownloadItem::CANCELLED:
// "Cancelled"
return l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_CANCELLED);
default:
NOTREACHED_IN_MIGRATION();
}
return std::u16string();
}
std::u16string DownloadItemNotification::GetStatusString() const {
if (item_->IsDangerous() || item_->IsInsecure())
return std::u16string();
if (IsScanning()) {
return l10n_util::GetStringFUTF16(
IDS_PROMPT_DEEP_SCANNING_APP_DOWNLOAD,
item_->GetFileNameToReportUser().LossyDisplayName());
}
// The hostname. (E.g.:"example.com" or "127.0.0.1")
std::u16string host_name = url_formatter::FormatUrlForSecurityDisplay(
item_->GetURL(), url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS);
bool show_size_ratio = true;
switch (item_->GetState()) {
case download::DownloadItem::IN_PROGRESS:
// The download is a CRX (app, extension, theme, ...) and it is being
// unpacked and validated.
if (item_->AllDataSaved() && IsExtensionDownload(item_.get())) {
show_size_ratio = false;
}
break;
case download::DownloadItem::COMPLETE:
// If the file has been removed: Removed
if (item_->GetFileExternallyRemoved()) {
show_size_ratio = false;
} else {
// Otherwise, the download should be completed.
// "3.4 MB from example.com"
std::u16string size = ui::FormatBytes(item_->GetCompletedBytes());
return l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_NOTIFICATION_STATUS_COMPLETED, size, host_name);
}
break;
default:
break;
}
// Indication of progress (E.g.:"100/200 MB" or "100 MB"), or just the
// received bytes if the |show_size_ratio| flag is false.
std::u16string size = show_size_ratio
? item_->GetProgressSizesString()
: ui::FormatBytes(item_->GetCompletedBytes());
return l10n_util::GetStringFUTF16(IDS_DOWNLOAD_NOTIFICATION_STATUS_SHORT,
size, host_name);
}
bool DownloadItemNotification::IsScanning() const {
return item_ && item_->GetState() == download::DownloadItem::IN_PROGRESS &&
item_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING;
}
bool DownloadItemNotification::AllowedToOpenWhileScanning() const {
auto* service =
enterprise_connectors::ConnectorsServiceFactory::GetForBrowserContext(
profile());
return !service ||
!service->DelayUntilVerdict(
enterprise_connectors::AnalysisConnector::FILE_DOWNLOADED);
}
Browser* DownloadItemNotification::GetBrowser() const {
chrome::ScopedTabbedBrowserDisplayer browser_displayer(profile());
DCHECK(browser_displayer.browser());
return browser_displayer.browser();
}
Profile* DownloadItemNotification::profile() const {
return profile_;
}