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.
5 #include "chrome/browser/download/download_status_updater.h"
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"
15 // NSProgress is public API in 10.9, but a version of it exists and is usable
18 #if !defined(MAC_OS_X_VERSION_10_9) || \
19 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9
21 @interface NSProgress : NSObject
23 - (instancetype)initWithParent:(NSProgress*)parentProgressOrNil
24 userInfo:(NSDictionary*)userInfoOrNil;
25 @property (copy) NSString* kind;
27 @property int64_t totalUnitCount;
28 @property int64_t completedUnitCount;
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);
39 - (void)setUserInfoObject:(id)objectOrNil forKey:(NSString*)key;
40 - (NSDictionary*)userInfo;
42 @property (readonly, getter=isIndeterminate) BOOL indeterminate;
43 @property (readonly) double fractionCompleted;
50 #endif // MAC_OS_X_VERSION_10_9
54 // These are not the keys themselves; they are the names for dynamic lookup via
55 // the ProgressString() function.
57 // Public keys, SPI in 10.8, API in 10.9:
58 NSString* const kNSProgressEstimatedTimeRemainingKeyName =
59 @"NSProgressEstimatedTimeRemainingKey";
60 NSString* const kNSProgressFileOperationKindDownloadingName =
61 @"NSProgressFileOperationKindDownloading";
62 NSString* const kNSProgressFileOperationKindKeyName =
63 @"NSProgressFileOperationKindKey";
64 NSString* const kNSProgressFileURLKeyName =
65 @"NSProgressFileURLKey";
66 NSString* const kNSProgressKindFileName =
67 @"NSProgressKindFile";
68 NSString* const kNSProgressThroughputKeyName =
69 @"NSProgressThroughputKey";
71 // Private keys, SPI in 10.8 and 10.9:
72 // TODO(avi): Are any of these actually needed for the NSProgress integration?
73 NSString* const kNSProgressFileDownloadingSourceURLKeyName =
74 @"NSProgressFileDownloadingSourceURLKey";
75 NSString* const kNSProgressFileLocationCanChangeKeyName =
76 @"NSProgressFileLocationCanChangeKey";
78 // Given an NSProgress string name (kNSProgress[...]Name above), looks up the
79 // real symbol of that name from Foundation and returns it.
80 NSString* ProgressString(NSString* string) {
81 static NSMutableDictionary* cache;
82 static CFBundleRef foundation;
84 cache = [[NSMutableDictionary alloc] init];
85 foundation = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Foundation"));
88 NSString* result = [cache objectForKey:string];
90 NSString** ref = static_cast<NSString**>(
91 CFBundleGetDataPointerForName(foundation,
92 base::mac::NSToCFCast(string)));
95 [cache setObject:result forKey:string];
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")));
106 [cache setObject:result forKey:string];
111 // Huh. At least return a local copy of the expected string.
113 NSString* const kKeySuffix = @"Key";
114 if ([result hasSuffix:kKeySuffix])
115 result = [result substringToIndex:[result length] - [kKeySuffix length]];
121 bool NSProgressSupported() {
122 static bool supported;
125 supported = NSClassFromString(@"NSProgress");
132 const char kCrNSProgressUserDataKey[] = "CrNSProgressUserData";
134 class CrNSProgressUserData : public base::SupportsUserData::Data {
136 CrNSProgressUserData(NSProgress* progress, const base::FilePath& target)
138 progress_.reset(progress);
140 virtual ~CrNSProgressUserData() {
141 [progress_.get() unpublish];
144 NSProgress* progress() const { return progress_.get(); }
145 base::FilePath target() const { return target_; }
146 void setTarget(const base::FilePath& target) { target_ = target; }
149 base::scoped_nsobject<NSProgress> progress_;
150 base::FilePath target_;
153 void UpdateAppIcon(int download_count,
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];
163 void 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)];
170 NSDictionary* user_info = @{
171 ProgressString(kNSProgressFileLocationCanChangeKeyName) : @true,
172 ProgressString(kNSProgressFileOperationKindKeyName) :
173 ProgressString(kNSProgressFileOperationKindDownloadingName),
174 ProgressString(kNSProgressFileURLKeyName) : destination_url
177 Class progress_class = NSClassFromString(@"NSProgress");
178 NSProgress* progress = [progress_class performSelector:@selector(alloc)];
179 progress = [progress performSelector:@selector(initWithParent:userInfo:)
181 withObject:user_info];
182 progress.kind = ProgressString(kNSProgressKindFileName);
185 [progress setUserInfoObject:source_url forKey:
186 ProgressString(kNSProgressFileDownloadingSourceURLKeyName)];
189 progress.pausable = NO;
190 progress.cancellable = YES;
191 [progress setCancellationHandler:^{
192 dispatch_async(dispatch_get_main_queue(), ^{
193 download->Cancel(true);
197 progress.totalUnitCount = download->GetTotalBytes();
198 progress.completedUnitCount = download->GetReceivedBytes();
202 download->SetUserData(&kCrNSProgressUserDataKey,
203 new CrNSProgressUserData(progress, destination_path));
206 void 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)];
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)];
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)];
231 void DestroyNSProgress(content::DownloadItem* download,
232 CrNSProgressUserData* progress_data) {
233 download->RemoveUserData(&kCrNSProgressUserDataKey);
238 void DownloadStatusUpdater::UpdateAppIconDownloadProgress(
239 content::DownloadItem* download) {
241 // Always update overall progress.
244 int download_count = 0;
245 bool progress_known = GetProgress(&progress, &download_count);
246 UpdateAppIcon(download_count, progress_known, progress);
248 // Update NSProgress-based indicators.
250 if (NSProgressSupported()) {
251 CrNSProgressUserData* progress_data = static_cast<CrNSProgressUserData*>(
252 download->GetUserData(&kCrNSProgressUserDataKey));
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()) {
262 CreateNSProgress(download);
264 UpdateNSProgress(download, progress_data);
266 DestroyNSProgress(download, progress_data);
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];
282 // Notify the Finder.
283 NSString* parent_path = [download_path stringByDeletingLastPathComponent];
285 reinterpret_cast<const UInt8*>([parent_path fileSystemRepresentation]),
286 kFNDirectoryModifiedMessage,