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