// 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_md.h"

#include <stddef.h>

#include <algorithm>
#include <vector>

#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/i18n/break_iterator.h"
#include "base/i18n/rtl.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/chrome_download_manager_delegate.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/extensions/api/experience_sampling_private/experience_sampling.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/download_feedback_service.h"
#include "chrome/browser/safe_browsing/download_protection_service.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/views/bar_control_button.h"
#include "chrome/browser/ui/views/download/download_feedback_dialog_view.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/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/download_danger_type.h"
#include "third_party/icu/source/common/unicode/uchar.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/theme_provider.h"
#include "ui/events/event.h"
#include "ui/gfx/animation/slide_animation.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/paint_vector_icon.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
#include "ui/gfx/vector_icons_public.h"
#include "ui/views/animation/ink_drop_delegate.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/mouse_constants.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"

using content::DownloadItem;
using extensions::ExperienceSamplingEvent;

namespace {

// All values in dp.
const int kTextWidth = 140;
const int kDangerousTextWidth = 200;

// The normal height of the item which may be exceeded if text is large.
const int kDefaultHeight = 48;

// The vertical distance between the item's visual upper bound (as delineated by
// the separator on the right) and the edge of the shelf.
const 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.
const int kMinimumVerticalPadding = 2 + kTopBottomPadding;

// Vertical padding between filename and status text.
const int kVerticalTextPadding = 1;

const int kTooltipMaxWidth = 800;

// Padding before the icon and at end of the item.
const int kStartPadding = 12;
const int kEndPadding = 6;

// Horizontal padding between progress indicator and filename/status text.
const int kProgressTextPadding = 8;

// The space between the Save and Discard buttons when prompting for a dangerous
// download.
const int kButtonPadding = 5;

// The touchable space around the dropdown button's icon.
const int kDropdownBorderWidth = 10;

// The space on the right side of the dangerous download label.
const int kLabelPadding = 8;

// Height/width of the warning icon, also in dp.
const int kWarningIconSize = 24;

// How long the 'download complete' animation should last for.
const int kCompleteAnimationDurationMs = 2500;

// How long the 'download interrupted' animation should last for.
const int kInterruptedAnimationDurationMs = 2500;

// How long we keep the item disabled after the user clicked it to open the
// downloaded item.
const int kDisabledOnOpenDuration = 3000;

// The separator is drawn as a border. It's one dp wide.
class SeparatorBorder : public views::Border {
 public:
  explicit SeparatorBorder(SkColor color) : color_(color) {}
  ~SeparatorBorder() override {}

  void Paint(const views::View& view, gfx::Canvas* canvas) override {
    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);
  }

 private:
  SkColor color_;

  DISALLOW_COPY_AND_ASSIGN(SeparatorBorder);
};

}  // namespace

DownloadItemViewMd::DownloadItemViewMd(DownloadItem* download_item,
                                       DownloadShelfView* parent)
    : shelf_(parent),
      status_text_(l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING)),
      dropdown_state_(NORMAL),
      mode_(NORMAL_MODE),
      dragging_(false),
      starting_drag_(false),
      model_(download_item),
      save_button_(nullptr),
      discard_button_(nullptr),
      dropdown_button_(new BarControlButton(this)),
      dangerous_download_label_(nullptr),
      dangerous_download_label_sized_(false),
      disabled_while_opening_(false),
      creation_time_(base::Time::Now()),
      time_download_warning_shown_(base::Time()),
      weak_ptr_factory_(this) {
  DCHECK(download());
  DCHECK(ui::MaterialDesignController::IsModeMaterial());
  download()->AddObserver(this);
  set_context_menu_controller(this);

  dropdown_button_->SetBorder(
      views::Border::CreateEmptyBorder(gfx::Insets(kDropdownBorderWidth)));
  dropdown_button_->set_ink_drop_size(gfx::Size(32, 32));
  AddChildView(dropdown_button_);

  LoadIcon();

  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  font_list_ =
      rb.GetFontList(ui::ResourceBundle::BaseFont).DeriveWithSizeDelta(1);
  status_font_list_ =
      rb.GetFontList(ui::ResourceBundle::BaseFont).DeriveWithSizeDelta(-2);

  body_hover_animation_.reset(new gfx::SlideAnimation(this));
  drop_hover_animation_.reset(new gfx::SlideAnimation(this));

  SetAccessibilityFocusable(true);

  OnDownloadUpdated(download());

  SetDropdownState(NORMAL);
  UpdateColorsFromTheme();
}

DownloadItemViewMd::~DownloadItemViewMd() {
  StopDownloadProgress();
  download()->RemoveObserver(this);

  // ExperienceSampling: If the user took no action to remove the warning
  // before it disappeared, then the user effectively dismissed the download
  // without keeping it.
  if (sampling_event_.get())
    sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kIgnore);
}

// Progress animation handlers.

void DownloadItemViewMd::StartDownloadProgress() {
  if (progress_timer_.IsRunning())
    return;
  progress_start_time_ = base::TimeTicks::Now();
  progress_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(
                                       DownloadShelf::kProgressRateMs),
                        base::Bind(&DownloadItemViewMd::ProgressTimerFired,
                                   base::Unretained(this)));
}

void DownloadItemViewMd::StopDownloadProgress() {
  if (!progress_timer_.IsRunning())
    return;
  previous_progress_elapsed_ += base::TimeTicks::Now() - progress_start_time_;
  progress_start_time_ = base::TimeTicks();
  progress_timer_.Stop();
}

// static
SkColor DownloadItemViewMd::GetTextColorForThemeProvider(
    const ui::ThemeProvider* theme) {
  return theme ? theme->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT)
               : gfx::kPlaceholderColor;
}

void DownloadItemViewMd::OnExtractIconComplete(gfx::Image* icon_bitmap) {
  if (icon_bitmap)
    shelf_->SchedulePaint();
}

// DownloadObserver interface.

// Update the progress graphic on the icon and our text status label
// to reflect our current bytes downloaded, time remaining.
void DownloadItemViewMd::OnDownloadUpdated(DownloadItem* download_item) {
  DCHECK_EQ(download(), download_item);

  if (!model_.ShouldShowInShelf()) {
    shelf_->RemoveDownloadView(this);  // This will delete us!
    return;
  }

  if (IsShowingWarningDialog() != model_.IsDangerous()) {
    ToggleWarningDialog();
  } else {
    switch (download()->GetState()) {
      case DownloadItem::IN_PROGRESS:
        download()->IsPaused() ? StopDownloadProgress()
                               : StartDownloadProgress();
        LoadIconIfItemPathChanged();
        break;
      case DownloadItem::INTERRUPTED:
        StopDownloadProgress();
        complete_animation_.reset(new gfx::SlideAnimation(this));
        complete_animation_->SetSlideDuration(kInterruptedAnimationDurationMs);
        complete_animation_->SetTweenType(gfx::Tween::LINEAR);
        complete_animation_->Show();
        LoadIcon();
        break;
      case DownloadItem::COMPLETE:
        if (model_.ShouldRemoveFromShelfWhenComplete()) {
          shelf_->RemoveDownloadView(this);  // This will delete us!
          return;
        }
        StopDownloadProgress();
        complete_animation_.reset(new gfx::SlideAnimation(this));
        complete_animation_->SetSlideDuration(kCompleteAnimationDurationMs);
        complete_animation_->SetTweenType(gfx::Tween::LINEAR);
        complete_animation_->Show();
        LoadIcon();
        break;
      case DownloadItem::CANCELLED:
        StopDownloadProgress();
        if (complete_animation_)
          complete_animation_->Stop();
        LoadIcon();
        break;
      default:
        NOTREACHED();
    }
    status_text_ = model_.GetStatusText();
    SchedulePaint();
  }

  base::string16 new_tip = model_.GetTooltipText(font_list_, kTooltipMaxWidth);
  if (new_tip != tooltip_text_) {
    tooltip_text_ = new_tip;
    TooltipTextChanged();
  }

  UpdateAccessibleName();
}

void DownloadItemViewMd::OnDownloadDestroyed(DownloadItem* download) {
  shelf_->RemoveDownloadView(this);  // This will delete us!
}

void DownloadItemViewMd::OnDownloadOpened(DownloadItem* download) {
  disabled_while_opening_ = true;
  SetEnabled(false);
  base::MessageLoop::current()->task_runner()->PostDelayedTask(
      FROM_HERE,
      base::Bind(&DownloadItemViewMd::Reenable, weak_ptr_factory_.GetWeakPtr()),
      base::TimeDelta::FromMilliseconds(kDisabledOnOpenDuration));

  // Notify our parent.
  shelf_->OpenedDownload();
}

// View overrides

// In dangerous mode we have to layout our buttons.
void DownloadItemViewMd::Layout() {
  UpdateColorsFromTheme();

  if (IsShowingWarningDialog()) {
    gfx::Point child_origin(
        kStartPadding + kWarningIconSize + 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() + kButtonPadding, 0);
    }
    discard_button_->SetBoundsRect(gfx::Rect(child_origin, button_size));
    DCHECK_EQ(GetPreferredSize().width(),
              discard_button_->bounds().right() + kEndPadding);
  } else {
    dropdown_button_->SizeToPreferredSize();
    dropdown_button_->SetPosition(
        gfx::Point(width() - dropdown_button_->width() - kEndPadding,
                   (height() - dropdown_button_->height()) / 2));
  }
}

gfx::Size DownloadItemViewMd::GetPreferredSize() const {
  int width = 0;
  // We set the height to the height of two rows or text plus margins.
  int child_height = font_list_.GetBaseline() + kVerticalTextPadding +
                     status_font_list_.GetHeight();

  if (IsShowingWarningDialog()) {
    // Width.
    width = kStartPadding + kWarningIconSize + kStartPadding +
            dangerous_download_label_->width() + kLabelPadding;
    gfx::Size button_size = GetButtonSize();
    if (save_button_)
      width += button_size.width() + kButtonPadding;
    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(), kWarningIconSize});
  } else {
    width = kStartPadding + DownloadShelf::kProgressIndicatorSize +
            kProgressTextPadding + kTextWidth +
            dropdown_button_->GetPreferredSize().width() + kEndPadding;
  }

  return gfx::Size(width, std::max(kDefaultHeight,
                                   2 * kMinimumVerticalPadding + child_height));
}

// Handle a mouse click and open the context menu if the mouse is
// over the drop-down region.
bool DownloadItemViewMd::OnMousePressed(const ui::MouseEvent& event) {
  HandlePressEvent(event, event.IsOnlyLeftMouseButton());
  return true;
}

// Handle drag (file copy) operations.
bool DownloadItemViewMd::OnMouseDragged(const ui::MouseEvent& event) {
  // Mouse should not activate us in dangerous mode.
  if (IsShowingWarningDialog())
    return true;

  if (!starting_drag_) {
    starting_drag_ = true;
    drag_start_point_ = event.location();
  }
  if (dragging_) {
    if (download()->GetState() == DownloadItem::COMPLETE) {
      IconManager* im = g_browser_process->icon_manager();
      gfx::Image* icon = im->LookupIconFromFilepath(
          download()->GetTargetFilePath(), IconLoader::SMALL);
      views::Widget* widget = GetWidget();
      DragDownloadItem(download(), icon,
                       widget ? widget->GetNativeView() : NULL);
    }
  } else if (ExceededDragThreshold(event.location() - drag_start_point_)) {
    dragging_ = true;
  }
  return true;
}

void DownloadItemViewMd::OnMouseReleased(const ui::MouseEvent& event) {
  HandleClickEvent(event, event.IsOnlyLeftMouseButton());
}

void DownloadItemViewMd::OnMouseCaptureLost() {
  // Mouse should not activate us in dangerous mode.
  if (mode_ == DANGEROUS_MODE)
    return;

  if (dragging_) {
    // Starting a drag results in a MouseCaptureLost.
    dragging_ = false;
    starting_drag_ = false;
  }
}

bool DownloadItemViewMd::OnKeyPressed(const ui::KeyEvent& event) {
  // Key press should not activate us in dangerous mode.
  if (IsShowingWarningDialog())
    return true;

  if (event.key_code() == ui::VKEY_SPACE ||
      event.key_code() == ui::VKEY_RETURN) {
    // OpenDownload may delete this, so don't add any code after this line.
    OpenDownload();
    return true;
  }
  return false;
}

bool DownloadItemViewMd::GetTooltipText(const gfx::Point& p,
                                        base::string16* tooltip) const {
  if (IsShowingWarningDialog()) {
    tooltip->clear();
    return false;
  }

  tooltip->assign(tooltip_text_);

  return true;
}

void DownloadItemViewMd::GetAccessibleState(ui::AXViewState* state) {
  state->name = accessible_name_;
  state->role = ui::AX_ROLE_BUTTON;
  if (model_.IsDangerous())
    state->AddStateFlag(ui::AX_STATE_DISABLED);
  else
    state->AddStateFlag(ui::AX_STATE_HASPOPUP);
}

void DownloadItemViewMd::OnThemeChanged() {
  UpdateColorsFromTheme();
}

void DownloadItemViewMd::OnGestureEvent(ui::GestureEvent* event) {
  if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
    HandlePressEvent(*event, true);
    event->SetHandled();
    return;
  }

  if (event->type() == ui::ET_GESTURE_TAP) {
    HandleClickEvent(*event, true);
    event->SetHandled();
    return;
  }

  views::View::OnGestureEvent(event);
}

void DownloadItemViewMd::ShowContextMenuForView(
    View* source,
    const gfx::Point& point,
    ui::MenuSourceType source_type) {
  ShowContextMenuImpl(gfx::Rect(point, gfx::Size()), source_type);
}

void DownloadItemViewMd::ButtonPressed(views::Button* sender,
                                       const ui::Event& event) {
  if (sender == dropdown_button_) {
    // TODO(estade): this is copied from ToolbarActionView but should be shared
    // one way or another.
    ui::MenuSourceType type = ui::MENU_SOURCE_NONE;
    if (event.IsMouseEvent())
      type = ui::MENU_SOURCE_MOUSE;
    else if (event.IsKeyEvent())
      type = ui::MENU_SOURCE_KEYBOARD;
    else if (event.IsGestureEvent())
      type = ui::MENU_SOURCE_TOUCH;
    SetDropdownState(PUSHED);
    ShowContextMenuImpl(dropdown_button_->GetBoundsInScreen(), type);
    return;
  }

  base::TimeDelta warning_duration;
  if (!time_download_warning_shown_.is_null())
    warning_duration = base::Time::Now() - time_download_warning_shown_;

  if (save_button_ && 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);
    // ExperienceSampling: User chose to proceed with a dangerous download.
    if (sampling_event_.get()) {
      sampling_event_->CreateUserDecisionEvent(
          ExperienceSamplingEvent::kProceed);
      sampling_event_.reset(NULL);
    }
    // This will change the state and notify us.
    download()->ValidateDangerousDownload();
    return;
  }

  // WARNING: all end states after this point delete |this|.
  DCHECK_EQ(discard_button_, sender);
  if (model_.IsMalicious()) {
    UMA_HISTOGRAM_LONG_TIMES("clickjacking.dismiss_download", warning_duration);
    // ExperienceSampling: User chose to dismiss the dangerous download.
    if (sampling_event_.get()) {
      sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kDeny);
      sampling_event_.reset(NULL);
    }
    shelf_->RemoveDownloadView(this);
    return;
  }
  UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download", warning_duration);
  if (model_.ShouldAllowDownloadFeedback() &&
      !shelf_->browser()->profile()->IsOffTheRecord()) {
    if (!shelf_->browser()->profile()->GetPrefs()->HasPrefPath(
            prefs::kSafeBrowsingExtendedReportingEnabled)) {
      // Show dialog, because the dialog hasn't been shown before.
      DownloadFeedbackDialogView::Show(
          shelf_->get_parent()->GetNativeWindow(), shelf_->browser()->profile(),
          shelf_->GetNavigator(),
          base::Bind(
              &DownloadItemViewMd::PossiblySubmitDownloadToFeedbackService,
              weak_ptr_factory_.GetWeakPtr()));
    } else {
      PossiblySubmitDownloadToFeedbackService(
          shelf_->browser()->profile()->GetPrefs()->GetBoolean(
              prefs::kSafeBrowsingExtendedReportingEnabled));
    }
    return;
  }
  download()->Remove();
}

void DownloadItemViewMd::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 DownloadItemViewMd::OnPaint(gfx::Canvas* canvas) {
  DrawStatusText(canvas);
  DrawFilename(canvas);
  DrawIcon(canvas);
  OnPaintBorder(canvas);

  if (HasFocus())
    canvas->DrawFocusRect(GetLocalBounds());
}

int DownloadItemViewMd::GetYForFilenameText() const {
  int text_height = font_list_.GetBaseline();
  if (!status_text_.empty())
    text_height += kVerticalTextPadding + status_font_list_.GetBaseline();
  return (height() - text_height) / 2;
}

void DownloadItemViewMd::DrawStatusText(gfx::Canvas* canvas) {
  if (status_text_.empty() || IsShowingWarningDialog())
    return;

  int mirrored_x = GetMirroredXWithWidthInView(
      kStartPadding + DownloadShelf::kProgressIndicatorSize +
          kProgressTextPadding,
      kTextWidth);
  int y =
      GetYForFilenameText() + font_list_.GetBaseline() + kVerticalTextPadding;
  canvas->DrawStringRect(
      status_text_, status_font_list_, GetDimmedTextColor(),
      gfx::Rect(mirrored_x, y, kTextWidth, status_font_list_.GetHeight()));
}

void DownloadItemViewMd::DrawFilename(gfx::Canvas* canvas) {
  if (IsShowingWarningDialog())
    return;

  // Print the text, left aligned and always print the file extension.
  // Last value of x was the end of the right image, just before the button.
  // Note that in dangerous mode we use a label (as the text is multi-line).
  base::string16 filename;
  if (!disabled_while_opening_) {
    filename = gfx::ElideFilename(download()->GetFileNameToReportUser(),
                                  font_list_, kTextWidth);
  } else {
    // 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(download()->GetFileNameToReportUser(), font_list_,
                           kTextWidth - status_string_width);
    // Last, concat the whole string.
    filename = l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
                                          filename_string);
  }

  int mirrored_x = GetMirroredXWithWidthInView(
      kStartPadding + DownloadShelf::kProgressIndicatorSize +
          kProgressTextPadding,
      kTextWidth);
  canvas->DrawStringRect(filename, font_list_,
                         enabled() ? GetTextColor() : GetDimmedTextColor(),
                         gfx::Rect(mirrored_x, GetYForFilenameText(),
                                   kTextWidth, font_list_.GetHeight()));
}

void DownloadItemViewMd::DrawIcon(gfx::Canvas* canvas) {
  if (IsShowingWarningDialog()) {
    int icon_x = base::i18n::IsRTL()
                     ? width() - kWarningIconSize - kStartPadding
                     : kStartPadding;
    int icon_y = (height() - kWarningIconSize) / 2;
    canvas->DrawImageInt(GetWarningIcon(), icon_x, icon_y);
    return;
  }

  // Paint download progress.
  DownloadItem::DownloadState state = download()->GetState();
  canvas->Save();
  int progress_x =
      base::i18n::IsRTL()
          ? width() - kStartPadding - DownloadShelf::kProgressIndicatorSize
          : kStartPadding;
  int progress_y = (height() - DownloadShelf::kProgressIndicatorSize) / 2;
  canvas->Translate(gfx::Vector2d(progress_x, progress_y));

  if (state == DownloadItem::IN_PROGRESS) {
    base::TimeDelta progress_time = previous_progress_elapsed_;
    if (!download()->IsPaused())
      progress_time += base::TimeTicks::Now() - progress_start_time_;
    DownloadShelf::PaintDownloadProgress(
        canvas, *GetThemeProvider(), progress_time, model_.PercentComplete());
  } else if (complete_animation_.get() && complete_animation_->is_animating()) {
    if (state == DownloadItem::INTERRUPTED) {
      DownloadShelf::PaintDownloadInterrupted(
          canvas, *GetThemeProvider(), complete_animation_->GetCurrentValue());
    } else {
      DCHECK_EQ(DownloadItem::COMPLETE, state);
      DownloadShelf::PaintDownloadComplete(
          canvas, *GetThemeProvider(), complete_animation_->GetCurrentValue());
    }
  }
  canvas->Restore();

  // Fetch the already-loaded icon.
  IconManager* im = g_browser_process->icon_manager();
  gfx::Image* icon = im->LookupIconFromFilepath(download()->GetTargetFilePath(),
                                                IconLoader::SMALL);
  if (!icon)
    return;

  // Draw the icon image.
  int icon_x = progress_x + DownloadShelf::kFiletypeIconOffset;
  int icon_y = progress_y + DownloadShelf::kFiletypeIconOffset;
  SkPaint paint;
  // Use an alpha to make the image look disabled.
  if (!enabled())
    paint.setAlpha(120);
  canvas->DrawImageInt(*icon->ToImageSkia(), icon_x, icon_y, paint);
}

void DownloadItemViewMd::OnFocus() {
  View::OnFocus();
  // We render differently when focused.
  SchedulePaint();
}

void DownloadItemViewMd::OnBlur() {
  View::OnBlur();
  // We render differently when focused.
  SchedulePaint();
}

void DownloadItemViewMd::OpenDownload() {
  DCHECK(!IsShowingWarningDialog());
  // 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_);

  UpdateAccessibleName();

  // Calling download()->OpenDownload may delete this, so this must be
  // the last thing we do.
  download()->OpenDownload();
}

bool DownloadItemViewMd::SubmitDownloadToFeedbackService() {
#if defined(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;
  download_protection_service->feedback_service()->BeginFeedbackForDownload(
      download());
  // WARNING: we are deleted at this point.  Don't access 'this'.
  return true;
#else
  NOTREACHED();
  return false;
#endif
}

void DownloadItemViewMd::PossiblySubmitDownloadToFeedbackService(bool enabled) {
  if (!enabled || !SubmitDownloadToFeedbackService())
    download()->Remove();
  // WARNING: 'this' is deleted at this point. Don't access 'this'.
}

void DownloadItemViewMd::LoadIcon() {
  IconManager* im = g_browser_process->icon_manager();
  last_download_item_path_ = download()->GetTargetFilePath();
  im->LoadIcon(last_download_item_path_, IconLoader::SMALL,
               base::Bind(&DownloadItemViewMd::OnExtractIconComplete,
                          base::Unretained(this)),
               &cancelable_task_tracker_);
}

void DownloadItemViewMd::LoadIconIfItemPathChanged() {
  base::FilePath current_download_path = download()->GetTargetFilePath();
  if (last_download_item_path_ == current_download_path)
    return;

  LoadIcon();
}

void DownloadItemViewMd::UpdateColorsFromTheme() {
  if (!GetThemeProvider())
    return;

  if (dangerous_download_label_)
    dangerous_download_label_->SetEnabledColor(GetTextColor());
  SetBorder(make_scoped_ptr(new SeparatorBorder(
      GetThemeProvider()->GetColor(
          ThemeProperties::COLOR_TOOLBAR_BOTTOM_SEPARATOR))));
}

void DownloadItemViewMd::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(NULL);

  // Post a task to release the button.  When we call the Run method on the menu
  // below, it runs an inner message loop that might cause us to be deleted.
  // Posting a task with a WeakPtr lets us safely handle the button release.
  base::MessageLoop::current()->task_runner()->PostNonNestableTask(
      FROM_HERE, base::Bind(&DownloadItemViewMd::ReleaseDropdown,
                            weak_ptr_factory_.GetWeakPtr()));

  if (!context_menu_.get())
    context_menu_.reset(new DownloadShelfContextMenuView(download()));
  context_menu_->Run(GetWidget()->GetTopLevelWidget(), rect, source_type);
  // We could be deleted now.
}

void DownloadItemViewMd::HandlePressEvent(const ui::LocatedEvent& event,
                                          bool active_event) {
  // The event should not activate us in dangerous mode.
  if (mode_ == DANGEROUS_MODE)
    return;

  // Stop any completion animation.
  if (complete_animation_.get() && complete_animation_->is_animating())
    complete_animation_->End();
}

void DownloadItemViewMd::HandleClickEvent(const ui::LocatedEvent& event,
                                          bool active_event) {
  // Mouse should not activate us in dangerous mode.
  if (mode_ == DANGEROUS_MODE)
    return;

  if (!active_event || IsShowingWarningDialog())
    return;

  // OpenDownload may delete this, so don't add any code after this line.
  OpenDownload();
}

void DownloadItemViewMd::SetDropdownState(State new_state) {
  // Avoid extra SchedulePaint()s if the state is going to be the same and
  // |dropdown_button_| has already been initialized.
  if (dropdown_state_ == new_state &&
      !dropdown_button_->GetImage(views::CustomButton::STATE_NORMAL).isNull())
    return;

  dropdown_button_->SetIcon(
      new_state == PUSHED ? gfx::VectorIconId::FIND_NEXT
                          : gfx::VectorIconId::FIND_PREV,
      base::Bind(&DownloadItemViewMd::GetTextColor, base::Unretained(this)));
  dropdown_button_->ink_drop_delegate()->OnAction(
      new_state == PUSHED ? views::InkDropState::ACTIVATED
                          : views::InkDropState::DEACTIVATED);
  dropdown_button_->OnThemeChanged();
  dropdown_state_ = new_state;
  SchedulePaint();
}

void DownloadItemViewMd::ToggleWarningDialog() {
  if (model_.IsDangerous())
    ShowWarningDialog();
  else
    ClearWarningDialog();

  // 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 DownloadItemViewMd::ClearWarningDialog() {
  DCHECK(download()->GetDangerType() ==
         content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED);
  DCHECK(mode_ == DANGEROUS_MODE || mode_ == MALICIOUS_MODE);

  mode_ = NORMAL_MODE;
  dropdown_state_ = NORMAL;

  // ExperienceSampling: User proceeded through the warning.
  if (sampling_event_.get()) {
    sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kProceed);
    sampling_event_.reset(NULL);
  }
  // Remove the views used by the warning dialog.
  if (save_button_) {
    RemoveChildView(save_button_);
    delete save_button_;
    save_button_ = NULL;
  }
  RemoveChildView(discard_button_);
  delete discard_button_;
  discard_button_ = NULL;
  RemoveChildView(dangerous_download_label_);
  delete dangerous_download_label_;
  dangerous_download_label_ = NULL;
  dangerous_download_label_sized_ = false;

  // We need to load the icon now that the download has the real path.
  LoadIcon();

  dropdown_button_->SetVisible(true);
}

void DownloadItemViewMd::ShowWarningDialog() {
  DCHECK(mode_ != DANGEROUS_MODE && mode_ != MALICIOUS_MODE);
  time_download_warning_shown_ = base::Time::Now();
  content::DownloadDangerType danger_type = download()->GetDangerType();
  RecordDangerousDownloadWarningShown(danger_type);
#if defined(FULL_SAFE_BROWSING)
  if (model_.ShouldAllowDownloadFeedback()) {
    safe_browsing::DownloadFeedbackService::RecordEligibleDownloadShown(
        danger_type);
  }
#endif
  mode_ = model_.MightBeMalicious() ? MALICIOUS_MODE : DANGEROUS_MODE;

  // ExperienceSampling: Dangerous or malicious download warning is being shown
  // to the user, so we start a new SamplingEvent and track it.
  std::string event_name = model_.MightBeMalicious()
                               ? ExperienceSamplingEvent::kMaliciousDownload
                               : ExperienceSamplingEvent::kDangerousDownload;
  sampling_event_.reset(new ExperienceSamplingEvent(
      event_name, download()->GetURL(), download()->GetReferrerUrl(),
      download()->GetBrowserContext()));

  dropdown_state_ = NORMAL;
  if (mode_ == DANGEROUS_MODE) {
    save_button_ = views::MdTextButton::CreateStandardButton(
        this, model_.GetWarningConfirmButtonText());
    save_button_->SetStyle(views::Button::STYLE_BUTTON);
    AddChildView(save_button_);
  }
  int discard_button_message =
      model_.IsMalicious() ? IDS_DISMISS_DOWNLOAD : IDS_DISCARD_DOWNLOAD;
  discard_button_ = views::MdTextButton::CreateStandardButton(
      this, l10n_util::GetStringUTF16(discard_button_message));
  discard_button_->SetStyle(views::Button::STYLE_BUTTON);
  AddChildView(discard_button_);

  base::string16 dangerous_label =
      model_.GetWarningText(font_list_, kTextWidth);
  dangerous_download_label_ = new views::Label(dangerous_label);
  dangerous_download_label_->SetMultiLine(true);
  dangerous_download_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  dangerous_download_label_->SetAutoColorReadabilityEnabled(false);
  AddChildView(dangerous_download_label_);
  SizeLabelToMinWidth();

  dropdown_button_->SetVisible(false);
}

gfx::ImageSkia DownloadItemViewMd::GetWarningIcon() {
  switch (download()->GetDangerType()) {
    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
    case content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
    case content::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED:
      return gfx::CreateVectorIcon(gfx::VectorIconId::REMOVE_CIRCLE,
                                   kWarningIconSize,
                                   gfx::kGoogleRed700);

    case content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS:
    case content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
    case content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED:
    case content::DOWNLOAD_DANGER_TYPE_MAX:
      NOTREACHED();
      break;

    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
      return gfx::CreateVectorIcon(gfx::VectorIconId::WARNING,
                                   kWarningIconSize,
                                   gfx::kGoogleYellow700);
  }
  return gfx::ImageSkia();
}

gfx::Size DownloadItemViewMd::GetButtonSize() const {
  DCHECK(discard_button_ && (mode_ == MALICIOUS_MODE || save_button_));
  gfx::Size size = discard_button_->GetPreferredSize();
  if (save_button_)
    size.SetToMax(save_button_->GetPreferredSize());
  return size;
}

// This method computes the minimum width of the label for displaying its text
// on 2 lines.  It just breaks the string in 2 lines on the spaces and keeps the
// configuration with minimum width.
void DownloadItemViewMd::SizeLabelToMinWidth() {
  if (dangerous_download_label_sized_)
    return;

  base::string16 label_text = dangerous_download_label_->text();
  base::TrimWhitespace(label_text, base::TRIM_ALL, &label_text);
  DCHECK_EQ(base::string16::npos, label_text.find('\n'));

  // Make the label big so that GetPreferredSize() is not constrained by the
  // current width.
  dangerous_download_label_->SetBounds(0, 0, 1000, 1000);

  // Use a const string from here. BreakIterator requies that text.data() not
  // change during its lifetime.
  const base::string16 original_text(label_text);
  // Using BREAK_WORD can work in most cases, but it can also break
  // lines where it should not. Using BREAK_LINE is safer although
  // slower for Chinese/Japanese. This is not perf-critical at all, though.
  base::i18n::BreakIterator iter(original_text,
                                 base::i18n::BreakIterator::BREAK_LINE);
  bool status = iter.Init();
  DCHECK(status);

  base::string16 prev_text = original_text;
  gfx::Size size = dangerous_download_label_->GetPreferredSize();
  int min_width = size.width();

  // Go through the string and try each line break (starting with no line break)
  // searching for the optimal line break position.  Stop if we find one that
  // yields one that is less than kDangerousTextWidth wide.  This is to prevent
  // a short string (e.g.: "This file is malicious") from being broken up
  // unnecessarily.
  while (iter.Advance() && min_width > kDangerousTextWidth) {
    size_t pos = iter.pos();
    if (pos >= original_text.length())
      break;
    base::string16 current_text = original_text;
    // This can be a low surrogate codepoint, but u_isUWhiteSpace will
    // return false and inserting a new line after a surrogate pair
    // is perfectly ok.
    base::char16 line_end_char = current_text[pos - 1];
    if (u_isUWhiteSpace(line_end_char))
      current_text.replace(pos - 1, 1, 1, base::char16('\n'));
    else
      current_text.insert(pos, 1, base::char16('\n'));
    dangerous_download_label_->SetText(current_text);
    size = dangerous_download_label_->GetPreferredSize();

    // If the width is growing again, it means we passed the optimal width spot.
    if (size.width() > min_width) {
      dangerous_download_label_->SetText(prev_text);
      break;
    } else {
      min_width = size.width();
    }
    prev_text = current_text;
  }

  dangerous_download_label_->SetSize(size);
  dangerous_download_label_sized_ = true;
}

void DownloadItemViewMd::Reenable() {
  disabled_while_opening_ = false;
  SetEnabled(true);  // Triggers a repaint.
}

void DownloadItemViewMd::ReleaseDropdown() {
  SetDropdownState(NORMAL);
}

void DownloadItemViewMd::UpdateAccessibleName() {
  base::string16 new_name;
  if (IsShowingWarningDialog()) {
    new_name = dangerous_download_label_->text();
  } else {
    new_name = status_text_ + base::char16(' ') +
               download()->GetFileNameToReportUser().LossyDisplayName();
  }

  // If the name has changed, notify assistive technology that the name
  // has changed so they can announce it immediately.
  if (new_name != accessible_name_) {
    accessible_name_ = new_name;
    NotifyAccessibilityEvent(ui::AX_EVENT_TEXT_CHANGED, true);
  }
}

void DownloadItemViewMd::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 DownloadItemViewMd::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();
}

SkColor DownloadItemViewMd::GetTextColor() {
  return GetTextColorForThemeProvider(GetThemeProvider());
}

SkColor DownloadItemViewMd::GetDimmedTextColor() {
  return SkColorSetA(GetTextColor(), 0xC7);
}