Home | History | Annotate | Download | only in download
      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