blob: 80c1490a2f5481553fd4b886349952c04fd10718 [file] [log] [blame]
// Copyright (c) 2012 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/views/download/download_item_view.h"
#include <stddef.h>
#include <algorithm>
#include <cmath>
#include <numeric>
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/i18n/rtl.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/math_constants.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/util/ranges/algorithm.h"
#include "base/util/ranges/functional.h"
#include "build/build_config.h"
#include "cc/paint/paint_flags.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_commands.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_stats.h"
#include "chrome/browser/download/drag_download_item.h"
#include "chrome/browser/enterprise/connectors/connectors_manager.h"
#include "chrome/browser/icon_loader.h"
#include "chrome/browser/profiles/profile.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/safe_browsing/download_protection/download_feedback_service.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/tab_modal_confirm_dialog.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/download/download_shelf_context_menu_view.h"
#include "chrome/browser/ui/views/download/download_shelf_view.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/safe_browsing/deep_scanning_modal_dialog.h"
#include "chrome/browser/ui/views/safe_browsing/prompt_for_scanning_modal_dialog.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_item.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/buildflags.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/safe_browsing/core/features.h"
#include "components/url_formatter/elide_url.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/download_item_utils.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkScalar.h"
#include "ui/accessibility/ax_node_data.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/base/theme_provider.h"
#include "ui/events/event.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/range/range.h"
#include "ui/gfx/skia_util.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/flood_fill_ink_drop_ripple.h"
#include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/mouse_constants.h"
#include "ui/views/style/typography.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"
using download::DownloadItem;
using MixedContentStatus = download::DownloadItem::MixedContentStatus;
enum class DownloadItemView::Mode {
kNormal, // Showing download item.
kDangerous, // Displaying the dangerous download warning.
kMalicious, // Displaying the malicious download warning.
kMixedContentWarn, // Displaying the mixed-content download warning.
kMixedContentBlock, // Displaying the mixed-content download block error.
kDeepScanning, // Displaying in-progress deep scanning information.
};
namespace {
// Size of the space used for the progress indicator.
constexpr int kProgressIndicatorSize = 25;
// The vertical distance between the item's visual upper bound (as delineated
// by the separator on the right) and the edge of the shelf.
constexpr int kTopBottomPadding = 6;
// The minimum vertical padding above and below contents of the download item.
// This is only used when the text size is large.
constexpr int kMinimumVerticalPadding = 2 + kTopBottomPadding;
// The normal height of the item which may be exceeded if text is large.
constexpr int kDefaultDownloadItemHeight = 48;
// Amount of time between accessible alert events.
constexpr auto kAccessibleAlertInterval = base::TimeDelta::FromSeconds(30);
// The size of the file icon.
constexpr int kFileIconSize = 24;
// The offset from the file icon to the danger icon.
constexpr int kDangerIconOffset = 8;
// The separator is drawn as a border. It's one dp wide.
class SeparatorBorder : public views::Border {
public:
explicit SeparatorBorder(SkColor separator_color)
: views::Border(separator_color) {}
~SeparatorBorder() override {}
void Paint(const views::View& view, gfx::Canvas* canvas) override {
// The FocusRing replaces the separator border when we have focus.
if (view.HasFocus())
return;
int end_x = base::i18n::IsRTL() ? 0 : view.width() - 1;
canvas->DrawLine(gfx::Point(end_x, kTopBottomPadding),
gfx::Point(end_x, view.height() - kTopBottomPadding),
color());
}
gfx::Insets GetInsets() const override { return gfx::Insets(0, 0, 0, 1); }
gfx::Size GetMinimumSize() const override {
return gfx::Size(1, 2 * kTopBottomPadding + 1);
}
DISALLOW_COPY_AND_ASSIGN(SeparatorBorder);
};
// A stub subclass of Button that has no visuals.
class TransparentButton : public views::Button {
public:
explicit TransparentButton(views::ButtonListener* listener)
: Button(listener) {
SetFocusForPlatform();
views::InstallRectHighlightPathGenerator(this);
SetInkDropMode(InkDropMode::ON);
}
~TransparentButton() override {}
// Button subclasses need to provide this because the default color is
// kPlaceholderColor. In theory we could statically compute it in the
// constructor but then it won't be correct after dark mode changes, and to
// deal with that this class would have to observe NativeTheme and so on.
SkColor GetInkDropBaseColor() const override {
// This button will be used like a LabelButton, so use the same foreground
// base color as a label button.
return color_utils::DeriveDefaultIconColor(GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_LabelEnabledColor));
}
const char* GetClassName() const override { return "TransparentButton"; }
// Forward dragging and capture loss events, since this class doesn't have
// enough context to handle them. Let the button class manage visual
// transitions.
bool OnMouseDragged(const ui::MouseEvent& event) override {
Button::OnMouseDragged(event);
return parent()->OnMouseDragged(event);
}
void OnMouseCaptureLost() override {
parent()->OnMouseCaptureLost();
Button::OnMouseCaptureLost();
}
base::string16 GetTooltipText(const gfx::Point& point) const override {
return parent()->GetTooltipText(point);
}
};
int GetFilenameStyle(const views::StyledLabel& label) {
#if !defined(OS_LINUX)
if (base::FeatureList::IsEnabled(safe_browsing::kUseNewDownloadWarnings))
return STYLE_EMPHASIZED;
#endif
return label.GetDefaultTextStyle();
}
void StyleFilename(views::StyledLabel& label, size_t pos, size_t len) {
// Ensure the label contains a nonempty filename.
if ((pos == base::string16::npos) || (len == 0))
return;
views::StyledLabel::RangeStyleInfo style;
style.text_style = GetFilenameStyle(label);
label.ClearStyleRanges();
label.AddStyleRange(gfx::Range(pos, pos + len), style);
}
// Whether we are warning about a dangerous/malicious download.
bool is_download_warning(DownloadItemView::Mode mode) {
return (mode == DownloadItemView::Mode::kDangerous) ||
(mode == DownloadItemView::Mode::kMalicious);
}
// Whether we are in the mixed content mode.
bool is_mixed_content(DownloadItemView::Mode mode) {
return (mode == DownloadItemView::Mode::kMixedContentWarn) ||
(mode == DownloadItemView::Mode::kMixedContentBlock);
}
// Whether a warning label is visible.
bool has_warning_label(DownloadItemView::Mode mode) {
return is_download_warning(mode) || is_mixed_content(mode);
}
} // namespace
DownloadItemView::DownloadItemView(DownloadUIModel::DownloadUIModelPtr download,
DownloadShelfView* parent,
views::View* accessible_alert)
: AnimationDelegateViews(this),
shelf_(parent),
dropdown_state_(NORMAL),
mode_(Mode::kNormal),
dragging_(false),
model_(std::move(download)),
save_button_(nullptr),
discard_button_(nullptr),
dangerous_download_label_(nullptr),
dangerous_download_label_sized_(false),
creation_time_(base::Time::Now()),
time_download_warning_shown_(base::Time()),
accessible_alert_(accessible_alert),
announce_accessible_alert_soon_(false),
deep_scanning_label_(nullptr),
open_now_button_(nullptr),
scan_button_(nullptr) {
views::InstallRectHighlightPathGenerator(this);
model_->AddObserver(this);
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
font_list_ = rb.GetFontListWithDelta(1);
status_font_list_ = rb.GetFontListWithDelta(-2);
auto open_button = std::make_unique<TransparentButton>(this);
open_button->set_context_menu_controller(this);
open_button_ = AddChildView(std::move(open_button));
int file_name_style = views::style::STYLE_PRIMARY;
#if !defined(OS_LINUX)
if (base::FeatureList::IsEnabled(safe_browsing::kUseNewDownloadWarnings))
file_name_style = STYLE_EMPHASIZED;
#endif
auto file_name_label = std::make_unique<views::Label>(
ElidedFilename(), CONTEXT_DOWNLOAD_SHELF, file_name_style);
file_name_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
file_name_label->GetViewAccessibility().OverrideIsIgnored(true);
file_name_label_ = AddChildView(std::move(file_name_label));
auto status_label = std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING),
CONTEXT_DOWNLOAD_SHELF_STATUS);
status_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
status_label->GetViewAccessibility().OverrideIsIgnored(true);
status_label_ = AddChildView(std::move(status_label));
auto dropdown_button = views::CreateVectorImageButton(this);
dropdown_button->SetAccessibleName(l10n_util::GetStringUTF16(
IDS_DOWNLOAD_ITEM_DROPDOWN_BUTTON_ACCESSIBLE_TEXT));
dropdown_button->SetBorder(views::CreateEmptyBorder(gfx::Insets(10)));
dropdown_button->set_has_ink_drop_action_on_click(false);
dropdown_button->SetFocusForPlatform();
dropdown_button_ = AddChildView(std::move(dropdown_button));
LoadIcon();
OnDownloadUpdated();
SetDropdownState(NORMAL);
UpdateColorsFromTheme();
}
DownloadItemView::~DownloadItemView() {
StopDownloadProgress();
model_->RemoveObserver(this);
}
// In dangerous mode we have to layout our buttons.
void DownloadItemView::Layout() {
View::Layout();
UpdateColorsFromTheme();
open_button_->SetBoundsRect(GetLocalBounds());
if (is_download_warning(mode_)) {
gfx::Point child_origin(
kStartPadding + GetWarningIconSize() + kStartPadding,
(height() - dangerous_download_label_->height()) / 2);
dangerous_download_label_->SetPosition(child_origin);
child_origin.Offset(dangerous_download_label_->width() + kLabelPadding, 0);
gfx::Size button_size = GetButtonSize();
child_origin.set_y((height() - button_size.height()) / 2);
if (save_button_) {
save_button_->SetBoundsRect(gfx::Rect(child_origin, button_size));
child_origin.Offset(button_size.width() + kSaveDiscardButtonPadding, 0);
}
if (discard_button_)
discard_button_->SetBoundsRect(gfx::Rect(child_origin, button_size));
if (scan_button_)
scan_button_->SetBoundsRect(gfx::Rect(child_origin, button_size));
} else if (is_mixed_content(mode_)) {
gfx::Point child_origin(
kStartPadding + GetWarningIconSize() + kStartPadding,
(height() - dangerous_download_label_->height()) / 2);
dangerous_download_label_->SetPosition(child_origin);
child_origin.Offset(dangerous_download_label_->width() + kLabelPadding, 0);
gfx::Size button_size = GetButtonSize();
child_origin.set_y((height() - button_size.height()) / 2);
if (save_button_)
save_button_->SetBoundsRect(gfx::Rect(child_origin, button_size));
if (discard_button_)
discard_button_->SetBoundsRect(gfx::Rect(child_origin, button_size));
} else if (mode_ == Mode::kDeepScanning) {
gfx::Point child_origin(
kStartPadding + GetWarningIconSize() + kStartPadding,
(height() - deep_scanning_label_->height()) / 2);
deep_scanning_label_->SetPosition(child_origin);
if (open_now_button_) {
child_origin.set_y(
(height() - open_now_button_->GetPreferredSize().height()) / 2);
child_origin.Offset(deep_scanning_label_->width() + kLabelPadding, 0);
open_now_button_->SetBoundsRect(
gfx::Rect(child_origin, open_now_button_->GetPreferredSize()));
}
} else {
int mirrored_x = GetMirroredXWithWidthInView(
kStartPadding + kProgressIndicatorSize + kProgressTextPadding,
kTextWidth);
int file_name_y = GetYForFilenameText();
file_name_label_->SetBoundsRect(
gfx::Rect(mirrored_x, file_name_y, kTextWidth, font_list_.GetHeight()));
int status_y = file_name_y + font_list_.GetHeight();
bool should_expand_for_status_text =
(model_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE);
int status_width = should_expand_for_status_text
? status_label_->GetPreferredSize().width()
: kTextWidth;
status_label_->SetBoundsRect(gfx::Rect(mirrored_x, status_y, status_width,
status_font_list_.GetHeight()));
}
if (mode_ != Mode::kDangerous) {
dropdown_button_->SizeToPreferredSize();
dropdown_button_->SetPosition(
gfx::Point(width() - dropdown_button_->width() - kEndPadding,
(height() - dropdown_button_->height()) / 2));
}
}
bool DownloadItemView::OnMouseDragged(const ui::MouseEvent& event) {
// Handle drag (file copy) operations.
// Mouse should not activate us in dangerous mode.
if (has_warning_label(mode_))
return true;
if (!drag_start_point_)
drag_start_point_ = event.location();
if (!dragging_) {
dragging_ = ExceededDragThreshold(event.location() - *drag_start_point_);
} else if ((model_->GetState() == DownloadItem::COMPLETE) &&
model_->download()) {
const gfx::Image* const file_icon =
g_browser_process->icon_manager()->LookupIconFromFilepath(
model_->GetTargetFilePath(), IconLoader::SMALL);
const views::Widget* const widget = GetWidget();
// TODO(shaktisahu): Make DragDownloadItem work with a model.
DragDownloadItem(model_->download(), file_icon,
widget ? widget->GetNativeView() : nullptr);
RecordDownloadShelfDragEvent(DownloadShelfDragEvent::STARTED);
}
return true;
}
void DownloadItemView::OnMouseCaptureLost() {
// Mouse should not activate us in dangerous mode.
if (mode_ != Mode::kNormal)
return;
if (dragging_) {
// Starting a drag results in a MouseCaptureLost.
dragging_ = false;
drag_start_point_.reset();
}
}
base::string16 DownloadItemView::GetTooltipText(const gfx::Point& p) const {
return has_warning_label(mode_) ? base::string16() : tooltip_text_;
}
void DownloadItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->SetName(accessible_name_);
node_data->role = ax::mojom::Role::kGroup;
// Set the description to the empty string, otherwise the tooltip will be
// used, which is redundant with the accessible name.
node_data->SetDescription(base::string16());
}
void DownloadItemView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
if (sender == open_now_button_) {
OpenDownloadDuringAsyncScanning();
return;
}
if (sender == dropdown_button_) {
SetDropdownState(PUSHED);
ShowContextMenuImpl(dropdown_button_->GetBoundsInScreen(),
ui::GetMenuSourceTypeForEvent(event));
return;
}
base::TimeDelta warning_duration;
if (!time_download_warning_shown_.is_null())
warning_duration = base::Time::Now() - time_download_warning_shown_;
if (mode_ == Mode::kMixedContentWarn && sender == save_button_) {
DownloadCommands(model_.get()).ExecuteCommand(DownloadCommands::KEEP);
return;
}
if (mode_ == Mode::kDangerous && sender == save_button_) {
// The user has confirmed a dangerous download. We'd record how quickly the
// user did this to detect whether we're being clickjacked.
UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download", warning_duration);
// This will call ValidateDangerousDownload(), change download state and
// notify us.
MaybeSubmitDownloadToFeedbackService(DownloadCommands::KEEP);
return;
}
if (sender == open_button_) {
if (mode_ == Mode::kDeepScanning) {
content::WebContents* current_web_contents =
shelf_->browser()->tab_strip_model()->GetActiveWebContents();
open_now_modal_dialog_ = TabModalConfirmDialog::Create(
std::make_unique<safe_browsing::DeepScanningModalDialog>(
current_web_contents,
base::BindOnce(&DownloadItemView::OpenDownloadDuringAsyncScanning,
weak_ptr_factory_.GetWeakPtr())),
current_web_contents);
return;
}
if (model_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING) {
content::WebContents* current_web_contents =
shelf_->browser()->tab_strip_model()->GetActiveWebContents();
safe_browsing::PromptForScanningModalDialog::ShowForWebContents(
current_web_contents, ElidedFilename(),
base::BindOnce(&DownloadItemView::ConfirmDeepScanning,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&DownloadItemView::BypassDeepScanning,
weak_ptr_factory_.GetWeakPtr()));
return;
}
if (has_warning_label(mode_))
return;
if (complete_animation_.get() && complete_animation_->is_animating())
complete_animation_->End();
OpenDownload();
return;
}
if (sender == scan_button_) {
ConfirmDeepScanning();
return;
}
DCHECK_EQ(discard_button_, sender);
if (is_mixed_content(mode_)) {
DownloadCommands(model_.get()).ExecuteCommand(DownloadCommands::DISCARD);
return;
}
UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download", warning_duration);
MaybeSubmitDownloadToFeedbackService(DownloadCommands::DISCARD);
// WARNING: 'this' maybe deleted at this point. Don't access 'this'.
}
void DownloadItemView::ShowContextMenuForViewImpl(
View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) {
ShowContextMenuImpl(gfx::Rect(point, gfx::Size()), source_type);
}
// Update the progress graphic on the icon and our text status label
// to reflect our current bytes downloaded, time remaining.
// Also updates the accessible status view for screen reader users.
void DownloadItemView::OnDownloadUpdated() {
if (!model_->ShouldShowInShelf()) {
shelf_->RemoveDownloadView(this); // This will delete us!
return;
}
bool is_danger_type_async_scanning =
(model_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING);
if (model_->IsMixedContent()) {
if (!is_mixed_content(mode_))
TransitionToMixedContentDialog();
} else if (model_->IsDangerous() &&
model_->GetState() != DownloadItem::CANCELLED) {
if (!is_download_warning(mode_))
TransitionToWarningDialog();
} else if (is_danger_type_async_scanning &&
model_->GetState() != DownloadItem::CANCELLED) {
if (mode_ != Mode::kDeepScanning)
TransitionToDeepScanningDialog();
} else {
TransitionToNormalMode();
}
if (model_->GetState() == DownloadItem::COMPLETE &&
model_->ShouldRemoveFromShelfWhenComplete()) {
shelf_->RemoveDownloadView(this); // This will delete us!
return;
}
base::string16 new_tip = model_->GetTooltipText();
if (new_tip != tooltip_text_) {
tooltip_text_ = new_tip;
TooltipTextChanged();
}
UpdateAccessibleName();
}
void DownloadItemView::OnDownloadOpened() {
// First, Calculate the download status opening string width.
base::string16 status_string =
l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING, base::string16());
int status_string_width = gfx::GetStringWidth(status_string, font_list_);
// Then, elide the file name.
base::string16 filename_string =
gfx::ElideFilename(model_->GetFileNameToReportUser(), font_list_,
kTextWidth - status_string_width);
// Last, concat the whole string to be set on the label.
file_name_label_->SetText(
l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING, filename_string));
SetEnabled(false);
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DownloadItemView::Reenable,
weak_ptr_factory_.GetWeakPtr()),
base::TimeDelta::FromSeconds(3));
// Notify our parent.
shelf_->AutoClose();
}
void DownloadItemView::OnDownloadDestroyed() {
shelf_->RemoveDownloadView(this); // This will delete us!
}
void DownloadItemView::AnimationProgressed(const gfx::Animation* animation) {
// We don't care if what animation (body button/drop button/complete),
// is calling back, as they all have to go through the same paint call.
SchedulePaint();
}
void DownloadItemView::MaybeSubmitDownloadToFeedbackService(
DownloadCommands::Command download_command) {
if (model_->ShouldAllowDownloadFeedback() &&
SubmitDownloadToFeedbackService(download_command)) {
} else {
DownloadCommands(model_.get()).ExecuteCommand(download_command);
}
}
gfx::Size DownloadItemView::CalculatePreferredSize() const {
int width = 0;
// We set the height to the height of two rows or text plus margins.
int child_height = font_list_.GetHeight() + status_font_list_.GetHeight();
if (has_warning_label(mode_)) {
// Width.
width = kStartPadding + GetWarningIconSize() + kStartPadding +
dangerous_download_label_->width() + kLabelPadding;
gfx::Size button_size = GetButtonSize();
if (save_button_ && discard_button_)
width += button_size.width() + kSaveDiscardButtonPadding;
width += button_size.width() + kEndPadding;
// Height: make sure the button fits and the warning icon fits.
child_height =
std::max({child_height, button_size.height(), GetWarningIconSize()});
} else if (mode_ == Mode::kDeepScanning) {
width = kStartPadding + GetWarningIconSize() + kStartPadding +
deep_scanning_label_->width() + kLabelPadding;
if (open_now_button_) {
width += open_now_button_->GetPreferredSize().width();
// Height: make sure the button fits and the warning icon fits.
child_height =
std::max({child_height, open_now_button_->GetPreferredSize().height(),
GetWarningIconSize()});
width += kEndPadding;
}
} else {
int status_width = kTextWidth;
if (model_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE) {
status_width =
std::max(status_width, status_label_->GetPreferredSize().width());
}
width = kStartPadding + kProgressIndicatorSize + kProgressTextPadding +
status_width + kEndPadding;
}
if (mode_ != Mode::kDangerous)
width += dropdown_button_->GetPreferredSize().width();
return gfx::Size(width, std::max(kDefaultDownloadItemHeight,
2 * kMinimumVerticalPadding + child_height));
}
void DownloadItemView::OnPaint(gfx::Canvas* canvas) {
// Make sure to draw |this| opaquely. Since the toolbar color can be partially
// transparent, start with a black backdrop (which is the default initialized
// color for opaque canvases).
canvas->DrawColor(SK_ColorBLACK);
canvas->DrawColor(
GetThemeProvider()->GetColor(ThemeProperties::COLOR_DOWNLOAD_SHELF));
DrawIcon(canvas);
OnPaintBorder(canvas);
}
void DownloadItemView::OnThemeChanged() {
views::View::OnThemeChanged();
UpdateColorsFromTheme();
SchedulePaint();
UpdateDropdownButton();
}
void DownloadItemView::OpenDownload() {
DCHECK(!is_download_warning(mode_));
// We're interested in how long it takes users to open downloads. If they
// open downloads super quickly, we should be concerned about clickjacking.
UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
base::Time::Now() - creation_time_);
// If this is still around for the next status update, it will be read.
announce_accessible_alert_soon_ = true;
// Calling download()->OpenDownload may delete this, so this must be
// the last thing we do.
model_->OpenDownload();
}
bool DownloadItemView::SubmitDownloadToFeedbackService(
DownloadCommands::Command download_command) {
#if BUILDFLAG(FULL_SAFE_BROWSING)
safe_browsing::SafeBrowsingService* sb_service =
g_browser_process->safe_browsing_service();
if (!sb_service)
return false;
safe_browsing::DownloadProtectionService* download_protection_service =
sb_service->download_protection_service();
if (!download_protection_service)
return false;
// TODO(shaktisahu): Enable feedback service for offline item.
if (model_->download()) {
return download_protection_service->MaybeBeginFeedbackForDownload(
shelf_->browser()->profile(), model_->download(), download_command);
}
// WARNING: we are deleted at this point. Don't access 'this'.
return true;
#else
NOTREACHED();
return false;
#endif
}
int DownloadItemView::GetYForFilenameText() const {
int text_height = font_list_.GetHeight();
if (!status_label_->GetText().empty())
text_height += status_font_list_.GetHeight();
return (height() - text_height) / 2;
}
void DownloadItemView::DrawIcon(gfx::Canvas* canvas) {
bool use_new_warnings =
base::FeatureList::IsEnabled(safe_browsing::kUseNewDownloadWarnings);
bool show_warning_icon = mode_ != Mode::kNormal;
if (show_warning_icon && !use_new_warnings) {
int icon_x =
(base::i18n::IsRTL() ? width() - GetWarningIconSize() - kStartPadding
: kStartPadding);
int icon_y = (height() - GetWarningIconSize()) / 2;
canvas->DrawImageInt(GetWarningIcon(), icon_x, icon_y);
return;
}
// Paint download progress.
DownloadItem::DownloadState state = model_->GetState();
int progress_x = base::i18n::IsRTL()
? width() - kStartPadding - kProgressIndicatorSize
: kStartPadding;
int progress_y = (height() - kProgressIndicatorSize) / 2;
const gfx::RectF progress_bounds(
progress_x, progress_y, kProgressIndicatorSize, kProgressIndicatorSize);
const gfx::ImageSkia* current_icon = nullptr;
IconManager* im = g_browser_process->icon_manager();
gfx::Image* image_ptr = im->LookupIconFromFilepath(
model_->GetTargetFilePath(), IconLoader::SMALL);
if (image_ptr)
current_icon = image_ptr->ToImageSkia();
if (state == DownloadItem::IN_PROGRESS &&
!(use_new_warnings && show_warning_icon)) {
base::TimeDelta progress_time = previous_progress_elapsed_;
if (!model_->IsPaused())
progress_time += base::TimeTicks::Now() - progress_start_time_;
PaintDownloadProgress(canvas, progress_bounds, progress_time,
model_->PercentComplete());
} else if (complete_animation_.get() && complete_animation_->is_animating()) {
DCHECK_EQ(Mode::kNormal, mode_);
// Loop back and forth five times.
double start = 0, end = 5;
if (model_->GetState() == download::DownloadItem::INTERRUPTED)
std::swap(start, end);
const double value = gfx::Tween::DoubleValueBetween(
complete_animation_->GetCurrentValue(), start, end);
const double opacity = std::sin((value + 0.5) * base::kPiDouble) / 2 + 0.5;
canvas->SaveLayerAlpha(
static_cast<uint8_t>(gfx::Tween::IntValueBetween(opacity, 0, 255)));
PaintDownloadProgress(canvas, progress_bounds, base::TimeDelta(), 100);
canvas->Restore();
} else if (use_new_warnings) {
current_icon = &icon_;
}
if (!current_icon)
return;
// Draw the icon image.
int kFiletypeIconOffset =
(kProgressIndicatorSize - current_icon->height()) / 2;
int icon_x = progress_x + kFiletypeIconOffset;
int icon_y = progress_y + kFiletypeIconOffset;
cc::PaintFlags flags;
// Use an alpha to make the image look disabled.
if (!GetEnabled())
flags.setAlpha(120);
canvas->DrawImageInt(*current_icon, icon_x, icon_y, flags);
// Overlay the danger icon if appropriate.
if (show_warning_icon && use_new_warnings) {
int icon_x =
(base::i18n::IsRTL() ? width() - GetWarningIconSize() - kStartPadding
: kStartPadding) +
kDangerIconOffset;
int icon_y = (height() - GetWarningIconSize()) / 2 + kDangerIconOffset;
canvas->DrawImageInt(GetWarningIcon(), icon_x, icon_y);
}
}
void DownloadItemView::LoadIcon() {
IconManager* im = g_browser_process->icon_manager();
last_download_item_path_ = model_->GetTargetFilePath();
im->LoadIcon(
last_download_item_path_, IconLoader::SMALL,
base::BindOnce(&DownloadItemView::OnExtractIconComplete,
base::Unretained(this), IconLoader::IconSize::SMALL),
&cancelable_task_tracker_);
im->LoadIcon(last_download_item_path_, IconLoader::NORMAL,
base::BindOnce(&DownloadItemView::OnExtractIconComplete,
base::Unretained(this), IconLoader::NORMAL),
&cancelable_task_tracker_);
}
void DownloadItemView::LoadIconIfItemPathChanged() {
base::FilePath current_download_path = model_->GetTargetFilePath();
if (last_download_item_path_ == current_download_path)
return;
LoadIcon();
}
void DownloadItemView::UpdateColorsFromTheme() {
if (!GetThemeProvider())
return;
open_button_->SetBorder(
std::make_unique<SeparatorBorder>(GetThemeProvider()->GetColor(
ThemeProperties::COLOR_TOOLBAR_VERTICAL_SEPARATOR)));
file_name_label_->SetTextStyle(GetEnabled() ? views::style::STYLE_PRIMARY
: views::style::STYLE_DISABLED);
if (model_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE) {
status_label_->SetTextStyle(STYLE_GREEN);
} else if (model_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS) {
status_label_->SetTextStyle(STYLE_RED);
} else {
status_label_->SetTextStyle(views::style::STYLE_PRIMARY);
}
SkColor background_color =
GetThemeProvider()->GetColor(ThemeProperties::COLOR_DOWNLOAD_SHELF);
file_name_label_->SetBackgroundColor(background_color);
status_label_->SetBackgroundColor(background_color);
if (save_button_)
shelf_->ConfigureButtonForTheme(save_button_);
if (discard_button_)
shelf_->ConfigureButtonForTheme(discard_button_);
if (open_now_button_)
shelf_->ConfigureButtonForTheme(open_now_button_);
if (scan_button_)
shelf_->ConfigureButtonForTheme(scan_button_);
}
void DownloadItemView::UpdateDropdownButton() {
views::SetImageFromVectorIcon(
dropdown_button_,
dropdown_state_ == PUSHED ? kCaretDownIcon : kCaretUpIcon,
GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT));
}
void DownloadItemView::ShowContextMenuImpl(const gfx::Rect& rect,
ui::MenuSourceType source_type) {
// Similar hack as in MenuButton.
// We're about to show the menu from a mouse press. By showing from the
// mouse press event we block RootView in mouse dispatching. This also
// appears to cause RootView to get a mouse pressed BEFORE the mouse
// release is seen, which means RootView sends us another mouse press no
// matter where the user pressed. To force RootView to recalculate the
// mouse target during the mouse press we explicitly set the mouse handler
// to null.
static_cast<views::internal::RootView*>(GetWidget()->GetRootView())
->SetMouseHandler(nullptr);
if (!context_menu_.get())
context_menu_ = std::make_unique<DownloadShelfContextMenuView>(this);
context_menu_->Run(GetWidget()->GetTopLevelWidget(), rect, source_type,
base::Bind(&DownloadItemView::ReleaseDropdown,
weak_ptr_factory_.GetWeakPtr()));
}
void DownloadItemView::SetDropdownState(State new_state) {
if (new_state != dropdown_state_) {
dropdown_button_->AnimateInkDrop(new_state == PUSHED
? views::InkDropState::ACTIVATED
: views::InkDropState::DEACTIVATED,
nullptr);
dropdown_state_ = new_state;
UpdateDropdownButton();
SchedulePaint();
}
}
void DownloadItemView::SetMode(Mode mode) {
mode_ = mode;
}
void DownloadItemView::TransitionToNormalMode() {
if (mode_ == Mode::kDeepScanning)
ClearDeepScanningDialog();
if (is_download_warning(mode_))
ClearWarningDialog();
if (is_mixed_content(mode_))
ClearMixedContentDialog();
status_label_->SetText(GetStatusText());
status_label_->GetViewAccessibility().OverrideIsIgnored(
status_label_->GetText().empty());
file_name_label_->SetY(GetYForFilenameText());
switch (model_->GetState()) {
case DownloadItem::IN_PROGRESS:
// No need to send accessible alert for "paused", as the button ends
// up being refocused in the actual use case, and the name of the
// button reports that the download has been paused.
// Reset the status counter so that user receives immediate feedback
// once the download is resumed.
if (!model_->IsPaused())
UpdateAccessibleAlert(GetInProgressAccessibleAlertText(), false);
model_->IsPaused() ? StopDownloadProgress() : StartDownloadProgress();
LoadIconIfItemPathChanged();
break;
case DownloadItem::INTERRUPTED:
model_->GetFileNameToReportUser().LossyDisplayName();
UpdateAccessibleAlert(
l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_FAILED_ACCESSIBLE_ALERT,
model_->GetFileNameToReportUser().LossyDisplayName()),
true);
StopDownloadProgress();
complete_animation_ = std::make_unique<gfx::SlideAnimation>(this);
complete_animation_->SetSlideDuration(
base::TimeDelta::FromMilliseconds(2500));
complete_animation_->SetTweenType(gfx::Tween::LINEAR);
complete_animation_->Show();
LoadIcon();
break;
case DownloadItem::COMPLETE:
UpdateAccessibleAlert(
l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_COMPLETE_ACCESSIBLE_ALERT,
model_->GetFileNameToReportUser().LossyDisplayName()),
true);
StopDownloadProgress();
complete_animation_ = std::make_unique<gfx::SlideAnimation>(this);
complete_animation_->SetSlideDuration(
base::TimeDelta::FromMilliseconds(2500));
complete_animation_->SetTweenType(gfx::Tween::LINEAR);
complete_animation_->Show();
LoadIcon();
break;
case DownloadItem::CANCELLED:
UpdateAccessibleAlert(
l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_CANCELLED_ACCESSIBLE_ALERT,
model_->GetFileNameToReportUser().LossyDisplayName()),
true);
StopDownloadProgress();
if (complete_animation_)
complete_animation_->Stop();
LoadIcon();
break;
default:
NOTREACHED();
}
// Force the shelf to layout as our size may have changed.
shelf_->Layout();
shelf_->SchedulePaint();
}
void DownloadItemView::TransitionToMixedContentDialog() {
if (is_mixed_content(mode_))
ClearMixedContentDialog();
if (mode_ == Mode::kDeepScanning)
ClearDeepScanningDialog();
ShowMixedContentDialog();
// We need to load the icon now that the download has the real path.
LoadIcon();
// Force the shelf to layout again as our size has changed.
shelf_->Layout();
shelf_->SchedulePaint();
}
void DownloadItemView::ClearMixedContentDialog() {
DCHECK(is_mixed_content(mode_));
SetMode(Mode::kNormal);
dropdown_state_ = NORMAL;
// Remove the views used by the mixed content dialog.
delete save_button_;
save_button_ = nullptr;
delete discard_button_;
discard_button_ = nullptr;
delete scan_button_;
scan_button_ = nullptr;
delete dangerous_download_label_;
dangerous_download_label_ = nullptr;
dangerous_download_label_sized_ = false;
// We need to load the icon now that the download has the real path.
LoadIcon();
open_button_->SetEnabled(true);
file_name_label_->SetVisible(true);
status_label_->SetVisible(true);
dropdown_button_->SetVisible(true);
}
void DownloadItemView::ShowMixedContentDialog() {
DCHECK(model_->IsMixedContent());
DCHECK(!is_mixed_content(mode_));
SetMode(model_->GetMixedContentStatus() == MixedContentStatus::WARN
? Mode::kMixedContentWarn
: Mode::kMixedContentBlock);
dropdown_state_ = NORMAL;
if (mode_ == Mode::kMixedContentWarn) {
auto save_button = views::MdTextButton::Create(
this, model_->GetWarningConfirmButtonText());
save_button_ = AddChildView(std::move(save_button));
} else {
auto discard_button = views::MdTextButton::Create(
this, l10n_util::GetStringUTF16(IDS_DISCARD_DOWNLOAD));
discard_button_ = AddChildView(std::move(discard_button));
}
const base::string16 filename = ElidedFilename();
size_t filename_offset;
auto dangerous_download_label = std::make_unique<views::StyledLabel>(
model_->GetWarningText(filename, &filename_offset), /*listener=*/nullptr);
dangerous_download_label->SetTextContext(CONTEXT_DOWNLOAD_SHELF);
dangerous_download_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
dangerous_download_label->SetAutoColorReadabilityEnabled(false);
dangerous_download_label->set_can_process_events_within_subtree(false);
dangerous_download_label_ = AddChildView(std::move(dangerous_download_label));
StyleFilename(*dangerous_download_label_, filename_offset, filename.length());
dangerous_download_label_->SizeToFit(
GetLabelWidth(*dangerous_download_label_));
open_button_->SetEnabled(false);
file_name_label_->SetVisible(false);
status_label_->SetVisible(false);
dropdown_button_->SetVisible(true);
}
void DownloadItemView::TransitionToWarningDialog() {
if (mode_ == Mode::kDeepScanning)
ClearDeepScanningDialog();
if (is_download_warning(mode_))
ClearWarningDialog();
ShowWarningDialog();
// We need to load the icon now that the download has the real path.
LoadIcon();
// Force the shelf to layout again as our size has changed.
shelf_->Layout();
shelf_->SchedulePaint();
}
void DownloadItemView::ClearWarningDialog() {
DCHECK(is_download_warning(mode_));
SetMode(Mode::kNormal);
dropdown_state_ = NORMAL;
// Remove the views used by the warning dialog.
delete save_button_;
save_button_ = nullptr;
delete discard_button_;
discard_button_ = nullptr;
delete scan_button_;
scan_button_ = nullptr;
delete dangerous_download_label_;
dangerous_download_label_ = nullptr;
dangerous_download_label_sized_ = false;
// We need to load the icon now that the download has the real path.
LoadIcon();
open_button_->SetEnabled(true);
file_name_label_->SetVisible(true);
status_label_->SetVisible(true);
dropdown_button_->SetVisible(true);
}
void DownloadItemView::ShowWarningDialog() {
DCHECK(!is_download_warning(mode_));
time_download_warning_shown_ = base::Time::Now();
download::DownloadDangerType danger_type = model_->GetDangerType();
RecordDangerousDownloadWarningShown(danger_type);
SetMode(model_->MightBeMalicious() ? Mode::kMalicious : Mode::kDangerous);
dropdown_state_ = NORMAL;
if (mode_ == Mode::kDangerous) {
auto save_button = views::MdTextButton::Create(
this, model_->GetWarningConfirmButtonText());
save_button_ = AddChildView(std::move(save_button));
}
const base::string16 unelided_filename =
model_->GetFileNameToReportUser().LossyDisplayName();
if (danger_type == download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING) {
auto scan_button = views::MdTextButton::Create(
this, l10n_util::GetStringUTF16(IDS_SCAN_DOWNLOAD));
scan_button_ = AddChildView(std::move(scan_button));
announce_accessible_alert_soon_ = true;
UpdateAccessibleAlert(
l10n_util::GetStringFUTF16(
IDS_PROMPT_APP_DEEP_SCANNING_ACCESSIBLE_ALERT, unelided_filename),
false);
} else {
if (!ChromeDownloadManagerDelegate::IsDangerTypeBlocked(danger_type)) {
auto discard_button = views::MdTextButton::Create(
this, l10n_util::GetStringUTF16(IDS_DISCARD_DOWNLOAD));
discard_button_ = AddChildView(std::move(discard_button));
}
size_t ignore;
UpdateAccessibleAlert(model_->GetWarningText(unelided_filename, &ignore),
true);
}
const base::string16 filename = ElidedFilename();
size_t filename_offset;
auto dangerous_download_label = std::make_unique<views::StyledLabel>(
model_->GetWarningText(filename, &filename_offset), /*listener=*/nullptr);
dangerous_download_label->SetTextContext(CONTEXT_DOWNLOAD_SHELF);
dangerous_download_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
dangerous_download_label->SetAutoColorReadabilityEnabled(false);
dangerous_download_label->set_can_process_events_within_subtree(false);
dangerous_download_label_ = AddChildView(std::move(dangerous_download_label));
StyleFilename(*dangerous_download_label_, filename_offset, filename.length());
dangerous_download_label_->SizeToFit(
GetLabelWidth(*dangerous_download_label_));
bool open_button_enabled =
(model_->GetDangerType() ==
download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING);
open_button_->SetEnabled(open_button_enabled);
file_name_label_->SetVisible(false);
status_label_->SetVisible(false);
dropdown_button_->SetVisible(mode_ == Mode::kMalicious);
}
void DownloadItemView::TransitionToDeepScanningDialog() {
if (is_download_warning(mode_))
ClearWarningDialog();
if (is_mixed_content(mode_))
ClearMixedContentDialog();
ShowDeepScanningDialog();
UpdateAccessibleAlert(
l10n_util::GetStringFUTF16(
IDS_DEEP_SCANNING_ACCESSIBLE_ALERT,
model_->GetFileNameToReportUser().LossyDisplayName()),
false);
// We need to load the icon now that the download has the real path.
LoadIcon();
// Force the shelf to layout again as our size has changed.
shelf_->Layout();
shelf_->SchedulePaint();
}
void DownloadItemView::ClearDeepScanningDialog() {
DCHECK_EQ(Mode::kDeepScanning, mode_);
SetMode(Mode::kNormal);
dropdown_state_ = NORMAL;
delete deep_scanning_label_;
deep_scanning_label_ = nullptr;
delete open_now_button_;
open_now_button_ = nullptr;
LoadIcon();
open_button_->SetEnabled(true);
file_name_label_->SetVisible(true);
status_label_->SetVisible(true);
}
void DownloadItemView::ShowDeepScanningDialog() {
DCHECK_EQ(mode_, Mode::kNormal);
SetMode(Mode::kDeepScanning);
const int id = (model_->download() &&
safe_browsing::DeepScanningRequest::ShouldUploadBinary(
model_->download()))
? IDS_PROMPT_DEEP_SCANNING_DOWNLOAD
: IDS_PROMPT_DEEP_SCANNING_APP_DOWNLOAD;
const base::string16 filename = ElidedFilename();
size_t filename_offset;
auto deep_scanning_label = std::make_unique<views::StyledLabel>(
l10n_util::GetStringFUTF16(id, filename, &filename_offset),
/*listener=*/nullptr);
deep_scanning_label->SetTextContext(CONTEXT_DOWNLOAD_SHELF);
deep_scanning_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
deep_scanning_label->SetAutoColorReadabilityEnabled(false);
deep_scanning_label->set_can_process_events_within_subtree(false);
deep_scanning_label_ = AddChildView(std::move(deep_scanning_label));
StyleFilename(*deep_scanning_label_, filename_offset, filename.length());
deep_scanning_label_->SizeToFit(GetLabelWidth(*deep_scanning_label_));
if (enterprise_connectors::ConnectorsManager::GetInstance()
->DelayUntilVerdict(
enterprise_connectors::AnalysisConnector::FILE_DOWNLOADED)) {
open_button_->SetEnabled(false);
} else {
auto open_now_button = views::MdTextButton::Create(
this, l10n_util::GetStringUTF16(IDS_OPEN_DOWNLOAD_NOW));
open_now_button_ = AddChildView(std::move(open_now_button));
open_button_->SetEnabled(true);
}
file_name_label_->SetVisible(false);
status_label_->SetVisible(false);
}
gfx::ImageSkia DownloadItemView::GetWarningIcon() {
switch (model_->GetDangerType()) {
case download::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
if (safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile(
model()->profile())
->IsUnderAdvancedProtection()) {
return gfx::CreateVectorIcon(
vector_icons::kErrorIcon, GetErrorIconSize(),
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_AlertSeverityMedium));
}
FALLTHROUGH;
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
case download::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED:
case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_TOO_LARGE:
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_PASSWORD_PROTECTED:
case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK:
return gfx::CreateVectorIcon(
vector_icons::kWarningIcon, GetWarningIconSize(),
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_AlertSeverityHigh));
case download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING:
case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING:
return gfx::CreateVectorIcon(
vector_icons::kErrorIcon, GetErrorIconSize(),
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_DefaultIconColor));
case download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING:
return gfx::CreateVectorIcon(
vector_icons::kHelpIcon, GetErrorIconSize(),
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_DefaultIconColor));
case download::DOWNLOAD_DANGER_TYPE_BLOCKED_UNSUPPORTED_FILETYPE:
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE:
case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS:
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_WHITELISTED_BY_POLICY:
case download::DOWNLOAD_DANGER_TYPE_MAX:
break;
}
switch (model_->GetMixedContentStatus()) {
case download::DownloadItem::MixedContentStatus::BLOCK:
return gfx::CreateVectorIcon(
vector_icons::kWarningIcon, GetWarningIconSize(),
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_AlertSeverityHigh));
case download::DownloadItem::MixedContentStatus::WARN:
return gfx::CreateVectorIcon(
vector_icons::kErrorIcon, GetErrorIconSize(),
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_AlertSeverityMedium));
case download::DownloadItem::MixedContentStatus::UNKNOWN:
case download::DownloadItem::MixedContentStatus::SAFE:
case download::DownloadItem::MixedContentStatus::VALIDATED:
case download::DownloadItem::MixedContentStatus::SILENT_BLOCK:
break;
}
NOTREACHED();
return gfx::ImageSkia();
}
gfx::Size DownloadItemView::GetButtonSize() const {
gfx::Size size;
if (discard_button_)
size.SetToMax(discard_button_->GetPreferredSize());
if (save_button_)
size.SetToMax(save_button_->GetPreferredSize());
if (scan_button_)
size.SetToMax(scan_button_->GetPreferredSize());
return size;
}
int DownloadItemView::GetLabelWidth(const views::StyledLabel& label) const {
auto lines_for_width = [&label](int width) {
return label.GetLayoutSizeInfoForWidth(width).line_sizes.size();
};
// Return 200 if that much width is sufficient to fit |label| on one line.
int width = 200;
if (lines_for_width(width) < 2)
return width;
// Find an upper bound width sufficient to fit |label| on two lines.
int min_width = 1, max_width;
for (max_width = width; lines_for_width(max_width) > 2; max_width *= 2)
min_width = max_width;
// Binary-search for the smallest width that fits on two lines.
// TODO(pkasting): Can use std::iota_view() when C++20 is available.
std::vector<int> widths(max_width + 1 - min_width);
std::iota(widths.begin(), widths.end(), min_width);
return *util::ranges::lower_bound(widths, 2, util::ranges::greater{},
std::move(lines_for_width));
}
void DownloadItemView::Reenable() {
file_name_label_->SetText(ElidedFilename());
SetEnabled(true); // Triggers a repaint.
}
void DownloadItemView::ReleaseDropdown() {
SetDropdownState(NORMAL);
// Make sure any new status from activating a context menu option is read.
announce_accessible_alert_soon_ = true;
}
void DownloadItemView::StartDownloadProgress() {
if (progress_timer_.IsRunning())
return;
progress_start_time_ = base::TimeTicks::Now();
progress_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(30),
base::Bind(&DownloadItemView::ProgressTimerFired,
base::Unretained(this)));
}
void DownloadItemView::StopDownloadProgress() {
accessible_alert_timer_.AbandonAndStop();
if (!progress_timer_.IsRunning())
return;
previous_progress_elapsed_ += base::TimeTicks::Now() - progress_start_time_;
progress_start_time_ = base::TimeTicks();
progress_timer_.Stop();
}
void DownloadItemView::UpdateAccessibleName() {
base::string16 new_name;
if (has_warning_label(mode_)) {
new_name = dangerous_download_label_->GetText();
} else {
new_name = status_label_->GetText() + base::char16(' ') +
model_->GetFileNameToReportUser().LossyDisplayName();
}
// Do not fire text changed notifications. Screen readers are notified of
// status changes via the accessible alert notifications, and text change
// notifications would be redundant.
accessible_name_ = new_name;
open_button_->SetAccessibleName(new_name);
}
void DownloadItemView::UpdateAccessibleAlert(
const base::string16& accessible_alert_text,
bool is_last_update) {
views::ViewAccessibility& ax = accessible_alert_->GetViewAccessibility();
ax.OverrideRole(ax::mojom::Role::kAlert);
ax.OverrideName(accessible_alert_text);
if (is_last_update) {
// Last update: stop the announcement interval timer and make the last
// announcement immediately.
accessible_alert_timer_.AbandonAndStop();
AnnounceAccessibleAlert();
} else if (!accessible_alert_timer_.IsRunning()) {
// First update: start the announcement interval timer and make the first
// announcement immediately.
accessible_alert_timer_.Start(FROM_HERE, kAccessibleAlertInterval, this,
&DownloadItemView::AnnounceAccessibleAlert);
AnnounceAccessibleAlert();
} else if (announce_accessible_alert_soon_) {
accessible_alert_timer_.Reset();
AnnounceAccessibleAlert();
}
}
base::string16 DownloadItemView::GetInProgressAccessibleAlertText() {
// If opening when complete or there is a warning, use the full status text.
if (model_->GetOpenWhenComplete() || has_warning_label(mode_)) {
UpdateAccessibleName();
return accessible_name_;
}
// Prefer to announce the time remaining, if known.
base::TimeDelta remaining;
if (model_->TimeRemaining(&remaining)) {
// If complete, skip this round: a completion status update is coming soon.
if (remaining.is_zero())
return base::string16();
base::string16 remaining_string =
ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_REMAINING,
ui::TimeFormat::LENGTH_SHORT, remaining);
return l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_TIME_REMAINING_ACCESSIBLE_ALERT, remaining_string);
}
// Time remaining is unknown, try to announce percent remaining.
if (model_->PercentComplete() > 0) {
DCHECK_LE(model_->PercentComplete(), 100);
return l10n_util::GetStringFUTF16Int(
IDS_DOWNLOAD_STATUS_PERCENT_COMPLETE_ACCESSIBLE_ALERT,
100 - model_->PercentComplete());
}
// Percent remaining is also unknown, announce bytes to download.
base::string16 file_name =
model_->GetFileNameToReportUser().LossyDisplayName();
return l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_IN_PROGRESS_ACCESSIBLE_ALERT,
ui::FormatBytes(model_->GetTotalBytes()), file_name);
}
void DownloadItemView::AnnounceAccessibleAlert() {
accessible_alert_->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
announce_accessible_alert_soon_ = false;
}
void DownloadItemView::OnExtractIconComplete(IconLoader::IconSize icon_size,
gfx::Image icon_bitmap) {
if (!icon_bitmap.IsEmpty()) {
if (icon_size == IconLoader::IconSize::NORMAL) {
// We want a 24x24 icon, but on Windows only 16x16 and 32x32 are
// available. So take the NORMAL icon and downsize it.
icon_ = gfx::ImageSkiaOperations::CreateResizedImage(
*icon_bitmap.ToImageSkia(), skia::ImageOperations::RESIZE_BEST,
gfx::Size(kFileIconSize, kFileIconSize));
}
shelf_->SchedulePaint();
}
}
void DownloadItemView::PaintDownloadProgress(
gfx::Canvas* canvas,
const gfx::RectF& bounds,
const base::TimeDelta& indeterminate_progress_time,
int percent_done) const {
const SkColor color = GetThemeProvider()->GetColor(
ThemeProperties::COLOR_TAB_THROBBER_SPINNING);
// Draw background.
cc::PaintFlags bg_flags;
bg_flags.setColor(SkColorSetA(color, 0x33));
bg_flags.setStyle(cc::PaintFlags::kFill_Style);
bg_flags.setAntiAlias(true);
canvas->DrawCircle(bounds.CenterPoint(), bounds.width() / 2, bg_flags);
// Calculate progress.
SkScalar start_pos = SkIntToScalar(270); // 12 o'clock
SkScalar sweep_angle = SkDoubleToScalar(360 * percent_done / 100.0);
if (percent_done < 0) {
// Download size unknown. Draw a 50 degree sweep that moves at 80 degrees
// per second.
start_pos +=
SkDoubleToScalar(indeterminate_progress_time.InSecondsF() * 80);
sweep_angle = SkIntToScalar(50);
}
// Draw progress.
SkPath progress;
progress.addArc(gfx::RectFToSkRect(bounds), start_pos, sweep_angle);
cc::PaintFlags progress_flags;
progress_flags.setColor(color);
progress_flags.setStyle(cc::PaintFlags::kStroke_Style);
progress_flags.setStrokeWidth(1.7f);
progress_flags.setAntiAlias(true);
canvas->DrawPath(progress, progress_flags);
}
void DownloadItemView::AnimateStateTransition(State from,
State to,
gfx::SlideAnimation* animation) {
if (from == NORMAL && to == HOT) {
animation->Show();
} else if (from == HOT && to == NORMAL) {
animation->Hide();
} else if (from != to) {
animation->Reset((to == HOT) ? 1.0 : 0.0);
}
}
void DownloadItemView::ProgressTimerFired() {
// Only repaint for the indeterminate size case. Otherwise, we'll repaint only
// when there's an update notified via OnDownloadUpdated().
if (model_->PercentComplete() < 0)
SchedulePaint();
}
base::string16 DownloadItemView::GetStatusText() const {
if (model_->GetDangerType() ==
download::DownloadDangerType::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE) {
return l10n_util::GetStringUTF16(IDS_PROMPT_DOWNLOAD_DEEP_SCANNED_SAFE);
} else if (model_->GetDangerType() ==
download::DownloadDangerType::
DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS) {
return l10n_util::GetStringUTF16(
IDS_PROMPT_DOWNLOAD_DEEP_SCANNED_OPENED_DANGEROUS);
}
if (!model_->ShouldPromoteOrigin() ||
model_->GetOriginalURL().GetOrigin().is_empty()) {
// Use the default status text.
return model_->GetStatusText();
}
#if !defined(OS_ANDROID)
return url_formatter::ElideUrl(model_->GetOriginalURL().GetOrigin(),
status_font_list_, kTextWidth);
#else
NOTREACHED();
return base::string16();
#endif
}
base::string16 DownloadItemView::ElidedFilename() {
return gfx::ElideFilename(model_->GetFileNameToReportUser(), font_list_,
kTextWidth);
}
void DownloadItemView::OpenDownloadDuringAsyncScanning() {
model_->CompleteSafeBrowsingScan();
model_->SetOpenWhenComplete(true);
}
// static
int DownloadItemView::GetWarningIconSize() {
// TODO(drubery): Replace this method with a constexpr variable when the new
// UX is fully launched.
return base::FeatureList::IsEnabled(safe_browsing::kUseNewDownloadWarnings)
? 20
: 24;
}
// static
int DownloadItemView::GetErrorIconSize() {
// TODO(drubery): Replace this method with a constexpr variable when the new
// UX is fully launched.
return base::FeatureList::IsEnabled(safe_browsing::kUseNewDownloadWarnings)
? 20
: 27;
}
void DownloadItemView::ConfirmDeepScanning() {
DownloadCommands(model_.get()).ExecuteCommand(DownloadCommands::DEEP_SCAN);
}
void DownloadItemView::BypassDeepScanning() {
DownloadCommands(model_.get())
.ExecuteCommand(DownloadCommands::BYPASS_DEEP_SCANNING);
}