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/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"
18 // These are not the keys themselves; they are the names for dynamic lookup via
19 // the ProgressString() function.
21 // Public keys, SPI in 10.8, API in 10.9:
22 NSString* const kNSProgressEstimatedTimeRemainingKeyName =
23 @"NSProgressEstimatedTimeRemainingKey";
24 NSString* const kNSProgressFileOperationKindDownloadingName =
25 @"NSProgressFileOperationKindDownloading";
26 NSString* const kNSProgressFileOperationKindKeyName =
27 @"NSProgressFileOperationKindKey";
28 NSString* const kNSProgressFileURLKeyName =
29 @"NSProgressFileURLKey";
30 NSString* const kNSProgressKindFileName =
31 @"NSProgressKindFile";
32 NSString* const kNSProgressThroughputKeyName =
33 @"NSProgressThroughputKey";
35 // Private keys, SPI in 10.8 and 10.9:
36 // TODO(avi): Are any of these actually needed for the NSProgress integration?
37 NSString* const kNSProgressFileDownloadingSourceURLKeyName =
38 @"NSProgressFileDownloadingSourceURLKey";
39 NSString* const kNSProgressFileLocationCanChangeKeyName =
40 @"NSProgressFileLocationCanChangeKey";
42 // Given an NSProgress string name (kNSProgress[...]Name above), looks up the
43 // real symbol of that name from Foundation and returns it.
44 NSString* ProgressString(NSString* string) {
45 static NSMutableDictionary* cache;
46 static CFBundleRef foundation;
48 cache = [[NSMutableDictionary alloc] init];
49 foundation = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Foundation"));
52 NSString* result = [cache objectForKey:string];
54 NSString** ref = static_cast<NSString**>(
55 CFBundleGetDataPointerForName(foundation,
56 base::mac::NSToCFCast(string)));
59 [cache setObject:result forKey:string];
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")));
70 [cache setObject:result forKey:string];
75 // Huh. At least return a local copy of the expected string.
77 NSString* const kKeySuffix = @"Key";
78 if ([result hasSuffix:kKeySuffix])
79 result = [result substringToIndex:[result length] - [kKeySuffix length]];
85 bool NSProgressSupported() {
86 static bool supported;
89 supported = NSClassFromString(@"NSProgress");
96 const char kCrNSProgressUserDataKey[] = "CrNSProgressUserData";
98 class CrNSProgressUserData : public base::SupportsUserData::Data {
100 CrNSProgressUserData(NSProgress* progress, const base::FilePath& target)
102 progress_.reset(progress);
104 virtual ~CrNSProgressUserData() {
105 [progress_.get() unpublish];
108 NSProgress* progress() const { return progress_.get(); }
109 base::FilePath target() const { return target_; }
110 void setTarget(const base::FilePath& target) { target_ = target; }
113 base::scoped_nsobject<NSProgress> progress_;
114 base::FilePath target_;
117 void UpdateAppIcon(int download_count,
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];
127 void 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)];
134 NSDictionary* user_info = @{
135 ProgressString(kNSProgressFileLocationCanChangeKeyName) : @true,
136 ProgressString(kNSProgressFileOperationKindKeyName) :
137 ProgressString(kNSProgressFileOperationKindDownloadingName),
138 ProgressString(kNSProgressFileURLKeyName) : destination_url
141 Class progress_class = NSClassFromString(@"NSProgress");
142 NSProgress* progress = [progress_class performSelector:@selector(alloc)];
143 progress = [progress performSelector:@selector(initWithParent:userInfo:)
145 withObject:user_info];
146 progress.kind = ProgressString(kNSProgressKindFileName);
149 [progress setUserInfoObject:source_url forKey:
150 ProgressString(kNSProgressFileDownloadingSourceURLKeyName)];
153 progress.pausable = NO;
154 progress.cancellable = YES;
155 [progress setCancellationHandler:^{
156 dispatch_async(dispatch_get_main_queue(), ^{
157 download->Cancel(true);
161 progress.totalUnitCount = download->GetTotalBytes();
162 progress.completedUnitCount = download->GetReceivedBytes();
166 download->SetUserData(&kCrNSProgressUserDataKey,
167 new CrNSProgressUserData(progress, destination_path));
170 void 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)];
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)];
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)];
195 void DestroyNSProgress(content::DownloadItem* download,
196 CrNSProgressUserData* progress_data) {
197 download->RemoveUserData(&kCrNSProgressUserDataKey);
202 void DownloadStatusUpdater::UpdateAppIconDownloadProgress(
203 content::DownloadItem* download) {
205 // Always update overall progress.
208 int download_count = 0;
209 bool progress_known = GetProgress(&progress, &download_count);
210 UpdateAppIcon(download_count, progress_known, progress);
212 // Update NSProgress-based indicators.
214 if (NSProgressSupported()) {
215 CrNSProgressUserData* progress_data = static_cast<CrNSProgressUserData*>(
216 download->GetUserData(&kCrNSProgressUserDataKey));
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()) {
226 CreateNSProgress(download);
228 UpdateNSProgress(download, progress_data);
230 DestroyNSProgress(download, progress_data);
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];
246 // Notify the Finder.
247 NSString* parent_path = [download_path stringByDeletingLastPathComponent];
249 reinterpret_cast<const UInt8*>([parent_path fileSystemRepresentation]),
250 kFNDirectoryModifiedMessage,