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