- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / download / download_status_updater_mac.mm
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 }