// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/download/download_status_updater.h" #include "base/mac/foundation_util.h" #include "base/memory/scoped_nsobject.h" #include "base/supports_user_data.h" #include "base/strings/sys_string_conversions.h" #include "content/public/browser/download_item.h" #import "chrome/browser/ui/cocoa/dock_icon.h" #include "googleurl/src/gurl.h" // --- Private 10.8 API for showing progress --- // rdar://12058866 http://www.openradar.me/12058866 namespace { NSString* const kNSProgressAppBundleIdentifierKey = @"NSProgressAppBundleIdentifierKey"; NSString* const kNSProgressEstimatedTimeKey = @"NSProgressEstimatedTimeKey"; NSString* const kNSProgressFileCompletedCountKey = @"NSProgressFileCompletedCountKey"; NSString* const kNSProgressFileContainerURLKey = @"NSProgressFileContainerURLKey"; NSString* const kNSProgressFileDownloadingSourceURLKey = @"NSProgressFileDownloadingSourceURLKey"; NSString* const kNSProgressFileIconKey = @"NSProgressFileIconKey"; NSString* const kNSProgressFileIconOriginalRectKey = @"NSProgressFileIconOriginalRectKey"; NSString* const kNSProgressFileLocationCanChangeKey = @"NSProgressFileLocationCanChangeKey"; NSString* const kNSProgressFileOperationKindAirDropping = @"NSProgressFileOperationKindAirDropping"; NSString* const kNSProgressFileOperationKindCopying = @"NSProgressFileOperationKindCopying"; NSString* const kNSProgressFileOperationKindDecompressingAfterDownloading = @"NSProgressFileOperationKindDecompressingAfterDownloading"; NSString* const kNSProgressFileOperationKindDownloading = @"NSProgressFileOperationKindDownloading"; NSString* const kNSProgressFileOperationKindEncrypting = @"NSProgressFileOperationKindEncrypting"; NSString* const kNSProgressFileOperationKindKey = @"NSProgressFileOperationKindKey"; NSString* const kNSProgressFileTotalCountKey = @"NSProgressFileTotalCountKey"; NSString* const kNSProgressFileURLKey = @"NSProgressFileURLKey"; NSString* const kNSProgressIsWaitingKey = @"NSProgressIsWaitingKey"; NSString* const kNSProgressKindFile = @"NSProgressKindFile"; NSString* const kNSProgressThroughputKey = @"NSProgressThroughputKey"; NSString* ProgressString(NSString* string) { static NSMutableDictionary* cache; static CFBundleRef foundation; if (!cache) { cache = [[NSMutableDictionary alloc] init]; foundation = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Foundation")); } NSString* result = [cache objectForKey:string]; if (!result) { NSString** ref = static_cast( CFBundleGetDataPointerForName(foundation, base::mac::NSToCFCast(string))); if (ref) { result = *ref; [cache setObject:result forKey:string]; } } if (!result) { // Huh. At least return a local copy of the expected string. result = string; NSString* const kKeySuffix = @"Key"; if ([result hasSuffix:kKeySuffix]) result = [result substringToIndex:[result length] - [kKeySuffix length]]; } return result; } } // namespace @interface NSProgress : NSObject - (id)initWithParent:(id)parent userInfo:(NSDictionary*)info; @property(copy) NSString* kind; - (void)unpublish; - (void)publish; - (void)setUserInfoObject:(id)object forKey:(NSString*)key; - (NSDictionary*)userInfo; @property(readonly) double fractionCompleted; // Set the totalUnitCount to -1 to indicate an indeterminate download. The dock // shows a non-filling progress bar; the Finder is lame and draws its progress // bar off the right side. @property(readonly, getter=isIndeterminate) BOOL indeterminate; @property long long completedUnitCount; @property long long totalUnitCount; // Pausing appears to be unimplemented in 10.8.0. - (void)pause; @property(readonly, getter=isPaused) BOOL paused; @property(getter=isPausable) BOOL pausable; - (void)setPausingHandler:(id)blockOfUnknownSignature; - (void)cancel; @property(readonly, getter=isCancelled) BOOL cancelled; @property(getter=isCancellable) BOOL cancellable; // Note that the cancellation handler block will be called on a random thread. - (void)setCancellationHandler:(void (^)())block; // Allows other applications to provide feedback as to whether the progress is // visible in that app. Note that the acknowledgement handler block will be // called on a random thread. // com.apple.dock => BOOL indicating whether the download target folder was // successfully "flown to" at the beginning of the download. // This primarily depends on whether the download target // folder is in the dock. Note that if the download target // folder is added or removed from the dock during the // duration of the download, it will not trigger a callback. // Note that if the "fly to the dock" keys were not set, the // callback's parameter will always be NO. // com.apple.Finder => always YES, no matter whether the download target // folder's window is open. - (void)handleAcknowledgementByAppWithBundleIdentifier:(NSString*)bundle usingBlock:(void (^)(BOOL success))block; @end // --- Private 10.8 API for showing progress --- namespace { bool NSProgressSupported() { static bool supported; static bool valid; if (!valid) { supported = NSClassFromString(@"NSProgress"); valid = true; } return supported; } const char kCrNSProgressUserDataKey[] = "CrNSProgressUserData"; class CrNSProgressUserData : public base::SupportsUserData::Data { public: CrNSProgressUserData(NSProgress* progress, const base::FilePath& target) : target_(target) { progress_.reset(progress); } virtual ~CrNSProgressUserData() { [progress_.get() unpublish]; } NSProgress* progress() const { return progress_.get(); } base::FilePath target() const { return target_; } void setTarget(const base::FilePath& target) { target_ = target; } private: scoped_nsobject progress_; base::FilePath target_; }; void UpdateAppIcon(int download_count, bool progress_known, float progress) { DockIcon* dock_icon = [DockIcon sharedDockIcon]; [dock_icon setDownloads:download_count]; [dock_icon setIndeterminate:!progress_known]; [dock_icon setProgress:progress]; [dock_icon updateIcon]; } void CreateNSProgress(content::DownloadItem* download) { NSURL* source_url = [NSURL URLWithString: base::SysUTF8ToNSString(download->GetURL().possibly_invalid_spec())]; base::FilePath destination_path = download->GetFullPath(); NSURL* destination_url = [NSURL fileURLWithPath: base::mac::FilePathToNSString(destination_path)]; // If there were an image to fly to the download folder in the dock, then // the keys in the userInfo to set would be: // - @"NSProgressFlyToImageKey" : NSImage // - kNSProgressFileIconOriginalRectKey : NSValue of NSRect in global coords NSDictionary* user_info = @{ ProgressString(kNSProgressFileLocationCanChangeKey) : @true, ProgressString(kNSProgressFileOperationKindKey) : ProgressString(kNSProgressFileOperationKindDownloading), ProgressString(kNSProgressFileURLKey) : destination_url }; Class progress_class = NSClassFromString(@"NSProgress"); NSProgress* progress = [progress_class performSelector:@selector(alloc)]; progress = [progress performSelector:@selector(initWithParent:userInfo:) withObject:nil withObject:user_info]; progress.kind = ProgressString(kNSProgressKindFile); if (source_url) { [progress setUserInfoObject:source_url forKey: ProgressString(kNSProgressFileDownloadingSourceURLKey)]; } progress.pausable = NO; progress.cancellable = YES; [progress setCancellationHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ download->Cancel(true); }); }]; progress.totalUnitCount = download->GetTotalBytes(); progress.completedUnitCount = download->GetReceivedBytes(); [progress publish]; download->SetUserData(&kCrNSProgressUserDataKey, new CrNSProgressUserData(progress, destination_path)); } void UpdateNSProgress(content::DownloadItem* download, CrNSProgressUserData* progress_data) { NSProgress* progress = progress_data->progress(); progress.totalUnitCount = download->GetTotalBytes(); progress.completedUnitCount = download->GetReceivedBytes(); base::FilePath download_path = download->GetFullPath(); if (progress_data->target() != download_path) { progress_data->setTarget(download_path); NSURL* download_url = [NSURL fileURLWithPath: base::mac::FilePathToNSString(download_path)]; [progress setUserInfoObject:download_url forKey:ProgressString(kNSProgressFileURLKey)]; } } void DestroyNSProgress(content::DownloadItem* download, CrNSProgressUserData* progress_data) { download->RemoveUserData(&kCrNSProgressUserDataKey); } } // namespace void DownloadStatusUpdater::UpdateAppIconDownloadProgress( content::DownloadItem* download) { // Always update overall progress. float progress = 0; int download_count = 0; bool progress_known = GetProgress(&progress, &download_count); UpdateAppIcon(download_count, progress_known, progress); // Update NSProgress-based indicators. if (NSProgressSupported()) { CrNSProgressUserData* progress_data = static_cast( download->GetUserData(&kCrNSProgressUserDataKey)); // Only show progress if the download is IN_PROGRESS and it hasn't been // renamed to its final name. Setting the progress after the final rename // results in the file being stuck in an in-progress state on the dock. See // http://crbug.com/166683. if (download->GetState() == content::DownloadItem::IN_PROGRESS && !download->GetFullPath().empty() && download->GetFullPath() != download->GetTargetFilePath()) { if (!progress_data) CreateNSProgress(download); else UpdateNSProgress(download, progress_data); } else { DestroyNSProgress(download, progress_data); } } // Handle downloads that ended. if (download->GetState() != content::DownloadItem::IN_PROGRESS && !download->GetTargetFilePath().empty()) { NSString* download_path = base::mac::FilePathToNSString(download->GetTargetFilePath()); if (download->GetState() == content::DownloadItem::COMPLETE) { // Bounce the dock icon. [[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.apple.DownloadFileFinished" object:download_path]; } // Notify the Finder. NSString* parent_path = [download_path stringByDeletingLastPathComponent]; FNNotifyByPath( reinterpret_cast([parent_path fileSystemRepresentation]), kFNDirectoryModifiedMessage, kNilOptions); } }