| // 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 <stdint.h> |
| |
| #include <algorithm> |
| #include <array> |
| #include <cmath> |
| #include <memory> |
| #include <numeric> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/containers/fixed_flat_map.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/location.h" |
| #include "base/notreached.h" |
| #include "base/numerics/math_constants.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/ranges/functional.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "build/build_config.h" |
| #include "build/buildflag.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_stats.h" |
| #include "chrome/browser/download/drag_download_item.h" |
| #include "chrome/browser/enterprise/connectors/analysis/content_analysis_dialog.h" |
| #include "chrome/browser/enterprise/connectors/analysis/content_analysis_downloads_delegate.h" |
| #include "chrome/browser/enterprise/connectors/connectors_service.h" |
| #include "chrome/browser/icon_manager.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_protection_service.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_service.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tab_modal_confirm_dialog.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/views/chrome_typography.h" |
| #include "chrome/browser/ui/views/download/download_shelf_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/grit/generated_resources.h" |
| #include "components/download/public/common/download_danger_type.h" |
| #include "components/download/public/common/download_item.h" |
| #include "components/safe_browsing/buildflags.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "components/vector_icons/vector_icons.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_enums.mojom.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/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/text/bytes_formatting.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/base/ui_base_types.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/display/screen.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/animation/tween.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/font_list.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/rect_f.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/range/range.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/gfx/text_constants.h" |
| #include "ui/gfx/text_elider.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/native_theme/native_theme_color_id.h" |
| #include "ui/native_theme/themed_vector_icon.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/animation/ink_drop.h" |
| #include "ui/views/animation/ink_drop_host_view.h" |
| #include "ui/views/background.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/style/typography.h" |
| #include "ui/views/vector_icons.h" |
| #include "ui/views/widget/root_view.h" |
| #include "ui/views/widget/widget.h" |
| #include "url/gurl.h" |
| #include "url/url_constants.h" |
| |
| namespace { |
| |
| // TODO(pkasting): Replace bespoke constants in file with standard metrics from |
| // e.g. LayoutProvider. |
| |
| constexpr int kTextWidth = 140; |
| |
| // Padding before the icon and at end of the item. |
| constexpr int kStartPadding = 12; |
| constexpr int kEndPadding = 6; |
| |
| // Horizontal padding between progress indicator and filename/status text. |
| constexpr int kProgressTextPadding = 8; |
| |
| // The space between the Save and Discard buttons when prompting for a |
| // dangerous download. |
| constexpr int kSaveDiscardButtonPadding = 5; |
| |
| // The space on the right side of the dangerous download label. |
| constexpr int kLabelPadding = 8; |
| |
| // 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 analysis service tag for data loss prevention. |
| const char kDlpTag[] = "dlp"; |
| |
| // The analysis service tag for malware. |
| const char kMalwareTag[] = "malware"; |
| |
| // A stub subclass of Button that has no visuals. |
| class TransparentButton : public views::Button { |
| public: |
| METADATA_HEADER(TransparentButton); |
| |
| explicit TransparentButton(DownloadItemView* parent) |
| : Button(Button::PressedCallback()) { |
| views::InstallRectHighlightPathGenerator(this); |
| views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON); |
| set_context_menu_controller(parent); |
| // 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. |
| views::InkDrop::Get(this)->SetBaseColorCallback(base::BindRepeating( |
| [](views::View* host) { |
| // This button will be used like a LabelButton, so use the same |
| // foreground base color as a label button. |
| return color_utils::DeriveDefaultIconColor( |
| views::style::GetColor(*host, views::style::CONTEXT_BUTTON, |
| views::style::STYLE_PRIMARY)); |
| }, |
| this)); |
| } |
| ~TransparentButton() override = default; |
| |
| // 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(); |
| } |
| |
| std::u16string GetTooltipText(const gfx::Point& point) const override { |
| return parent()->GetTooltipText(point); |
| } |
| }; |
| |
| BEGIN_METADATA(TransparentButton, views::Button) |
| END_METADATA |
| |
| bool UseNewWarnings() { |
| return base::FeatureList::IsEnabled(safe_browsing::kUseNewDownloadWarnings); |
| } |
| |
| int GetFilenameStyle(const views::Label& label) { |
| #if !defined(OS_LINUX) && !defined(OS_CHROMEOS) |
| if (UseNewWarnings()) |
| return STYLE_EMPHASIZED; |
| #endif |
| return label.GetTextStyle(); |
| } |
| |
| int GetFilenameStyle(const views::StyledLabel& label) { |
| #if !defined(OS_LINUX) && !defined(OS_CHROMEOS) |
| if (UseNewWarnings()) |
| return STYLE_EMPHASIZED; |
| #endif |
| return label.GetDefaultTextStyle(); |
| } |
| |
| void StyleFilename(views::Label& label, size_t pos, size_t len) { |
| label.SetTextStyleRange(GetFilenameStyle(label), gfx::Range(pos, pos + len)); |
| } |
| |
| void StyleFilename(views::StyledLabel& label, size_t pos, size_t len) { |
| // Ensure the label contains a nonempty filename. |
| if ((pos == std::u16string::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(download::DownloadItemMode mode) { |
| return (mode == download::DownloadItemMode::kDangerous) || |
| (mode == download::DownloadItemMode::kMalicious); |
| } |
| |
| // Whether we are in the mixed content mode. |
| bool is_mixed_content(download::DownloadItemMode mode) { |
| return (mode == download::DownloadItemMode::kMixedContentWarn) || |
| (mode == download::DownloadItemMode::kMixedContentBlock); |
| } |
| |
| // Whether a warning label is visible. |
| bool has_warning_label(download::DownloadItemMode mode) { |
| return is_download_warning(mode) || is_mixed_content(mode); |
| } |
| |
| float GetDPIScaleForView(views::View* view) { |
| const display::Screen* const screen = display::Screen::GetScreen(); |
| DCHECK(screen); |
| return screen->GetDisplayNearestView(view->GetWidget()->GetNativeView()) |
| .device_scale_factor(); |
| } |
| } // namespace |
| |
| class DownloadItemView::ContextMenuButton : public views::ImageButton { |
| public: |
| METADATA_HEADER(ContextMenuButton); |
| |
| explicit ContextMenuButton(DownloadItemView* owner) |
| : views::ImageButton( |
| base::BindRepeating(&DownloadItemView::DropdownButtonPressed, |
| base::Unretained(owner))), |
| owner_(owner) { |
| views::ConfigureVectorImageButton(this); |
| SetAccessibleName(l10n_util::GetStringUTF16( |
| IDS_DOWNLOAD_ITEM_DROPDOWN_BUTTON_ACCESSIBLE_TEXT)); |
| SetBorder(views::CreateEmptyBorder(gfx::Insets(10))); |
| SetHasInkDropActionOnClick(false); |
| } |
| |
| bool OnMousePressed(const ui::MouseEvent& event) override { |
| suppress_button_release_ = owner_->GetDropdownPressed(); |
| return ImageButton::OnMousePressed(event); |
| } |
| |
| bool IsTriggerableEvent(const ui::Event& event) override { |
| return !event.IsMouseEvent() || !suppress_button_release_; |
| } |
| |
| private: |
| DownloadItemView* const owner_; |
| bool suppress_button_release_ = false; |
| }; |
| |
| BEGIN_METADATA(DownloadItemView, ContextMenuButton, views::ImageButton) |
| END_METADATA |
| |
| DownloadItemView::DownloadItemView(DownloadUIModel::DownloadUIModelPtr model, |
| DownloadShelfView* shelf, |
| views::View* accessible_alert) |
| : AnimationDelegateViews(this), |
| model_(std::move(model)), |
| shelf_(shelf), |
| mode_(download::DownloadItemMode::kNormal), |
| indeterminate_progress_timer_( |
| FROM_HERE, |
| base::TimeDelta::FromMilliseconds(30), |
| base::BindRepeating( |
| [](DownloadItemView* view) { |
| if (view->model_->PercentComplete() < 0) |
| view->SchedulePaint(); |
| }, |
| base::Unretained(this))), |
| accessible_alert_(accessible_alert), |
| accessible_alert_timer_( |
| FROM_HERE, |
| base::TimeDelta::FromMinutes(3), |
| base::BindRepeating(&DownloadItemView::AnnounceAccessibleAlert, |
| base::Unretained(this))), |
| current_scale_(/*AddedToWidget() set the right DPI*/ 1.0f) { |
| views::InstallRectHighlightPathGenerator(this); |
| observation_.Observe(this->model()); |
| |
| // TODO(pkasting): Use bespoke file-scope subclasses for some of these child |
| // views to localize functionality and simplify this class. |
| |
| open_button_ = AddChildView(std::make_unique<TransparentButton>(this)); |
| open_button_->SetCallback(base::BindRepeating( |
| &DownloadItemView::OpenButtonPressed, base::Unretained(this))); |
| |
| file_name_label_ = AddChildView(std::make_unique<views::Label>()); |
| file_name_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| file_name_label_->SetTextContext(CONTEXT_DOWNLOAD_SHELF); |
| file_name_label_->GetViewAccessibility().OverrideIsIgnored(true); |
| const std::u16string filename = ElidedFilename(*file_name_label_); |
| file_name_label_->SetText(filename); |
| file_name_label_->SetCanProcessEventsWithinSubtree(false); |
| StyleFilename(*file_name_label_, 0, filename.length()); |
| |
| status_label_ = AddChildView(std::make_unique<views::Label>( |
| std::u16string(), CONTEXT_DOWNLOAD_SHELF_STATUS)); |
| status_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| |
| warning_label_ = AddChildView(std::make_unique<views::StyledLabel>()); |
| warning_label_->SetTextContext(CONTEXT_DOWNLOAD_SHELF); |
| warning_label_->SetCanProcessEventsWithinSubtree(false); |
| |
| deep_scanning_label_ = AddChildView(std::make_unique<views::StyledLabel>()); |
| deep_scanning_label_->SetTextContext(CONTEXT_DOWNLOAD_SHELF); |
| deep_scanning_label_->SetCanProcessEventsWithinSubtree(false); |
| |
| open_now_button_ = AddChildView(std::make_unique<views::MdTextButton>( |
| base::BindRepeating(&DownloadItemView::OpenDownloadDuringAsyncScanning, |
| base::Unretained(this)), |
| l10n_util::GetStringUTF16(IDS_OPEN_DOWNLOAD_NOW))); |
| |
| save_button_ = AddChildView(std::make_unique<views::MdTextButton>( |
| base::BindRepeating(&DownloadItemView::SaveOrDiscardButtonPressed, |
| base::Unretained(this), DownloadCommands::KEEP))); |
| |
| discard_button_ = AddChildView(std::make_unique<views::MdTextButton>( |
| base::BindRepeating(&DownloadItemView::SaveOrDiscardButtonPressed, |
| base::Unretained(this), DownloadCommands::DISCARD), |
| l10n_util::GetStringUTF16(IDS_DISCARD_DOWNLOAD))); |
| |
| scan_button_ = AddChildView(std::make_unique<views::MdTextButton>( |
| base::BindRepeating(&DownloadItemView::ExecuteCommand, |
| base::Unretained(this), DownloadCommands::DEEP_SCAN), |
| l10n_util::GetStringUTF16(IDS_SCAN_DOWNLOAD))); |
| review_button_ = AddChildView(std::make_unique<views::MdTextButton>( |
| base::BindRepeating(&DownloadItemView::ReviewButtonPressed, |
| base::Unretained(this)), |
| l10n_util::GetStringUTF16(IDS_REVIEW_DOWNLOAD))); |
| |
| dropdown_button_ = AddChildView(std::make_unique<ContextMenuButton>(this)); |
| |
| complete_animation_.SetSlideDuration(base::TimeDelta::FromMilliseconds(2500)); |
| complete_animation_.SetTweenType(gfx::Tween::LINEAR); |
| |
| scanning_animation_.SetThrobDuration(base::TimeDelta::FromMilliseconds(2500)); |
| scanning_animation_.SetTweenType(gfx::Tween::LINEAR); |
| |
| // Further configure default state, e.g. child visibility. |
| OnDownloadUpdated(); |
| } |
| |
| DownloadItemView::~DownloadItemView() = default; |
| |
| void DownloadItemView::AddedToWidget() { |
| current_scale_ = GetDPIScaleForView(this); |
| // As the icon depends upon DPI, reload the icon when DPI changes. |
| StartLoadIcons(); |
| } |
| |
| void DownloadItemView::Layout() { |
| // TODO(crbug.com/1005568): Replace Layout()/CalculatePreferredSize() with a |
| // LayoutManager. |
| |
| View::Layout(); |
| |
| open_button_->SetBoundsRect(GetLocalBounds()); |
| dropdown_button_->SetPosition( |
| gfx::Point(width() - kEndPadding - dropdown_button_->width(), |
| CenterY(dropdown_button_->height()))); |
| |
| if (mode_ == download::DownloadItemMode::kNormal) { |
| const int text_x = |
| kStartPadding + kProgressIndicatorSize + kProgressTextPadding; |
| const int text_end = dropdown_button_->GetVisible() |
| ? (dropdown_button_->x() - kEndPadding) |
| : dropdown_button_->bounds().right(); |
| const int text_width = text_end - text_x; |
| const int file_name_height = file_name_label_->GetLineHeight(); |
| int text_height = file_name_height; |
| if (!status_label_->GetText().empty()) |
| text_height += status_label_->GetLineHeight(); |
| |
| file_name_label_->SetBounds(text_x, CenterY(text_height), text_width, |
| file_name_height); |
| status_label_->SetBounds(text_x, file_name_label_->bounds().bottom(), |
| text_width, |
| status_label_->GetPreferredSize().height()); |
| } else { |
| auto* const label = (mode_ == download::DownloadItemMode::kDeepScanning) |
| ? deep_scanning_label_ |
| : warning_label_; |
| label->SetPosition(gfx::Point(kStartPadding * 2 + GetIcon().Size().width(), |
| CenterY(label->height()))); |
| |
| const gfx::Size button_size = GetButtonSize(); |
| gfx::Rect button_bounds(gfx::Point(label->bounds().right() + kLabelPadding, |
| CenterY(button_size.height())), |
| button_size); |
| for (auto* button : {save_button_, discard_button_, scan_button_, |
| open_now_button_, review_button_}) { |
| button->SetBoundsRect(button_bounds); |
| if (button->GetVisible()) |
| button_bounds.set_x(button_bounds.right() + kSaveDiscardButtonPadding); |
| } |
| } |
| } |
| |
| 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() == download::DownloadItem::COMPLETE) && |
| model_->download()) { |
| const gfx::Image* const file_icon = |
| g_browser_process->icon_manager()->LookupIconFromFilepath( |
| model_->GetTargetFilePath(), IconLoader::SMALL, current_scale_); |
| 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_ != download::DownloadItemMode::kNormal) |
| return; |
| |
| if (dragging_) { |
| // Starting a drag results in a MouseCaptureLost. |
| dragging_ = false; |
| drag_start_point_.reset(); |
| } |
| } |
| |
| std::u16string DownloadItemView::GetTooltipText(const gfx::Point& p) const { |
| return has_warning_label(mode_) ? std::u16string() : 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(std::u16string()); |
| } |
| |
| void DownloadItemView::ShowContextMenuForViewImpl( |
| View* source, |
| const gfx::Point& point, |
| ui::MenuSourceType source_type) { |
| ShowContextMenuImpl(gfx::Rect(point, gfx::Size()), source_type); |
| } |
| |
| void DownloadItemView::OnDownloadUpdated() { |
| if (!model_->ShouldShowInShelf()) { |
| shelf_->RemoveDownloadView(this); |
| // WARNING: |this| has been deleted! |
| return; |
| } |
| |
| SetMode(download::GetDesiredDownloadItemMode(model_.get())); |
| |
| if (model_->GetState() == download::DownloadItem::COMPLETE && |
| model_->ShouldRemoveFromShelfWhenComplete()) { |
| shelf_->RemoveDownloadView(this); |
| // WARNING: |this| has been deleted! |
| return; |
| } |
| |
| const std::u16string new_tooltip_text = model_->GetTooltipText(); |
| if (new_tooltip_text != tooltip_text_) { |
| tooltip_text_ = new_tooltip_text; |
| TooltipTextChanged(); |
| } |
| } |
| |
| void DownloadItemView::OnDownloadOpened() { |
| SetEnabled(false); |
| file_name_label_->SetTextStyle(views::style::STYLE_DISABLED); |
| const std::u16string filename = ElidedFilename(*file_name_label_); |
| size_t filename_offset; |
| file_name_label_->SetText(l10n_util::GetStringFUTF16( |
| IDS_DOWNLOAD_STATUS_OPENING, filename, &filename_offset)); |
| StyleFilename(*file_name_label_, filename_offset, filename.length()); |
| |
| const auto reenable = [](base::WeakPtr<DownloadItemView> view) { |
| if (!view) |
| return; |
| view->SetEnabled(true); |
| auto* label = view->file_name_label_; |
| label->SetTextStyle(views::style::STYLE_PRIMARY); |
| const std::u16string filename = view->ElidedFilename(*label); |
| label->SetText(filename); |
| StyleFilename(*label, 0, filename.length()); |
| }; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(std::move(reenable), weak_ptr_factory_.GetWeakPtr()), |
| base::TimeDelta::FromSeconds(3)); |
| |
| shelf_->AutoClose(); |
| } |
| |
| void DownloadItemView::OnDownloadDestroyed() { |
| shelf_->RemoveDownloadView(this); // This will delete us! |
| } |
| |
| void DownloadItemView::AnimationProgressed(const gfx::Animation* animation) { |
| SchedulePaint(); |
| } |
| |
| void DownloadItemView::AnimationEnded(const gfx::Animation* animation) { |
| AnimationProgressed(animation); |
| } |
| |
| void DownloadItemView::MaybeSubmitDownloadToFeedbackService( |
| DownloadCommands::Command command) { |
| if (!model_->ShouldAllowDownloadFeedback() || |
| !SubmitDownloadToFeedbackService(command)) |
| ExecuteCommand(command); |
| } |
| |
| gfx::Size DownloadItemView::CalculatePreferredSize() const { |
| int height, width = dropdown_button_->GetVisible() |
| ? (dropdown_button_->width() + kEndPadding) |
| : 0; |
| |
| if (mode_ == download::DownloadItemMode::kNormal) { |
| int label_width = |
| std::max(file_name_label_->GetPreferredSize().width(), kTextWidth); |
| if (model_->GetDangerType() == |
| download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE) { |
| label_width = |
| std::max(label_width, status_label_->GetPreferredSize().width()); |
| } |
| width += kStartPadding + kProgressIndicatorSize + kProgressTextPadding + |
| label_width + kEndPadding; |
| height = file_name_label_->GetLineHeight() + status_label_->GetLineHeight(); |
| } else { |
| auto* const label = (mode_ == download::DownloadItemMode::kDeepScanning) |
| ? deep_scanning_label_ |
| : warning_label_; |
| height = label->GetLineHeight() * 2; |
| const gfx::Size icon_size = GetIcon().Size(); |
| width += |
| kStartPadding * 2 + icon_size.width() + label->width() + kEndPadding; |
| height = std::max(height, icon_size.height()); |
| const int visible_buttons = base::ranges::count( |
| std::array<const views::View*, 5>{save_button_, discard_button_, |
| scan_button_, open_now_button_, |
| review_button_}, |
| true, &views::View::GetVisible); |
| if (visible_buttons > 0) { |
| const gfx::Size button_size = GetButtonSize(); |
| width += kLabelPadding + button_size.width() * visible_buttons + |
| kSaveDiscardButtonPadding * (visible_buttons - 1); |
| height = std::max(height, button_size.height()); |
| } |
| } |
| |
| // The normal height of the item which may be exceeded if text is large. |
| constexpr int kDefaultDownloadItemHeight = 48; |
| return gfx::Size(width, std::max(kDefaultDownloadItemHeight, |
| 2 * kMinimumVerticalPadding + height)); |
| } |
| |
| void DownloadItemView::OnPaintBackground(gfx::Canvas* canvas) { |
| View::OnPaintBackground(canvas); |
| |
| // Draw the separator as part of the background. It will be covered by the |
| // focus ring when the view has focus. |
| gfx::Rect rect(width() - 1, 0, 1, height()); |
| rect.Inset(0, kTopBottomPadding); |
| canvas->FillRect(GetMirroredRect(rect), |
| GetThemeProvider()->GetColor( |
| ThemeProperties::COLOR_TOOLBAR_VERTICAL_SEPARATOR)); |
| } |
| |
| void DownloadItemView::OnPaint(gfx::Canvas* canvas) { |
| OnPaintBackground(canvas); |
| |
| const bool use_new_warnings = UseNewWarnings(); |
| |
| const gfx::Image* const file_icon_image = |
| g_browser_process->icon_manager()->LookupIconFromFilepath( |
| model_->GetTargetFilePath(), IconLoader::SMALL, current_scale_); |
| const gfx::ImageSkia* file_icon = |
| (file_icon_image && mode_ == download::DownloadItemMode::kNormal) |
| ? file_icon_image->ToImageSkia() |
| : nullptr; |
| |
| // Paint download progress. |
| // TODO(pkasting): Use a child view to display this. |
| const int progress_x = |
| GetMirroredXWithWidthInView(kStartPadding, kProgressIndicatorSize); |
| const int progress_y = CenterY(kProgressIndicatorSize); |
| const gfx::RectF progress_bounds( |
| progress_x, progress_y, kProgressIndicatorSize, kProgressIndicatorSize); |
| const download::DownloadItem::DownloadState state = model_->GetState(); |
| if (mode_ == download::DownloadItemMode::kNormal && |
| state == download::DownloadItem::IN_PROGRESS) { |
| base::TimeDelta indeterminate_progress_time = |
| indeterminate_progress_time_elapsed_; |
| if (!model_->IsPaused()) { |
| indeterminate_progress_time += |
| base::TimeTicks::Now() - indeterminate_progress_start_time_; |
| } |
| PaintDownloadProgress(canvas, progress_bounds, indeterminate_progress_time, |
| model_->PercentComplete()); |
| } else if (complete_animation_.is_animating()) { |
| DCHECK_EQ(download::DownloadItemMode::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 (scanning_animation_.is_animating()) { |
| DCHECK_EQ(download::DownloadItemMode::kDeepScanning, mode_); |
| const double value = gfx::Tween::DoubleValueBetween( |
| scanning_animation_.GetCurrentValue(), 0, 2 * base::kPiDouble); |
| const double opacity = std::sin(value + base::kPiDouble / 2) / 2 + 0.5; |
| canvas->SaveLayerAlpha( |
| static_cast<uint8_t>(gfx::Tween::IntValueBetween(opacity, 0, 255))); |
| PaintDownloadProgress(canvas, GetIconBounds(), base::TimeDelta(), 100); |
| canvas->Restore(); |
| } else if (use_new_warnings) { |
| file_icon = &file_icon_; |
| } |
| |
| // Draw the file icon. |
| if (file_icon) { |
| const int offset = (progress_bounds.height() - file_icon->height()) / 2; |
| cc::PaintFlags flags; |
| // Use an alpha to make the image look disabled. |
| if (!GetEnabled()) |
| flags.setAlpha(120); |
| canvas->DrawImageInt(*file_icon, progress_x + offset, progress_y + offset, |
| flags); |
| } |
| |
| // Overlay the warning icon if appropriate. |
| if (mode_ != download::DownloadItemMode::kNormal) { |
| const gfx::ImageSkia icon = ui::ThemedVectorIcon(GetIcon().GetVectorIcon()) |
| .GetImageSkia(GetNativeTheme()); |
| gfx::RectF bounds = GetIconBounds(); |
| canvas->DrawImageInt(icon, bounds.x(), bounds.y()); |
| } |
| |
| OnPaintBorder(canvas); |
| } |
| |
| void DownloadItemView::OnThemeChanged() { |
| views::View::OnThemeChanged(); |
| |
| const SkColor background_color = |
| GetThemeProvider()->GetColor(ThemeProperties::COLOR_DOWNLOAD_SHELF); |
| SetBackground(views::CreateSolidBackground(background_color)); |
| file_name_label_->SetBackgroundColor(background_color); |
| status_label_->SetBackgroundColor(background_color); |
| warning_label_->SetDisplayedOnBackgroundColor(background_color); |
| deep_scanning_label_->SetDisplayedOnBackgroundColor(background_color); |
| |
| shelf_->ConfigureButtonForTheme(open_now_button_); |
| shelf_->ConfigureButtonForTheme(save_button_); |
| shelf_->ConfigureButtonForTheme(discard_button_); |
| shelf_->ConfigureButtonForTheme(scan_button_); |
| shelf_->ConfigureButtonForTheme(review_button_); |
| |
| UpdateDropdownButtonImage(); |
| } |
| |
| // ui::LayerDelegate: |
| void DownloadItemView::OnDeviceScaleFactorChanged( |
| float old_device_scale_factor, |
| float new_device_scale_factor) { |
| current_scale_ = new_device_scale_factor; |
| StartLoadIcons(); |
| } |
| |
| void DownloadItemView::SetMode(download::DownloadItemMode mode) { |
| if (mode_ == mode && mode != download::DownloadItemMode::kNormal) |
| return; |
| mode_ = mode; |
| UpdateFilePathAndIcons(); |
| UpdateLabels(); |
| UpdateButtons(); |
| UpdateAnimationForDeepScanningMode(); |
| |
| // Update the accessible name to contain the status text, filename, and |
| // warning message (if any). The name will be presented when the download item |
| // receives focus. |
| const std::u16string unelided_filename = |
| model_->GetFileNameToReportUser().LossyDisplayName(); |
| accessible_name_ = |
| has_warning_label(mode_) |
| ? warning_label_->GetText() |
| : (status_label_->GetText() + u' ' + unelided_filename); |
| open_button_->SetAccessibleName(accessible_name_); |
| // 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. |
| |
| if (mode_ == download::DownloadItemMode::kNormal) { |
| UpdateAccessibleAlertAndAnimationsForNormalMode(); |
| } else if (is_download_warning(mode_)) { |
| const auto danger_type = model_->GetDangerType(); |
| const auto file_path = model_->GetTargetFilePath(); |
| bool is_https = model_->GetURL().SchemeIs(url::kHttpsScheme); |
| bool has_user_gesture = model_->HasUserGesture(); |
| RecordDangerousDownloadWarningShown(danger_type, file_path, is_https, |
| has_user_gesture); |
| announce_accessible_alert_soon_ = true; |
| if (danger_type == download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING) { |
| UpdateAccessibleAlert(l10n_util::GetStringFUTF16( |
| IDS_PROMPT_DEEP_SCANNING_ACCESSIBLE_ALERT, unelided_filename)); |
| } else { |
| size_t ignore; |
| UpdateAccessibleAlert(model_->GetWarningText(unelided_filename, &ignore)); |
| accessible_alert_timer_.Stop(); |
| } |
| } else if (is_mixed_content(mode_)) { |
| announce_accessible_alert_soon_ = true; |
| UpdateAccessibleAlert(l10n_util::GetStringFUTF16( |
| IDS_PROMPT_DOWNLOAD_MIXED_CONTENT_BLOCKED_ACCESSIBLE_ALERT, |
| unelided_filename)); |
| } else if (mode_ == download::DownloadItemMode::kDeepScanning) { |
| UpdateAccessibleAlert(l10n_util::GetStringFUTF16( |
| IDS_DEEP_SCANNING_ACCESSIBLE_ALERT, unelided_filename)); |
| } |
| |
| shelf_->InvalidateLayout(); |
| OnPropertyChanged(&mode_, views::kPropertyEffectsNone); |
| } |
| |
| download::DownloadItemMode DownloadItemView::GetMode() const { |
| return mode_; |
| } |
| |
| void DownloadItemView::UpdateFilePathAndIcons() { |
| // The file icon may change when the download completes, and the path to look |
| // up is |file_path_| and thus changes if that changes. If neither of those |
| // is the case, there's nothing to do. |
| const base::FilePath file_path = model_->GetTargetFilePath(); |
| if ((model_->GetState() != download::DownloadItem::COMPLETE) && |
| (file_path_ == file_path)) |
| return; |
| |
| file_path_ = file_path; |
| cancelable_task_tracker_.TryCancelAll(); |
| |
| StartLoadIcons(); |
| } |
| |
| void DownloadItemView::StartLoadIcons() { |
| // The correct scale_factor is set only in the AddedToWidget() |
| if (!GetWidget()) |
| return; |
| // The small icon is not stored directly, but will be requested in other |
| // functions, so ask the icon manager to load it so it's cached. |
| IconManager* const im = g_browser_process->icon_manager(); |
| im->LoadIcon(file_path_, IconLoader::SMALL, current_scale_, |
| base::BindOnce(&DownloadItemView::OnFileIconLoaded, |
| base::Unretained(this), IconLoader::SMALL), |
| &cancelable_task_tracker_); |
| |
| im->LoadIcon(file_path_, IconLoader::NORMAL, current_scale_, |
| base::BindOnce(&DownloadItemView::OnFileIconLoaded, |
| base::Unretained(this), IconLoader::NORMAL), |
| &cancelable_task_tracker_); |
| } |
| |
| void DownloadItemView::UpdateLabels() { |
| file_name_label_->SetVisible(mode_ == download::DownloadItemMode::kNormal); |
| |
| status_label_->SetVisible(mode_ == download::DownloadItemMode::kNormal); |
| if (status_label_->GetVisible()) { |
| const auto text_and_style = GetStatusTextAndStyle(); |
| status_label_->SetText(text_and_style.first); |
| status_label_->SetTextStyle(text_and_style.second); |
| status_label_->GetViewAccessibility().OverrideIsIgnored( |
| status_label_->GetText().empty()); |
| } |
| |
| warning_label_->SetVisible(has_warning_label(mode_)); |
| if (warning_label_->GetVisible()) { |
| const std::u16string filename = ElidedFilename(*warning_label_); |
| size_t filename_offset; |
| warning_label_->SetText(model_->GetWarningText(filename, &filename_offset)); |
| StyleFilename(*warning_label_, filename_offset, filename.length()); |
| warning_label_->SizeToFit(GetLabelWidth(*warning_label_)); |
| } |
| |
| deep_scanning_label_->SetVisible(mode_ == |
| download::DownloadItemMode::kDeepScanning); |
| if (deep_scanning_label_->GetVisible()) { |
| const int id = (model_->download() && |
| safe_browsing::DeepScanningRequest::ShouldUploadBinary( |
| model_->download())) |
| ? IDS_PROMPT_DEEP_SCANNING_DOWNLOAD |
| : IDS_PROMPT_DEEP_SCANNING_APP_DOWNLOAD; |
| const std::u16string filename = ElidedFilename(*deep_scanning_label_); |
| size_t filename_offset; |
| deep_scanning_label_->SetText( |
| l10n_util::GetStringFUTF16(id, filename, &filename_offset)); |
| StyleFilename(*deep_scanning_label_, filename_offset, filename.length()); |
| deep_scanning_label_->SizeToFit(GetLabelWidth(*deep_scanning_label_)); |
| } |
| } |
| |
| void DownloadItemView::UpdateButtons() { |
| bool prompt_to_scan = false, prompt_to_discard = false, |
| prompt_to_review = false; |
| if (is_download_warning(mode_)) { |
| const auto danger_type = model_->GetDangerType(); |
| prompt_to_scan = |
| danger_type == download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING; |
| |
| if (danger_type == |
| download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING || |
| danger_type == download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK) { |
| prompt_to_review = |
| enterprise_connectors::ConnectorsServiceFactory::GetForBrowserContext( |
| model_->profile()) |
| ->HasCustomInfoToDisplay( |
| enterprise_connectors::AnalysisConnector::FILE_DOWNLOADED, |
| kDlpTag); |
| } else if (danger_type == download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE || |
| danger_type == download::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL || |
| danger_type == |
| download::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT) { |
| prompt_to_review = |
| enterprise_connectors::ConnectorsServiceFactory::GetForBrowserContext( |
| model_->profile()) |
| ->HasCustomInfoToDisplay( |
| enterprise_connectors::AnalysisConnector::FILE_DOWNLOADED, |
| kMalwareTag); |
| } |
| |
| prompt_to_discard = |
| !prompt_to_review && !prompt_to_scan && |
| !ChromeDownloadManagerDelegate::IsDangerTypeBlocked(danger_type); |
| } |
| |
| const bool allow_open_during_deep_scan = |
| (mode_ == download::DownloadItemMode::kDeepScanning) && |
| !enterprise_connectors::ConnectorsServiceFactory::GetForBrowserContext( |
| model_->profile()) |
| ->DelayUntilVerdict( |
| enterprise_connectors::AnalysisConnector::FILE_DOWNLOADED); |
| open_button_->SetEnabled((mode_ == download::DownloadItemMode::kNormal) || |
| prompt_to_scan || allow_open_during_deep_scan); |
| |
| open_now_button_->SetVisible(allow_open_during_deep_scan); |
| |
| save_button_->SetVisible( |
| (mode_ == download::DownloadItemMode::kDangerous) || |
| (mode_ == download::DownloadItemMode::kMixedContentWarn)); |
| save_button_->SetText(model_->GetWarningConfirmButtonText()); |
| |
| discard_button_->SetVisible( |
| (mode_ == download::DownloadItemMode::kMixedContentBlock) || |
| prompt_to_discard); |
| scan_button_->SetVisible(prompt_to_scan); |
| review_button_->SetVisible(prompt_to_review); |
| |
| dropdown_button_->SetVisible(model_->ShouldShowDropdown()); |
| } |
| |
| void DownloadItemView::UpdateAccessibleAlertAndAnimationsForNormalMode() { |
| using State = download::DownloadItem::DownloadState; |
| const State state = model_->GetState(); |
| if ((state == State::IN_PROGRESS) && !model_->IsPaused()) { |
| UpdateAccessibleAlert(GetInProgressAccessibleAlertText()); |
| |
| if (!indeterminate_progress_timer_.IsRunning()) { |
| indeterminate_progress_start_time_ = base::TimeTicks::Now(); |
| indeterminate_progress_timer_.Reset(); |
| } |
| |
| // For determinate progress, this function is called each time more data is |
| // received, which should result in updating the progress indicator. |
| if (model_->PercentComplete() > 0) |
| SchedulePaint(); |
| return; |
| } |
| |
| if (state != State::IN_PROGRESS) { |
| if (state == State::CANCELLED) { |
| complete_animation_.Stop(); |
| } else { |
| complete_animation_.Reset(); |
| complete_animation_.Show(); |
| } |
| |
| // Send accessible alert since the download has terminated. No need to alert |
| // for "in progress but 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. |
| static constexpr auto kMap = base::MakeFixedFlatMap<State, int>({ |
| {State::INTERRUPTED, IDS_DOWNLOAD_FAILED_ACCESSIBLE_ALERT}, |
| {State::COMPLETE, IDS_DOWNLOAD_COMPLETE_ACCESSIBLE_ALERT}, |
| {State::CANCELLED, IDS_DOWNLOAD_CANCELLED_ACCESSIBLE_ALERT}, |
| }); |
| const std::u16string alert_text = l10n_util::GetStringFUTF16( |
| kMap.at(state), model_->GetFileNameToReportUser().LossyDisplayName()); |
| announce_accessible_alert_soon_ = true; |
| UpdateAccessibleAlert(alert_text); |
| } |
| |
| accessible_alert_timer_.Stop(); |
| if (indeterminate_progress_timer_.IsRunning()) { |
| indeterminate_progress_time_elapsed_ += |
| base::TimeTicks::Now() - indeterminate_progress_start_time_; |
| indeterminate_progress_timer_.Stop(); |
| } |
| } |
| |
| void DownloadItemView::UpdateAccessibleAlert( |
| const std::u16string& accessible_alert_text) { |
| views::ViewAccessibility& ax = accessible_alert_->GetViewAccessibility(); |
| ax.OverrideRole(ax::mojom::Role::kAlert); |
| ax.OverrideName(accessible_alert_text); |
| if (announce_accessible_alert_soon_ || !accessible_alert_timer_.IsRunning()) { |
| AnnounceAccessibleAlert(); |
| accessible_alert_timer_.Reset(); |
| } |
| } |
| |
| void DownloadItemView::UpdateAnimationForDeepScanningMode() { |
| if (mode_ == download::DownloadItemMode::kDeepScanning) { |
| // -1 to throb indefinitely. |
| scanning_animation_.StartThrobbing(-1); |
| } else { |
| scanning_animation_.End(); |
| } |
| } |
| |
| std::u16string DownloadItemView::GetInProgressAccessibleAlertText() const { |
| // If opening when complete or there is a warning, use the full status text. |
| if (model_->GetOpenWhenComplete() || has_warning_label(mode_)) |
| 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 std::u16string(); |
| |
| const std::u16string 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. |
| return l10n_util::GetStringFUTF16( |
| IDS_DOWNLOAD_STATUS_IN_PROGRESS_ACCESSIBLE_ALERT, |
| ui::FormatBytes(model_->GetTotalBytes()), |
| model_->GetFileNameToReportUser().LossyDisplayName()); |
| } |
| |
| void DownloadItemView::AnnounceAccessibleAlert() { |
| accessible_alert_->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true); |
| announce_accessible_alert_soon_ = false; |
| } |
| |
| void DownloadItemView::OnFileIconLoaded(IconLoader::IconSize icon_size, |
| gfx::Image icon_bitmap) { |
| if (!icon_bitmap.IsEmpty()) { |
| if (icon_size == IconLoader::NORMAL) { |
| // We want a 24x24 icon, but on Windows only 16x16 and 32x32 are |
| // available. So take the NORMAL icon and downsize it. |
| constexpr gfx::Size kFileIconSize(24, 24); |
| file_icon_ = gfx::ImageSkiaOperations::CreateResizedImage( |
| *icon_bitmap.ToImageSkia(), skia::ImageOperations::RESIZE_BEST, |
| kFileIconSize); |
| } |
| 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); |
| } |
| |
| ui::ImageModel DownloadItemView::GetIcon() const { |
| // TODO(pkasting): Use a child view (ImageView subclass?) to display the icon |
| // instead of recomputing this and drawing manually. |
| |
| // TODO(drubery): Replace these sizes with layout provider constants when the |
| // new UX is fully launched. |
| const int non_error_icon_size = UseNewWarnings() ? 20 : 27; |
| const auto kWarning = ui::ImageModel::FromVectorIcon( |
| vector_icons::kWarningIcon, ui::NativeTheme::kColorId_AlertSeverityMedium, |
| non_error_icon_size); |
| const auto kError = ui::ImageModel::FromVectorIcon( |
| vector_icons::kErrorIcon, ui::NativeTheme::kColorId_AlertSeverityHigh, |
| UseNewWarnings() ? 20 : 24); |
| |
| const auto danger_type = model_->GetDangerType(); |
| switch (danger_type) { |
| case download::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT: |
| return safe_browsing::AdvancedProtectionStatusManagerFactory:: |
| GetForProfile(model_->profile()) |
| ->IsUnderAdvancedProtection() |
| ? kWarning |
| : kError; |
| 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: |
| case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_ACCOUNT_COMPROMISE: |
| return kError; |
| case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING: |
| return kWarning; |
| case download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING: |
| case download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING: |
| return ui::ImageModel::FromVectorIcon( |
| (danger_type == download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING) |
| ? views::kInfoIcon |
| : vector_icons::kHelpIcon, |
| ui::NativeTheme::kColorId_DefaultIconColor, non_error_icon_size); |
| 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_ALLOWLISTED_BY_POLICY: |
| case download::DOWNLOAD_DANGER_TYPE_MAX: |
| break; |
| } |
| |
| switch (model_->GetMixedContentStatus()) { |
| case download::DownloadItem::MixedContentStatus::BLOCK: |
| return kError; |
| case download::DownloadItem::MixedContentStatus::WARN: |
| return kWarning; |
| case download::DownloadItem::MixedContentStatus::UNKNOWN: |
| case download::DownloadItem::MixedContentStatus::SAFE: |
| case download::DownloadItem::MixedContentStatus::VALIDATED: |
| case download::DownloadItem::MixedContentStatus::SILENT_BLOCK: |
| break; |
| } |
| |
| NOTREACHED(); |
| return ui::ImageModel(); |
| } |
| |
| gfx::RectF DownloadItemView::GetIconBounds() const { |
| // TODO(drubery): When launching the new warnings, turn these numbers into |
| // appropriately named constants. |
| const int offset = UseNewWarnings() ? 8 : 0; |
| const gfx::Size size = GetIcon().Size(); |
| const int icon_x = |
| GetMirroredXWithWidthInView(kStartPadding, size.width()) + offset; |
| const int icon_y = CenterY(size.height()) + offset; |
| return gfx::RectF(icon_x, icon_y, size.width(), size.height()); |
| } |
| |
| std::pair<std::u16string, int> DownloadItemView::GetStatusTextAndStyle() const { |
| using DangerType = download::DownloadDangerType; |
| const auto type = model_->GetDangerType(); |
| if (type == DangerType::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE) { |
| return {l10n_util::GetStringUTF16(IDS_PROMPT_DOWNLOAD_DEEP_SCANNED_SAFE), |
| STYLE_GREEN}; |
| } |
| constexpr int kDangerous = IDS_PROMPT_DOWNLOAD_DEEP_SCANNED_OPENED_DANGEROUS; |
| if (type == DangerType::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS) |
| return {l10n_util::GetStringUTF16(kDangerous), STYLE_RED}; |
| |
| const GURL url = model_->GetOriginalURL().GetOrigin(); |
| const std::u16string text = |
| (!model_->ShouldPromoteOrigin() || url.is_empty()) |
| ? model_->GetStatusText() |
| #if defined(OS_ANDROID) |
| // url_formatter::ElideUrl() doesn't exist on Android. |
| : std::u16string(); |
| #else |
| : url_formatter::ElideUrl(url, status_label_->font_list(), |
| kTextWidth); |
| #endif |
| return {text, views::style::STYLE_PRIMARY}; |
| } |
| |
| gfx::Size DownloadItemView::GetButtonSize() const { |
| if (mode_ == download::DownloadItemMode::kDeepScanning) |
| return open_now_button_->GetPreferredSize(); |
| |
| gfx::Size size; |
| if (discard_button_->GetVisible()) |
| size.SetToMax(discard_button_->GetPreferredSize()); |
| if (save_button_->GetVisible()) |
| size.SetToMax(save_button_->GetPreferredSize()); |
| if (scan_button_->GetVisible()) |
| size.SetToMax(scan_button_->GetPreferredSize()); |
| if (review_button_->GetVisible()) |
| size.SetToMax(review_button_->GetPreferredSize()); |
| return size; |
| } |
| |
| std::u16string DownloadItemView::ElidedFilename( |
| const views::Label& label) const { |
| const gfx::FontList& font_list = |
| views::style::GetFont(CONTEXT_DOWNLOAD_SHELF, GetFilenameStyle(label)); |
| return gfx::ElideFilename(model_->GetFileNameToReportUser(), font_list, |
| kTextWidth); |
| } |
| |
| std::u16string DownloadItemView::ElidedFilename( |
| const views::StyledLabel& label) const { |
| const gfx::FontList& font_list = |
| views::style::GetFont(CONTEXT_DOWNLOAD_SHELF, GetFilenameStyle(label)); |
| return gfx::ElideFilename(model_->GetFileNameToReportUser(), font_list, |
| kTextWidth); |
| } |
| |
| int DownloadItemView::CenterY(int element_height) const { |
| return (height() - element_height) / 2; |
| } |
| |
| 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 *base::ranges::lower_bound(widths, 2, base::ranges::greater{}, |
| std::move(lines_for_width)); |
| } |
| |
| void DownloadItemView::SetDropdownPressed(bool pressed) { |
| if (dropdown_pressed_ == pressed) |
| return; |
| dropdown_pressed_ = pressed; |
| dropdown_button_->SetHighlighted(dropdown_pressed_); |
| UpdateDropdownButtonImage(); |
| OnPropertyChanged(&dropdown_pressed_, views::kPropertyEffectsNone); |
| } |
| |
| bool DownloadItemView::GetDropdownPressed() const { |
| return dropdown_pressed_; |
| } |
| |
| void DownloadItemView::UpdateDropdownButtonImage() { |
| views::SetImageFromVectorIcon( |
| dropdown_button_, |
| dropdown_pressed_ ? vector_icons::kCaretDownIcon |
| : vector_icons::kCaretUpIcon, |
| GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT)); |
| dropdown_button_->SizeToPreferredSize(); |
| } |
| |
| void DownloadItemView::OpenButtonPressed() { |
| if (mode_ == download::DownloadItemMode::kNormal) { |
| complete_animation_.End(); |
| announce_accessible_alert_soon_ = true; |
| model_->OpenDownload(); |
| // WARNING: |this| may be deleted! |
| } else { |
| ShowOpenDialog( |
| shelf_->browser()->tab_strip_model()->GetActiveWebContents()); |
| } |
| } |
| |
| void DownloadItemView::SaveOrDiscardButtonPressed( |
| DownloadCommands::Command command) { |
| if (is_mixed_content(mode_)) |
| ExecuteCommand(command); |
| else |
| MaybeSubmitDownloadToFeedbackService(command); |
| // WARNING: |this| may be deleted! |
| } |
| |
| void DownloadItemView::DropdownButtonPressed(const ui::Event& event) { |
| SetDropdownPressed(true); |
| ShowContextMenuImpl(dropdown_button_->GetBoundsInScreen(), |
| ui::GetMenuSourceTypeForEvent(event)); |
| } |
| |
| void DownloadItemView::ReviewButtonPressed() { |
| review_button_->SetEnabled(false); |
| |
| auto danger_type = model_->GetDangerType(); |
| auto state = |
| enterprise_connectors::ContentAnalysisDelegateBase::FinalResult::FAILURE; |
| if (danger_type == download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING) { |
| state = enterprise_connectors::ContentAnalysisDelegateBase::FinalResult:: |
| WARNING; |
| } |
| |
| const char* tag = |
| (danger_type == |
| download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING || |
| danger_type == |
| download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK |
| ? kDlpTag |
| : kMalwareTag); |
| |
| auto* connectors_service = |
| enterprise_connectors::ConnectorsServiceFactory::GetForBrowserContext( |
| model_->profile()); |
| |
| const std::u16string filename = ElidedFilename(*file_name_label_); |
| std::u16string custom_message = |
| connectors_service |
| ->GetCustomMessage( |
| enterprise_connectors::AnalysisConnector::FILE_DOWNLOADED, tag) |
| .value_or(u""); |
| GURL learn_more_url = |
| connectors_service |
| ->GetLearnMoreUrl( |
| enterprise_connectors::AnalysisConnector::FILE_DOWNLOADED, tag) |
| .value_or(GURL()); |
| |
| // This dialog opens itself, and is thereafter owned by constrained window |
| // code. |
| new enterprise_connectors::ContentAnalysisDialog( |
| std::make_unique<enterprise_connectors::ContentAnalysisDownloadsDelegate>( |
| filename, custom_message, learn_more_url, |
| base::BindOnce(&DownloadItemView::ExecuteCommand, |
| base::Unretained(this), DownloadCommands::KEEP), |
| base::BindOnce(&DownloadItemView::ExecuteCommand, |
| base::Unretained(this), DownloadCommands::DISCARD)), |
| shelf_->browser()->tab_strip_model()->GetActiveWebContents(), |
| safe_browsing::DeepScanAccessPoint::DOWNLOAD, /* file_count */ 1, state); |
| } |
| |
| void DownloadItemView::ShowOpenDialog(content::WebContents* web_contents) { |
| if (mode_ == download::DownloadItemMode::kDeepScanning) { |
| TabModalConfirmDialog::Create( |
| std::make_unique<safe_browsing::DeepScanningModalDialog>( |
| web_contents, |
| base::BindOnce(&DownloadItemView::OpenDownloadDuringAsyncScanning, |
| weak_ptr_factory_.GetWeakPtr())), |
| web_contents); |
| } else { |
| safe_browsing::PromptForScanningModalDialog::ShowForWebContents( |
| web_contents, model_->GetFileNameToReportUser().LossyDisplayName(), |
| base::BindOnce(&DownloadItemView::ExecuteCommand, |
| weak_ptr_factory_.GetWeakPtr(), |
| DownloadCommands::DEEP_SCAN), |
| base::BindOnce(&DownloadItemView::ExecuteCommand, |
| weak_ptr_factory_.GetWeakPtr(), |
| DownloadCommands::BYPASS_DEEP_SCANNING)); |
| } |
| } |
| |
| void DownloadItemView::ShowContextMenuImpl(const gfx::Rect& rect, |
| ui::MenuSourceType source_type) { |
| context_menu_.SetOnMenuWillShowCallback(base::BindRepeating( |
| [](base::TimeTicks start_time_ticks) { |
| base::UmaHistogramTimes("Download.Shelf.Views.ShowContextMenuTime", |
| base::TimeTicks::Now() - start_time_ticks); |
| }, |
| base::TimeTicks::Now())); |
| |
| // Similar hack as in MenuButtonController. |
| // 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. |
| // TODO(pkasting): Use an actual MenuButtonController and get rid of the |
| // one-off reimplementation of pressed-locking and similar. |
| static_cast<views::internal::RootView*>(GetWidget()->GetRootView()) |
| ->SetMouseAndGestureHandler(nullptr); |
| |
| const auto release_dropdown = [](base::WeakPtr<DownloadItemView> view) { |
| // Make sure any new status from activating a context menu option is read. |
| view->announce_accessible_alert_soon_ = true; |
| |
| // The context menu is destroyed before the button's MousePressed() |
| // function (which wants to know if the button was already pressed) is |
| // reached -- so delay marking the button as "released" until the callstack |
| // unwinds. |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::BindOnce(&DownloadItemView::SetDropdownPressed, |
| std::move(view), false)); |
| }; |
| |
| context_menu_.Run(GetWidget()->GetTopLevelWidget(), rect, source_type, |
| base::BindRepeating(std::move(release_dropdown), |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void DownloadItemView::OpenDownloadDuringAsyncScanning() { |
| model_->CompleteSafeBrowsingScan(); |
| model_->SetOpenWhenComplete(true); |
| } |
| |
| bool DownloadItemView::SubmitDownloadToFeedbackService( |
| DownloadCommands::Command command) const { |
| #if BUILDFLAG(FULL_SAFE_BROWSING) |
| auto* const sb_service = g_browser_process->safe_browsing_service(); |
| if (!sb_service) |
| return false; |
| auto* const dp_service = sb_service->download_protection_service(); |
| if (!dp_service) |
| return false; |
| // TODO(shaktisahu): Enable feedback service for offline item. |
| return !model_->download() || |
| dp_service->MaybeBeginFeedbackForDownload(shelf_->browser()->profile(), |
| model_->download(), command); |
| #else |
| NOTREACHED(); |
| return false; |
| #endif |
| } |
| |
| void DownloadItemView::ExecuteCommand(DownloadCommands::Command command) { |
| commands_.ExecuteCommand(command); |
| } |
| |
| DEFINE_ENUM_CONVERTERS(download::DownloadItemMode, |
| {download::DownloadItemMode::kNormal, u"kNormal"}, |
| {download::DownloadItemMode::kDangerous, u"kDangerous"}, |
| {download::DownloadItemMode::kMalicious, u"kMalicious"}, |
| {download::DownloadItemMode::kMixedContentWarn, |
| u"kMixedContentWarn"}, |
| {download::DownloadItemMode::kMixedContentBlock, |
| u"kMixedContentBlock"}) |
| |
| BEGIN_METADATA(DownloadItemView, views::View) |
| ADD_READONLY_PROPERTY_METADATA(download::DownloadItemMode, Mode) |
| ADD_READONLY_PROPERTY_METADATA(std::u16string, InProgressAccessibleAlertText) |
| ADD_READONLY_PROPERTY_METADATA(gfx::RectF, IconBounds) |
| ADD_READONLY_PROPERTY_METADATA(gfx::Size, ButtonSize) |
| ADD_PROPERTY_METADATA(bool, DropdownPressed) |
| END_METADATA |