|
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 |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 /** |
|
8 * This file includes the following constructors and global objects: |
|
9 * |
|
10 * DownloadList |
|
11 * Represents a collection of Download objects that can be viewed and managed by |
|
12 * the user interface, and persisted across sessions. |
|
13 * |
|
14 * DownloadCombinedList |
|
15 * Provides a unified, unordered list combining public and private downloads. |
|
16 * |
|
17 * DownloadSummary |
|
18 * Provides an aggregated view on the contents of a DownloadList. |
|
19 */ |
|
20 |
|
21 "use strict"; |
|
22 |
|
23 this.EXPORTED_SYMBOLS = [ |
|
24 "DownloadList", |
|
25 "DownloadCombinedList", |
|
26 "DownloadSummary", |
|
27 ]; |
|
28 |
|
29 //////////////////////////////////////////////////////////////////////////////// |
|
30 //// Globals |
|
31 |
|
32 const Cc = Components.classes; |
|
33 const Ci = Components.interfaces; |
|
34 const Cu = Components.utils; |
|
35 const Cr = Components.results; |
|
36 |
|
37 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
38 |
|
39 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
40 "resource://gre/modules/Promise.jsm"); |
|
41 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
42 "resource://gre/modules/Task.jsm"); |
|
43 |
|
44 //////////////////////////////////////////////////////////////////////////////// |
|
45 //// DownloadList |
|
46 |
|
47 /** |
|
48 * Represents a collection of Download objects that can be viewed and managed by |
|
49 * the user interface, and persisted across sessions. |
|
50 */ |
|
51 this.DownloadList = function () |
|
52 { |
|
53 this._downloads = []; |
|
54 this._views = new Set(); |
|
55 } |
|
56 |
|
57 this.DownloadList.prototype = { |
|
58 /** |
|
59 * Array of Download objects currently in the list. |
|
60 */ |
|
61 _downloads: null, |
|
62 |
|
63 /** |
|
64 * Retrieves a snapshot of the downloads that are currently in the list. The |
|
65 * returned array does not change when downloads are added or removed, though |
|
66 * the Download objects it contains are still updated in real time. |
|
67 * |
|
68 * @return {Promise} |
|
69 * @resolves An array of Download objects. |
|
70 * @rejects JavaScript exception. |
|
71 */ |
|
72 getAll: function DL_getAll() { |
|
73 return Promise.resolve(Array.slice(this._downloads, 0)); |
|
74 }, |
|
75 |
|
76 /** |
|
77 * Adds a new download to the end of the items list. |
|
78 * |
|
79 * @note When a download is added to the list, its "onchange" event is |
|
80 * registered by the list, thus it cannot be used to monitor the |
|
81 * download. To receive change notifications for downloads that are |
|
82 * added to the list, use the addView method to register for |
|
83 * onDownloadChanged notifications. |
|
84 * |
|
85 * @param aDownload |
|
86 * The Download object to add. |
|
87 * |
|
88 * @return {Promise} |
|
89 * @resolves When the download has been added. |
|
90 * @rejects JavaScript exception. |
|
91 */ |
|
92 add: function DL_add(aDownload) { |
|
93 this._downloads.push(aDownload); |
|
94 aDownload.onchange = this._change.bind(this, aDownload); |
|
95 this._notifyAllViews("onDownloadAdded", aDownload); |
|
96 |
|
97 return Promise.resolve(); |
|
98 }, |
|
99 |
|
100 /** |
|
101 * Removes a download from the list. If the download was already removed, |
|
102 * this method has no effect. |
|
103 * |
|
104 * This method does not change the state of the download, to allow adding it |
|
105 * to another list, or control it directly. If you want to dispose of the |
|
106 * download object, you should cancel it afterwards, and remove any partially |
|
107 * downloaded data if needed. |
|
108 * |
|
109 * @param aDownload |
|
110 * The Download object to remove. |
|
111 * |
|
112 * @return {Promise} |
|
113 * @resolves When the download has been removed. |
|
114 * @rejects JavaScript exception. |
|
115 */ |
|
116 remove: function DL_remove(aDownload) { |
|
117 let index = this._downloads.indexOf(aDownload); |
|
118 if (index != -1) { |
|
119 this._downloads.splice(index, 1); |
|
120 aDownload.onchange = null; |
|
121 this._notifyAllViews("onDownloadRemoved", aDownload); |
|
122 } |
|
123 |
|
124 return Promise.resolve(); |
|
125 }, |
|
126 |
|
127 /** |
|
128 * This function is called when "onchange" events of downloads occur. |
|
129 * |
|
130 * @param aDownload |
|
131 * The Download object that changed. |
|
132 */ |
|
133 _change: function DL_change(aDownload) { |
|
134 this._notifyAllViews("onDownloadChanged", aDownload); |
|
135 }, |
|
136 |
|
137 /** |
|
138 * Set of currently registered views. |
|
139 */ |
|
140 _views: null, |
|
141 |
|
142 /** |
|
143 * Adds a view that will be notified of changes to downloads. The newly added |
|
144 * view will receive onDownloadAdded notifications for all the downloads that |
|
145 * are already in the list. |
|
146 * |
|
147 * @param aView |
|
148 * The view object to add. The following methods may be defined: |
|
149 * { |
|
150 * onDownloadAdded: function (aDownload) { |
|
151 * // Called after aDownload is added to the end of the list. |
|
152 * }, |
|
153 * onDownloadChanged: function (aDownload) { |
|
154 * // Called after the properties of aDownload change. |
|
155 * }, |
|
156 * onDownloadRemoved: function (aDownload) { |
|
157 * // Called after aDownload is removed from the list. |
|
158 * }, |
|
159 * } |
|
160 * |
|
161 * @return {Promise} |
|
162 * @resolves When the view has been registered and all the onDownloadAdded |
|
163 * notifications for the existing downloads have been sent. |
|
164 * @rejects JavaScript exception. |
|
165 */ |
|
166 addView: function DL_addView(aView) |
|
167 { |
|
168 this._views.add(aView); |
|
169 |
|
170 if ("onDownloadAdded" in aView) { |
|
171 for (let download of this._downloads) { |
|
172 try { |
|
173 aView.onDownloadAdded(download); |
|
174 } catch (ex) { |
|
175 Cu.reportError(ex); |
|
176 } |
|
177 } |
|
178 } |
|
179 |
|
180 return Promise.resolve(); |
|
181 }, |
|
182 |
|
183 /** |
|
184 * Removes a view that was previously added using addView. |
|
185 * |
|
186 * @param aView |
|
187 * The view object to remove. |
|
188 * |
|
189 * @return {Promise} |
|
190 * @resolves When the view has been removed. At this point, the removed view |
|
191 * will not receive any more notifications. |
|
192 * @rejects JavaScript exception. |
|
193 */ |
|
194 removeView: function DL_removeView(aView) |
|
195 { |
|
196 this._views.delete(aView); |
|
197 |
|
198 return Promise.resolve(); |
|
199 }, |
|
200 |
|
201 /** |
|
202 * Notifies all the views of a download addition, change, or removal. |
|
203 * |
|
204 * @param aMethodName |
|
205 * String containing the name of the method to call on the view. |
|
206 * @param aDownload |
|
207 * The Download object that changed. |
|
208 */ |
|
209 _notifyAllViews: function (aMethodName, aDownload) { |
|
210 for (let view of this._views) { |
|
211 try { |
|
212 if (aMethodName in view) { |
|
213 view[aMethodName](aDownload); |
|
214 } |
|
215 } catch (ex) { |
|
216 Cu.reportError(ex); |
|
217 } |
|
218 } |
|
219 }, |
|
220 |
|
221 /** |
|
222 * Removes downloads from the list that have finished, have failed, or have |
|
223 * been canceled without keeping partial data. A filter function may be |
|
224 * specified to remove only a subset of those downloads. |
|
225 * |
|
226 * This method finalizes each removed download, ensuring that any partially |
|
227 * downloaded data associated with it is also removed. |
|
228 * |
|
229 * @param aFilterFn |
|
230 * The filter function is called with each download as its only |
|
231 * argument, and should return true to remove the download and false |
|
232 * to keep it. This parameter may be null or omitted to have no |
|
233 * additional filter. |
|
234 */ |
|
235 removeFinished: function DL_removeFinished(aFilterFn) { |
|
236 Task.spawn(function() { |
|
237 let list = yield this.getAll(); |
|
238 for (let download of list) { |
|
239 // Remove downloads that have been canceled, even if the cancellation |
|
240 // operation hasn't completed yet so we don't check "stopped" here. |
|
241 // Failed downloads with partial data are also removed. |
|
242 if (download.stopped && (!download.hasPartialData || download.error) && |
|
243 (!aFilterFn || aFilterFn(download))) { |
|
244 // Remove the download first, so that the views don't get the change |
|
245 // notifications that may occur during finalization. |
|
246 yield this.remove(download); |
|
247 // Ensure that the download is stopped and no partial data is kept. |
|
248 // This works even if the download state has changed meanwhile. We |
|
249 // don't need to wait for the procedure to be complete before |
|
250 // processing the other downloads in the list. |
|
251 download.finalize(true).then(null, Cu.reportError); |
|
252 } |
|
253 } |
|
254 }.bind(this)).then(null, Cu.reportError); |
|
255 }, |
|
256 }; |
|
257 |
|
258 //////////////////////////////////////////////////////////////////////////////// |
|
259 //// DownloadCombinedList |
|
260 |
|
261 /** |
|
262 * Provides a unified, unordered list combining public and private downloads. |
|
263 * |
|
264 * Download objects added to this list are also added to one of the two |
|
265 * underlying lists, based on their "source.isPrivate" property. Views on this |
|
266 * list will receive notifications for both public and private downloads. |
|
267 * |
|
268 * @param aPublicList |
|
269 * Underlying DownloadList containing public downloads. |
|
270 * @param aPrivateList |
|
271 * Underlying DownloadList containing private downloads. |
|
272 */ |
|
273 this.DownloadCombinedList = function (aPublicList, aPrivateList) |
|
274 { |
|
275 DownloadList.call(this); |
|
276 this._publicList = aPublicList; |
|
277 this._privateList = aPrivateList; |
|
278 aPublicList.addView(this).then(null, Cu.reportError); |
|
279 aPrivateList.addView(this).then(null, Cu.reportError); |
|
280 } |
|
281 |
|
282 this.DownloadCombinedList.prototype = { |
|
283 __proto__: DownloadList.prototype, |
|
284 |
|
285 /** |
|
286 * Underlying DownloadList containing public downloads. |
|
287 */ |
|
288 _publicList: null, |
|
289 |
|
290 /** |
|
291 * Underlying DownloadList containing private downloads. |
|
292 */ |
|
293 _privateList: null, |
|
294 |
|
295 /** |
|
296 * Adds a new download to the end of the items list. |
|
297 * |
|
298 * @note When a download is added to the list, its "onchange" event is |
|
299 * registered by the list, thus it cannot be used to monitor the |
|
300 * download. To receive change notifications for downloads that are |
|
301 * added to the list, use the addView method to register for |
|
302 * onDownloadChanged notifications. |
|
303 * |
|
304 * @param aDownload |
|
305 * The Download object to add. |
|
306 * |
|
307 * @return {Promise} |
|
308 * @resolves When the download has been added. |
|
309 * @rejects JavaScript exception. |
|
310 */ |
|
311 add: function (aDownload) |
|
312 { |
|
313 if (aDownload.source.isPrivate) { |
|
314 return this._privateList.add(aDownload); |
|
315 } else { |
|
316 return this._publicList.add(aDownload); |
|
317 } |
|
318 }, |
|
319 |
|
320 /** |
|
321 * Removes a download from the list. If the download was already removed, |
|
322 * this method has no effect. |
|
323 * |
|
324 * This method does not change the state of the download, to allow adding it |
|
325 * to another list, or control it directly. If you want to dispose of the |
|
326 * download object, you should cancel it afterwards, and remove any partially |
|
327 * downloaded data if needed. |
|
328 * |
|
329 * @param aDownload |
|
330 * The Download object to remove. |
|
331 * |
|
332 * @return {Promise} |
|
333 * @resolves When the download has been removed. |
|
334 * @rejects JavaScript exception. |
|
335 */ |
|
336 remove: function (aDownload) |
|
337 { |
|
338 if (aDownload.source.isPrivate) { |
|
339 return this._privateList.remove(aDownload); |
|
340 } else { |
|
341 return this._publicList.remove(aDownload); |
|
342 } |
|
343 }, |
|
344 |
|
345 ////////////////////////////////////////////////////////////////////////////// |
|
346 //// DownloadList view |
|
347 |
|
348 onDownloadAdded: function (aDownload) |
|
349 { |
|
350 this._downloads.push(aDownload); |
|
351 this._notifyAllViews("onDownloadAdded", aDownload); |
|
352 }, |
|
353 |
|
354 onDownloadChanged: function (aDownload) |
|
355 { |
|
356 this._notifyAllViews("onDownloadChanged", aDownload); |
|
357 }, |
|
358 |
|
359 onDownloadRemoved: function (aDownload) |
|
360 { |
|
361 let index = this._downloads.indexOf(aDownload); |
|
362 if (index != -1) { |
|
363 this._downloads.splice(index, 1); |
|
364 } |
|
365 this._notifyAllViews("onDownloadRemoved", aDownload); |
|
366 }, |
|
367 }; |
|
368 |
|
369 //////////////////////////////////////////////////////////////////////////////// |
|
370 //// DownloadSummary |
|
371 |
|
372 /** |
|
373 * Provides an aggregated view on the contents of a DownloadList. |
|
374 */ |
|
375 this.DownloadSummary = function () |
|
376 { |
|
377 this._downloads = []; |
|
378 this._views = new Set(); |
|
379 } |
|
380 |
|
381 this.DownloadSummary.prototype = { |
|
382 /** |
|
383 * Array of Download objects that are currently part of the summary. |
|
384 */ |
|
385 _downloads: null, |
|
386 |
|
387 /** |
|
388 * Underlying DownloadList whose contents should be summarized. |
|
389 */ |
|
390 _list: null, |
|
391 |
|
392 /** |
|
393 * This method may be called once to bind this object to a DownloadList. |
|
394 * |
|
395 * Views on the summarized data can be registered before this object is bound |
|
396 * to an actual list. This allows the summary to be used without requiring |
|
397 * the initialization of the DownloadList first. |
|
398 * |
|
399 * @param aList |
|
400 * Underlying DownloadList whose contents should be summarized. |
|
401 * |
|
402 * @return {Promise} |
|
403 * @resolves When the view on the underlying list has been registered. |
|
404 * @rejects JavaScript exception. |
|
405 */ |
|
406 bindToList: function (aList) |
|
407 { |
|
408 if (this._list) { |
|
409 throw new Error("bindToList may be called only once."); |
|
410 } |
|
411 |
|
412 return aList.addView(this).then(() => { |
|
413 // Set the list reference only after addView has returned, so that we don't |
|
414 // send a notification to our views for each download that is added. |
|
415 this._list = aList; |
|
416 this._onListChanged(); |
|
417 }); |
|
418 }, |
|
419 |
|
420 /** |
|
421 * Set of currently registered views. |
|
422 */ |
|
423 _views: null, |
|
424 |
|
425 /** |
|
426 * Adds a view that will be notified of changes to the summary. The newly |
|
427 * added view will receive an initial onSummaryChanged notification. |
|
428 * |
|
429 * @param aView |
|
430 * The view object to add. The following methods may be defined: |
|
431 * { |
|
432 * onSummaryChanged: function () { |
|
433 * // Called after any property of the summary has changed. |
|
434 * }, |
|
435 * } |
|
436 * |
|
437 * @return {Promise} |
|
438 * @resolves When the view has been registered and the onSummaryChanged |
|
439 * notification has been sent. |
|
440 * @rejects JavaScript exception. |
|
441 */ |
|
442 addView: function (aView) |
|
443 { |
|
444 this._views.add(aView); |
|
445 |
|
446 if ("onSummaryChanged" in aView) { |
|
447 try { |
|
448 aView.onSummaryChanged(); |
|
449 } catch (ex) { |
|
450 Cu.reportError(ex); |
|
451 } |
|
452 } |
|
453 |
|
454 return Promise.resolve(); |
|
455 }, |
|
456 |
|
457 /** |
|
458 * Removes a view that was previously added using addView. |
|
459 * |
|
460 * @param aView |
|
461 * The view object to remove. |
|
462 * |
|
463 * @return {Promise} |
|
464 * @resolves When the view has been removed. At this point, the removed view |
|
465 * will not receive any more notifications. |
|
466 * @rejects JavaScript exception. |
|
467 */ |
|
468 removeView: function (aView) |
|
469 { |
|
470 this._views.delete(aView); |
|
471 |
|
472 return Promise.resolve(); |
|
473 }, |
|
474 |
|
475 /** |
|
476 * Indicates whether all the downloads are currently stopped. |
|
477 */ |
|
478 allHaveStopped: true, |
|
479 |
|
480 /** |
|
481 * Indicates the total number of bytes to be transferred before completing all |
|
482 * the downloads that are currently in progress. |
|
483 * |
|
484 * For downloads that do not have a known final size, the number of bytes |
|
485 * currently transferred is reported as part of this property. |
|
486 * |
|
487 * This is zero if no downloads are currently in progress. |
|
488 */ |
|
489 progressTotalBytes: 0, |
|
490 |
|
491 /** |
|
492 * Number of bytes currently transferred as part of all the downloads that are |
|
493 * currently in progress. |
|
494 * |
|
495 * This is zero if no downloads are currently in progress. |
|
496 */ |
|
497 progressCurrentBytes: 0, |
|
498 |
|
499 /** |
|
500 * This function is called when any change in the list of downloads occurs, |
|
501 * and will recalculate the summary and notify the views in case the |
|
502 * aggregated properties are different. |
|
503 */ |
|
504 _onListChanged: function () { |
|
505 let allHaveStopped = true; |
|
506 let progressTotalBytes = 0; |
|
507 let progressCurrentBytes = 0; |
|
508 |
|
509 // Recalculate the aggregated state. See the description of the individual |
|
510 // properties for an explanation of the summarization logic. |
|
511 for (let download of this._downloads) { |
|
512 if (!download.stopped) { |
|
513 allHaveStopped = false; |
|
514 progressTotalBytes += download.hasProgress ? download.totalBytes |
|
515 : download.currentBytes; |
|
516 progressCurrentBytes += download.currentBytes; |
|
517 } |
|
518 } |
|
519 |
|
520 // Exit now if the properties did not change. |
|
521 if (this.allHaveStopped == allHaveStopped && |
|
522 this.progressTotalBytes == progressTotalBytes && |
|
523 this.progressCurrentBytes == progressCurrentBytes) { |
|
524 return; |
|
525 } |
|
526 |
|
527 this.allHaveStopped = allHaveStopped; |
|
528 this.progressTotalBytes = progressTotalBytes; |
|
529 this.progressCurrentBytes = progressCurrentBytes; |
|
530 |
|
531 // Notify all the views that our properties changed. |
|
532 for (let view of this._views) { |
|
533 try { |
|
534 if ("onSummaryChanged" in view) { |
|
535 view.onSummaryChanged(); |
|
536 } |
|
537 } catch (ex) { |
|
538 Cu.reportError(ex); |
|
539 } |
|
540 } |
|
541 }, |
|
542 |
|
543 ////////////////////////////////////////////////////////////////////////////// |
|
544 //// DownloadList view |
|
545 |
|
546 onDownloadAdded: function (aDownload) |
|
547 { |
|
548 this._downloads.push(aDownload); |
|
549 if (this._list) { |
|
550 this._onListChanged(); |
|
551 } |
|
552 }, |
|
553 |
|
554 onDownloadChanged: function (aDownload) |
|
555 { |
|
556 this._onListChanged(); |
|
557 }, |
|
558 |
|
559 onDownloadRemoved: function (aDownload) |
|
560 { |
|
561 let index = this._downloads.indexOf(aDownload); |
|
562 if (index != -1) { |
|
563 this._downloads.splice(index, 1); |
|
564 } |
|
565 this._onListChanged(); |
|
566 }, |
|
567 }; |