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/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 
     16 namespace {
     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:
     22 NSString* const kNSProgressEstimatedTimeRemainingKeyName =
     23     @"NSProgressEstimatedTimeRemainingKey";
     24 NSString* const kNSProgressFileOperationKindDownloadingName =
     25     @"NSProgressFileOperationKindDownloading";
     26 NSString* const kNSProgressFileOperationKindKeyName =
     27     @"NSProgressFileOperationKindKey";
     28 NSString* const kNSProgressFileURLKeyName =
     29     @"NSProgressFileURLKey";
     30 NSString* const kNSProgressKindFileName =
     31     @"NSProgressKindFile";
     32 NSString* 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?
     37 NSString* const kNSProgressFileDownloadingSourceURLKeyName =
     38     @"NSProgressFileDownloadingSourceURLKey";
     39 NSString* 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.
     44 NSString* 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 
     85 bool 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 
     96 const char kCrNSProgressUserDataKey[] = "CrNSProgressUserData";
     97 
     98 class 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 
    117 void 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 
    127 void 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 
    170 void 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 
    195 void DestroyNSProgress(content::DownloadItem* download,
    196                        CrNSProgressUserData* progress_data) {
    197   download->RemoveUserData(&kCrNSProgressUserDataKey);
    198 }
    199 
    200 }  // namespace
    201 
    202 void 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