| // 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/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<NSString**>( |
| 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 FilePath& target) |
| : target_(target) { |
| progress_.reset(progress); |
| } |
| virtual ~CrNSProgressUserData() { |
| [progress_.get() unpublish]; |
| } |
| |
| NSProgress* progress() const { return progress_.get(); } |
| FilePath target() const { return target_; } |
| void setTarget(const FilePath& target) { target_ = target; } |
| |
| private: |
| scoped_nsobject<NSProgress> progress_; |
| 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())]; |
| 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(); |
| |
| 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); |
| |
| if (download->GetFullPath().empty()) |
| return; |
| |
| // Update NSProgress-based indicators. |
| |
| if (NSProgressSupported()) { |
| CrNSProgressUserData* progress_data = static_cast<CrNSProgressUserData*>( |
| download->GetUserData(&kCrNSProgressUserDataKey)); |
| if (!progress_data) |
| CreateNSProgress(download); |
| else if (download->GetState() != content::DownloadItem::IN_PROGRESS) |
| DestroyNSProgress(download, progress_data); |
| else |
| UpdateNSProgress(download, progress_data); |
| } |
| |
| // Handle downloads that ended. |
| if (download->GetState() != content::DownloadItem::IN_PROGRESS) { |
| NSString* download_path = |
| base::mac::FilePathToNSString(download->GetFullPath()); |
| 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<const UInt8*>([parent_path fileSystemRepresentation]), |
| kFNDirectoryModifiedMessage, |
| kNilOptions); |
| } |
| } |