| // 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); |
| } |