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