download_item_view.cc revision cedac228d2dd51db4b79ea1e72c7f249408ee061
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/ui/views/download/download_item_view.h"
6
7#include <algorithm>
8#include <vector>
9
10#include "base/bind.h"
11#include "base/callback.h"
12#include "base/files/file_path.h"
13#include "base/i18n/break_iterator.h"
14#include "base/i18n/rtl.h"
15#include "base/metrics/histogram.h"
16#include "base/prefs/pref_service.h"
17#include "base/strings/string_util.h"
18#include "base/strings/stringprintf.h"
19#include "base/strings/sys_string_conversions.h"
20#include "base/strings/utf_string_conversions.h"
21#include "chrome/browser/browser_process.h"
22#include "chrome/browser/download/chrome_download_manager_delegate.h"
23#include "chrome/browser/download/download_item_model.h"
24#include "chrome/browser/download/download_stats.h"
25#include "chrome/browser/download/drag_download_item.h"
26#include "chrome/browser/profiles/profile.h"
27#include "chrome/browser/safe_browsing/download_feedback_service.h"
28#include "chrome/browser/safe_browsing/download_protection_service.h"
29#include "chrome/browser/safe_browsing/safe_browsing_service.h"
30#include "chrome/browser/themes/theme_properties.h"
31#include "chrome/browser/ui/views/download/download_feedback_dialog_view.h"
32#include "chrome/browser/ui/views/download/download_shelf_context_menu_view.h"
33#include "chrome/browser/ui/views/download/download_shelf_view.h"
34#include "chrome/browser/ui/views/frame/browser_view.h"
35#include "content/public/browser/download_danger_type.h"
36#include "grit/generated_resources.h"
37#include "grit/theme_resources.h"
38#include "third_party/icu/source/common/unicode/uchar.h"
39#include "ui/accessibility/ax_view_state.h"
40#include "ui/base/l10n/l10n_util.h"
41#include "ui/base/resource/resource_bundle.h"
42#include "ui/base/theme_provider.h"
43#include "ui/events/event.h"
44#include "ui/gfx/animation/slide_animation.h"
45#include "ui/gfx/canvas.h"
46#include "ui/gfx/color_utils.h"
47#include "ui/gfx/image/image.h"
48#include "ui/gfx/text_elider.h"
49#include "ui/gfx/text_utils.h"
50#include "ui/views/controls/button/label_button.h"
51#include "ui/views/controls/label.h"
52#include "ui/views/mouse_constants.h"
53#include "ui/views/widget/root_view.h"
54#include "ui/views/widget/widget.h"
55
56// TODO(paulg): These may need to be adjusted when download progress
57//              animation is added, and also possibly to take into account
58//              different screen resolutions.
59static const int kTextWidth = 140;            // Pixels
60static const int kDangerousTextWidth = 200;   // Pixels
61static const int kVerticalPadding = 3;        // Pixels
62static const int kVerticalTextPadding = 2;    // Pixels
63static const int kTooltipMaxWidth = 800;      // Pixels
64
65// We add some padding before the left image so that the progress animation icon
66// hides the corners of the left image.
67static const int kLeftPadding = 0;  // Pixels.
68
69// The space between the Save and Discard buttons when prompting for a dangerous
70// download.
71static const int kButtonPadding = 5;  // Pixels.
72
73// The space on the left and right side of the dangerous download label.
74static const int kLabelPadding = 4;  // Pixels.
75
76static const SkColor kFileNameDisabledColor = SkColorSetRGB(171, 192, 212);
77
78// How long the 'download complete' animation should last for.
79static const int kCompleteAnimationDurationMs = 2500;
80
81// How long the 'download interrupted' animation should last for.
82static const int kInterruptedAnimationDurationMs = 2500;
83
84// How long we keep the item disabled after the user clicked it to open the
85// downloaded item.
86static const int kDisabledOnOpenDuration = 3000;
87
88// Darken light-on-dark download status text by 20% before drawing, thus
89// creating a "muted" version of title text for both dark-on-light and
90// light-on-dark themes.
91static const double kDownloadItemLuminanceMod = 0.8;
92
93using content::DownloadItem;
94
95DownloadItemView::DownloadItemView(DownloadItem* download_item,
96    DownloadShelfView* parent)
97  : warning_icon_(NULL),
98    shelf_(parent),
99    status_text_(l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING)),
100    body_state_(NORMAL),
101    drop_down_state_(NORMAL),
102    mode_(NORMAL_MODE),
103    progress_angle_(DownloadShelf::kStartAngleDegrees),
104    drop_down_pressed_(false),
105    dragging_(false),
106    starting_drag_(false),
107    model_(download_item),
108    save_button_(NULL),
109    discard_button_(NULL),
110    dangerous_download_label_(NULL),
111    dangerous_download_label_sized_(false),
112    disabled_while_opening_(false),
113    creation_time_(base::Time::Now()),
114    time_download_warning_shown_(base::Time()),
115    weak_ptr_factory_(this) {
116  DCHECK(download());
117  download()->AddObserver(this);
118  set_context_menu_controller(this);
119
120  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
121
122  BodyImageSet normal_body_image_set = {
123    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP),
124    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE),
125    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM),
126    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP),
127    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE),
128    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM),
129    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP),
130    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE),
131    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM)
132  };
133  normal_body_image_set_ = normal_body_image_set;
134
135  DropDownImageSet normal_drop_down_image_set = {
136    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP),
137    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE),
138    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM)
139  };
140  normal_drop_down_image_set_ = normal_drop_down_image_set;
141
142  BodyImageSet hot_body_image_set = {
143    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP_H),
144    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_H),
145    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_H),
146    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP_H),
147    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_H),
148    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_H),
149    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_H),
150    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_H),
151    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_H)
152  };
153  hot_body_image_set_ = hot_body_image_set;
154
155  DropDownImageSet hot_drop_down_image_set = {
156    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP_H),
157    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_H),
158    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_H)
159  };
160  hot_drop_down_image_set_ = hot_drop_down_image_set;
161
162  BodyImageSet pushed_body_image_set = {
163    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP_P),
164    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE_P),
165    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM_P),
166    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP_P),
167    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE_P),
168    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM_P),
169    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_P),
170    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_P),
171    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_P)
172  };
173  pushed_body_image_set_ = pushed_body_image_set;
174
175  DropDownImageSet pushed_drop_down_image_set = {
176    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_TOP_P),
177    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_MIDDLE_P),
178    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_MENU_BOTTOM_P)
179  };
180  pushed_drop_down_image_set_ = pushed_drop_down_image_set;
181
182  BodyImageSet dangerous_mode_body_image_set = {
183    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_TOP),
184    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_MIDDLE),
185    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_LEFT_BOTTOM),
186    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_TOP),
187    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_MIDDLE),
188    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_CENTER_BOTTOM),
189    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_TOP_NO_DD),
190    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_MIDDLE_NO_DD),
191    rb.GetImageSkiaNamed(IDR_DOWNLOAD_BUTTON_RIGHT_BOTTOM_NO_DD)
192  };
193  dangerous_mode_body_image_set_ = dangerous_mode_body_image_set;
194
195  malicious_mode_body_image_set_ = normal_body_image_set;
196
197  LoadIcon();
198
199  font_list_ = rb.GetFontList(ui::ResourceBundle::BaseFont);
200  box_height_ = std::max<int>(2 * kVerticalPadding + font_list_.GetHeight() +
201                                  kVerticalTextPadding + font_list_.GetHeight(),
202                              2 * kVerticalPadding +
203                                  normal_body_image_set_.top_left->height() +
204                                  normal_body_image_set_.bottom_left->height());
205
206  if (DownloadShelf::kSmallProgressIconSize > box_height_)
207    box_y_ = (DownloadShelf::kSmallProgressIconSize - box_height_) / 2;
208  else
209    box_y_ = 0;
210
211  body_hover_animation_.reset(new gfx::SlideAnimation(this));
212  drop_hover_animation_.reset(new gfx::SlideAnimation(this));
213
214  SetAccessibilityFocusable(true);
215
216  OnDownloadUpdated(download());
217  UpdateDropDownButtonPosition();
218}
219
220DownloadItemView::~DownloadItemView() {
221  StopDownloadProgress();
222  download()->RemoveObserver(this);
223}
224
225// Progress animation handlers.
226
227void DownloadItemView::UpdateDownloadProgress() {
228  progress_angle_ =
229      (progress_angle_ + DownloadShelf::kUnknownIncrementDegrees) %
230      DownloadShelf::kMaxDegrees;
231  SchedulePaint();
232}
233
234void DownloadItemView::StartDownloadProgress() {
235  if (progress_timer_.IsRunning())
236    return;
237  progress_timer_.Start(FROM_HERE,
238      base::TimeDelta::FromMilliseconds(DownloadShelf::kProgressRateMs), this,
239      &DownloadItemView::UpdateDownloadProgress);
240}
241
242void DownloadItemView::StopDownloadProgress() {
243  progress_timer_.Stop();
244}
245
246void DownloadItemView::OnExtractIconComplete(gfx::Image* icon_bitmap) {
247  if (icon_bitmap)
248    shelf_->SchedulePaint();
249}
250
251// DownloadObserver interface.
252
253// Update the progress graphic on the icon and our text status label
254// to reflect our current bytes downloaded, time remaining.
255void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) {
256  DCHECK_EQ(download(), download_item);
257
258  if (IsShowingWarningDialog() && !model_.IsDangerous()) {
259    // We have been approved.
260    ClearWarningDialog();
261  } else if (!IsShowingWarningDialog() && model_.IsDangerous()) {
262    ShowWarningDialog();
263    // Force the shelf to layout again as our size has changed.
264    shelf_->Layout();
265    SchedulePaint();
266  } else {
267    base::string16 status_text = model_.GetStatusText();
268    switch (download()->GetState()) {
269      case DownloadItem::IN_PROGRESS:
270        download()->IsPaused() ?
271            StopDownloadProgress() : StartDownloadProgress();
272        LoadIconIfItemPathChanged();
273        break;
274      case DownloadItem::INTERRUPTED:
275        StopDownloadProgress();
276        complete_animation_.reset(new gfx::SlideAnimation(this));
277        complete_animation_->SetSlideDuration(kInterruptedAnimationDurationMs);
278        complete_animation_->SetTweenType(gfx::Tween::LINEAR);
279        complete_animation_->Show();
280        SchedulePaint();
281        LoadIcon();
282        break;
283      case DownloadItem::COMPLETE:
284        if (model_.ShouldRemoveFromShelfWhenComplete()) {
285          shelf_->RemoveDownloadView(this);  // This will delete us!
286          return;
287        }
288        StopDownloadProgress();
289        complete_animation_.reset(new gfx::SlideAnimation(this));
290        complete_animation_->SetSlideDuration(kCompleteAnimationDurationMs);
291        complete_animation_->SetTweenType(gfx::Tween::LINEAR);
292        complete_animation_->Show();
293        SchedulePaint();
294        LoadIcon();
295        break;
296      case DownloadItem::CANCELLED:
297        StopDownloadProgress();
298        if (complete_animation_)
299          complete_animation_->Stop();
300        LoadIcon();
301        break;
302      default:
303        NOTREACHED();
304    }
305    status_text_ = status_text;
306  }
307
308  base::string16 new_tip = model_.GetTooltipText(font_list_, kTooltipMaxWidth);
309  if (new_tip != tooltip_text_) {
310    tooltip_text_ = new_tip;
311    TooltipTextChanged();
312  }
313
314  UpdateAccessibleName();
315
316  // We use the parent's (DownloadShelfView's) SchedulePaint, since there
317  // are spaces between each DownloadItemView that the parent is responsible
318  // for painting.
319  shelf_->SchedulePaint();
320}
321
322void DownloadItemView::OnDownloadDestroyed(DownloadItem* download) {
323  shelf_->RemoveDownloadView(this);  // This will delete us!
324}
325
326void DownloadItemView::OnDownloadOpened(DownloadItem* download) {
327  disabled_while_opening_ = true;
328  SetEnabled(false);
329  base::MessageLoop::current()->PostDelayedTask(
330      FROM_HERE,
331      base::Bind(&DownloadItemView::Reenable, weak_ptr_factory_.GetWeakPtr()),
332      base::TimeDelta::FromMilliseconds(kDisabledOnOpenDuration));
333
334  // Notify our parent.
335  shelf_->OpenedDownload(this);
336}
337
338// View overrides
339
340// In dangerous mode we have to layout our buttons.
341void DownloadItemView::Layout() {
342  if (IsShowingWarningDialog()) {
343    BodyImageSet* body_image_set =
344        (mode_ == DANGEROUS_MODE) ? &dangerous_mode_body_image_set_ :
345            &malicious_mode_body_image_set_;
346    int x = kLeftPadding + body_image_set->top_left->width() +
347        warning_icon_->width() + kLabelPadding;
348    int y = (height() - dangerous_download_label_->height()) / 2;
349    dangerous_download_label_->SetBounds(x, y,
350                                         dangerous_download_label_->width(),
351                                         dangerous_download_label_->height());
352    gfx::Size button_size = GetButtonSize();
353    x += dangerous_download_label_->width() + kLabelPadding;
354    y = (height() - button_size.height()) / 2;
355    if (save_button_) {
356      save_button_->SetBounds(x, y, button_size.width(), button_size.height());
357      x += button_size.width() + kButtonPadding;
358    }
359    discard_button_->SetBounds(x, y, button_size.width(), button_size.height());
360    UpdateColorsFromTheme();
361  }
362}
363
364gfx::Size DownloadItemView::GetPreferredSize() const {
365  int width, height;
366
367  // First, we set the height to the height of two rows or text plus margins.
368  height = 2 * kVerticalPadding + 2 * font_list_.GetHeight() +
369      kVerticalTextPadding;
370  // Then we increase the size if the progress icon doesn't fit.
371  height = std::max<int>(height, DownloadShelf::kSmallProgressIconSize);
372
373  if (IsShowingWarningDialog()) {
374    const BodyImageSet* body_image_set =
375        (mode_ == DANGEROUS_MODE) ? &dangerous_mode_body_image_set_ :
376            &malicious_mode_body_image_set_;
377    width = kLeftPadding + body_image_set->top_left->width();
378    width += warning_icon_->width() + kLabelPadding;
379    width += dangerous_download_label_->width() + kLabelPadding;
380    gfx::Size button_size = GetButtonSize();
381    // Make sure the button fits.
382    height = std::max<int>(height, 2 * kVerticalPadding + button_size.height());
383    // Then we make sure the warning icon fits.
384    height = std::max<int>(height, 2 * kVerticalPadding +
385                                   warning_icon_->height());
386    if (save_button_)
387      width += button_size.width() + kButtonPadding;
388    width += button_size.width();
389    width += body_image_set->top_right->width();
390    if (mode_ == MALICIOUS_MODE)
391      width += normal_drop_down_image_set_.top->width();
392  } else {
393    width = kLeftPadding + normal_body_image_set_.top_left->width();
394    width += DownloadShelf::kSmallProgressIconSize;
395    width += kTextWidth;
396    width += normal_body_image_set_.top_right->width();
397    width += normal_drop_down_image_set_.top->width();
398  }
399  return gfx::Size(width, height);
400}
401
402// Handle a mouse click and open the context menu if the mouse is
403// over the drop-down region.
404bool DownloadItemView::OnMousePressed(const ui::MouseEvent& event) {
405  HandlePressEvent(event, event.IsOnlyLeftMouseButton());
406  return true;
407}
408
409// Handle drag (file copy) operations.
410bool DownloadItemView::OnMouseDragged(const ui::MouseEvent& event) {
411  // Mouse should not activate us in dangerous mode.
412  if (IsShowingWarningDialog())
413    return true;
414
415  if (!starting_drag_) {
416    starting_drag_ = true;
417    drag_start_point_ = event.location();
418  }
419  if (dragging_) {
420    if (download()->GetState() == DownloadItem::COMPLETE) {
421      IconManager* im = g_browser_process->icon_manager();
422      gfx::Image* icon = im->LookupIconFromFilepath(
423          download()->GetTargetFilePath(), IconLoader::SMALL);
424      views::Widget* widget = GetWidget();
425      DragDownloadItem(
426          download(), icon, widget ? widget->GetNativeView() : NULL);
427    }
428  } else if (ExceededDragThreshold(event.location() - drag_start_point_)) {
429    dragging_ = true;
430  }
431  return true;
432}
433
434void DownloadItemView::OnMouseReleased(const ui::MouseEvent& event) {
435  HandleClickEvent(event, event.IsOnlyLeftMouseButton());
436}
437
438void DownloadItemView::OnMouseCaptureLost() {
439  // Mouse should not activate us in dangerous mode.
440  if (mode_ == DANGEROUS_MODE)
441    return;
442
443  if (dragging_) {
444    // Starting a drag results in a MouseCaptureLost.
445    dragging_ = false;
446    starting_drag_ = false;
447  }
448  SetState(NORMAL, NORMAL);
449}
450
451void DownloadItemView::OnMouseMoved(const ui::MouseEvent& event) {
452  // Mouse should not activate us in dangerous mode.
453  if (mode_ == DANGEROUS_MODE)
454    return;
455
456  bool on_body = !InDropDownButtonXCoordinateRange(event.x());
457  SetState(on_body ? HOT : NORMAL, on_body ? NORMAL : HOT);
458}
459
460void DownloadItemView::OnMouseExited(const ui::MouseEvent& event) {
461  // Mouse should not activate us in dangerous mode.
462  if (mode_ == DANGEROUS_MODE)
463    return;
464
465  SetState(NORMAL, drop_down_pressed_ ? PUSHED : NORMAL);
466}
467
468bool DownloadItemView::OnKeyPressed(const ui::KeyEvent& event) {
469  // Key press should not activate us in dangerous mode.
470  if (IsShowingWarningDialog())
471    return true;
472
473  if (event.key_code() == ui::VKEY_SPACE ||
474      event.key_code() == ui::VKEY_RETURN) {
475    // OpenDownload may delete this, so don't add any code after this line.
476    OpenDownload();
477    return true;
478  }
479  return false;
480}
481
482bool DownloadItemView::GetTooltipText(const gfx::Point& p,
483                                      base::string16* tooltip) const {
484  if (IsShowingWarningDialog()) {
485    tooltip->clear();
486    return false;
487  }
488
489  tooltip->assign(tooltip_text_);
490
491  return true;
492}
493
494void DownloadItemView::GetAccessibleState(ui::AXViewState* state) {
495  state->name = accessible_name_;
496  state->role = ui::AX_ROLE_BUTTON;
497  if (model_.IsDangerous())
498    state->AddStateFlag(ui::AX_STATE_DISABLED);
499  else
500    state->AddStateFlag(ui::AX_STATE_HASPOPUP);
501}
502
503void DownloadItemView::OnThemeChanged() {
504  UpdateColorsFromTheme();
505}
506
507void DownloadItemView::OnGestureEvent(ui::GestureEvent* event) {
508  if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
509    HandlePressEvent(*event, true);
510    event->SetHandled();
511    return;
512  }
513
514  if (event->type() == ui::ET_GESTURE_TAP) {
515    HandleClickEvent(*event, true);
516    event->SetHandled();
517    return;
518  }
519
520  SetState(NORMAL, NORMAL);
521  views::View::OnGestureEvent(event);
522}
523
524void DownloadItemView::ShowContextMenuForView(View* source,
525                                              const gfx::Point& point,
526                                              ui::MenuSourceType source_type) {
527  // |point| is in screen coordinates. So convert it to local coordinates first.
528  gfx::Point local_point = point;
529  ConvertPointFromScreen(this, &local_point);
530  ShowContextMenuImpl(local_point, source_type);
531}
532
533void DownloadItemView::ButtonPressed(views::Button* sender,
534                                     const ui::Event& event) {
535  base::TimeDelta warning_duration;
536  if (!time_download_warning_shown_.is_null())
537    warning_duration = base::Time::Now() - time_download_warning_shown_;
538
539  if (save_button_ && sender == save_button_) {
540    // The user has confirmed a dangerous download.  We'd record how quickly the
541    // user did this to detect whether we're being clickjacked.
542    UMA_HISTOGRAM_LONG_TIMES("clickjacking.save_download", warning_duration);
543    // This will change the state and notify us.
544    download()->ValidateDangerousDownload();
545    return;
546  }
547
548  // WARNING: all end states after this point delete |this|.
549  DCHECK_EQ(discard_button_, sender);
550  if (model_.IsMalicious()) {
551    UMA_HISTOGRAM_LONG_TIMES("clickjacking.dismiss_download", warning_duration);
552    shelf_->RemoveDownloadView(this);
553    return;
554  }
555  UMA_HISTOGRAM_LONG_TIMES("clickjacking.discard_download", warning_duration);
556  if (model_.ShouldAllowDownloadFeedback() &&
557      !shelf_->browser()->profile()->IsOffTheRecord()) {
558    if (!shelf_->browser()->profile()->GetPrefs()->HasPrefPath(
559        prefs::kSafeBrowsingDownloadFeedbackEnabled)) {
560      // Show dialog, because the dialog hasn't been shown before.
561      DownloadFeedbackDialogView::Show(
562          shelf_->get_parent()->GetNativeWindow(),
563          shelf_->browser()->profile(),
564          base::Bind(
565              &DownloadItemView::PossiblySubmitDownloadToFeedbackService,
566              weak_ptr_factory_.GetWeakPtr()));
567    } else {
568      PossiblySubmitDownloadToFeedbackService(
569          shelf_->browser()->profile()->GetPrefs()->GetBoolean(
570               prefs::kSafeBrowsingDownloadFeedbackEnabled));
571    }
572    return;
573  }
574  download()->Remove();
575}
576
577void DownloadItemView::AnimationProgressed(const gfx::Animation* animation) {
578  // We don't care if what animation (body button/drop button/complete),
579  // is calling back, as they all have to go through the same paint call.
580  SchedulePaint();
581}
582
583void DownloadItemView::OnPaint(gfx::Canvas* canvas) {
584  OnPaintBackground(canvas);
585  if (HasFocus())
586    canvas->DrawFocusRect(GetLocalBounds());
587}
588
589// The DownloadItemView can be in three major modes (NORMAL_MODE, DANGEROUS_MODE
590// and MALICIOUS_MODE).
591//
592// NORMAL_MODE: We are displaying an in-progress or completed download.
593// .-------------------------------+-.
594// | [icon] Filename               |v|
595// | [    ] Status                 | |
596// `-------------------------------+-'
597//  |  |                            \_ Drop down button. Invokes menu. Responds
598//  |  |                               to mouse. (NORMAL, HOT or PUSHED).
599//  |   \_ Icon is overlaid on top of in-progress animation.
600//   \_ Both the body and the drop down button respond to mouse hover and can be
601//      pushed (NORMAL, HOT or PUSHED).
602//
603// DANGEROUS_MODE: The file could be potentially dangerous.
604// .-------------------------------------------------------.
605// | [ ! ] [This type of file can  ]  [ Keep ] [ Discard ] |
606// | [   ] [destroy your computer..]  [      ] [         ] |
607// `-------------------------------------------------------'
608//  |  |    |                          |                 \_ No drop down button.
609//  |  |    |                           \_ Buttons are views::LabelButtons.
610//  |  |     \_ Text is in a label (dangerous_download_label_)
611//  |   \_ Warning icon.  No progress animation.
612//   \_ Body is static.  Doesn't respond to mouse hover or press. (NORMAL only)
613//
614// MALICIOUS_MODE: The file is known malware.
615// .---------------------------------------------+-.
616// | [ - ] [This file is malicious.] [ Discard ] |v|
617// | [   ] [                       ] [         ] | |-.
618// `---------------------------------------------+-' |
619//  |  |    |                         |            Drop down button. Responds to
620//  |  |    |                         |            mouse.(NORMAL, HOT or PUSHED)
621//  |  |    |                          \_ Button is a views::LabelButton.
622//  |  |     \_ Text is in a label (dangerous_download_label_)
623//  |   \_ Warning icon.  No progress animation.
624//   \_ Body is static.  Doesn't respond to mouse hover or press. (NORMAL only)
625//
626void DownloadItemView::OnPaintBackground(gfx::Canvas* canvas) {
627  BodyImageSet* body_image_set = NULL;
628  switch (mode_) {
629    case NORMAL_MODE:
630      if (body_state_ == PUSHED)
631        body_image_set = &pushed_body_image_set_;
632      else                      // NORMAL or HOT
633        body_image_set = &normal_body_image_set_;
634      break;
635    case DANGEROUS_MODE:
636      body_image_set = &dangerous_mode_body_image_set_;
637      break;
638    case MALICIOUS_MODE:
639      body_image_set = &malicious_mode_body_image_set_;
640      break;
641    default:
642      NOTREACHED();
643  }
644
645  DropDownImageSet* drop_down_image_set = NULL;
646  switch (mode_) {
647    case NORMAL_MODE:
648    case MALICIOUS_MODE:
649      if (drop_down_state_ == PUSHED)
650        drop_down_image_set = &pushed_drop_down_image_set_;
651      else                        // NORMAL or HOT
652        drop_down_image_set = &normal_drop_down_image_set_;
653      break;
654    case DANGEROUS_MODE:
655      // We don't use a drop down button for mode_ == DANGEROUS_MODE.  So we let
656      // drop_down_image_set == NULL.
657      break;
658    default:
659      NOTREACHED();
660  }
661
662  int center_width = width() - kLeftPadding -
663                     body_image_set->left->width() -
664                     body_image_set->right->width() -
665                     (drop_down_image_set ?
666                        normal_drop_down_image_set_.center->width() :
667                        0);
668
669  // May be caused by animation.
670  if (center_width <= 0)
671    return;
672
673  // Draw status before button image to effectively lighten text.  No status for
674  // warning dialogs.
675  if (!IsShowingWarningDialog()) {
676    if (!status_text_.empty()) {
677      int mirrored_x = GetMirroredXWithWidthInView(
678          DownloadShelf::kSmallProgressIconSize, kTextWidth);
679      // Add font_list_.height() to compensate for title, which is drawn later.
680      int y = box_y_ + kVerticalPadding + font_list_.GetHeight() +
681              kVerticalTextPadding;
682      SkColor file_name_color = GetThemeProvider()->GetColor(
683          ThemeProperties::COLOR_BOOKMARK_TEXT);
684      // If text is light-on-dark, lightening it alone will do nothing.
685      // Therefore we mute luminance a wee bit before drawing in this case.
686      if (color_utils::RelativeLuminance(file_name_color) > 0.5)
687          file_name_color = SkColorSetRGB(
688              static_cast<int>(kDownloadItemLuminanceMod *
689                               SkColorGetR(file_name_color)),
690              static_cast<int>(kDownloadItemLuminanceMod *
691                               SkColorGetG(file_name_color)),
692              static_cast<int>(kDownloadItemLuminanceMod *
693                               SkColorGetB(file_name_color)));
694      canvas->DrawStringRect(status_text_, font_list_, file_name_color,
695                             gfx::Rect(mirrored_x, y, kTextWidth,
696                                       font_list_.GetHeight()));
697    }
698  }
699
700  // Paint the background images.
701  int x = kLeftPadding;
702  canvas->Save();
703  if (base::i18n::IsRTL()) {
704    // Since we do not have the mirrored images for
705    // (hot_)body_image_set->top_left, (hot_)body_image_set->left,
706    // (hot_)body_image_set->bottom_left, and drop_down_image_set,
707    // for RTL UI, we flip the canvas to draw those images mirrored.
708    // Consequently, we do not need to mirror the x-axis of those images.
709    canvas->Translate(gfx::Vector2d(width(), 0));
710    canvas->Scale(-1, 1);
711  }
712  PaintImages(canvas,
713              body_image_set->top_left, body_image_set->left,
714              body_image_set->bottom_left,
715              x, box_y_, box_height_, body_image_set->top_left->width());
716  x += body_image_set->top_left->width();
717  PaintImages(canvas,
718              body_image_set->top, body_image_set->center,
719              body_image_set->bottom,
720              x, box_y_, box_height_, center_width);
721  x += center_width;
722  PaintImages(canvas,
723              body_image_set->top_right, body_image_set->right,
724              body_image_set->bottom_right,
725              x, box_y_, box_height_, body_image_set->top_right->width());
726
727  // Overlay our body hot state. Warning dialogs don't display body a hot state.
728  if (!IsShowingWarningDialog() &&
729      body_hover_animation_->GetCurrentValue() > 0) {
730    canvas->SaveLayerAlpha(
731        static_cast<int>(body_hover_animation_->GetCurrentValue() * 255));
732
733    int x = kLeftPadding;
734    PaintImages(canvas,
735                hot_body_image_set_.top_left, hot_body_image_set_.left,
736                hot_body_image_set_.bottom_left,
737                x, box_y_, box_height_, hot_body_image_set_.top_left->width());
738    x += body_image_set->top_left->width();
739    PaintImages(canvas,
740                hot_body_image_set_.top, hot_body_image_set_.center,
741                hot_body_image_set_.bottom,
742                x, box_y_, box_height_, center_width);
743    x += center_width;
744    PaintImages(canvas,
745                hot_body_image_set_.top_right, hot_body_image_set_.right,
746                hot_body_image_set_.bottom_right,
747                x, box_y_, box_height_,
748                hot_body_image_set_.top_right->width());
749    canvas->Restore();
750  }
751
752  x += body_image_set->top_right->width();
753
754  // Paint the drop-down.
755  if (drop_down_image_set) {
756    PaintImages(canvas,
757                drop_down_image_set->top, drop_down_image_set->center,
758                drop_down_image_set->bottom,
759                x, box_y_, box_height_, drop_down_image_set->top->width());
760
761    // Overlay our drop-down hot state.
762    if (drop_hover_animation_->GetCurrentValue() > 0) {
763      canvas->SaveLayerAlpha(
764          static_cast<int>(drop_hover_animation_->GetCurrentValue() * 255));
765
766      PaintImages(canvas,
767                  drop_down_image_set->top, drop_down_image_set->center,
768                  drop_down_image_set->bottom,
769                  x, box_y_, box_height_, drop_down_image_set->top->width());
770
771      canvas->Restore();
772    }
773  }
774
775  // Restore the canvas to avoid file name etc. text are drawn flipped.
776  // Consequently, the x-axis of following canvas->DrawXXX() method should be
777  // mirrored so the text and images are down in the right positions.
778  canvas->Restore();
779
780  // Print the text, left aligned and always print the file extension.
781  // Last value of x was the end of the right image, just before the button.
782  // Note that in dangerous mode we use a label (as the text is multi-line).
783  if (!IsShowingWarningDialog()) {
784    base::string16 filename;
785    if (!disabled_while_opening_) {
786      filename = gfx::ElideFilename(download()->GetFileNameToReportUser(),
787                                   font_list_, kTextWidth);
788    } else {
789      // First, Calculate the download status opening string width.
790      base::string16 status_string =
791          l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
792                                     base::string16());
793      int status_string_width = gfx::GetStringWidth(status_string, font_list_);
794      // Then, elide the file name.
795      base::string16 filename_string =
796          gfx::ElideFilename(download()->GetFileNameToReportUser(), font_list_,
797                            kTextWidth - status_string_width);
798      // Last, concat the whole string.
799      filename = l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
800                                            filename_string);
801    }
802
803    int mirrored_x = GetMirroredXWithWidthInView(
804        DownloadShelf::kSmallProgressIconSize, kTextWidth);
805    SkColor file_name_color = GetThemeProvider()->GetColor(
806        ThemeProperties::COLOR_BOOKMARK_TEXT);
807    int y =
808        box_y_ + (status_text_.empty() ?
809            ((box_height_ - font_list_.GetHeight()) / 2) : kVerticalPadding);
810
811    // Draw the file's name.
812    canvas->DrawStringRect(
813        filename, font_list_,
814        enabled() ? file_name_color : kFileNameDisabledColor,
815        gfx::Rect(mirrored_x, y, kTextWidth, font_list_.GetHeight()));
816  }
817
818  // Load the icon.
819  IconManager* im = g_browser_process->icon_manager();
820  gfx::Image* image = im->LookupIconFromFilepath(
821      download()->GetTargetFilePath(), IconLoader::SMALL);
822  const gfx::ImageSkia* icon = NULL;
823  if (IsShowingWarningDialog())
824    icon = warning_icon_;
825  else if (image)
826    icon = image->ToImageSkia();
827
828  // We count on the fact that the icon manager will cache the icons and if one
829  // is available, it will be cached here. We *don't* want to request the icon
830  // to be loaded here, since this will also get called if the icon can't be
831  // loaded, in which case LookupIcon will always be NULL. The loading will be
832  // triggered only when we think the status might change.
833  if (icon) {
834    if (!IsShowingWarningDialog()) {
835      DownloadItem::DownloadState state = download()->GetState();
836      if (state == DownloadItem::IN_PROGRESS) {
837        DownloadShelf::PaintDownloadProgress(canvas,
838                                             this,
839                                             0,
840                                             0,
841                                             progress_angle_,
842                                             model_.PercentComplete(),
843                                             DownloadShelf::SMALL);
844      } else if (complete_animation_.get() &&
845                 complete_animation_->is_animating()) {
846        if (state == DownloadItem::INTERRUPTED) {
847          DownloadShelf::PaintDownloadInterrupted(
848              canvas,
849              this,
850              0,
851              0,
852              complete_animation_->GetCurrentValue(),
853              DownloadShelf::SMALL);
854        } else {
855          DCHECK_EQ(DownloadItem::COMPLETE, state);
856          DownloadShelf::PaintDownloadComplete(
857              canvas,
858              this,
859              0,
860              0,
861              complete_animation_->GetCurrentValue(),
862              DownloadShelf::SMALL);
863        }
864      }
865    }
866
867    // Draw the icon image.
868    int icon_x, icon_y;
869
870    if (IsShowingWarningDialog()) {
871      icon_x = kLeftPadding + body_image_set->top_left->width();
872      icon_y = (height() - icon->height()) / 2;
873    } else {
874      icon_x = DownloadShelf::kSmallProgressIconOffset;
875      icon_y = DownloadShelf::kSmallProgressIconOffset;
876    }
877    icon_x = GetMirroredXWithWidthInView(icon_x, icon->width());
878    if (enabled()) {
879      canvas->DrawImageInt(*icon, icon_x, icon_y);
880    } else {
881      // Use an alpha to make the image look disabled.
882      SkPaint paint;
883      paint.setAlpha(120);
884      canvas->DrawImageInt(*icon, icon_x, icon_y, paint);
885    }
886  }
887}
888
889void DownloadItemView::OnFocus() {
890  View::OnFocus();
891  // We render differently when focused.
892  SchedulePaint();
893}
894
895void DownloadItemView::OnBlur() {
896  View::OnBlur();
897  // We render differently when focused.
898  SchedulePaint();
899}
900
901void DownloadItemView::OpenDownload() {
902  DCHECK(!IsShowingWarningDialog());
903  // We're interested in how long it takes users to open downloads.  If they
904  // open downloads super quickly, we should be concerned about clickjacking.
905  UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
906                           base::Time::Now() - creation_time_);
907
908  UpdateAccessibleName();
909
910  // Calling download()->OpenDownload may delete this, so this must be
911  // the last thing we do.
912  download()->OpenDownload();
913}
914
915bool DownloadItemView::SubmitDownloadToFeedbackService() {
916#if defined(FULL_SAFE_BROWSING)
917  SafeBrowsingService* sb_service = g_browser_process->safe_browsing_service();
918  if (!sb_service)
919    return false;
920  safe_browsing::DownloadProtectionService* download_protection_service =
921      sb_service->download_protection_service();
922  if (!download_protection_service)
923    return false;
924  download_protection_service->feedback_service()->BeginFeedbackForDownload(
925      download());
926  // WARNING: we are deleted at this point.  Don't access 'this'.
927  return true;
928#else
929  NOTREACHED();
930  return false;
931#endif
932}
933
934void DownloadItemView::PossiblySubmitDownloadToFeedbackService(bool enabled) {
935  if (!enabled || !SubmitDownloadToFeedbackService())
936    download()->Remove();
937  // WARNING: 'this' is deleted at this point. Don't access 'this'.
938}
939
940void DownloadItemView::LoadIcon() {
941  IconManager* im = g_browser_process->icon_manager();
942  last_download_item_path_ = download()->GetTargetFilePath();
943  im->LoadIcon(last_download_item_path_,
944               IconLoader::SMALL,
945               base::Bind(&DownloadItemView::OnExtractIconComplete,
946                          base::Unretained(this)),
947               &cancelable_task_tracker_);
948}
949
950void DownloadItemView::LoadIconIfItemPathChanged() {
951  base::FilePath current_download_path = download()->GetTargetFilePath();
952  if (last_download_item_path_ == current_download_path)
953    return;
954
955  LoadIcon();
956}
957
958void DownloadItemView::UpdateColorsFromTheme() {
959  if (dangerous_download_label_ && GetThemeProvider()) {
960    dangerous_download_label_->SetEnabledColor(
961        GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT));
962  }
963}
964
965void DownloadItemView::ShowContextMenuImpl(const gfx::Point& p,
966                                           ui::MenuSourceType source_type) {
967  gfx::Point point = p;
968  gfx::Size size;
969
970  // Similar hack as in MenuButton.
971  // We're about to show the menu from a mouse press. By showing from the
972  // mouse press event we block RootView in mouse dispatching. This also
973  // appears to cause RootView to get a mouse pressed BEFORE the mouse
974  // release is seen, which means RootView sends us another mouse press no
975  // matter where the user pressed. To force RootView to recalculate the
976  // mouse target during the mouse press we explicitly set the mouse handler
977  // to NULL.
978  static_cast<views::internal::RootView*>(GetWidget()->GetRootView())->
979      SetMouseHandler(NULL);
980
981  // If |is_mouse_gesture| is false, |p| is ignored. The menu is shown aligned
982  // to drop down arrow button.
983  if (source_type != ui::MENU_SOURCE_MOUSE &&
984      source_type != ui::MENU_SOURCE_TOUCH) {
985    drop_down_pressed_ = true;
986    SetState(NORMAL, PUSHED);
987    point.SetPoint(drop_down_x_left_, box_y_);
988    size.SetSize(drop_down_x_right_ - drop_down_x_left_, box_height_);
989  }
990  // Post a task to release the button.  When we call the Run method on the menu
991  // below, it runs an inner message loop that might cause us to be deleted.
992  // Posting a task with a WeakPtr lets us safely handle the button release.
993  base::MessageLoop::current()->PostNonNestableTask(
994      FROM_HERE,
995      base::Bind(&DownloadItemView::ReleaseDropDown,
996                 weak_ptr_factory_.GetWeakPtr()));
997  views::View::ConvertPointToScreen(this, &point);
998
999  if (!context_menu_.get()) {
1000    context_menu_.reset(
1001        new DownloadShelfContextMenuView(download(), shelf_->GetNavigator()));
1002  }
1003  context_menu_->Run(GetWidget()->GetTopLevelWidget(),
1004                     gfx::Rect(point, size), source_type);
1005  // We could be deleted now.
1006}
1007
1008void DownloadItemView::HandlePressEvent(const ui::LocatedEvent& event,
1009                                        bool active_event) {
1010  // The event should not activate us in dangerous mode.
1011  if (mode_ == DANGEROUS_MODE)
1012    return;
1013
1014  // Stop any completion animation.
1015  if (complete_animation_.get() && complete_animation_->is_animating())
1016    complete_animation_->End();
1017
1018  if (active_event) {
1019    if (InDropDownButtonXCoordinateRange(event.x())) {
1020      if (context_menu_.get()) {
1021        // Ignore two close clicks. This typically happens when the user clicks
1022        // the button to close the menu.
1023        base::TimeDelta delta =
1024            base::TimeTicks::Now() - context_menu_->close_time();
1025        if (delta.InMilliseconds() < views::kMinimumMsBetweenButtonClicks)
1026          return;
1027      }
1028      drop_down_pressed_ = true;
1029      SetState(NORMAL, PUSHED);
1030      // We are setting is_mouse_gesture to false when calling ShowContextMenu
1031      // so that the positioning of the context menu will be similar to a
1032      // keyboard invocation.  I.e. we want the menu to always be positioned
1033      // next to the drop down button instead of the next to the pointer.
1034      ShowContextMenuImpl(event.location(), ui::MENU_SOURCE_KEYBOARD);
1035      // Once called, it is possible that *this was deleted (e.g.: due to
1036      // invoking the 'Discard' action.)
1037    } else if (!IsShowingWarningDialog()) {
1038      SetState(PUSHED, NORMAL);
1039    }
1040  }
1041}
1042
1043void DownloadItemView::HandleClickEvent(const ui::LocatedEvent& event,
1044                                        bool active_event) {
1045  // Mouse should not activate us in dangerous mode.
1046  if (mode_ == DANGEROUS_MODE)
1047    return;
1048
1049  SetState(NORMAL, NORMAL);
1050
1051  if (!active_event ||
1052      InDropDownButtonXCoordinateRange(event.x()) ||
1053      IsShowingWarningDialog()) {
1054    return;
1055  }
1056
1057  // OpenDownload may delete this, so don't add any code after this line.
1058  OpenDownload();
1059}
1060
1061// Load an icon for the file type we're downloading, and animate any in progress
1062// download state.
1063void DownloadItemView::PaintImages(gfx::Canvas* canvas,
1064                                   const gfx::ImageSkia* top_image,
1065                                   const gfx::ImageSkia* center_image,
1066                                   const gfx::ImageSkia* bottom_image,
1067                                   int x, int y, int height, int width) {
1068  int middle_height = height - top_image->height() - bottom_image->height();
1069  // Draw the top.
1070  canvas->DrawImageInt(*top_image,
1071                       0, 0, top_image->width(), top_image->height(),
1072                       x, y, width, top_image->height(), false);
1073  y += top_image->height();
1074  // Draw the center.
1075  canvas->DrawImageInt(*center_image,
1076                       0, 0, center_image->width(), center_image->height(),
1077                       x, y, width, middle_height, false);
1078  y += middle_height;
1079  // Draw the bottom.
1080  canvas->DrawImageInt(*bottom_image,
1081                       0, 0, bottom_image->width(), bottom_image->height(),
1082                       x, y, width, bottom_image->height(), false);
1083}
1084
1085void DownloadItemView::SetState(State new_body_state, State new_drop_state) {
1086  // If we are showing a warning dialog, we don't change body state.
1087  if (IsShowingWarningDialog()) {
1088    new_body_state = NORMAL;
1089
1090    // Current body_state_ should always be NORMAL for warning dialogs.
1091    DCHECK_EQ(NORMAL, body_state_);
1092    // We shouldn't be calling SetState if we are in DANGEROUS_MODE.
1093    DCHECK_NE(DANGEROUS_MODE, mode_);
1094  }
1095  // Avoid extra SchedulePaint()s if the state is going to be the same.
1096  if (body_state_ == new_body_state && drop_down_state_ == new_drop_state)
1097    return;
1098
1099  AnimateStateTransition(body_state_, new_body_state,
1100                         body_hover_animation_.get());
1101  AnimateStateTransition(drop_down_state_, new_drop_state,
1102                         drop_hover_animation_.get());
1103  body_state_ = new_body_state;
1104  drop_down_state_ = new_drop_state;
1105  SchedulePaint();
1106}
1107
1108void DownloadItemView::ClearWarningDialog() {
1109  DCHECK(download()->GetDangerType() ==
1110         content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED);
1111  DCHECK(mode_ == DANGEROUS_MODE || mode_ == MALICIOUS_MODE);
1112
1113  mode_ = NORMAL_MODE;
1114  body_state_ = NORMAL;
1115  drop_down_state_ = NORMAL;
1116
1117  // Remove the views used by the warning dialog.
1118  if (save_button_) {
1119    RemoveChildView(save_button_);
1120    delete save_button_;
1121    save_button_ = NULL;
1122  }
1123  RemoveChildView(discard_button_);
1124  delete discard_button_;
1125  discard_button_ = NULL;
1126  RemoveChildView(dangerous_download_label_);
1127  delete dangerous_download_label_;
1128  dangerous_download_label_ = NULL;
1129  dangerous_download_label_sized_ = false;
1130  cached_button_size_.SetSize(0,0);
1131
1132  // Set the accessible name back to the status and filename instead of the
1133  // download warning.
1134  UpdateAccessibleName();
1135  UpdateDropDownButtonPosition();
1136
1137  // We need to load the icon now that the download has the real path.
1138  LoadIcon();
1139
1140  // Force the shelf to layout again as our size has changed.
1141  shelf_->Layout();
1142  shelf_->SchedulePaint();
1143
1144  TooltipTextChanged();
1145}
1146
1147void DownloadItemView::ShowWarningDialog() {
1148  DCHECK(mode_ != DANGEROUS_MODE && mode_ != MALICIOUS_MODE);
1149  time_download_warning_shown_ = base::Time::Now();
1150  content::DownloadDangerType danger_type = download()->GetDangerType();
1151  RecordDangerousDownloadWarningShown(danger_type);
1152#if defined(FULL_SAFE_BROWSING)
1153  if (model_.ShouldAllowDownloadFeedback()) {
1154    safe_browsing::DownloadFeedbackService::RecordEligibleDownloadShown(
1155        danger_type);
1156  }
1157#endif
1158  mode_ = model_.MightBeMalicious() ? MALICIOUS_MODE : DANGEROUS_MODE;
1159
1160  body_state_ = NORMAL;
1161  drop_down_state_ = NORMAL;
1162  if (mode_ == DANGEROUS_MODE) {
1163    save_button_ = new views::LabelButton(
1164        this, model_.GetWarningConfirmButtonText());
1165    save_button_->SetStyle(views::Button::STYLE_BUTTON);
1166    AddChildView(save_button_);
1167  }
1168  int discard_button_message = model_.IsMalicious() ?
1169      IDS_DISMISS_DOWNLOAD : IDS_DISCARD_DOWNLOAD;
1170  discard_button_ = new views::LabelButton(
1171      this, l10n_util::GetStringUTF16(discard_button_message));
1172  discard_button_->SetStyle(views::Button::STYLE_BUTTON);
1173  AddChildView(discard_button_);
1174
1175  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
1176  switch (danger_type) {
1177    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
1178    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
1179    case content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
1180    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
1181    case content::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED:
1182      warning_icon_ = rb.GetImageSkiaNamed(IDR_SAFEBROWSING_WARNING);
1183      break;
1184
1185    case content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS:
1186    case content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
1187    case content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED:
1188    case content::DOWNLOAD_DANGER_TYPE_MAX:
1189      NOTREACHED();
1190      // fallthrough
1191
1192    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
1193      warning_icon_ = rb.GetImageSkiaNamed(IDR_WARNING);
1194  }
1195  base::string16 dangerous_label =
1196      model_.GetWarningText(font_list_, kTextWidth);
1197  dangerous_download_label_ = new views::Label(dangerous_label);
1198  dangerous_download_label_->SetMultiLine(true);
1199  dangerous_download_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
1200  dangerous_download_label_->SetAutoColorReadabilityEnabled(false);
1201  AddChildView(dangerous_download_label_);
1202  SizeLabelToMinWidth();
1203  UpdateDropDownButtonPosition();
1204  TooltipTextChanged();
1205}
1206
1207gfx::Size DownloadItemView::GetButtonSize() const {
1208  DCHECK(discard_button_ && (mode_ == MALICIOUS_MODE || save_button_));
1209  gfx::Size size;
1210
1211  // We cache the size when successfully retrieved, not for performance reasons
1212  // but because if this DownloadItemView is being animated while the tab is
1213  // not showing, the native buttons are not parented and their preferred size
1214  // is 0, messing-up the layout.
1215  if (cached_button_size_.width() != 0)
1216    return cached_button_size_;
1217
1218  if (save_button_)
1219    size = save_button_->GetMinimumSize();
1220  gfx::Size discard_size = discard_button_->GetMinimumSize();
1221
1222  size.SetSize(std::max(size.width(), discard_size.width()),
1223               std::max(size.height(), discard_size.height()));
1224
1225  if (size.width() != 0)
1226    cached_button_size_ = size;
1227
1228  return size;
1229}
1230
1231// This method computes the minimum width of the label for displaying its text
1232// on 2 lines.  It just breaks the string in 2 lines on the spaces and keeps the
1233// configuration with minimum width.
1234void DownloadItemView::SizeLabelToMinWidth() {
1235  if (dangerous_download_label_sized_)
1236    return;
1237
1238  base::string16 label_text = dangerous_download_label_->text();
1239  base::TrimWhitespace(label_text, base::TRIM_ALL, &label_text);
1240  DCHECK_EQ(base::string16::npos, label_text.find('\n'));
1241
1242  // Make the label big so that GetPreferredSize() is not constrained by the
1243  // current width.
1244  dangerous_download_label_->SetBounds(0, 0, 1000, 1000);
1245
1246  // Use a const string from here. BreakIterator requies that text.data() not
1247  // change during its lifetime.
1248  const base::string16 original_text(label_text);
1249  // Using BREAK_WORD can work in most cases, but it can also break
1250  // lines where it should not. Using BREAK_LINE is safer although
1251  // slower for Chinese/Japanese. This is not perf-critical at all, though.
1252  base::i18n::BreakIterator iter(original_text,
1253                                 base::i18n::BreakIterator::BREAK_LINE);
1254  bool status = iter.Init();
1255  DCHECK(status);
1256
1257  base::string16 prev_text = original_text;
1258  gfx::Size size = dangerous_download_label_->GetPreferredSize();
1259  int min_width = size.width();
1260
1261  // Go through the string and try each line break (starting with no line break)
1262  // searching for the optimal line break position.  Stop if we find one that
1263  // yields one that is less than kDangerousTextWidth wide.  This is to prevent
1264  // a short string (e.g.: "This file is malicious") from being broken up
1265  // unnecessarily.
1266  while (iter.Advance() && min_width > kDangerousTextWidth) {
1267    size_t pos = iter.pos();
1268    if (pos >= original_text.length())
1269      break;
1270    base::string16 current_text = original_text;
1271    // This can be a low surrogate codepoint, but u_isUWhiteSpace will
1272    // return false and inserting a new line after a surrogate pair
1273    // is perfectly ok.
1274    base::char16 line_end_char = current_text[pos - 1];
1275    if (u_isUWhiteSpace(line_end_char))
1276      current_text.replace(pos - 1, 1, 1, base::char16('\n'));
1277    else
1278      current_text.insert(pos, 1, base::char16('\n'));
1279    dangerous_download_label_->SetText(current_text);
1280    size = dangerous_download_label_->GetPreferredSize();
1281
1282    // If the width is growing again, it means we passed the optimal width spot.
1283    if (size.width() > min_width) {
1284      dangerous_download_label_->SetText(prev_text);
1285      break;
1286    } else {
1287      min_width = size.width();
1288    }
1289    prev_text = current_text;
1290  }
1291
1292  dangerous_download_label_->SetBounds(0, 0, size.width(), size.height());
1293  dangerous_download_label_sized_ = true;
1294}
1295
1296void DownloadItemView::Reenable() {
1297  disabled_while_opening_ = false;
1298  SetEnabled(true);  // Triggers a repaint.
1299}
1300
1301void DownloadItemView::ReleaseDropDown() {
1302  drop_down_pressed_ = false;
1303  SetState(NORMAL, NORMAL);
1304}
1305
1306bool DownloadItemView::InDropDownButtonXCoordinateRange(int x) {
1307  if (x > drop_down_x_left_ && x < drop_down_x_right_)
1308    return true;
1309  return false;
1310}
1311
1312void DownloadItemView::UpdateAccessibleName() {
1313  base::string16 new_name;
1314  if (IsShowingWarningDialog()) {
1315    new_name = dangerous_download_label_->text();
1316  } else {
1317    new_name = status_text_ + base::char16(' ') +
1318        download()->GetFileNameToReportUser().LossyDisplayName();
1319  }
1320
1321  // If the name has changed, notify assistive technology that the name
1322  // has changed so they can announce it immediately.
1323  if (new_name != accessible_name_) {
1324    accessible_name_ = new_name;
1325    NotifyAccessibilityEvent(ui::AX_EVENT_TEXT_CHANGED, true);
1326  }
1327}
1328
1329void DownloadItemView::UpdateDropDownButtonPosition() {
1330  gfx::Size size = GetPreferredSize();
1331  if (base::i18n::IsRTL()) {
1332    // Drop down button is glued to the left of the download shelf.
1333    drop_down_x_left_ = 0;
1334    drop_down_x_right_ = normal_drop_down_image_set_.top->width();
1335  } else {
1336    // Drop down button is glued to the right of the download shelf.
1337    drop_down_x_left_ =
1338      size.width() - normal_drop_down_image_set_.top->width();
1339    drop_down_x_right_ = size.width();
1340  }
1341}
1342
1343void DownloadItemView::AnimateStateTransition(State from, State to,
1344                                              gfx::SlideAnimation* animation) {
1345  if (from == NORMAL && to == HOT) {
1346    animation->Show();
1347  } else if (from == HOT && to == NORMAL) {
1348    animation->Hide();
1349  } else if (from != to) {
1350    animation->Reset((to == HOT) ? 1.0 : 0.0);
1351  }
1352}
1353