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#define _USE_MATH_DEFINES  // For VC++ to get M_PI. This has to be first.
6
7#include "chrome/browser/download/download_shelf.h"
8
9#include <cmath>
10
11#include "base/bind.h"
12#include "base/callback.h"
13#include "base/message_loop/message_loop.h"
14#include "base/strings/string_number_conversions.h"
15#include "chrome/browser/download/download_item_model.h"
16#include "chrome/browser/download/download_service.h"
17#include "chrome/browser/download/download_service_factory.h"
18#include "chrome/browser/download/download_started_animation.h"
19#include "chrome/browser/platform_util.h"
20#include "chrome/browser/profiles/profile.h"
21#include "chrome/browser/ui/browser.h"
22#include "chrome/browser/ui/tabs/tab_strip_model.h"
23#include "chrome/grit/locale_settings.h"
24#include "content/public/browser/browser_context.h"
25#include "content/public/browser/download_item.h"
26#include "content/public/browser/download_manager.h"
27#include "content/public/browser/web_contents.h"
28#include "grit/theme_resources.h"
29#include "ui/base/l10n/l10n_util.h"
30#include "ui/base/resource/resource_bundle.h"
31#include "ui/gfx/animation/animation.h"
32#include "ui/gfx/canvas.h"
33#include "ui/gfx/image/image_skia.h"
34
35using content::DownloadItem;
36
37namespace {
38
39// Delay before we show a transient download.
40const int64 kDownloadShowDelayInSeconds = 2;
41
42// Get the opacity based on |animation_progress|, with values in [0.0, 1.0].
43// Range of return value is [0, 255].
44int GetOpacity(double animation_progress) {
45  DCHECK(animation_progress >= 0 && animation_progress <= 1);
46
47  // How many times to cycle the complete animation. This should be an odd
48  // number so that the animation ends faded out.
49  static const int kCompleteAnimationCycles = 5;
50  double temp = animation_progress * kCompleteAnimationCycles * M_PI + M_PI_2;
51  temp = sin(temp) / 2 + 0.5;
52  return static_cast<int>(255.0 * temp);
53}
54
55} // namespace
56
57DownloadShelf::DownloadShelf()
58    : should_show_on_unhide_(false),
59      is_hidden_(false),
60      weak_ptr_factory_(this) {
61}
62
63DownloadShelf::~DownloadShelf() {}
64
65// static
66int DownloadShelf::GetBigProgressIconSize() {
67  static int big_progress_icon_size = 0;
68  if (big_progress_icon_size == 0) {
69    base::string16 locale_size_str =
70        l10n_util::GetStringUTF16(IDS_DOWNLOAD_BIG_PROGRESS_SIZE);
71    bool rc = base::StringToInt(locale_size_str, &big_progress_icon_size);
72    if (!rc || big_progress_icon_size < kBigProgressIconSize) {
73      NOTREACHED();
74      big_progress_icon_size = kBigProgressIconSize;
75    }
76  }
77
78  return big_progress_icon_size;
79}
80
81// static
82int DownloadShelf::GetBigProgressIconOffset() {
83  return (GetBigProgressIconSize() - kBigIconSize) / 2;
84}
85
86// Download progress painting --------------------------------------------------
87
88// Common images used for download progress animations. We load them once the
89// first time we do a progress paint, then reuse them as they are always the
90// same.
91gfx::ImageSkia* g_foreground_16 = NULL;
92gfx::ImageSkia* g_background_16 = NULL;
93gfx::ImageSkia* g_foreground_32 = NULL;
94gfx::ImageSkia* g_background_32 = NULL;
95
96// static
97void DownloadShelf::PaintCustomDownloadProgress(
98    gfx::Canvas* canvas,
99    const gfx::ImageSkia& background_image,
100    const gfx::ImageSkia& foreground_image,
101    int image_size,
102    const gfx::Rect& bounds,
103    int start_angle,
104    int percent_done) {
105  // Draw the background progress image.
106  canvas->DrawImageInt(background_image,
107                       bounds.x(),
108                       bounds.y());
109
110  // Layer the foreground progress image in an arc proportional to the download
111  // progress. The arc grows clockwise, starting in the midnight position, as
112  // the download progresses. However, if the download does not have known total
113  // size (the server didn't give us one), then we just spin an arc around until
114  // we're done.
115  float sweep_angle = 0.0;
116  float start_pos = static_cast<float>(kStartAngleDegrees);
117  if (percent_done < 0) {
118    sweep_angle = kUnknownAngleDegrees;
119    start_pos = static_cast<float>(start_angle);
120  } else if (percent_done > 0) {
121    sweep_angle = static_cast<float>(kMaxDegrees / 100.0 * percent_done);
122  }
123
124  // Set up an arc clipping region for the foreground image. Don't bother using
125  // a clipping region if it would round to 360 (really 0) degrees, since that
126  // would eliminate the foreground completely and be quite confusing (it would
127  // look like 0% complete when it should be almost 100%).
128  canvas->Save();
129  if (sweep_angle < static_cast<float>(kMaxDegrees - 1)) {
130    SkRect oval;
131    oval.set(SkIntToScalar(bounds.x()),
132             SkIntToScalar(bounds.y()),
133             SkIntToScalar(bounds.x() + image_size),
134             SkIntToScalar(bounds.y() + image_size));
135    SkPath path;
136    path.arcTo(oval,
137               SkFloatToScalar(start_pos),
138               SkFloatToScalar(sweep_angle), false);
139    path.lineTo(SkIntToScalar(bounds.x() + image_size / 2),
140                SkIntToScalar(bounds.y() + image_size / 2));
141
142    // gfx::Canvas::ClipPath does not provide for anti-aliasing.
143    canvas->sk_canvas()->clipPath(path, SkRegion::kIntersect_Op, true);
144  }
145
146  canvas->DrawImageInt(foreground_image,
147                       bounds.x(),
148                       bounds.y());
149  canvas->Restore();
150}
151
152// static
153void DownloadShelf::PaintDownloadProgress(
154    gfx::Canvas* canvas,
155    const BoundsAdjusterCallback& rtl_mirror,
156    int origin_x,
157    int origin_y,
158    int start_angle,
159    int percent_done,
160    PaintDownloadProgressSize size) {
161  // Load up our common images.
162  if (!g_background_16) {
163    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
164    g_foreground_16 = rb.GetImageSkiaNamed(IDR_DOWNLOAD_PROGRESS_FOREGROUND_16);
165    g_background_16 = rb.GetImageSkiaNamed(IDR_DOWNLOAD_PROGRESS_BACKGROUND_16);
166    g_foreground_32 = rb.GetImageSkiaNamed(IDR_DOWNLOAD_PROGRESS_FOREGROUND_32);
167    g_background_32 = rb.GetImageSkiaNamed(IDR_DOWNLOAD_PROGRESS_BACKGROUND_32);
168    DCHECK_EQ(g_foreground_16->width(), g_background_16->width());
169    DCHECK_EQ(g_foreground_16->height(), g_background_16->height());
170    DCHECK_EQ(g_foreground_32->width(), g_background_32->width());
171    DCHECK_EQ(g_foreground_32->height(), g_background_32->height());
172  }
173
174  gfx::ImageSkia* background =
175      (size == BIG) ? g_background_32 : g_background_16;
176  gfx::ImageSkia* foreground =
177      (size == BIG) ? g_foreground_32 : g_foreground_16;
178
179  const int kProgressIconSize =
180      (size == BIG) ? kBigProgressIconSize : kSmallProgressIconSize;
181
182  // We start by storing the bounds of the images so that it is easy to mirror
183  // the bounds if the UI layout is RTL.
184  gfx::Rect bounds(origin_x, origin_y,
185                   background->width(), background->height());
186
187  // Mirror the positions if necessary.
188  rtl_mirror.Run(&bounds);
189
190  // Draw the background progress image.
191  canvas->DrawImageInt(*background,
192                       bounds.x(),
193                       bounds.y());
194
195  PaintCustomDownloadProgress(canvas,
196                              *background,
197                              *foreground,
198                              kProgressIconSize,
199                              bounds,
200                              start_angle,
201                              percent_done);
202}
203
204// static
205void DownloadShelf::PaintDownloadComplete(
206    gfx::Canvas* canvas,
207    const BoundsAdjusterCallback& rtl_mirror,
208    int origin_x,
209    int origin_y,
210    double animation_progress,
211    PaintDownloadProgressSize size) {
212  // Load up our common images.
213  if (!g_foreground_16) {
214    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
215    g_foreground_16 = rb.GetImageSkiaNamed(IDR_DOWNLOAD_PROGRESS_FOREGROUND_16);
216    g_foreground_32 = rb.GetImageSkiaNamed(IDR_DOWNLOAD_PROGRESS_FOREGROUND_32);
217  }
218
219  gfx::ImageSkia* complete = (size == BIG) ? g_foreground_32 : g_foreground_16;
220
221  gfx::Rect complete_bounds(origin_x, origin_y,
222                            complete->width(), complete->height());
223  // Mirror the positions if necessary.
224  rtl_mirror.Run(&complete_bounds);
225
226  // Start at full opacity, then loop back and forth five times before ending
227  // at zero opacity.
228  canvas->DrawImageInt(*complete, complete_bounds.x(), complete_bounds.y(),
229                       GetOpacity(animation_progress));
230}
231
232// static
233void DownloadShelf::PaintDownloadInterrupted(
234    gfx::Canvas* canvas,
235    const BoundsAdjusterCallback& rtl_mirror,
236    int origin_x,
237    int origin_y,
238    double animation_progress,
239    PaintDownloadProgressSize size) {
240  // Load up our common images.
241  if (!g_foreground_16) {
242    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
243    g_foreground_16 = rb.GetImageSkiaNamed(IDR_DOWNLOAD_PROGRESS_FOREGROUND_16);
244    g_foreground_32 = rb.GetImageSkiaNamed(IDR_DOWNLOAD_PROGRESS_FOREGROUND_32);
245  }
246
247  gfx::ImageSkia* complete = (size == BIG) ? g_foreground_32 : g_foreground_16;
248
249  gfx::Rect complete_bounds(origin_x, origin_y,
250                            complete->width(), complete->height());
251  // Mirror the positions if necessary.
252  rtl_mirror.Run(&complete_bounds);
253
254  // Start at zero opacity, then loop back and forth five times before ending
255  // at full opacity.
256  canvas->DrawImageInt(*complete, complete_bounds.x(), complete_bounds.y(),
257                       GetOpacity(1.0 - animation_progress));
258}
259
260void DownloadShelf::AddDownload(DownloadItem* download) {
261  DCHECK(download);
262  if (DownloadItemModel(download).ShouldRemoveFromShelfWhenComplete()) {
263    // If we are going to remove the download from the shelf upon completion,
264    // wait a few seconds to see if it completes quickly. If it's a small
265    // download, then the user won't have time to interact with it.
266    base::MessageLoop::current()->PostDelayedTask(
267        FROM_HERE,
268        base::Bind(&DownloadShelf::ShowDownloadById,
269                   weak_ptr_factory_.GetWeakPtr(),
270                   download->GetId()),
271        GetTransientDownloadShowDelay());
272  } else {
273    ShowDownload(download);
274  }
275}
276
277void DownloadShelf::Show() {
278  if (is_hidden_) {
279    should_show_on_unhide_ = true;
280    return;
281  }
282  DoShow();
283}
284
285void DownloadShelf::Close(CloseReason reason) {
286  if (is_hidden_) {
287    should_show_on_unhide_ = false;
288    return;
289  }
290  DoClose(reason);
291}
292
293void DownloadShelf::Hide() {
294  if (is_hidden_)
295    return;
296  is_hidden_ = true;
297  if (IsShowing()) {
298    should_show_on_unhide_ = true;
299    DoClose(AUTOMATIC);
300  }
301}
302
303void DownloadShelf::Unhide() {
304  if (!is_hidden_)
305    return;
306  is_hidden_ = false;
307  if (should_show_on_unhide_) {
308    should_show_on_unhide_ = false;
309    DoShow();
310  }
311}
312
313base::TimeDelta DownloadShelf::GetTransientDownloadShowDelay() {
314  return base::TimeDelta::FromSeconds(kDownloadShowDelayInSeconds);
315}
316
317content::DownloadManager* DownloadShelf::GetDownloadManager() {
318  return content::BrowserContext::GetDownloadManager(browser()->profile());
319}
320
321void DownloadShelf::ShowDownload(DownloadItem* download) {
322  if (download->GetState() == DownloadItem::COMPLETE &&
323      DownloadItemModel(download).ShouldRemoveFromShelfWhenComplete())
324    return;
325  if (!DownloadServiceFactory::GetForBrowserContext(
326        download->GetBrowserContext())->IsShelfEnabled())
327    return;
328
329  if (is_hidden_)
330    Unhide();
331  Show();
332  DoAddDownload(download);
333
334  // browser() can be NULL for tests.
335  if (!browser())
336    return;
337
338  // Show the download started animation if:
339  // - Download started animation is enabled for this download. It is disabled
340  //   for "Save As" downloads and extension installs, for example.
341  // - The browser has an active visible WebContents. (browser isn't minimized,
342  //   or running under a test etc.)
343  // - Rich animations are enabled.
344  content::WebContents* shelf_tab =
345      browser()->tab_strip_model()->GetActiveWebContents();
346  if (DownloadItemModel(download).ShouldShowDownloadStartedAnimation() &&
347      shelf_tab &&
348      platform_util::IsVisible(shelf_tab->GetNativeView()) &&
349      gfx::Animation::ShouldRenderRichAnimation()) {
350    DownloadStartedAnimation::Show(shelf_tab);
351  }
352}
353
354void DownloadShelf::ShowDownloadById(int32 download_id) {
355  content::DownloadManager* download_manager = GetDownloadManager();
356  if (!download_manager)
357    return;
358
359  DownloadItem* download = download_manager->GetDownload(download_id);
360  if (!download)
361    return;
362
363  ShowDownload(download);
364}
365