• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 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/download/download_status_updater.h"
6
7#include "base/mac/foundation_util.h"
8#include "base/mac/scoped_nsobject.h"
9#include "base/mac/sdk_forward_declarations.h"
10#include "base/strings/sys_string_conversions.h"
11#include "base/supports_user_data.h"
12#import "chrome/browser/ui/cocoa/dock_icon.h"
13#include "content/public/browser/download_item.h"
14#include "url/gurl.h"
15
16namespace {
17
18// These are not the keys themselves; they are the names for dynamic lookup via
19// the ProgressString() function.
20
21// Public keys, SPI in 10.8, API in 10.9:
22NSString* const kNSProgressEstimatedTimeRemainingKeyName =
23    @"NSProgressEstimatedTimeRemainingKey";
24NSString* const kNSProgressFileOperationKindDownloadingName =
25    @"NSProgressFileOperationKindDownloading";
26NSString* const kNSProgressFileOperationKindKeyName =
27    @"NSProgressFileOperationKindKey";
28NSString* const kNSProgressFileURLKeyName =
29    @"NSProgressFileURLKey";
30NSString* const kNSProgressKindFileName =
31    @"NSProgressKindFile";
32NSString* const kNSProgressThroughputKeyName =
33    @"NSProgressThroughputKey";
34
35// Private keys, SPI in 10.8 and 10.9:
36// TODO(avi): Are any of these actually needed for the NSProgress integration?
37NSString* const kNSProgressFileDownloadingSourceURLKeyName =
38    @"NSProgressFileDownloadingSourceURLKey";
39NSString* const kNSProgressFileLocationCanChangeKeyName =
40    @"NSProgressFileLocationCanChangeKey";
41
42// Given an NSProgress string name (kNSProgress[...]Name above), looks up the
43// real symbol of that name from Foundation and returns it.
44NSString* ProgressString(NSString* string) {
45  static NSMutableDictionary* cache;
46  static CFBundleRef foundation;
47  if (!cache) {
48    cache = [[NSMutableDictionary alloc] init];
49    foundation = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Foundation"));
50  }
51
52  NSString* result = [cache objectForKey:string];
53  if (!result) {
54    NSString** ref = static_cast<NSString**>(
55        CFBundleGetDataPointerForName(foundation,
56                                      base::mac::NSToCFCast(string)));
57    if (ref) {
58      result = *ref;
59      [cache setObject:result forKey:string];
60    }
61  }
62
63  if (!result && string == kNSProgressEstimatedTimeRemainingKeyName) {
64    // Perhaps this is 10.8; try the old name of this key.
65    NSString** ref = static_cast<NSString**>(
66        CFBundleGetDataPointerForName(foundation,
67                                      CFSTR("NSProgressEstimatedTimeKey")));
68    if (ref) {
69      result = *ref;
70      [cache setObject:result forKey:string];
71    }
72  }
73
74  if (!result) {
75    // Huh. At least return a local copy of the expected string.
76    result = string;
77    NSString* const kKeySuffix = @"Key";
78    if ([result hasSuffix:kKeySuffix])
79      result = [result substringToIndex:[result length] - [kKeySuffix length]];
80  }
81
82  return result;
83}
84
85bool NSProgressSupported() {
86  static bool supported;
87  static bool valid;
88  if (!valid) {
89    supported = NSClassFromString(@"NSProgress");
90    valid = true;
91  }
92
93  return supported;
94}
95
96const char kCrNSProgressUserDataKey[] = "CrNSProgressUserData";
97
98class CrNSProgressUserData : public base::SupportsUserData::Data {
99 public:
100  CrNSProgressUserData(NSProgress* progress, const base::FilePath& target)
101      : target_(target) {
102    progress_.reset(progress);
103  }
104  virtual ~CrNSProgressUserData() {
105    [progress_.get() unpublish];
106  }
107
108  NSProgress* progress() const { return progress_.get(); }
109  base::FilePath target() const { return target_; }
110  void setTarget(const base::FilePath& target) { target_ = target; }
111
112 private:
113  base::scoped_nsobject<NSProgress> progress_;
114  base::FilePath target_;
115};
116
117void UpdateAppIcon(int download_count,
118                   bool progress_known,
119                   float progress) {
120  DockIcon* dock_icon = [DockIcon sharedDockIcon];
121  [dock_icon setDownloads:download_count];
122  [dock_icon setIndeterminate:!progress_known];
123  [dock_icon setProgress:progress];
124  [dock_icon updateIcon];
125}
126
127void CreateNSProgress(content::DownloadItem* download) {
128  NSURL* source_url = [NSURL URLWithString:
129      base::SysUTF8ToNSString(download->GetURL().possibly_invalid_spec())];
130  base::FilePath destination_path = download->GetFullPath();
131  NSURL* destination_url = [NSURL fileURLWithPath:
132      base::mac::FilePathToNSString(destination_path)];
133
134  NSDictionary* user_info = @{
135    ProgressString(kNSProgressFileLocationCanChangeKeyName) : @true,
136    ProgressString(kNSProgressFileOperationKindKeyName) :
137        ProgressString(kNSProgressFileOperationKindDownloadingName),
138    ProgressString(kNSProgressFileURLKeyName) : destination_url
139  };
140
141  Class progress_class = NSClassFromString(@"NSProgress");
142  NSProgress* progress = [progress_class performSelector:@selector(alloc)];
143  progress = [progress performSelector:@selector(initWithParent:userInfo:)
144                            withObject:nil
145                            withObject:user_info];
146  progress.kind = ProgressString(kNSProgressKindFileName);
147
148  if (source_url) {
149    [progress setUserInfoObject:source_url forKey:
150        ProgressString(kNSProgressFileDownloadingSourceURLKeyName)];
151  }
152
153  progress.pausable = NO;
154  progress.cancellable = YES;
155  [progress setCancellationHandler:^{
156      dispatch_async(dispatch_get_main_queue(), ^{
157          download->Cancel(true);
158      });
159  }];
160
161  progress.totalUnitCount = download->GetTotalBytes();
162  progress.completedUnitCount = download->GetReceivedBytes();
163
164  [progress publish];
165
166  download->SetUserData(&kCrNSProgressUserDataKey,
167                        new CrNSProgressUserData(progress, destination_path));
168}
169
170void UpdateNSProgress(content::DownloadItem* download,
171                      CrNSProgressUserData* progress_data) {
172  NSProgress* progress = progress_data->progress();
173  progress.totalUnitCount = download->GetTotalBytes();
174  progress.completedUnitCount = download->GetReceivedBytes();
175  [progress setUserInfoObject:@(download->CurrentSpeed())
176                       forKey:ProgressString(kNSProgressThroughputKeyName)];
177
178  base::TimeDelta time_remaining;
179  NSNumber* time_remaining_ns = nil;
180  if (download->TimeRemaining(&time_remaining))
181    time_remaining_ns = @(time_remaining.InSeconds());
182  [progress setUserInfoObject:time_remaining_ns
183               forKey:ProgressString(kNSProgressEstimatedTimeRemainingKeyName)];
184
185  base::FilePath download_path = download->GetFullPath();
186  if (progress_data->target() != download_path) {
187    progress_data->setTarget(download_path);
188    NSURL* download_url = [NSURL fileURLWithPath:
189        base::mac::FilePathToNSString(download_path)];
190    [progress setUserInfoObject:download_url
191                         forKey:ProgressString(kNSProgressFileURLKeyName)];
192  }
193}
194
195void DestroyNSProgress(content::DownloadItem* download,
196                       CrNSProgressUserData* progress_data) {
197  download->RemoveUserData(&kCrNSProgressUserDataKey);
198}
199
200}  // namespace
201
202void DownloadStatusUpdater::UpdateAppIconDownloadProgress(
203    content::DownloadItem* download) {
204
205  // Always update overall progress.
206
207  float progress = 0;
208  int download_count = 0;
209  bool progress_known = GetProgress(&progress, &download_count);
210  UpdateAppIcon(download_count, progress_known, progress);
211
212  // Update NSProgress-based indicators.
213
214  if (NSProgressSupported()) {
215    CrNSProgressUserData* progress_data = static_cast<CrNSProgressUserData*>(
216        download->GetUserData(&kCrNSProgressUserDataKey));
217
218    // Only show progress if the download is IN_PROGRESS and it hasn't been
219    // renamed to its final name. Setting the progress after the final rename
220    // results in the file being stuck in an in-progress state on the dock. See
221    // http://crbug.com/166683.
222    if (download->GetState() == content::DownloadItem::IN_PROGRESS &&
223        !download->GetFullPath().empty() &&
224        download->GetFullPath() != download->GetTargetFilePath()) {
225      if (!progress_data)
226        CreateNSProgress(download);
227      else
228        UpdateNSProgress(download, progress_data);
229    } else {
230      DestroyNSProgress(download, progress_data);
231    }
232  }
233
234  // Handle downloads that ended.
235  if (download->GetState() != content::DownloadItem::IN_PROGRESS &&
236      !download->GetTargetFilePath().empty()) {
237    NSString* download_path =
238        base::mac::FilePathToNSString(download->GetTargetFilePath());
239    if (download->GetState() == content::DownloadItem::COMPLETE) {
240      // Bounce the dock icon.
241      [[NSDistributedNotificationCenter defaultCenter]
242          postNotificationName:@"com.apple.DownloadFileFinished"
243                        object:download_path];
244    }
245
246    // Notify the Finder.
247    NSString* parent_path = [download_path stringByDeletingLastPathComponent];
248    FNNotifyByPath(
249        reinterpret_cast<const UInt8*>([parent_path fileSystemRepresentation]),
250        kFNDirectoryModifiedMessage,
251        kNilOptions);
252  }
253}
254