toolkit/components/jsdownloads/src/DownloadList.jsm

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:4d6b460b4b0d
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 };

mercurial