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 52 namespace { 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: 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"; 70 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"; 77 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; 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 121 bool 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 132 const char kCrNSProgressUserDataKey[] = "CrNSProgressUserData"; 133 134 class 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 153 void 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 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)]; 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 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)]; 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 231 void DestroyNSProgress(content::DownloadItem* download, 232 CrNSProgressUserData* progress_data) { 233 download->RemoveUserData(&kCrNSProgressUserDataKey); 234 } 235 236 } // namespace 237 238 void 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