download_item_view.cc revision f8ee788a64d60abd8f2d742a5fdedde054ecd910
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::kSafeBrowsingExtendedReportingEnabled)) {
560      // Show dialog, because the dialog hasn't been shown before.
561      DownloadFeedbackDialogView::Show(
562          shelf_->get_parent()->GetNativeWindow(),
563          shelf_->browser()->profile(),
564          shelf_->GetNavigator(),
565          base::Bind(
566              &DownloadItemView::PossiblySubmitDownloadToFeedbackService,
567              weak_ptr_factory_.GetWeakPtr()));
568    } else {
569      PossiblySubmitDownloadToFeedbackService(
570          shelf_->browser()->profile()->GetPrefs()->GetBoolean(
571               prefs::kSafeBrowsingExtendedReportingEnabled));
572    }
573    return;
574  }
575  download()->Remove();
576}
577
578void DownloadItemView::AnimationProgressed(const gfx::Animation* animation) {
579  // We don't care if what animation (body button/drop button/complete),
580  // is calling back, as they all have to go through the same paint call.
581  SchedulePaint();
582}
583
584void DownloadItemView::OnPaint(gfx::Canvas* canvas) {
585  OnPaintBackground(canvas);
586  if (HasFocus())
587    canvas->DrawFocusRect(GetLocalBounds());
588}
589
590// The DownloadItemView can be in three major modes (NORMAL_MODE, DANGEROUS_MODE
591// and MALICIOUS_MODE).
592//
593// NORMAL_MODE: We are displaying an in-progress or completed download.
594// .-------------------------------+-.
595// | [icon] Filename               |v|
596// | [    ] Status                 | |
597// `-------------------------------+-'
598//  |  |                            \_ Drop down button. Invokes menu. Responds
599//  |  |                               to mouse. (NORMAL, HOT or PUSHED).
600//  |   \_ Icon is overlaid on top of in-progress animation.
601//   \_ Both the body and the drop down button respond to mouse hover and can be
602//      pushed (NORMAL, HOT or PUSHED).
603//
604// DANGEROUS_MODE: The file could be potentially dangerous.
605// .-------------------------------------------------------.
606// | [ ! ] [This type of file can  ]  [ Keep ] [ Discard ] |
607// | [   ] [destroy your computer..]  [      ] [         ] |
608// `-------------------------------------------------------'
609//  |  |    |                          |                 \_ No drop down button.
610//  |  |    |                           \_ Buttons are views::LabelButtons.
611//  |  |     \_ Text is in a label (dangerous_download_label_)
612//  |   \_ Warning icon.  No progress animation.
613//   \_ Body is static.  Doesn't respond to mouse hover or press. (NORMAL only)
614//
615// MALICIOUS_MODE: The file is known malware.
616// .---------------------------------------------+-.
617// | [ - ] [This file is malicious.] [ Discard ] |v|
618// | [   ] [                       ] [         ] | |-.
619// `---------------------------------------------+-' |
620//  |  |    |                         |            Drop down button. Responds to
621//  |  |    |                         |            mouse.(NORMAL, HOT or PUSHED)
622//  |  |    |                          \_ Button is a views::LabelButton.
623//  |  |     \_ Text is in a label (dangerous_download_label_)
624//  |   \_ Warning icon.  No progress animation.
625//   \_ Body is static.  Doesn't respond to mouse hover or press. (NORMAL only)
626//
627void DownloadItemView::OnPaintBackground(gfx::Canvas* canvas) {
628  BodyImageSet* body_image_set = NULL;
629  switch (mode_) {
630    case NORMAL_MODE:
631      if (body_state_ == PUSHED)
632        body_image_set = &pushed_body_image_set_;
633      else                      // NORMAL or HOT
634        body_image_set = &normal_body_image_set_;
635      break;
636    case DANGEROUS_MODE:
637      body_image_set = &dangerous_mode_body_image_set_;
638      break;
639    case MALICIOUS_MODE:
640      body_image_set = &malicious_mode_body_image_set_;
641      break;
642    default:
643      NOTREACHED();
644  }
645
646  DropDownImageSet* drop_down_image_set = NULL;
647  switch (mode_) {
648    case NORMAL_MODE:
649    case MALICIOUS_MODE:
650      if (drop_down_state_ == PUSHED)
651        drop_down_image_set = &pushed_drop_down_image_set_;
652      else                        // NORMAL or HOT
653        drop_down_image_set = &normal_drop_down_image_set_;
654      break;
655    case DANGEROUS_MODE:
656      // We don't use a drop down button for mode_ == DANGEROUS_MODE.  So we let
657      // drop_down_image_set == NULL.
658      break;
659    default:
660      NOTREACHED();
661  }
662
663  int center_width = width() - kLeftPadding -
664                     body_image_set->left->width() -
665                     body_image_set->right->width() -
666                     (drop_down_image_set ?
667                        normal_drop_down_image_set_.center->width() :
668                        0);
669
670  // May be caused by animation.
671  if (center_width <= 0)
672    return;
673
674  // Draw status before button image to effectively lighten text.  No status for
675  // warning dialogs.
676  if (!IsShowingWarningDialog()) {
677    if (!status_text_.empty()) {
678      int mirrored_x = GetMirroredXWithWidthInView(
679          DownloadShelf::kSmallProgressIconSize, kTextWidth);
680      // Add font_list_.height() to compensate for title, which is drawn later.
681      int y = box_y_ + kVerticalPadding + font_list_.GetHeight() +
682              kVerticalTextPadding;
683      SkColor file_name_color = GetThemeProvider()->GetColor(
684          ThemeProperties::COLOR_BOOKMARK_TEXT);
685      // If text is light-on-dark, lightening it alone will do nothing.
686      // Therefore we mute luminance a wee bit before drawing in this case.
687      if (color_utils::RelativeLuminance(file_name_color) > 0.5)
688          file_name_color = SkColorSetRGB(
689              static_cast<int>(kDownloadItemLuminanceMod *
690                               SkColorGetR(file_name_color)),
691              static_cast<int>(kDownloadItemLuminanceMod *
692                               SkColorGetG(file_name_color)),
693              static_cast<int>(kDownloadItemLuminanceMod *
694                               SkColorGetB(file_name_color)));
695      canvas->DrawStringRect(status_text_, font_list_, file_name_color,
696                             gfx::Rect(mirrored_x, y, kTextWidth,
697                                       font_list_.GetHeight()));
698    }
699  }
700
701  // Paint the background images.
702  int x = kLeftPadding;
703  canvas->Save();
704  if (base::i18n::IsRTL()) {
705    // Since we do not have the mirrored images for
706    // (hot_)body_image_set->top_left, (hot_)body_image_set->left,
707    // (hot_)body_image_set->bottom_left, and drop_down_image_set,
708    // for RTL UI, we flip the canvas to draw those images mirrored.
709    // Consequently, we do not need to mirror the x-axis of those images.
710    canvas->Translate(gfx::Vector2d(width(), 0));
711    canvas->Scale(-1, 1);
712  }
713  PaintImages(canvas,
714              body_image_set->top_left, body_image_set->left,
715              body_image_set->bottom_left,
716              x, box_y_, box_height_, body_image_set->top_left->width());
717  x += body_image_set->top_left->width();
718  PaintImages(canvas,
719              body_image_set->top, body_image_set->center,
720              body_image_set->bottom,
721              x, box_y_, box_height_, center_width);
722  x += center_width;
723  PaintImages(canvas,
724              body_image_set->top_right, body_image_set->right,
725              body_image_set->bottom_right,
726              x, box_y_, box_height_, body_image_set->top_right->width());
727
728  // Overlay our body hot state. Warning dialogs don't display body a hot state.
729  if (!IsShowingWarningDialog() &&
730      body_hover_animation_->GetCurrentValue() > 0) {
731    canvas->SaveLayerAlpha(
732        static_cast<int>(body_hover_animation_->GetCurrentValue() * 255));
733
734    int x = kLeftPadding;
735    PaintImages(canvas,
736                hot_body_image_set_.top_left, hot_body_image_set_.left,
737                hot_body_image_set_.bottom_left,
738                x, box_y_, box_height_, hot_body_image_set_.top_left->width());
739    x += body_image_set->top_left->width();
740    PaintImages(canvas,
741                hot_body_image_set_.top, hot_body_image_set_.center,
742                hot_body_image_set_.bottom,
743                x, box_y_, box_height_, center_width);
744    x += center_width;
745    PaintImages(canvas,
746                hot_body_image_set_.top_right, hot_body_image_set_.right,
747                hot_body_image_set_.bottom_right,
748                x, box_y_, box_height_,
749                hot_body_image_set_.top_right->width());
750    canvas->Restore();
751  }
752
753  x += body_image_set->top_right->width();
754
755  // Paint the drop-down.
756  if (drop_down_image_set) {
757    PaintImages(canvas,
758                drop_down_image_set->top, drop_down_image_set->center,
759                drop_down_image_set->bottom,
760                x, box_y_, box_height_, drop_down_image_set->top->width());
761
762    // Overlay our drop-down hot state.
763    if (drop_hover_animation_->GetCurrentValue() > 0) {
764      canvas->SaveLayerAlpha(
765          static_cast<int>(drop_hover_animation_->GetCurrentValue() * 255));
766
767      PaintImages(canvas,
768                  drop_down_image_set->top, drop_down_image_set->center,
769                  drop_down_image_set->bottom,
770                  x, box_y_, box_height_, drop_down_image_set->top->width());
771
772      canvas->Restore();
773    }
774  }
775
776  // Restore the canvas to avoid file name etc. text are drawn flipped.
777  // Consequently, the x-axis of following canvas->DrawXXX() method should be
778  // mirrored so the text and images are down in the right positions.
779  canvas->Restore();
780
781  // Print the text, left aligned and always print the file extension.
782  // Last value of x was the end of the right image, just before the button.
783  // Note that in dangerous mode we use a label (as the text is multi-line).
784  if (!IsShowingWarningDialog()) {
785    base::string16 filename;
786    if (!disabled_while_opening_) {
787      filename = gfx::ElideFilename(download()->GetFileNameToReportUser(),
788                                   font_list_, kTextWidth);
789    } else {
790      // First, Calculate the download status opening string width.
791      base::string16 status_string =
792          l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
793                                     base::string16());
794      int status_string_width = gfx::GetStringWidth(status_string, font_list_);
795      // Then, elide the file name.
796      base::string16 filename_string =
797          gfx::ElideFilename(download()->GetFileNameToReportUser(), font_list_,
798                            kTextWidth - status_string_width);
799      // Last, concat the whole string.
800      filename = l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_OPENING,
801                                            filename_string);
802    }
803
804    int mirrored_x = GetMirroredXWithWidthInView(
805        DownloadShelf::kSmallProgressIconSize, kTextWidth);
806    SkColor file_name_color = GetThemeProvider()->GetColor(
807        ThemeProperties::COLOR_BOOKMARK_TEXT);
808    int y =
809        box_y_ + (status_text_.empty() ?
810            ((box_height_ - font_list_.GetHeight()) / 2) : kVerticalPadding);
811
812    // Draw the file's name.
813    canvas->DrawStringRect(
814        filename, font_list_,
815        enabled() ? file_name_color : kFileNameDisabledColor,
816        gfx::Rect(mirrored_x, y, kTextWidth, font_list_.GetHeight()));
817  }
818
819  // Load the icon.
820  IconManager* im = g_browser_process->icon_manager();
821  gfx::Image* image = im->LookupIconFromFilepath(
822      download()->GetTargetFilePath(), IconLoader::SMALL);
823  const gfx::ImageSkia* icon = NULL;
824  if (IsShowingWarningDialog())
825    icon = warning_icon_;
826  else if (image)
827    icon = image->ToImageSkia();
828
829  // We count on the fact that the icon manager will cache the icons and if one
830  // is available, it will be cached here. We *don't* want to request the icon
831  // to be loaded here, since this will also get called if the icon can't be
832  // loaded, in which case LookupIcon will always be NULL. The loading will be
833  // triggered only when we think the status might change.
834  if (icon) {
835    if (!IsShowingWarningDialog()) {
836      DownloadItem::DownloadState state = download()->GetState();
837      if (state == DownloadItem::IN_PROGRESS) {
838        DownloadShelf::PaintDownloadProgress(canvas,
839                                             this,
840                                             0,
841                                             0,
842                                             progress_angle_,
843                                             model_.PercentComplete(),
844                                             DownloadShelf::SMALL);
845      } else if (complete_animation_.get() &&
846                 complete_animation_->is_animating()) {
847        if (state == DownloadItem::INTERRUPTED) {
848          DownloadShelf::PaintDownloadInterrupted(
849              canvas,
850              this,
851              0,
852              0,
853              complete_animation_->GetCurrentValue(),
854              DownloadShelf::SMALL);
855        } else {
856          DCHECK_EQ(DownloadItem::COMPLETE, state);
857          DownloadShelf::PaintDownloadComplete(
858              canvas,
859              this,
860              0,
861              0,
862              complete_animation_->GetCurrentValue(),
863              DownloadShelf::SMALL);
864        }
865      }
866    }
867
868    // Draw the icon image.
869    int icon_x, icon_y;
870
871    if (IsShowingWarningDialog()) {
872      icon_x = kLeftPadding + body_image_set->top_left->width();
873      icon_y = (height() - icon->height()) / 2;
874    } else {
875      icon_x = DownloadShelf::kSmallProgressIconOffset;
876      icon_y = DownloadShelf::kSmallProgressIconOffset;
877    }
878    icon_x = GetMirroredXWithWidthInView(icon_x, icon->width());
879    if (enabled()) {
880      canvas->DrawImageInt(*icon, icon_x, icon_y);
881    } else {
882      // Use an alpha to make the image look disabled.
883      SkPaint paint;
884      paint.setAlpha(120);
885      canvas->DrawImageInt(*icon, icon_x, icon_y, paint);
886    }
887  }
888}
889
890void DownloadItemView::OnFocus() {
891  View::OnFocus();
892  // We render differently when focused.
893  SchedulePaint();
894}
895
896void DownloadItemView::OnBlur() {
897  View::OnBlur();
898  // We render differently when focused.
899  SchedulePaint();
900}
901
902void DownloadItemView::OpenDownload() {
903  DCHECK(!IsShowingWarningDialog());
904  // We're interested in how long it takes users to open downloads.  If they
905  // open downloads super quickly, we should be concerned about clickjacking.
906  UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
907                           base::Time::Now() - creation_time_);
908
909  UpdateAccessibleName();
910
911  // Calling download()->OpenDownload may delete this, so this must be
912  // the last thing we do.
913  download()->OpenDownload();
914}
915
916bool DownloadItemView::SubmitDownloadToFeedbackService() {
917#if defined(FULL_SAFE_BROWSING)
918  SafeBrowsingService* sb_service = g_browser_process->safe_browsing_service();
919  if (!sb_service)
920    return false;
921  safe_browsing::DownloadProtectionService* download_protection_service =
922      sb_service->download_protection_service();
923  if (!download_protection_service)
924    return false;
925  download_protection_service->feedback_service()->BeginFeedbackForDownload(
926      download());
927  // WARNING: we are deleted at this point.  Don't access 'this'.
928  return true;
929#else
930  NOTREACHED();
931  return false;
932#endif
933}
934
935void DownloadItemView::PossiblySubmitDownloadToFeedbackService(bool enabled) {
936  if (!enabled || !SubmitDownloadToFeedbackService())
937    download()->Remove();
938  // WARNING: 'this' is deleted at this point. Don't access 'this'.
939}
940
941void DownloadItemView::LoadIcon() {
942  IconManager* im = g_browser_process->icon_manager();
943  last_download_item_path_ = download()->GetTargetFilePath();
944  im->LoadIcon(last_download_item_path_,
945               IconLoader::SMALL,
946               base::Bind(&DownloadItemView::OnExtractIconComplete,
947                          base::Unretained(this)),
948               &cancelable_task_tracker_);
949}
950
951void DownloadItemView::LoadIconIfItemPathChanged() {
952  base::FilePath current_download_path = download()->GetTargetFilePath();
953  if (last_download_item_path_ == current_download_path)
954    return;
955
956  LoadIcon();
957}
958
959void DownloadItemView::UpdateColorsFromTheme() {
960  if (dangerous_download_label_ && GetThemeProvider()) {
961    dangerous_download_label_->SetEnabledColor(
962        GetThemeProvider()->GetColor(ThemeProperties::COLOR_BOOKMARK_TEXT));
963  }
964}
965
966void DownloadItemView::ShowContextMenuImpl(const gfx::Point& p,
967                                           ui::MenuSourceType source_type) {
968  gfx::Point point = p;
969  gfx::Size size;
970
971  // Similar hack as in MenuButton.
972  // We're about to show the menu from a mouse press. By showing from the
973  // mouse press event we block RootView in mouse dispatching. This also
974  // appears to cause RootView to get a mouse pressed BEFORE the mouse
975  // release is seen, which means RootView sends us another mouse press no
976  // matter where the user pressed. To force RootView to recalculate the
977  // mouse target during the mouse press we explicitly set the mouse handler
978  // to NULL.
979  static_cast<views::internal::RootView*>(GetWidget()->GetRootView())->
980      SetMouseHandler(NULL);
981
982  // If |is_mouse_gesture| is false, |p| is ignored. The menu is shown aligned
983  // to drop down arrow button.
984  if (source_type != ui::MENU_SOURCE_MOUSE &&
985      source_type != ui::MENU_SOURCE_TOUCH) {
986    drop_down_pressed_ = true;
987    SetState(NORMAL, PUSHED);
988    point.SetPoint(drop_down_x_left_, box_y_);
989    size.SetSize(drop_down_x_right_ - drop_down_x_left_, box_height_);
990  }
991  // Post a task to release the button.  When we call the Run method on the menu
992  // below, it runs an inner message loop that might cause us to be deleted.
993  // Posting a task with a WeakPtr lets us safely handle the button release.
994  base::MessageLoop::current()->PostNonNestableTask(
995      FROM_HERE,
996      base::Bind(&DownloadItemView::ReleaseDropDown,
997                 weak_ptr_factory_.GetWeakPtr()));
998  views::View::ConvertPointToScreen(this, &point);
999
1000  if (!context_menu_.get()) {
1001    context_menu_.reset(
1002        new DownloadShelfContextMenuView(download(), shelf_->GetNavigator()));
1003  }
1004  context_menu_->Run(GetWidget()->GetTopLevelWidget(),
1005                     gfx::Rect(point, size), source_type);
1006  // We could be deleted now.
1007}
1008
1009void DownloadItemView::HandlePressEvent(const ui::LocatedEvent& event,
1010                                        bool active_event) {
1011  // The event should not activate us in dangerous mode.
1012  if (mode_ == DANGEROUS_MODE)
1013    return;
1014
1015  // Stop any completion animation.
1016  if (complete_animation_.get() && complete_animation_->is_animating())
1017    complete_animation_->End();
1018
1019  if (active_event) {
1020    if (InDropDownButtonXCoordinateRange(event.x())) {
1021      if (context_menu_.get()) {
1022        // Ignore two close clicks. This typically happens when the user clicks
1023        // the button to close the menu.
1024        base::TimeDelta delta =
1025            base::TimeTicks::Now() - context_menu_->close_time();
1026        if (delta.InMilliseconds() < views::kMinimumMsBetweenButtonClicks)
1027          return;
1028      }
1029      drop_down_pressed_ = true;
1030      SetState(NORMAL, PUSHED);
1031      // We are setting is_mouse_gesture to false when calling ShowContextMenu
1032      // so that the positioning of the context menu will be similar to a
1033      // keyboard invocation.  I.e. we want the menu to always be positioned
1034      // next to the drop down button instead of the next to the pointer.
1035      ShowContextMenuImpl(event.location(), ui::MENU_SOURCE_KEYBOARD);
1036      // Once called, it is possible that *this was deleted (e.g.: due to
1037      // invoking the 'Discard' action.)
1038    } else if (!IsShowingWarningDialog()) {
1039      SetState(PUSHED, NORMAL);
1040    }
1041  }
1042}
1043
1044void DownloadItemView::HandleClickEvent(const ui::LocatedEvent& event,
1045                                        bool active_event) {
1046  // Mouse should not activate us in dangerous mode.
1047  if (mode_ == DANGEROUS_MODE)
1048    return;
1049
1050  SetState(NORMAL, NORMAL);
1051
1052  if (!active_event ||
1053      InDropDownButtonXCoordinateRange(event.x()) ||
1054      IsShowingWarningDialog()) {
1055    return;
1056  }
1057
1058  // OpenDownload may delete this, so don't add any code after this line.
1059  OpenDownload();
1060}
1061
1062// Load an icon for the file type we're downloading, and animate any in progress
1063// download state.
1064void DownloadItemView::PaintImages(gfx::Canvas* canvas,
1065                                   const gfx::ImageSkia* top_image,
1066                                   const gfx::ImageSkia* center_image,
1067                                   const gfx::ImageSkia* bottom_image,
1068                                   int x, int y, int height, int width) {
1069  int middle_height = height - top_image->height() - bottom_image->height();
1070  // Draw the top.
1071  canvas->DrawImageInt(*top_image,
1072                       0, 0, top_image->width(), top_image->height(),
1073                       x, y, width, top_image->height(), false);
1074  y += top_image->height();
1075  // Draw the center.
1076  canvas->DrawImageInt(*center_image,
1077                       0, 0, center_image->width(), center_image->height(),
1078                       x, y, width, middle_height, false);
1079  y += middle_height;
1080  // Draw the bottom.
1081  canvas->DrawImageInt(*bottom_image,
1082                       0, 0, bottom_image->width(), bottom_image->height(),
1083                       x, y, width, bottom_image->height(), false);
1084}
1085
1086void DownloadItemView::SetState(State new_body_state, State new_drop_state) {
1087  // If we are showing a warning dialog, we don't change body state.
1088  if (IsShowingWarningDialog()) {
1089    new_body_state = NORMAL;
1090
1091    // Current body_state_ should always be NORMAL for warning dialogs.
1092    DCHECK_EQ(NORMAL, body_state_);
1093    // We shouldn't be calling SetState if we are in DANGEROUS_MODE.
1094    DCHECK_NE(DANGEROUS_MODE, mode_);
1095  }
1096  // Avoid extra SchedulePaint()s if the state is going to be the same.
1097  if (body_state_ == new_body_state && drop_down_state_ == new_drop_state)
1098    return;
1099
1100  AnimateStateTransition(body_state_, new_body_state,
1101                         body_hover_animation_.get());
1102  AnimateStateTransition(drop_down_state_, new_drop_state,
1103                         drop_hover_animation_.get());
1104  body_state_ = new_body_state;
1105  drop_down_state_ = new_drop_state;
1106  SchedulePaint();
1107}
1108
1109void DownloadItemView::ClearWarningDialog() {
1110  DCHECK(download()->GetDangerType() ==
1111         content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED);
1112  DCHECK(mode_ == DANGEROUS_MODE || mode_ == MALICIOUS_MODE);
1113
1114  mode_ = NORMAL_MODE;
1115  body_state_ = NORMAL;
1116  drop_down_state_ = NORMAL;
1117
1118  // Remove the views used by the warning dialog.
1119  if (save_button_) {
1120    RemoveChildView(save_button_);
1121    delete save_button_;
1122    save_button_ = NULL;
1123  }
1124  RemoveChildView(discard_button_);
1125  delete discard_button_;
1126  discard_button_ = NULL;
1127  RemoveChildView(dangerous_download_label_);
1128  delete dangerous_download_label_;
1129  dangerous_download_label_ = NULL;
1130  dangerous_download_label_sized_ = false;
1131  cached_button_size_.SetSize(0,0);
1132
1133  // Set the accessible name back to the status and filename instead of the
1134  // download warning.
1135  UpdateAccessibleName();
1136  UpdateDropDownButtonPosition();
1137
1138  // We need to load the icon now that the download has the real path.
1139  LoadIcon();
1140
1141  // Force the shelf to layout again as our size has changed.
1142  shelf_->Layout();
1143  shelf_->SchedulePaint();
1144
1145  TooltipTextChanged();
1146}
1147
1148void DownloadItemView::ShowWarningDialog() {
1149  DCHECK(mode_ != DANGEROUS_MODE && mode_ != MALICIOUS_MODE);
1150  time_download_warning_shown_ = base::Time::Now();
1151  content::DownloadDangerType danger_type = download()->GetDangerType();
1152  RecordDangerousDownloadWarningShown(danger_type);
1153#if defined(FULL_SAFE_BROWSING)
1154  if (model_.ShouldAllowDownloadFeedback()) {
1155    safe_browsing::DownloadFeedbackService::RecordEligibleDownloadShown(
1156        danger_type);
1157  }
1158#endif
1159  mode_ = model_.MightBeMalicious() ? MALICIOUS_MODE : DANGEROUS_MODE;
1160
1161  body_state_ = NORMAL;
1162  drop_down_state_ = NORMAL;
1163  if (mode_ == DANGEROUS_MODE) {
1164    save_button_ = new views::LabelButton(
1165        this, model_.GetWarningConfirmButtonText());
1166    save_button_->SetStyle(views::Button::STYLE_BUTTON);
1167    AddChildView(save_button_);
1168  }
1169  int discard_button_message = model_.IsMalicious() ?
1170      IDS_DISMISS_DOWNLOAD : IDS_DISCARD_DOWNLOAD;
1171  discard_button_ = new views::LabelButton(
1172      this, l10n_util::GetStringUTF16(discard_button_message));
1173  discard_button_->SetStyle(views::Button::STYLE_BUTTON);
1174  AddChildView(discard_button_);
1175
1176  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
1177  switch (danger_type) {
1178    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
1179    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
1180    case content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
1181    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
1182    case content::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED:
1183      warning_icon_ = rb.GetImageSkiaNamed(IDR_SAFEBROWSING_WARNING);
1184      break;
1185
1186    case content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS:
1187    case content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
1188    case content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED:
1189    case content::DOWNLOAD_DANGER_TYPE_MAX:
1190      NOTREACHED();
1191      // fallthrough
1192
1193    case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
1194      warning_icon_ = rb.GetImageSkiaNamed(IDR_WARNING);
1195  }
1196  base::string16 dangerous_label =
1197      model_.GetWarningText(font_list_, kTextWidth);
1198  dangerous_download_label_ = new views::Label(dangerous_label);
1199  dangerous_download_label_->SetMultiLine(true);
1200  dangerous_download_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
1201  dangerous_download_label_->SetAutoColorReadabilityEnabled(false);
1202  AddChildView(dangerous_download_label_);
1203  SizeLabelToMinWidth();
1204  UpdateDropDownButtonPosition();
1205  TooltipTextChanged();
1206}
1207
1208gfx::Size DownloadItemView::GetButtonSize() const {
1209  DCHECK(discard_button_ && (mode_ == MALICIOUS_MODE || save_button_));
1210  gfx::Size size;
1211
1212  // We cache the size when successfully retrieved, not for performance reasons
1213  // but because if this DownloadItemView is being animated while the tab is
1214  // not showing, the native buttons are not parented and their preferred size
1215  // is 0, messing-up the layout.
1216  if (cached_button_size_.width() != 0)
1217    return cached_button_size_;
1218
1219  if (save_button_)
1220    size = save_button_->GetMinimumSize();
1221  gfx::Size discard_size = discard_button_->GetMinimumSize();
1222
1223  size.SetSize(std::max(size.width(), discard_size.width()),
1224               std::max(size.height(), discard_size.height()));
1225
1226  if (size.width() != 0)
1227    cached_button_size_ = size;
1228
1229  return size;
1230}
1231
1232// This method computes the minimum width of the label for displaying its text
1233// on 2 lines.  It just breaks the string in 2 lines on the spaces and keeps the
1234// configuration with minimum width.
1235void DownloadItemView::SizeLabelToMinWidth() {
1236  if (dangerous_download_label_sized_)
1237    return;
1238
1239  base::string16 label_text = dangerous_download_label_->text();
1240  base::TrimWhitespace(label_text, base::TRIM_ALL, &label_text);
1241  DCHECK_EQ(base::string16::npos, label_text.find('\n'));
1242
1243  // Make the label big so that GetPreferredSize() is not constrained by the
1244  // current width.
1245  dangerous_download_label_->SetBounds(0, 0, 1000, 1000);
1246
1247  // Use a const string from here. BreakIterator requies that text.data() not
1248  // change during its lifetime.
1249  const base::string16 original_text(label_text);
1250  // Using BREAK_WORD can work in most cases, but it can also break
1251  // lines where it should not. Using BREAK_LINE is safer although
1252  // slower for Chinese/Japanese. This is not perf-critical at all, though.
1253  base::i18n::BreakIterator iter(original_text,
1254                                 base::i18n::BreakIterator::BREAK_LINE);
1255  bool status = iter.Init();
1256  DCHECK(status);
1257
1258  base::string16 prev_text = original_text;
1259  gfx::Size size = dangerous_download_label_->GetPreferredSize();
1260  int min_width = size.width();
1261
1262  // Go through the string and try each line break (starting with no line break)
1263  // searching for the optimal line break position.  Stop if we find one that
1264  // yields one that is less than kDangerousTextWidth wide.  This is to prevent
1265  // a short string (e.g.: "This file is malicious") from being broken up
1266  // unnecessarily.
1267  while (iter.Advance() && min_width > kDangerousTextWidth) {
1268    size_t pos = iter.pos();
1269    if (pos >= original_text.length())
1270      break;
1271    base::string16 current_text = original_text;
1272    // This can be a low surrogate codepoint, but u_isUWhiteSpace will
1273    // return false and inserting a new line after a surrogate pair
1274    // is perfectly ok.
1275    base::char16 line_end_char = current_text[pos - 1];
1276    if (u_isUWhiteSpace(line_end_char))
1277      current_text.replace(pos - 1, 1, 1, base::char16('\n'));
1278    else
1279      current_text.insert(pos, 1, base::char16('\n'));
1280    dangerous_download_label_->SetText(current_text);
1281    size = dangerous_download_label_->GetPreferredSize();
1282
1283    // If the width is growing again, it means we passed the optimal width spot.
1284    if (size.width() > min_width) {
1285      dangerous_download_label_->SetText(prev_text);
1286      break;
1287    } else {
1288      min_width = size.width();
1289    }
1290    prev_text = current_text;
1291  }
1292
1293  dangerous_download_label_->SetBounds(0, 0, size.width(), size.height());
1294  dangerous_download_label_sized_ = true;
1295}
1296
1297void DownloadItemView::Reenable() {
1298  disabled_while_opening_ = false;
1299  SetEnabled(true);  // Triggers a repaint.
1300}
1301
1302void DownloadItemView::ReleaseDropDown() {
1303  drop_down_pressed_ = false;
1304  SetState(NORMAL, NORMAL);
1305}
1306
1307bool DownloadItemView::InDropDownButtonXCoordinateRange(int x) {
1308  if (x > drop_down_x_left_ && x < drop_down_x_right_)
1309    return true;
1310  return false;
1311}
1312
1313void DownloadItemView::UpdateAccessibleName() {
1314  base::string16 new_name;
1315  if (IsShowingWarningDialog()) {
1316    new_name = dangerous_download_label_->text();
1317  } else {
1318    new_name = status_text_ + base::char16(' ') +
1319        download()->GetFileNameToReportUser().LossyDisplayName();
1320  }
1321
1322  // If the name has changed, notify assistive technology that the name
1323  // has changed so they can announce it immediately.
1324  if (new_name != accessible_name_) {
1325    accessible_name_ = new_name;
1326    NotifyAccessibilityEvent(ui::AX_EVENT_TEXT_CHANGED, true);
1327  }
1328}
1329
1330void DownloadItemView::UpdateDropDownButtonPosition() {
1331  gfx::Size size = GetPreferredSize();
1332  if (base::i18n::IsRTL()) {
1333    // Drop down button is glued to the left of the download shelf.
1334    drop_down_x_left_ = 0;
1335    drop_down_x_right_ = normal_drop_down_image_set_.top->width();
1336  } else {
1337    // Drop down button is glued to the right of the download shelf.
1338    drop_down_x_left_ =
1339      size.width() - normal_drop_down_image_set_.top->width();
1340    drop_down_x_right_ = size.width();
1341  }
1342}
1343
1344void DownloadItemView::AnimateStateTransition(State from, State to,
1345                                              gfx::SlideAnimation* animation) {
1346  if (from == NORMAL && to == HOT) {
1347    animation->Show();
1348  } else if (from == HOT && to == NORMAL) {
1349    animation->Hide();
1350  } else if (from != to) {
1351    animation->Reset((to == HOT) ? 1.0 : 0.0);
1352  }
1353}
1354