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