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/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" 14#include "url/gurl.h" 15 16namespace { 17 18// These are not the keys themselves; they are the names for dynamic lookup via 19// the ProgressString() function. 20 21// Public keys, SPI in 10.8, API in 10.9: 22NSString* const kNSProgressEstimatedTimeRemainingKeyName = 23 @"NSProgressEstimatedTimeRemainingKey"; 24NSString* const kNSProgressFileOperationKindDownloadingName = 25 @"NSProgressFileOperationKindDownloading"; 26NSString* const kNSProgressFileOperationKindKeyName = 27 @"NSProgressFileOperationKindKey"; 28NSString* const kNSProgressFileURLKeyName = 29 @"NSProgressFileURLKey"; 30NSString* const kNSProgressKindFileName = 31 @"NSProgressKindFile"; 32NSString* const kNSProgressThroughputKeyName = 33 @"NSProgressThroughputKey"; 34 35// Private keys, SPI in 10.8 and 10.9: 36// TODO(avi): Are any of these actually needed for the NSProgress integration? 37NSString* const kNSProgressFileDownloadingSourceURLKeyName = 38 @"NSProgressFileDownloadingSourceURLKey"; 39NSString* const kNSProgressFileLocationCanChangeKeyName = 40 @"NSProgressFileLocationCanChangeKey"; 41 42// Given an NSProgress string name (kNSProgress[...]Name above), looks up the 43// real symbol of that name from Foundation and returns it. 44NSString* ProgressString(NSString* string) { 45 static NSMutableDictionary* cache; 46 static CFBundleRef foundation; 47 if (!cache) { 48 cache = [[NSMutableDictionary alloc] init]; 49 foundation = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Foundation")); 50 } 51 52 NSString* result = [cache objectForKey:string]; 53 if (!result) { 54 NSString** ref = static_cast<NSString**>( 55 CFBundleGetDataPointerForName(foundation, 56 base::mac::NSToCFCast(string))); 57 if (ref) { 58 result = *ref; 59 [cache setObject:result forKey:string]; 60 } 61 } 62 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"))); 68 if (ref) { 69 result = *ref; 70 [cache setObject:result forKey:string]; 71 } 72 } 73 74 if (!result) { 75 // Huh. At least return a local copy of the expected string. 76 result = string; 77 NSString* const kKeySuffix = @"Key"; 78 if ([result hasSuffix:kKeySuffix]) 79 result = [result substringToIndex:[result length] - [kKeySuffix length]]; 80 } 81 82 return result; 83} 84 85bool NSProgressSupported() { 86 static bool supported; 87 static bool valid; 88 if (!valid) { 89 supported = NSClassFromString(@"NSProgress"); 90 valid = true; 91 } 92 93 return supported; 94} 95 96const char kCrNSProgressUserDataKey[] = "CrNSProgressUserData"; 97 98class CrNSProgressUserData : public base::SupportsUserData::Data { 99 public: 100 CrNSProgressUserData(NSProgress* progress, const base::FilePath& target) 101 : target_(target) { 102 progress_.reset(progress); 103 } 104 virtual ~CrNSProgressUserData() { 105 [progress_.get() unpublish]; 106 } 107 108 NSProgress* progress() const { return progress_.get(); } 109 base::FilePath target() const { return target_; } 110 void setTarget(const base::FilePath& target) { target_ = target; } 111 112 private: 113 base::scoped_nsobject<NSProgress> progress_; 114 base::FilePath target_; 115}; 116 117void UpdateAppIcon(int download_count, 118 bool progress_known, 119 float progress) { 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]; 125} 126 127void 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)]; 133 134 NSDictionary* user_info = @{ 135 ProgressString(kNSProgressFileLocationCanChangeKeyName) : @true, 136 ProgressString(kNSProgressFileOperationKindKeyName) : 137 ProgressString(kNSProgressFileOperationKindDownloadingName), 138 ProgressString(kNSProgressFileURLKeyName) : destination_url 139 }; 140 141 Class progress_class = NSClassFromString(@"NSProgress"); 142 NSProgress* progress = [progress_class performSelector:@selector(alloc)]; 143 progress = [progress performSelector:@selector(initWithParent:userInfo:) 144 withObject:nil 145 withObject:user_info]; 146 progress.kind = ProgressString(kNSProgressKindFileName); 147 148 if (source_url) { 149 [progress setUserInfoObject:source_url forKey: 150 ProgressString(kNSProgressFileDownloadingSourceURLKeyName)]; 151 } 152 153 progress.pausable = NO; 154 progress.cancellable = YES; 155 [progress setCancellationHandler:^{ 156 dispatch_async(dispatch_get_main_queue(), ^{ 157 download->Cancel(true); 158 }); 159 }]; 160 161 progress.totalUnitCount = download->GetTotalBytes(); 162 progress.completedUnitCount = download->GetReceivedBytes(); 163 164 [progress publish]; 165 166 download->SetUserData(&kCrNSProgressUserDataKey, 167 new CrNSProgressUserData(progress, destination_path)); 168} 169 170void 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)]; 177 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)]; 184 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)]; 192 } 193} 194 195void DestroyNSProgress(content::DownloadItem* download, 196 CrNSProgressUserData* progress_data) { 197 download->RemoveUserData(&kCrNSProgressUserDataKey); 198} 199 200} // namespace 201 202void DownloadStatusUpdater::UpdateAppIconDownloadProgress( 203 content::DownloadItem* download) { 204 205 // Always update overall progress. 206 207 float progress = 0; 208 int download_count = 0; 209 bool progress_known = GetProgress(&progress, &download_count); 210 UpdateAppIcon(download_count, progress_known, progress); 211 212 // Update NSProgress-based indicators. 213 214 if (NSProgressSupported()) { 215 CrNSProgressUserData* progress_data = static_cast<CrNSProgressUserData*>( 216 download->GetUserData(&kCrNSProgressUserDataKey)); 217 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()) { 225 if (!progress_data) 226 CreateNSProgress(download); 227 else 228 UpdateNSProgress(download, progress_data); 229 } else { 230 DestroyNSProgress(download, progress_data); 231 } 232 } 233 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]; 244 } 245 246 // Notify the Finder. 247 NSString* parent_path = [download_path stringByDeletingLastPathComponent]; 248 FNNotifyByPath( 249 reinterpret_cast<const UInt8*>([parent_path fileSystemRepresentation]), 250 kFNDirectoryModifiedMessage, 251 kNilOptions); 252 } 253} 254