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