|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 this.EXPORTED_SYMBOLS = [ |
|
10 "DownloadsCommon", |
|
11 ]; |
|
12 |
|
13 /** |
|
14 * Handles the Downloads panel shared methods and data access. |
|
15 * |
|
16 * This file includes the following constructors and global objects: |
|
17 * |
|
18 * DownloadsCommon |
|
19 * This object is exposed directly to the consumers of this JavaScript module, |
|
20 * and provides shared methods for all the instances of the user interface. |
|
21 * |
|
22 * DownloadsData |
|
23 * Retrieves the list of past and completed downloads from the underlying |
|
24 * Download Manager data, and provides asynchronous notifications allowing |
|
25 * to build a consistent view of the available data. |
|
26 * |
|
27 * DownloadsDataItem |
|
28 * Represents a single item in the list of downloads. This object either wraps |
|
29 * an existing nsIDownload from the Download Manager, or provides the same |
|
30 * information read directly from the downloads database, with the possibility |
|
31 * of querying the nsIDownload lazily, for performance reasons. |
|
32 * |
|
33 * DownloadsIndicatorData |
|
34 * This object registers itself with DownloadsData as a view, and transforms the |
|
35 * notifications it receives into overall status data, that is then broadcast to |
|
36 * the registered download status indicators. |
|
37 */ |
|
38 |
|
39 //////////////////////////////////////////////////////////////////////////////// |
|
40 //// Globals |
|
41 |
|
42 const Cc = Components.classes; |
|
43 const Ci = Components.interfaces; |
|
44 const Cu = Components.utils; |
|
45 const Cr = Components.results; |
|
46 |
|
47 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
48 Cu.import("resource://gre/modules/Services.jsm"); |
|
49 |
|
50 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
51 "resource://gre/modules/NetUtil.jsm"); |
|
52 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
53 "resource://gre/modules/PluralForm.jsm"); |
|
54 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", |
|
55 "resource://gre/modules/Downloads.jsm"); |
|
56 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", |
|
57 "resource://gre/modules/DownloadUIHelper.jsm"); |
|
58 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", |
|
59 "resource://gre/modules/DownloadUtils.jsm"); |
|
60 XPCOMUtils.defineLazyModuleGetter(this, "OS", |
|
61 "resource://gre/modules/osfile.jsm") |
|
62 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
|
63 "resource://gre/modules/PlacesUtils.jsm"); |
|
64 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", |
|
65 "resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
66 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", |
|
67 "resource:///modules/RecentWindow.jsm"); |
|
68 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
69 "resource://gre/modules/Promise.jsm"); |
|
70 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger", |
|
71 "resource:///modules/DownloadsLogger.jsm"); |
|
72 |
|
73 const nsIDM = Ci.nsIDownloadManager; |
|
74 |
|
75 const kDownloadsStringBundleUrl = |
|
76 "chrome://browser/locale/downloads/downloads.properties"; |
|
77 |
|
78 const kDownloadsStringsRequiringFormatting = { |
|
79 sizeWithUnits: true, |
|
80 shortTimeLeftSeconds: true, |
|
81 shortTimeLeftMinutes: true, |
|
82 shortTimeLeftHours: true, |
|
83 shortTimeLeftDays: true, |
|
84 statusSeparator: true, |
|
85 statusSeparatorBeforeNumber: true, |
|
86 fileExecutableSecurityWarning: true |
|
87 }; |
|
88 |
|
89 const kDownloadsStringsRequiringPluralForm = { |
|
90 otherDownloads2: true |
|
91 }; |
|
92 |
|
93 XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () { |
|
94 return Components.Constructor("@mozilla.org/file/local;1", |
|
95 "nsILocalFile", "initWithPath"); |
|
96 }); |
|
97 |
|
98 const kPartialDownloadSuffix = ".part"; |
|
99 |
|
100 const kPrefBranch = Services.prefs.getBranch("browser.download."); |
|
101 |
|
102 let PrefObserver = { |
|
103 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
|
104 Ci.nsISupportsWeakReference]), |
|
105 getPref: function PO_getPref(name) { |
|
106 try { |
|
107 switch (typeof this.prefs[name]) { |
|
108 case "boolean": |
|
109 return kPrefBranch.getBoolPref(name); |
|
110 } |
|
111 } catch (ex) { } |
|
112 return this.prefs[name]; |
|
113 }, |
|
114 observe: function PO_observe(aSubject, aTopic, aData) { |
|
115 if (this.prefs.hasOwnProperty(aData)) { |
|
116 return this[aData] = this.getPref(aData); |
|
117 } |
|
118 }, |
|
119 register: function PO_register(prefs) { |
|
120 this.prefs = prefs; |
|
121 kPrefBranch.addObserver("", this, true); |
|
122 for (let key in prefs) { |
|
123 let name = key; |
|
124 XPCOMUtils.defineLazyGetter(this, name, function () { |
|
125 return PrefObserver.getPref(name); |
|
126 }); |
|
127 } |
|
128 }, |
|
129 }; |
|
130 |
|
131 PrefObserver.register({ |
|
132 // prefName: defaultValue |
|
133 debug: false, |
|
134 animateNotifications: true |
|
135 }); |
|
136 |
|
137 |
|
138 //////////////////////////////////////////////////////////////////////////////// |
|
139 //// DownloadsCommon |
|
140 |
|
141 /** |
|
142 * This object is exposed directly to the consumers of this JavaScript module, |
|
143 * and provides shared methods for all the instances of the user interface. |
|
144 */ |
|
145 this.DownloadsCommon = { |
|
146 log: function DC_log(...aMessageArgs) { |
|
147 delete this.log; |
|
148 this.log = function DC_log(...aMessageArgs) { |
|
149 if (!PrefObserver.debug) { |
|
150 return; |
|
151 } |
|
152 DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs); |
|
153 } |
|
154 this.log.apply(this, aMessageArgs); |
|
155 }, |
|
156 |
|
157 error: function DC_error(...aMessageArgs) { |
|
158 delete this.error; |
|
159 this.error = function DC_error(...aMessageArgs) { |
|
160 if (!PrefObserver.debug) { |
|
161 return; |
|
162 } |
|
163 DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs); |
|
164 } |
|
165 this.error.apply(this, aMessageArgs); |
|
166 }, |
|
167 /** |
|
168 * Returns an object whose keys are the string names from the downloads string |
|
169 * bundle, and whose values are either the translated strings or functions |
|
170 * returning formatted strings. |
|
171 */ |
|
172 get strings() |
|
173 { |
|
174 let strings = {}; |
|
175 let sb = Services.strings.createBundle(kDownloadsStringBundleUrl); |
|
176 let enumerator = sb.getSimpleEnumeration(); |
|
177 while (enumerator.hasMoreElements()) { |
|
178 let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); |
|
179 let stringName = string.key; |
|
180 if (stringName in kDownloadsStringsRequiringFormatting) { |
|
181 strings[stringName] = function () { |
|
182 // Convert "arguments" to a real array before calling into XPCOM. |
|
183 return sb.formatStringFromName(stringName, |
|
184 Array.slice(arguments, 0), |
|
185 arguments.length); |
|
186 }; |
|
187 } else if (stringName in kDownloadsStringsRequiringPluralForm) { |
|
188 strings[stringName] = function (aCount) { |
|
189 // Convert "arguments" to a real array before calling into XPCOM. |
|
190 let formattedString = sb.formatStringFromName(stringName, |
|
191 Array.slice(arguments, 0), |
|
192 arguments.length); |
|
193 return PluralForm.get(aCount, formattedString); |
|
194 }; |
|
195 } else { |
|
196 strings[stringName] = string.value; |
|
197 } |
|
198 } |
|
199 delete this.strings; |
|
200 return this.strings = strings; |
|
201 }, |
|
202 |
|
203 /** |
|
204 * Generates a very short string representing the given time left. |
|
205 * |
|
206 * @param aSeconds |
|
207 * Value to be formatted. It represents the number of seconds, it must |
|
208 * be positive but does not need to be an integer. |
|
209 * |
|
210 * @return Formatted string, for example "30s" or "2h". The returned value is |
|
211 * maximum three characters long, at least in English. |
|
212 */ |
|
213 formatTimeLeft: function DC_formatTimeLeft(aSeconds) |
|
214 { |
|
215 // Decide what text to show for the time |
|
216 let seconds = Math.round(aSeconds); |
|
217 if (!seconds) { |
|
218 return ""; |
|
219 } else if (seconds <= 30) { |
|
220 return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds); |
|
221 } |
|
222 let minutes = Math.round(aSeconds / 60); |
|
223 if (minutes < 60) { |
|
224 return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes); |
|
225 } |
|
226 let hours = Math.round(minutes / 60); |
|
227 if (hours < 48) { // two days |
|
228 return DownloadsCommon.strings["shortTimeLeftHours"](hours); |
|
229 } |
|
230 let days = Math.round(hours / 24); |
|
231 return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99)); |
|
232 }, |
|
233 |
|
234 /** |
|
235 * Indicates whether we should show visual notification on the indicator |
|
236 * when a download event is triggered. |
|
237 */ |
|
238 get animateNotifications() |
|
239 { |
|
240 return PrefObserver.animateNotifications; |
|
241 }, |
|
242 |
|
243 /** |
|
244 * Get access to one of the DownloadsData or PrivateDownloadsData objects, |
|
245 * depending on the privacy status of the window in question. |
|
246 * |
|
247 * @param aWindow |
|
248 * The browser window which owns the download button. |
|
249 */ |
|
250 getData: function DC_getData(aWindow) { |
|
251 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { |
|
252 return PrivateDownloadsData; |
|
253 } else { |
|
254 return DownloadsData; |
|
255 } |
|
256 }, |
|
257 |
|
258 /** |
|
259 * Initializes the Downloads back-end and starts receiving events for both the |
|
260 * private and non-private downloads data objects. |
|
261 */ |
|
262 initializeAllDataLinks: function () { |
|
263 DownloadsData.initializeDataLink(); |
|
264 PrivateDownloadsData.initializeDataLink(); |
|
265 }, |
|
266 |
|
267 /** |
|
268 * Get access to one of the DownloadsIndicatorData or |
|
269 * PrivateDownloadsIndicatorData objects, depending on the privacy status of |
|
270 * the window in question. |
|
271 */ |
|
272 getIndicatorData: function DC_getIndicatorData(aWindow) { |
|
273 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { |
|
274 return PrivateDownloadsIndicatorData; |
|
275 } else { |
|
276 return DownloadsIndicatorData; |
|
277 } |
|
278 }, |
|
279 |
|
280 /** |
|
281 * Returns a reference to the DownloadsSummaryData singleton - creating one |
|
282 * in the process if one hasn't been instantiated yet. |
|
283 * |
|
284 * @param aWindow |
|
285 * The browser window which owns the download button. |
|
286 * @param aNumToExclude |
|
287 * The number of items on the top of the downloads list to exclude |
|
288 * from the summary. |
|
289 */ |
|
290 getSummary: function DC_getSummary(aWindow, aNumToExclude) |
|
291 { |
|
292 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { |
|
293 if (this._privateSummary) { |
|
294 return this._privateSummary; |
|
295 } |
|
296 return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude); |
|
297 } else { |
|
298 if (this._summary) { |
|
299 return this._summary; |
|
300 } |
|
301 return this._summary = new DownloadsSummaryData(false, aNumToExclude); |
|
302 } |
|
303 }, |
|
304 _summary: null, |
|
305 _privateSummary: null, |
|
306 |
|
307 /** |
|
308 * Given an iterable collection of DownloadDataItems, generates and returns |
|
309 * statistics about that collection. |
|
310 * |
|
311 * @param aDataItems An iterable collection of DownloadDataItems. |
|
312 * |
|
313 * @return Object whose properties are the generated statistics. Currently, |
|
314 * we return the following properties: |
|
315 * |
|
316 * numActive : The total number of downloads. |
|
317 * numPaused : The total number of paused downloads. |
|
318 * numScanning : The total number of downloads being scanned. |
|
319 * numDownloading : The total number of downloads being downloaded. |
|
320 * totalSize : The total size of all downloads once completed. |
|
321 * totalTransferred: The total amount of transferred data for these |
|
322 * downloads. |
|
323 * slowestSpeed : The slowest download rate. |
|
324 * rawTimeLeft : The estimated time left for the downloads to |
|
325 * complete. |
|
326 * percentComplete : The percentage of bytes successfully downloaded. |
|
327 */ |
|
328 summarizeDownloads: function DC_summarizeDownloads(aDataItems) |
|
329 { |
|
330 let summary = { |
|
331 numActive: 0, |
|
332 numPaused: 0, |
|
333 numScanning: 0, |
|
334 numDownloading: 0, |
|
335 totalSize: 0, |
|
336 totalTransferred: 0, |
|
337 // slowestSpeed is Infinity so that we can use Math.min to |
|
338 // find the slowest speed. We'll set this to 0 afterwards if |
|
339 // it's still at Infinity by the time we're done iterating all |
|
340 // dataItems. |
|
341 slowestSpeed: Infinity, |
|
342 rawTimeLeft: -1, |
|
343 percentComplete: -1 |
|
344 } |
|
345 |
|
346 for (let dataItem of aDataItems) { |
|
347 summary.numActive++; |
|
348 switch (dataItem.state) { |
|
349 case nsIDM.DOWNLOAD_PAUSED: |
|
350 summary.numPaused++; |
|
351 break; |
|
352 case nsIDM.DOWNLOAD_SCANNING: |
|
353 summary.numScanning++; |
|
354 break; |
|
355 case nsIDM.DOWNLOAD_DOWNLOADING: |
|
356 summary.numDownloading++; |
|
357 if (dataItem.maxBytes > 0 && dataItem.speed > 0) { |
|
358 let sizeLeft = dataItem.maxBytes - dataItem.currBytes; |
|
359 summary.rawTimeLeft = Math.max(summary.rawTimeLeft, |
|
360 sizeLeft / dataItem.speed); |
|
361 summary.slowestSpeed = Math.min(summary.slowestSpeed, |
|
362 dataItem.speed); |
|
363 } |
|
364 break; |
|
365 } |
|
366 // Only add to total values if we actually know the download size. |
|
367 if (dataItem.maxBytes > 0 && |
|
368 dataItem.state != nsIDM.DOWNLOAD_CANCELED && |
|
369 dataItem.state != nsIDM.DOWNLOAD_FAILED) { |
|
370 summary.totalSize += dataItem.maxBytes; |
|
371 summary.totalTransferred += dataItem.currBytes; |
|
372 } |
|
373 } |
|
374 |
|
375 if (summary.numActive != 0 && summary.totalSize != 0 && |
|
376 summary.numActive != summary.numScanning) { |
|
377 summary.percentComplete = (summary.totalTransferred / |
|
378 summary.totalSize) * 100; |
|
379 } |
|
380 |
|
381 if (summary.slowestSpeed == Infinity) { |
|
382 summary.slowestSpeed = 0; |
|
383 } |
|
384 |
|
385 return summary; |
|
386 }, |
|
387 |
|
388 /** |
|
389 * If necessary, smooths the estimated number of seconds remaining for one |
|
390 * or more downloads to complete. |
|
391 * |
|
392 * @param aSeconds |
|
393 * Current raw estimate on number of seconds left for one or more |
|
394 * downloads. This is a floating point value to help get sub-second |
|
395 * accuracy for current and future estimates. |
|
396 */ |
|
397 smoothSeconds: function DC_smoothSeconds(aSeconds, aLastSeconds) |
|
398 { |
|
399 // We apply an algorithm similar to the DownloadUtils.getTimeLeft function, |
|
400 // though tailored to a single time estimation for all downloads. We never |
|
401 // apply sommothing if the new value is less than half the previous value. |
|
402 let shouldApplySmoothing = aLastSeconds >= 0 && |
|
403 aSeconds > aLastSeconds / 2; |
|
404 if (shouldApplySmoothing) { |
|
405 // Apply hysteresis to favor downward over upward swings. Trust only 30% |
|
406 // of the new value if lower, and 10% if higher (exponential smoothing). |
|
407 let (diff = aSeconds - aLastSeconds) { |
|
408 aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff; |
|
409 } |
|
410 |
|
411 // If the new time is similar, reuse something close to the last time |
|
412 // left, but subtract a little to provide forward progress. |
|
413 let diff = aSeconds - aLastSeconds; |
|
414 let diffPercent = diff / aLastSeconds * 100; |
|
415 if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { |
|
416 aSeconds = aLastSeconds - (diff < 0 ? .4 : .2); |
|
417 } |
|
418 } |
|
419 |
|
420 // In the last few seconds of downloading, we are always subtracting and |
|
421 // never adding to the time left. Ensure that we never fall below one |
|
422 // second left until all downloads are actually finished. |
|
423 return aLastSeconds = Math.max(aSeconds, 1); |
|
424 }, |
|
425 |
|
426 /** |
|
427 * Opens a downloaded file. |
|
428 * If you've a dataItem, you should call dataItem.openLocalFile. |
|
429 * @param aFile |
|
430 * the downloaded file to be opened. |
|
431 * @param aMimeInfo |
|
432 * the mime type info object. May be null. |
|
433 * @param aOwnerWindow |
|
434 * the window with which this action is associated. |
|
435 */ |
|
436 openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) { |
|
437 if (!(aFile instanceof Ci.nsIFile)) |
|
438 throw new Error("aFile must be a nsIFile object"); |
|
439 if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) |
|
440 throw new Error("Invalid value passed for aMimeInfo"); |
|
441 if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) |
|
442 throw new Error("aOwnerWindow must be a dom-window object"); |
|
443 |
|
444 let promiseShouldLaunch; |
|
445 if (aFile.isExecutable()) { |
|
446 // We get a prompter for the provided window here, even though anchoring |
|
447 // to the most recently active window should work as well. |
|
448 promiseShouldLaunch = |
|
449 DownloadUIHelper.getPrompter(aOwnerWindow) |
|
450 .confirmLaunchExecutable(aFile.path); |
|
451 } else { |
|
452 promiseShouldLaunch = Promise.resolve(true); |
|
453 } |
|
454 |
|
455 promiseShouldLaunch.then(shouldLaunch => { |
|
456 if (!shouldLaunch) { |
|
457 return; |
|
458 } |
|
459 |
|
460 // Actually open the file. |
|
461 try { |
|
462 if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) { |
|
463 aMimeInfo.launchWithFile(aFile); |
|
464 return; |
|
465 } |
|
466 } |
|
467 catch(ex) { } |
|
468 |
|
469 // If either we don't have the mime info, or the preferred action failed, |
|
470 // attempt to launch the file directly. |
|
471 try { |
|
472 aFile.launch(); |
|
473 } |
|
474 catch(ex) { |
|
475 // If launch fails, try sending it through the system's external "file:" |
|
476 // URL handler. |
|
477 Cc["@mozilla.org/uriloader/external-protocol-service;1"] |
|
478 .getService(Ci.nsIExternalProtocolService) |
|
479 .loadUrl(NetUtil.newURI(aFile)); |
|
480 } |
|
481 }).then(null, Cu.reportError); |
|
482 }, |
|
483 |
|
484 /** |
|
485 * Show a donwloaded file in the system file manager. |
|
486 * If you have a dataItem, use dataItem.showLocalFile. |
|
487 * |
|
488 * @param aFile |
|
489 * a downloaded file. |
|
490 */ |
|
491 showDownloadedFile: function DC_showDownloadedFile(aFile) { |
|
492 if (!(aFile instanceof Ci.nsIFile)) |
|
493 throw new Error("aFile must be a nsIFile object"); |
|
494 try { |
|
495 // Show the directory containing the file and select the file. |
|
496 aFile.reveal(); |
|
497 } catch (ex) { |
|
498 // If reveal fails for some reason (e.g., it's not implemented on unix |
|
499 // or the file doesn't exist), try using the parent if we have it. |
|
500 let parent = aFile.parent; |
|
501 if (parent) { |
|
502 try { |
|
503 // Open the parent directory to show where the file should be. |
|
504 parent.launch(); |
|
505 } catch (ex) { |
|
506 // If launch also fails (probably because it's not implemented), let |
|
507 // the OS handler try to open the parent. |
|
508 Cc["@mozilla.org/uriloader/external-protocol-service;1"] |
|
509 .getService(Ci.nsIExternalProtocolService) |
|
510 .loadUrl(NetUtil.newURI(parent)); |
|
511 } |
|
512 } |
|
513 } |
|
514 } |
|
515 }; |
|
516 |
|
517 /** |
|
518 * Returns true if we are executing on Windows Vista or a later version. |
|
519 */ |
|
520 XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () { |
|
521 let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; |
|
522 if (os != "WINNT") { |
|
523 return false; |
|
524 } |
|
525 let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); |
|
526 return parseFloat(sysInfo.getProperty("version")) >= 6; |
|
527 }); |
|
528 |
|
529 //////////////////////////////////////////////////////////////////////////////// |
|
530 //// DownloadsData |
|
531 |
|
532 /** |
|
533 * Retrieves the list of past and completed downloads from the underlying |
|
534 * Download Manager data, and provides asynchronous notifications allowing to |
|
535 * build a consistent view of the available data. |
|
536 * |
|
537 * This object responds to real-time changes in the underlying Download Manager |
|
538 * data. For example, the deletion of one or more downloads is notified through |
|
539 * the nsIObserver interface, while any state or progress change is notified |
|
540 * through the nsIDownloadProgressListener interface. |
|
541 * |
|
542 * Note that using this object does not automatically start the Download Manager |
|
543 * service. Consumers will see an empty list of downloads until the service is |
|
544 * actually started. This is useful to display a neutral progress indicator in |
|
545 * the main browser window until the autostart timeout elapses. |
|
546 * |
|
547 * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton |
|
548 * objects, one accessing non-private downloads, and the other accessing private |
|
549 * ones. |
|
550 */ |
|
551 function DownloadsDataCtor(aPrivate) { |
|
552 this._isPrivate = aPrivate; |
|
553 |
|
554 // This Object contains all the available DownloadsDataItem objects, indexed by |
|
555 // their globally unique identifier. The identifiers of downloads that have |
|
556 // been removed from the Download Manager data are still present, however the |
|
557 // associated objects are replaced with the value "null". This is required to |
|
558 // prevent race conditions when populating the list asynchronously. |
|
559 this.dataItems = {}; |
|
560 |
|
561 // Array of view objects that should be notified when the available download |
|
562 // data changes. |
|
563 this._views = []; |
|
564 |
|
565 // Maps Download objects to DownloadDataItem objects. |
|
566 this._downloadToDataItemMap = new Map(); |
|
567 } |
|
568 |
|
569 DownloadsDataCtor.prototype = { |
|
570 /** |
|
571 * Starts receiving events for current downloads. |
|
572 */ |
|
573 initializeDataLink: function () |
|
574 { |
|
575 if (!this._dataLinkInitialized) { |
|
576 let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE |
|
577 : Downloads.PUBLIC); |
|
578 promiseList.then(list => list.addView(this)).then(null, Cu.reportError); |
|
579 this._dataLinkInitialized = true; |
|
580 } |
|
581 }, |
|
582 _dataLinkInitialized: false, |
|
583 |
|
584 /** |
|
585 * True if there are finished downloads that can be removed from the list. |
|
586 */ |
|
587 get canRemoveFinished() |
|
588 { |
|
589 for (let [, dataItem] of Iterator(this.dataItems)) { |
|
590 if (dataItem && !dataItem.inProgress) { |
|
591 return true; |
|
592 } |
|
593 } |
|
594 return false; |
|
595 }, |
|
596 |
|
597 /** |
|
598 * Asks the back-end to remove finished downloads from the list. |
|
599 */ |
|
600 removeFinished: function DD_removeFinished() |
|
601 { |
|
602 let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE |
|
603 : Downloads.PUBLIC); |
|
604 promiseList.then(list => list.removeFinished()) |
|
605 .then(null, Cu.reportError); |
|
606 }, |
|
607 |
|
608 ////////////////////////////////////////////////////////////////////////////// |
|
609 //// Integration with the asynchronous Downloads back-end |
|
610 |
|
611 onDownloadAdded: function (aDownload) |
|
612 { |
|
613 let dataItem = new DownloadsDataItem(aDownload); |
|
614 this._downloadToDataItemMap.set(aDownload, dataItem); |
|
615 this.dataItems[dataItem.downloadGuid] = dataItem; |
|
616 |
|
617 for (let view of this._views) { |
|
618 view.onDataItemAdded(dataItem, true); |
|
619 } |
|
620 |
|
621 this._updateDataItemState(dataItem); |
|
622 }, |
|
623 |
|
624 onDownloadChanged: function (aDownload) |
|
625 { |
|
626 let dataItem = this._downloadToDataItemMap.get(aDownload); |
|
627 if (!dataItem) { |
|
628 Cu.reportError("Download doesn't exist."); |
|
629 return; |
|
630 } |
|
631 |
|
632 this._updateDataItemState(dataItem); |
|
633 }, |
|
634 |
|
635 onDownloadRemoved: function (aDownload) |
|
636 { |
|
637 let dataItem = this._downloadToDataItemMap.get(aDownload); |
|
638 if (!dataItem) { |
|
639 Cu.reportError("Download doesn't exist."); |
|
640 return; |
|
641 } |
|
642 |
|
643 this._downloadToDataItemMap.delete(aDownload); |
|
644 this.dataItems[dataItem.downloadGuid] = null; |
|
645 for (let view of this._views) { |
|
646 view.onDataItemRemoved(dataItem); |
|
647 } |
|
648 }, |
|
649 |
|
650 /** |
|
651 * Updates the given data item and sends related notifications. |
|
652 */ |
|
653 _updateDataItemState: function (aDataItem) |
|
654 { |
|
655 let oldState = aDataItem.state; |
|
656 let wasInProgress = aDataItem.inProgress; |
|
657 let wasDone = aDataItem.done; |
|
658 |
|
659 aDataItem.updateFromDownload(); |
|
660 |
|
661 if (wasInProgress && !aDataItem.inProgress) { |
|
662 aDataItem.endTime = Date.now(); |
|
663 } |
|
664 |
|
665 if (oldState != aDataItem.state) { |
|
666 for (let view of this._views) { |
|
667 try { |
|
668 view.getViewItem(aDataItem).onStateChange(oldState); |
|
669 } catch (ex) { |
|
670 Cu.reportError(ex); |
|
671 } |
|
672 } |
|
673 |
|
674 // This state transition code should actually be located in a Downloads |
|
675 // API module (bug 941009). Moreover, the fact that state is stored as |
|
676 // annotations should be ideally hidden behind methods of |
|
677 // nsIDownloadHistory (bug 830415). |
|
678 if (!this._isPrivate && !aDataItem.inProgress) { |
|
679 try { |
|
680 let downloadMetaData = { state: aDataItem.state, |
|
681 endTime: aDataItem.endTime }; |
|
682 if (aDataItem.done) { |
|
683 downloadMetaData.fileSize = aDataItem.maxBytes; |
|
684 } |
|
685 |
|
686 PlacesUtils.annotations.setPageAnnotation( |
|
687 NetUtil.newURI(aDataItem.uri), "downloads/metaData", |
|
688 JSON.stringify(downloadMetaData), 0, |
|
689 PlacesUtils.annotations.EXPIRE_WITH_HISTORY); |
|
690 } catch (ex) { |
|
691 Cu.reportError(ex); |
|
692 } |
|
693 } |
|
694 } |
|
695 |
|
696 if (!aDataItem.newDownloadNotified) { |
|
697 aDataItem.newDownloadNotified = true; |
|
698 this._notifyDownloadEvent("start"); |
|
699 } |
|
700 |
|
701 if (!wasDone && aDataItem.done) { |
|
702 this._notifyDownloadEvent("finish"); |
|
703 } |
|
704 |
|
705 for (let view of this._views) { |
|
706 view.getViewItem(aDataItem).onProgressChange(); |
|
707 } |
|
708 }, |
|
709 |
|
710 ////////////////////////////////////////////////////////////////////////////// |
|
711 //// Registration of views |
|
712 |
|
713 /** |
|
714 * Adds an object to be notified when the available download data changes. |
|
715 * The specified object is initialized with the currently available downloads. |
|
716 * |
|
717 * @param aView |
|
718 * DownloadsView object to be added. This reference must be passed to |
|
719 * removeView before termination. |
|
720 */ |
|
721 addView: function DD_addView(aView) |
|
722 { |
|
723 this._views.push(aView); |
|
724 this._updateView(aView); |
|
725 }, |
|
726 |
|
727 /** |
|
728 * Removes an object previously added using addView. |
|
729 * |
|
730 * @param aView |
|
731 * DownloadsView object to be removed. |
|
732 */ |
|
733 removeView: function DD_removeView(aView) |
|
734 { |
|
735 let index = this._views.indexOf(aView); |
|
736 if (index != -1) { |
|
737 this._views.splice(index, 1); |
|
738 } |
|
739 }, |
|
740 |
|
741 /** |
|
742 * Ensures that the currently loaded data is added to the specified view. |
|
743 * |
|
744 * @param aView |
|
745 * DownloadsView object to be initialized. |
|
746 */ |
|
747 _updateView: function DD_updateView(aView) |
|
748 { |
|
749 // Indicate to the view that a batch loading operation is in progress. |
|
750 aView.onDataLoadStarting(); |
|
751 |
|
752 // Sort backwards by start time, ensuring that the most recent |
|
753 // downloads are added first regardless of their state. |
|
754 let loadedItemsArray = [dataItem |
|
755 for each (dataItem in this.dataItems) |
|
756 if (dataItem)]; |
|
757 loadedItemsArray.sort(function(a, b) b.startTime - a.startTime); |
|
758 loadedItemsArray.forEach( |
|
759 function (dataItem) aView.onDataItemAdded(dataItem, false) |
|
760 ); |
|
761 |
|
762 // Notify the view that all data is available. |
|
763 aView.onDataLoadCompleted(); |
|
764 }, |
|
765 |
|
766 ////////////////////////////////////////////////////////////////////////////// |
|
767 //// Notifications sent to the most recent browser window only |
|
768 |
|
769 /** |
|
770 * Set to true after the first download causes the downloads panel to be |
|
771 * displayed. |
|
772 */ |
|
773 get panelHasShownBefore() { |
|
774 try { |
|
775 return Services.prefs.getBoolPref("browser.download.panel.shown"); |
|
776 } catch (ex) { } |
|
777 return false; |
|
778 }, |
|
779 |
|
780 set panelHasShownBefore(aValue) { |
|
781 Services.prefs.setBoolPref("browser.download.panel.shown", aValue); |
|
782 return aValue; |
|
783 }, |
|
784 |
|
785 /** |
|
786 * Displays a new or finished download notification in the most recent browser |
|
787 * window, if one is currently available with the required privacy type. |
|
788 * |
|
789 * @param aType |
|
790 * Set to "start" for new downloads, "finish" for completed downloads. |
|
791 */ |
|
792 _notifyDownloadEvent: function DD_notifyDownloadEvent(aType) |
|
793 { |
|
794 DownloadsCommon.log("Attempting to notify that a new download has started or finished."); |
|
795 |
|
796 // Show the panel in the most recent browser window, if present. |
|
797 let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate }); |
|
798 if (!browserWin) { |
|
799 return; |
|
800 } |
|
801 |
|
802 if (this.panelHasShownBefore) { |
|
803 // For new downloads after the first one, don't show the panel |
|
804 // automatically, but provide a visible notification in the topmost |
|
805 // browser window, if the status indicator is already visible. |
|
806 DownloadsCommon.log("Showing new download notification."); |
|
807 browserWin.DownloadsIndicatorView.showEventNotification(aType); |
|
808 return; |
|
809 } |
|
810 this.panelHasShownBefore = true; |
|
811 browserWin.DownloadsPanel.showPanel(); |
|
812 } |
|
813 }; |
|
814 |
|
815 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() { |
|
816 return new DownloadsDataCtor(true); |
|
817 }); |
|
818 |
|
819 XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() { |
|
820 return new DownloadsDataCtor(false); |
|
821 }); |
|
822 |
|
823 //////////////////////////////////////////////////////////////////////////////// |
|
824 //// DownloadsDataItem |
|
825 |
|
826 /** |
|
827 * Represents a single item in the list of downloads. |
|
828 * |
|
829 * The endTime property is initialized to the current date and time. |
|
830 * |
|
831 * @param aDownload |
|
832 * The Download object with the current state. |
|
833 */ |
|
834 function DownloadsDataItem(aDownload) |
|
835 { |
|
836 this._download = aDownload; |
|
837 |
|
838 this.downloadGuid = "id:" + this._autoIncrementId; |
|
839 this.file = aDownload.target.path; |
|
840 this.target = OS.Path.basename(aDownload.target.path); |
|
841 this.uri = aDownload.source.url; |
|
842 this.endTime = Date.now(); |
|
843 |
|
844 this.updateFromDownload(); |
|
845 } |
|
846 |
|
847 DownloadsDataItem.prototype = { |
|
848 /** |
|
849 * The JavaScript API does not need identifiers for Download objects, so they |
|
850 * are generated sequentially for the corresponding DownloadDataItem. |
|
851 */ |
|
852 get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId, |
|
853 __lastId: 0, |
|
854 |
|
855 /** |
|
856 * Updates this object from the underlying Download object. |
|
857 */ |
|
858 updateFromDownload: function () |
|
859 { |
|
860 // Collapse state using the correct priority. |
|
861 if (this._download.succeeded) { |
|
862 this.state = nsIDM.DOWNLOAD_FINISHED; |
|
863 } else if (this._download.error && |
|
864 this._download.error.becauseBlockedByParentalControls) { |
|
865 this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL; |
|
866 } else if (this._download.error && |
|
867 this._download.error.becauseBlockedByReputationCheck) { |
|
868 this.state = nsIDM.DOWNLOAD_DIRTY; |
|
869 } else if (this._download.error) { |
|
870 this.state = nsIDM.DOWNLOAD_FAILED; |
|
871 } else if (this._download.canceled && this._download.hasPartialData) { |
|
872 this.state = nsIDM.DOWNLOAD_PAUSED; |
|
873 } else if (this._download.canceled) { |
|
874 this.state = nsIDM.DOWNLOAD_CANCELED; |
|
875 } else if (this._download.stopped) { |
|
876 this.state = nsIDM.DOWNLOAD_NOTSTARTED; |
|
877 } else { |
|
878 this.state = nsIDM.DOWNLOAD_DOWNLOADING; |
|
879 } |
|
880 |
|
881 this.referrer = this._download.source.referrer; |
|
882 this.startTime = this._download.startTime; |
|
883 this.currBytes = this._download.currentBytes; |
|
884 this.resumable = this._download.hasPartialData; |
|
885 this.speed = this._download.speed; |
|
886 |
|
887 if (this._download.succeeded) { |
|
888 // If the download succeeded, show the final size if available, otherwise |
|
889 // use the last known number of bytes transferred. The final size on disk |
|
890 // will be available when bug 941063 is resolved. |
|
891 this.maxBytes = this._download.hasProgress ? |
|
892 this._download.totalBytes : |
|
893 this._download.currentBytes; |
|
894 this.percentComplete = 100; |
|
895 } else if (this._download.hasProgress) { |
|
896 // If the final size and progress are known, use them. |
|
897 this.maxBytes = this._download.totalBytes; |
|
898 this.percentComplete = this._download.progress; |
|
899 } else { |
|
900 // The download final size and progress percentage is unknown. |
|
901 this.maxBytes = -1; |
|
902 this.percentComplete = -1; |
|
903 } |
|
904 }, |
|
905 |
|
906 /** |
|
907 * Indicates whether the download is proceeding normally, and not finished |
|
908 * yet. This includes paused downloads. When this property is true, the |
|
909 * "progress" property represents the current progress of the download. |
|
910 */ |
|
911 get inProgress() |
|
912 { |
|
913 return [ |
|
914 nsIDM.DOWNLOAD_NOTSTARTED, |
|
915 nsIDM.DOWNLOAD_QUEUED, |
|
916 nsIDM.DOWNLOAD_DOWNLOADING, |
|
917 nsIDM.DOWNLOAD_PAUSED, |
|
918 nsIDM.DOWNLOAD_SCANNING, |
|
919 ].indexOf(this.state) != -1; |
|
920 }, |
|
921 |
|
922 /** |
|
923 * This is true during the initial phases of a download, before the actual |
|
924 * download of data bytes starts. |
|
925 */ |
|
926 get starting() |
|
927 { |
|
928 return this.state == nsIDM.DOWNLOAD_NOTSTARTED || |
|
929 this.state == nsIDM.DOWNLOAD_QUEUED; |
|
930 }, |
|
931 |
|
932 /** |
|
933 * Indicates whether the download is paused. |
|
934 */ |
|
935 get paused() |
|
936 { |
|
937 return this.state == nsIDM.DOWNLOAD_PAUSED; |
|
938 }, |
|
939 |
|
940 /** |
|
941 * Indicates whether the download is in a final state, either because it |
|
942 * completed successfully or because it was blocked. |
|
943 */ |
|
944 get done() |
|
945 { |
|
946 return [ |
|
947 nsIDM.DOWNLOAD_FINISHED, |
|
948 nsIDM.DOWNLOAD_BLOCKED_PARENTAL, |
|
949 nsIDM.DOWNLOAD_BLOCKED_POLICY, |
|
950 nsIDM.DOWNLOAD_DIRTY, |
|
951 ].indexOf(this.state) != -1; |
|
952 }, |
|
953 |
|
954 /** |
|
955 * Indicates whether the download is finished and can be opened. |
|
956 */ |
|
957 get openable() |
|
958 { |
|
959 return this.state == nsIDM.DOWNLOAD_FINISHED; |
|
960 }, |
|
961 |
|
962 /** |
|
963 * Indicates whether the download stopped because of an error, and can be |
|
964 * resumed manually. |
|
965 */ |
|
966 get canRetry() |
|
967 { |
|
968 return this.state == nsIDM.DOWNLOAD_CANCELED || |
|
969 this.state == nsIDM.DOWNLOAD_FAILED; |
|
970 }, |
|
971 |
|
972 /** |
|
973 * Returns the nsILocalFile for the download target. |
|
974 * |
|
975 * @throws if the native path is not valid. This can happen if the same |
|
976 * profile is used on different platforms, for example if a native |
|
977 * Windows path is stored and then the item is accessed on a Mac. |
|
978 */ |
|
979 get localFile() |
|
980 { |
|
981 return this._getFile(this.file); |
|
982 }, |
|
983 |
|
984 /** |
|
985 * Returns the nsILocalFile for the partially downloaded target. |
|
986 * |
|
987 * @throws if the native path is not valid. This can happen if the same |
|
988 * profile is used on different platforms, for example if a native |
|
989 * Windows path is stored and then the item is accessed on a Mac. |
|
990 */ |
|
991 get partFile() |
|
992 { |
|
993 return this._getFile(this.file + kPartialDownloadSuffix); |
|
994 }, |
|
995 |
|
996 /** |
|
997 * Returns an nsILocalFile for aFilename. aFilename might be a file URL or |
|
998 * a native path. |
|
999 * |
|
1000 * @param aFilename the filename of the file to retrieve. |
|
1001 * @return an nsILocalFile for the file. |
|
1002 * @throws if the native path is not valid. This can happen if the same |
|
1003 * profile is used on different platforms, for example if a native |
|
1004 * Windows path is stored and then the item is accessed on a Mac. |
|
1005 * @note This function makes no guarantees about the file's existence - |
|
1006 * callers should check that the returned file exists. |
|
1007 */ |
|
1008 _getFile: function DDI__getFile(aFilename) |
|
1009 { |
|
1010 // The download database may contain targets stored as file URLs or native |
|
1011 // paths. This can still be true for previously stored items, even if new |
|
1012 // items are stored using their file URL. See also bug 239948 comment 12. |
|
1013 if (aFilename.startsWith("file:")) { |
|
1014 // Assume the file URL we obtained from the downloads database or from the |
|
1015 // "spec" property of the target has the UTF-8 charset. |
|
1016 let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL); |
|
1017 return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile); |
|
1018 } else { |
|
1019 // The downloads database contains a native path. Try to create a local |
|
1020 // file, though this may throw an exception if the path is invalid. |
|
1021 return new DownloadsLocalFileCtor(aFilename); |
|
1022 } |
|
1023 }, |
|
1024 |
|
1025 /** |
|
1026 * Open the target file for this download. |
|
1027 */ |
|
1028 openLocalFile: function () { |
|
1029 this._download.launch().then(null, Cu.reportError); |
|
1030 }, |
|
1031 |
|
1032 /** |
|
1033 * Show the downloaded file in the system file manager. |
|
1034 */ |
|
1035 showLocalFile: function DDI_showLocalFile() { |
|
1036 DownloadsCommon.showDownloadedFile(this.localFile); |
|
1037 }, |
|
1038 |
|
1039 /** |
|
1040 * Resumes the download if paused, pauses it if active. |
|
1041 * @throws if the download is not resumable or if has already done. |
|
1042 */ |
|
1043 togglePauseResume: function DDI_togglePauseResume() { |
|
1044 if (this._download.stopped) { |
|
1045 this._download.start(); |
|
1046 } else { |
|
1047 this._download.cancel(); |
|
1048 } |
|
1049 }, |
|
1050 |
|
1051 /** |
|
1052 * Attempts to retry the download. |
|
1053 * @throws if we cannot. |
|
1054 */ |
|
1055 retry: function DDI_retry() { |
|
1056 this._download.start(); |
|
1057 }, |
|
1058 |
|
1059 /** |
|
1060 * Cancels the download. |
|
1061 */ |
|
1062 cancel: function() { |
|
1063 this._download.cancel(); |
|
1064 this._download.removePartialData().then(null, Cu.reportError); |
|
1065 }, |
|
1066 |
|
1067 /** |
|
1068 * Remove the download. |
|
1069 */ |
|
1070 remove: function DDI_remove() { |
|
1071 Downloads.getList(Downloads.ALL) |
|
1072 .then(list => list.remove(this._download)) |
|
1073 .then(() => this._download.finalize(true)) |
|
1074 .then(null, Cu.reportError); |
|
1075 } |
|
1076 }; |
|
1077 |
|
1078 //////////////////////////////////////////////////////////////////////////////// |
|
1079 //// DownloadsViewPrototype |
|
1080 |
|
1081 /** |
|
1082 * A prototype for an object that registers itself with DownloadsData as soon |
|
1083 * as a view is registered with it. |
|
1084 */ |
|
1085 const DownloadsViewPrototype = { |
|
1086 ////////////////////////////////////////////////////////////////////////////// |
|
1087 //// Registration of views |
|
1088 |
|
1089 /** |
|
1090 * Array of view objects that should be notified when the available status |
|
1091 * data changes. |
|
1092 * |
|
1093 * SUBCLASSES MUST OVERRIDE THIS PROPERTY. |
|
1094 */ |
|
1095 _views: null, |
|
1096 |
|
1097 /** |
|
1098 * Determines whether this view object is over the private or non-private |
|
1099 * downloads. |
|
1100 * |
|
1101 * SUBCLASSES MUST OVERRIDE THIS PROPERTY. |
|
1102 */ |
|
1103 _isPrivate: false, |
|
1104 |
|
1105 /** |
|
1106 * Adds an object to be notified when the available status data changes. |
|
1107 * The specified object is initialized with the currently available status. |
|
1108 * |
|
1109 * @param aView |
|
1110 * View object to be added. This reference must be |
|
1111 * passed to removeView before termination. |
|
1112 */ |
|
1113 addView: function DVP_addView(aView) |
|
1114 { |
|
1115 // Start receiving events when the first of our views is registered. |
|
1116 if (this._views.length == 0) { |
|
1117 if (this._isPrivate) { |
|
1118 PrivateDownloadsData.addView(this); |
|
1119 } else { |
|
1120 DownloadsData.addView(this); |
|
1121 } |
|
1122 } |
|
1123 |
|
1124 this._views.push(aView); |
|
1125 this.refreshView(aView); |
|
1126 }, |
|
1127 |
|
1128 /** |
|
1129 * Updates the properties of an object previously added using addView. |
|
1130 * |
|
1131 * @param aView |
|
1132 * View object to be updated. |
|
1133 */ |
|
1134 refreshView: function DVP_refreshView(aView) |
|
1135 { |
|
1136 // Update immediately even if we are still loading data asynchronously. |
|
1137 // Subclasses must provide these two functions! |
|
1138 this._refreshProperties(); |
|
1139 this._updateView(aView); |
|
1140 }, |
|
1141 |
|
1142 /** |
|
1143 * Removes an object previously added using addView. |
|
1144 * |
|
1145 * @param aView |
|
1146 * View object to be removed. |
|
1147 */ |
|
1148 removeView: function DVP_removeView(aView) |
|
1149 { |
|
1150 let index = this._views.indexOf(aView); |
|
1151 if (index != -1) { |
|
1152 this._views.splice(index, 1); |
|
1153 } |
|
1154 |
|
1155 // Stop receiving events when the last of our views is unregistered. |
|
1156 if (this._views.length == 0) { |
|
1157 if (this._isPrivate) { |
|
1158 PrivateDownloadsData.removeView(this); |
|
1159 } else { |
|
1160 DownloadsData.removeView(this); |
|
1161 } |
|
1162 } |
|
1163 }, |
|
1164 |
|
1165 ////////////////////////////////////////////////////////////////////////////// |
|
1166 //// Callback functions from DownloadsData |
|
1167 |
|
1168 /** |
|
1169 * Indicates whether we are still loading downloads data asynchronously. |
|
1170 */ |
|
1171 _loading: false, |
|
1172 |
|
1173 /** |
|
1174 * Called before multiple downloads are about to be loaded. |
|
1175 */ |
|
1176 onDataLoadStarting: function DVP_onDataLoadStarting() |
|
1177 { |
|
1178 this._loading = true; |
|
1179 }, |
|
1180 |
|
1181 /** |
|
1182 * Called after data loading finished. |
|
1183 */ |
|
1184 onDataLoadCompleted: function DVP_onDataLoadCompleted() |
|
1185 { |
|
1186 this._loading = false; |
|
1187 }, |
|
1188 |
|
1189 /** |
|
1190 * Called when a new download data item is available, either during the |
|
1191 * asynchronous data load or when a new download is started. |
|
1192 * |
|
1193 * @param aDataItem |
|
1194 * DownloadsDataItem object that was just added. |
|
1195 * @param aNewest |
|
1196 * When true, indicates that this item is the most recent and should be |
|
1197 * added in the topmost position. This happens when a new download is |
|
1198 * started. When false, indicates that the item is the least recent |
|
1199 * with regard to the items that have been already added. The latter |
|
1200 * generally happens during the asynchronous data load. |
|
1201 * |
|
1202 * @note Subclasses should override this. |
|
1203 */ |
|
1204 onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest) |
|
1205 { |
|
1206 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
1207 }, |
|
1208 |
|
1209 /** |
|
1210 * Called when a data item is removed, ensures that the widget associated with |
|
1211 * the view item is removed from the user interface. |
|
1212 * |
|
1213 * @param aDataItem |
|
1214 * DownloadsDataItem object that is being removed. |
|
1215 * |
|
1216 * @note Subclasses should override this. |
|
1217 */ |
|
1218 onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem) |
|
1219 { |
|
1220 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
1221 }, |
|
1222 |
|
1223 /** |
|
1224 * Returns the view item associated with the provided data item for this view. |
|
1225 * |
|
1226 * @param aDataItem |
|
1227 * DownloadsDataItem object for which the view item is requested. |
|
1228 * |
|
1229 * @return Object that can be used to notify item status events. |
|
1230 * |
|
1231 * @note Subclasses should override this. |
|
1232 */ |
|
1233 getViewItem: function DID_getViewItem(aDataItem) |
|
1234 { |
|
1235 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
1236 }, |
|
1237 |
|
1238 /** |
|
1239 * Private function used to refresh the internal properties being sent to |
|
1240 * each registered view. |
|
1241 * |
|
1242 * @note Subclasses should override this. |
|
1243 */ |
|
1244 _refreshProperties: function DID_refreshProperties() |
|
1245 { |
|
1246 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
1247 }, |
|
1248 |
|
1249 /** |
|
1250 * Private function used to refresh an individual view. |
|
1251 * |
|
1252 * @note Subclasses should override this. |
|
1253 */ |
|
1254 _updateView: function DID_updateView() |
|
1255 { |
|
1256 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
1257 } |
|
1258 }; |
|
1259 |
|
1260 //////////////////////////////////////////////////////////////////////////////// |
|
1261 //// DownloadsIndicatorData |
|
1262 |
|
1263 /** |
|
1264 * This object registers itself with DownloadsData as a view, and transforms the |
|
1265 * notifications it receives into overall status data, that is then broadcast to |
|
1266 * the registered download status indicators. |
|
1267 * |
|
1268 * Note that using this object does not automatically start the Download Manager |
|
1269 * service. Consumers will see an empty list of downloads until the service is |
|
1270 * actually started. This is useful to display a neutral progress indicator in |
|
1271 * the main browser window until the autostart timeout elapses. |
|
1272 */ |
|
1273 function DownloadsIndicatorDataCtor(aPrivate) { |
|
1274 this._isPrivate = aPrivate; |
|
1275 this._views = []; |
|
1276 } |
|
1277 DownloadsIndicatorDataCtor.prototype = { |
|
1278 __proto__: DownloadsViewPrototype, |
|
1279 |
|
1280 /** |
|
1281 * Removes an object previously added using addView. |
|
1282 * |
|
1283 * @param aView |
|
1284 * DownloadsIndicatorView object to be removed. |
|
1285 */ |
|
1286 removeView: function DID_removeView(aView) |
|
1287 { |
|
1288 DownloadsViewPrototype.removeView.call(this, aView); |
|
1289 |
|
1290 if (this._views.length == 0) { |
|
1291 this._itemCount = 0; |
|
1292 } |
|
1293 }, |
|
1294 |
|
1295 ////////////////////////////////////////////////////////////////////////////// |
|
1296 //// Callback functions from DownloadsData |
|
1297 |
|
1298 /** |
|
1299 * Called after data loading finished. |
|
1300 */ |
|
1301 onDataLoadCompleted: function DID_onDataLoadCompleted() |
|
1302 { |
|
1303 DownloadsViewPrototype.onDataLoadCompleted.call(this); |
|
1304 this._updateViews(); |
|
1305 }, |
|
1306 |
|
1307 /** |
|
1308 * Called when a new download data item is available, either during the |
|
1309 * asynchronous data load or when a new download is started. |
|
1310 * |
|
1311 * @param aDataItem |
|
1312 * DownloadsDataItem object that was just added. |
|
1313 * @param aNewest |
|
1314 * When true, indicates that this item is the most recent and should be |
|
1315 * added in the topmost position. This happens when a new download is |
|
1316 * started. When false, indicates that the item is the least recent |
|
1317 * with regard to the items that have been already added. The latter |
|
1318 * generally happens during the asynchronous data load. |
|
1319 */ |
|
1320 onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest) |
|
1321 { |
|
1322 this._itemCount++; |
|
1323 this._updateViews(); |
|
1324 }, |
|
1325 |
|
1326 /** |
|
1327 * Called when a data item is removed, ensures that the widget associated with |
|
1328 * the view item is removed from the user interface. |
|
1329 * |
|
1330 * @param aDataItem |
|
1331 * DownloadsDataItem object that is being removed. |
|
1332 */ |
|
1333 onDataItemRemoved: function DID_onDataItemRemoved(aDataItem) |
|
1334 { |
|
1335 this._itemCount--; |
|
1336 this._updateViews(); |
|
1337 }, |
|
1338 |
|
1339 /** |
|
1340 * Returns the view item associated with the provided data item for this view. |
|
1341 * |
|
1342 * @param aDataItem |
|
1343 * DownloadsDataItem object for which the view item is requested. |
|
1344 * |
|
1345 * @return Object that can be used to notify item status events. |
|
1346 */ |
|
1347 getViewItem: function DID_getViewItem(aDataItem) |
|
1348 { |
|
1349 let data = this._isPrivate ? PrivateDownloadsIndicatorData |
|
1350 : DownloadsIndicatorData; |
|
1351 return Object.freeze({ |
|
1352 onStateChange: function DIVI_onStateChange(aOldState) |
|
1353 { |
|
1354 if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED || |
|
1355 aDataItem.state == nsIDM.DOWNLOAD_FAILED) { |
|
1356 data.attention = true; |
|
1357 } |
|
1358 |
|
1359 // Since the state of a download changed, reset the estimated time left. |
|
1360 data._lastRawTimeLeft = -1; |
|
1361 data._lastTimeLeft = -1; |
|
1362 |
|
1363 data._updateViews(); |
|
1364 }, |
|
1365 onProgressChange: function DIVI_onProgressChange() |
|
1366 { |
|
1367 data._updateViews(); |
|
1368 } |
|
1369 }); |
|
1370 }, |
|
1371 |
|
1372 ////////////////////////////////////////////////////////////////////////////// |
|
1373 //// Propagation of properties to our views |
|
1374 |
|
1375 // The following properties are updated by _refreshProperties and are then |
|
1376 // propagated to the views. See _refreshProperties for details. |
|
1377 _hasDownloads: false, |
|
1378 _counter: "", |
|
1379 _percentComplete: -1, |
|
1380 _paused: false, |
|
1381 |
|
1382 /** |
|
1383 * Indicates whether the download indicators should be highlighted. |
|
1384 */ |
|
1385 set attention(aValue) |
|
1386 { |
|
1387 this._attention = aValue; |
|
1388 this._updateViews(); |
|
1389 return aValue; |
|
1390 }, |
|
1391 _attention: false, |
|
1392 |
|
1393 /** |
|
1394 * Indicates whether the user is interacting with downloads, thus the |
|
1395 * attention indication should not be shown even if requested. |
|
1396 */ |
|
1397 set attentionSuppressed(aValue) |
|
1398 { |
|
1399 this._attentionSuppressed = aValue; |
|
1400 this._attention = false; |
|
1401 this._updateViews(); |
|
1402 return aValue; |
|
1403 }, |
|
1404 _attentionSuppressed: false, |
|
1405 |
|
1406 /** |
|
1407 * Computes aggregate values and propagates the changes to our views. |
|
1408 */ |
|
1409 _updateViews: function DID_updateViews() |
|
1410 { |
|
1411 // Do not update the status indicators during batch loads of download items. |
|
1412 if (this._loading) { |
|
1413 return; |
|
1414 } |
|
1415 |
|
1416 this._refreshProperties(); |
|
1417 this._views.forEach(this._updateView, this); |
|
1418 }, |
|
1419 |
|
1420 /** |
|
1421 * Updates the specified view with the current aggregate values. |
|
1422 * |
|
1423 * @param aView |
|
1424 * DownloadsIndicatorView object to be updated. |
|
1425 */ |
|
1426 _updateView: function DID_updateView(aView) |
|
1427 { |
|
1428 aView.hasDownloads = this._hasDownloads; |
|
1429 aView.counter = this._counter; |
|
1430 aView.percentComplete = this._percentComplete; |
|
1431 aView.paused = this._paused; |
|
1432 aView.attention = this._attention && !this._attentionSuppressed; |
|
1433 }, |
|
1434 |
|
1435 ////////////////////////////////////////////////////////////////////////////// |
|
1436 //// Property updating based on current download status |
|
1437 |
|
1438 /** |
|
1439 * Number of download items that are available to be displayed. |
|
1440 */ |
|
1441 _itemCount: 0, |
|
1442 |
|
1443 /** |
|
1444 * Floating point value indicating the last number of seconds estimated until |
|
1445 * the longest download will finish. We need to store this value so that we |
|
1446 * don't continuously apply smoothing if the actual download state has not |
|
1447 * changed. This is set to -1 if the previous value is unknown. |
|
1448 */ |
|
1449 _lastRawTimeLeft: -1, |
|
1450 |
|
1451 /** |
|
1452 * Last number of seconds estimated until all in-progress downloads with a |
|
1453 * known size and speed will finish. This value is stored to allow smoothing |
|
1454 * in case of small variations. This is set to -1 if the previous value is |
|
1455 * unknown. |
|
1456 */ |
|
1457 _lastTimeLeft: -1, |
|
1458 |
|
1459 /** |
|
1460 * A generator function for the dataItems that this summary is currently |
|
1461 * interested in. This generator is passed off to summarizeDownloads in order |
|
1462 * to generate statistics about the dataItems we care about - in this case, |
|
1463 * it's all dataItems for active downloads. |
|
1464 */ |
|
1465 _activeDataItems: function DID_activeDataItems() |
|
1466 { |
|
1467 let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems |
|
1468 : DownloadsData.dataItems; |
|
1469 for each (let dataItem in dataItems) { |
|
1470 if (dataItem && dataItem.inProgress) { |
|
1471 yield dataItem; |
|
1472 } |
|
1473 } |
|
1474 }, |
|
1475 |
|
1476 /** |
|
1477 * Computes aggregate values based on the current state of downloads. |
|
1478 */ |
|
1479 _refreshProperties: function DID_refreshProperties() |
|
1480 { |
|
1481 let summary = |
|
1482 DownloadsCommon.summarizeDownloads(this._activeDataItems()); |
|
1483 |
|
1484 // Determine if the indicator should be shown or get attention. |
|
1485 this._hasDownloads = (this._itemCount > 0); |
|
1486 |
|
1487 // If all downloads are paused, show the progress indicator as paused. |
|
1488 this._paused = summary.numActive > 0 && |
|
1489 summary.numActive == summary.numPaused; |
|
1490 |
|
1491 this._percentComplete = summary.percentComplete; |
|
1492 |
|
1493 // Display the estimated time left, if present. |
|
1494 if (summary.rawTimeLeft == -1) { |
|
1495 // There are no downloads with a known time left. |
|
1496 this._lastRawTimeLeft = -1; |
|
1497 this._lastTimeLeft = -1; |
|
1498 this._counter = ""; |
|
1499 } else { |
|
1500 // Compute the new time left only if state actually changed. |
|
1501 if (this._lastRawTimeLeft != summary.rawTimeLeft) { |
|
1502 this._lastRawTimeLeft = summary.rawTimeLeft; |
|
1503 this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, |
|
1504 this._lastTimeLeft); |
|
1505 } |
|
1506 this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft); |
|
1507 } |
|
1508 } |
|
1509 }; |
|
1510 |
|
1511 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() { |
|
1512 return new DownloadsIndicatorDataCtor(true); |
|
1513 }); |
|
1514 |
|
1515 XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() { |
|
1516 return new DownloadsIndicatorDataCtor(false); |
|
1517 }); |
|
1518 |
|
1519 //////////////////////////////////////////////////////////////////////////////// |
|
1520 //// DownloadsSummaryData |
|
1521 |
|
1522 /** |
|
1523 * DownloadsSummaryData is a view for DownloadsData that produces a summary |
|
1524 * of all downloads after a certain exclusion point aNumToExclude. For example, |
|
1525 * if there were 5 downloads in progress, and a DownloadsSummaryData was |
|
1526 * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData |
|
1527 * would produce a summary of the last 2 downloads. |
|
1528 * |
|
1529 * @param aIsPrivate |
|
1530 * True if the browser window which owns the download button is a private |
|
1531 * window. |
|
1532 * @param aNumToExclude |
|
1533 * The number of items to exclude from the summary, starting from the |
|
1534 * top of the list. |
|
1535 */ |
|
1536 function DownloadsSummaryData(aIsPrivate, aNumToExclude) { |
|
1537 this._numToExclude = aNumToExclude; |
|
1538 // Since we can have multiple instances of DownloadsSummaryData, we |
|
1539 // override these values from the prototype so that each instance can be |
|
1540 // completely separated from one another. |
|
1541 this._loading = false; |
|
1542 |
|
1543 this._dataItems = []; |
|
1544 |
|
1545 // Floating point value indicating the last number of seconds estimated until |
|
1546 // the longest download will finish. We need to store this value so that we |
|
1547 // don't continuously apply smoothing if the actual download state has not |
|
1548 // changed. This is set to -1 if the previous value is unknown. |
|
1549 this._lastRawTimeLeft = -1; |
|
1550 |
|
1551 // Last number of seconds estimated until all in-progress downloads with a |
|
1552 // known size and speed will finish. This value is stored to allow smoothing |
|
1553 // in case of small variations. This is set to -1 if the previous value is |
|
1554 // unknown. |
|
1555 this._lastTimeLeft = -1; |
|
1556 |
|
1557 // The following properties are updated by _refreshProperties and are then |
|
1558 // propagated to the views. |
|
1559 this._showingProgress = false; |
|
1560 this._details = ""; |
|
1561 this._description = ""; |
|
1562 this._numActive = 0; |
|
1563 this._percentComplete = -1; |
|
1564 |
|
1565 this._isPrivate = aIsPrivate; |
|
1566 this._views = []; |
|
1567 } |
|
1568 |
|
1569 DownloadsSummaryData.prototype = { |
|
1570 __proto__: DownloadsViewPrototype, |
|
1571 |
|
1572 /** |
|
1573 * Removes an object previously added using addView. |
|
1574 * |
|
1575 * @param aView |
|
1576 * DownloadsSummary view to be removed. |
|
1577 */ |
|
1578 removeView: function DSD_removeView(aView) |
|
1579 { |
|
1580 DownloadsViewPrototype.removeView.call(this, aView); |
|
1581 |
|
1582 if (this._views.length == 0) { |
|
1583 // Clear out our collection of DownloadDataItems. If we ever have |
|
1584 // another view registered with us, this will get re-populated. |
|
1585 this._dataItems = []; |
|
1586 } |
|
1587 }, |
|
1588 |
|
1589 ////////////////////////////////////////////////////////////////////////////// |
|
1590 //// Callback functions from DownloadsData - see the documentation in |
|
1591 //// DownloadsViewPrototype for more information on what these functions |
|
1592 //// are used for. |
|
1593 |
|
1594 onDataLoadCompleted: function DSD_onDataLoadCompleted() |
|
1595 { |
|
1596 DownloadsViewPrototype.onDataLoadCompleted.call(this); |
|
1597 this._updateViews(); |
|
1598 }, |
|
1599 |
|
1600 onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest) |
|
1601 { |
|
1602 if (aNewest) { |
|
1603 this._dataItems.unshift(aDataItem); |
|
1604 } else { |
|
1605 this._dataItems.push(aDataItem); |
|
1606 } |
|
1607 |
|
1608 this._updateViews(); |
|
1609 }, |
|
1610 |
|
1611 onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem) |
|
1612 { |
|
1613 let itemIndex = this._dataItems.indexOf(aDataItem); |
|
1614 this._dataItems.splice(itemIndex, 1); |
|
1615 this._updateViews(); |
|
1616 }, |
|
1617 |
|
1618 getViewItem: function DSD_getViewItem(aDataItem) |
|
1619 { |
|
1620 let self = this; |
|
1621 return Object.freeze({ |
|
1622 onStateChange: function DIVI_onStateChange(aOldState) |
|
1623 { |
|
1624 // Since the state of a download changed, reset the estimated time left. |
|
1625 self._lastRawTimeLeft = -1; |
|
1626 self._lastTimeLeft = -1; |
|
1627 self._updateViews(); |
|
1628 }, |
|
1629 onProgressChange: function DIVI_onProgressChange() |
|
1630 { |
|
1631 self._updateViews(); |
|
1632 } |
|
1633 }); |
|
1634 }, |
|
1635 |
|
1636 ////////////////////////////////////////////////////////////////////////////// |
|
1637 //// Propagation of properties to our views |
|
1638 |
|
1639 /** |
|
1640 * Computes aggregate values and propagates the changes to our views. |
|
1641 */ |
|
1642 _updateViews: function DSD_updateViews() |
|
1643 { |
|
1644 // Do not update the status indicators during batch loads of download items. |
|
1645 if (this._loading) { |
|
1646 return; |
|
1647 } |
|
1648 |
|
1649 this._refreshProperties(); |
|
1650 this._views.forEach(this._updateView, this); |
|
1651 }, |
|
1652 |
|
1653 /** |
|
1654 * Updates the specified view with the current aggregate values. |
|
1655 * |
|
1656 * @param aView |
|
1657 * DownloadsIndicatorView object to be updated. |
|
1658 */ |
|
1659 _updateView: function DSD_updateView(aView) |
|
1660 { |
|
1661 aView.showingProgress = this._showingProgress; |
|
1662 aView.percentComplete = this._percentComplete; |
|
1663 aView.description = this._description; |
|
1664 aView.details = this._details; |
|
1665 }, |
|
1666 |
|
1667 ////////////////////////////////////////////////////////////////////////////// |
|
1668 //// Property updating based on current download status |
|
1669 |
|
1670 /** |
|
1671 * A generator function for the dataItems that this summary is currently |
|
1672 * interested in. This generator is passed off to summarizeDownloads in order |
|
1673 * to generate statistics about the dataItems we care about - in this case, |
|
1674 * it's the dataItems in this._dataItems after the first few to exclude, |
|
1675 * which was set when constructing this DownloadsSummaryData instance. |
|
1676 */ |
|
1677 _dataItemsForSummary: function DSD_dataItemsForSummary() |
|
1678 { |
|
1679 if (this._dataItems.length > 0) { |
|
1680 for (let i = this._numToExclude; i < this._dataItems.length; ++i) { |
|
1681 yield this._dataItems[i]; |
|
1682 } |
|
1683 } |
|
1684 }, |
|
1685 |
|
1686 /** |
|
1687 * Computes aggregate values based on the current state of downloads. |
|
1688 */ |
|
1689 _refreshProperties: function DSD_refreshProperties() |
|
1690 { |
|
1691 // Pre-load summary with default values. |
|
1692 let summary = |
|
1693 DownloadsCommon.summarizeDownloads(this._dataItemsForSummary()); |
|
1694 |
|
1695 this._description = DownloadsCommon.strings |
|
1696 .otherDownloads2(summary.numActive); |
|
1697 this._percentComplete = summary.percentComplete; |
|
1698 |
|
1699 // If all downloads are paused, show the progress indicator as paused. |
|
1700 this._showingProgress = summary.numDownloading > 0 || |
|
1701 summary.numPaused > 0; |
|
1702 |
|
1703 // Display the estimated time left, if present. |
|
1704 if (summary.rawTimeLeft == -1) { |
|
1705 // There are no downloads with a known time left. |
|
1706 this._lastRawTimeLeft = -1; |
|
1707 this._lastTimeLeft = -1; |
|
1708 this._details = ""; |
|
1709 } else { |
|
1710 // Compute the new time left only if state actually changed. |
|
1711 if (this._lastRawTimeLeft != summary.rawTimeLeft) { |
|
1712 this._lastRawTimeLeft = summary.rawTimeLeft; |
|
1713 this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, |
|
1714 this._lastTimeLeft); |
|
1715 } |
|
1716 [this._details] = DownloadUtils.getDownloadStatusNoRate( |
|
1717 summary.totalTransferred, summary.totalSize, summary.slowestSpeed, |
|
1718 this._lastTimeLeft); |
|
1719 } |
|
1720 } |
|
1721 } |