Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
7 "use strict";
9 this.EXPORTED_SYMBOLS = [
10 "DownloadsCommon",
11 ];
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 */
39 ////////////////////////////////////////////////////////////////////////////////
40 //// Globals
42 const Cc = Components.classes;
43 const Ci = Components.interfaces;
44 const Cu = Components.utils;
45 const Cr = Components.results;
47 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
48 Cu.import("resource://gre/modules/Services.jsm");
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");
73 const nsIDM = Ci.nsIDownloadManager;
75 const kDownloadsStringBundleUrl =
76 "chrome://browser/locale/downloads/downloads.properties";
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 };
89 const kDownloadsStringsRequiringPluralForm = {
90 otherDownloads2: true
91 };
93 XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
94 return Components.Constructor("@mozilla.org/file/local;1",
95 "nsILocalFile", "initWithPath");
96 });
98 const kPartialDownloadSuffix = ".part";
100 const kPrefBranch = Services.prefs.getBranch("browser.download.");
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 };
131 PrefObserver.register({
132 // prefName: defaultValue
133 debug: false,
134 animateNotifications: true
135 });
138 ////////////////////////////////////////////////////////////////////////////////
139 //// DownloadsCommon
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 },
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 },
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 },
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 },
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 },
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 },
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 },
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,
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 }
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 }
375 if (summary.numActive != 0 && summary.totalSize != 0 &&
376 summary.numActive != summary.numScanning) {
377 summary.percentComplete = (summary.totalTransferred /
378 summary.totalSize) * 100;
379 }
381 if (summary.slowestSpeed == Infinity) {
382 summary.slowestSpeed = 0;
383 }
385 return summary;
386 },
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 }
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 }
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 },
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");
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 }
455 promiseShouldLaunch.then(shouldLaunch => {
456 if (!shouldLaunch) {
457 return;
458 }
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) { }
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 },
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 };
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 });
529 ////////////////////////////////////////////////////////////////////////////////
530 //// DownloadsData
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;
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 = {};
561 // Array of view objects that should be notified when the available download
562 // data changes.
563 this._views = [];
565 // Maps Download objects to DownloadDataItem objects.
566 this._downloadToDataItemMap = new Map();
567 }
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,
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 },
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 },
608 //////////////////////////////////////////////////////////////////////////////
609 //// Integration with the asynchronous Downloads back-end
611 onDownloadAdded: function (aDownload)
612 {
613 let dataItem = new DownloadsDataItem(aDownload);
614 this._downloadToDataItemMap.set(aDownload, dataItem);
615 this.dataItems[dataItem.downloadGuid] = dataItem;
617 for (let view of this._views) {
618 view.onDataItemAdded(dataItem, true);
619 }
621 this._updateDataItemState(dataItem);
622 },
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 }
632 this._updateDataItemState(dataItem);
633 },
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 }
643 this._downloadToDataItemMap.delete(aDownload);
644 this.dataItems[dataItem.downloadGuid] = null;
645 for (let view of this._views) {
646 view.onDataItemRemoved(dataItem);
647 }
648 },
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;
659 aDataItem.updateFromDownload();
661 if (wasInProgress && !aDataItem.inProgress) {
662 aDataItem.endTime = Date.now();
663 }
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 }
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 }
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 }
696 if (!aDataItem.newDownloadNotified) {
697 aDataItem.newDownloadNotified = true;
698 this._notifyDownloadEvent("start");
699 }
701 if (!wasDone && aDataItem.done) {
702 this._notifyDownloadEvent("finish");
703 }
705 for (let view of this._views) {
706 view.getViewItem(aDataItem).onProgressChange();
707 }
708 },
710 //////////////////////////////////////////////////////////////////////////////
711 //// Registration of views
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 },
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 },
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();
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 );
762 // Notify the view that all data is available.
763 aView.onDataLoadCompleted();
764 },
766 //////////////////////////////////////////////////////////////////////////////
767 //// Notifications sent to the most recent browser window only
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 },
780 set panelHasShownBefore(aValue) {
781 Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
782 return aValue;
783 },
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.");
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 }
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 };
815 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() {
816 return new DownloadsDataCtor(true);
817 });
819 XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
820 return new DownloadsDataCtor(false);
821 });
823 ////////////////////////////////////////////////////////////////////////////////
824 //// DownloadsDataItem
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;
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();
844 this.updateFromDownload();
845 }
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,
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 }
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;
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 },
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 },
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 },
932 /**
933 * Indicates whether the download is paused.
934 */
935 get paused()
936 {
937 return this.state == nsIDM.DOWNLOAD_PAUSED;
938 },
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 },
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 },
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 },
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 },
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 },
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 },
1025 /**
1026 * Open the target file for this download.
1027 */
1028 openLocalFile: function () {
1029 this._download.launch().then(null, Cu.reportError);
1030 },
1032 /**
1033 * Show the downloaded file in the system file manager.
1034 */
1035 showLocalFile: function DDI_showLocalFile() {
1036 DownloadsCommon.showDownloadedFile(this.localFile);
1037 },
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 },
1051 /**
1052 * Attempts to retry the download.
1053 * @throws if we cannot.
1054 */
1055 retry: function DDI_retry() {
1056 this._download.start();
1057 },
1059 /**
1060 * Cancels the download.
1061 */
1062 cancel: function() {
1063 this._download.cancel();
1064 this._download.removePartialData().then(null, Cu.reportError);
1065 },
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 };
1078 ////////////////////////////////////////////////////////////////////////////////
1079 //// DownloadsViewPrototype
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
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,
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,
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 }
1124 this._views.push(aView);
1125 this.refreshView(aView);
1126 },
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 },
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 }
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 },
1165 //////////////////////////////////////////////////////////////////////////////
1166 //// Callback functions from DownloadsData
1168 /**
1169 * Indicates whether we are still loading downloads data asynchronously.
1170 */
1171 _loading: false,
1173 /**
1174 * Called before multiple downloads are about to be loaded.
1175 */
1176 onDataLoadStarting: function DVP_onDataLoadStarting()
1177 {
1178 this._loading = true;
1179 },
1181 /**
1182 * Called after data loading finished.
1183 */
1184 onDataLoadCompleted: function DVP_onDataLoadCompleted()
1185 {
1186 this._loading = false;
1187 },
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 },
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 },
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 },
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 },
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 };
1260 ////////////////////////////////////////////////////////////////////////////////
1261 //// DownloadsIndicatorData
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,
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);
1290 if (this._views.length == 0) {
1291 this._itemCount = 0;
1292 }
1293 },
1295 //////////////////////////////////////////////////////////////////////////////
1296 //// Callback functions from DownloadsData
1298 /**
1299 * Called after data loading finished.
1300 */
1301 onDataLoadCompleted: function DID_onDataLoadCompleted()
1302 {
1303 DownloadsViewPrototype.onDataLoadCompleted.call(this);
1304 this._updateViews();
1305 },
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 },
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 },
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 }
1359 // Since the state of a download changed, reset the estimated time left.
1360 data._lastRawTimeLeft = -1;
1361 data._lastTimeLeft = -1;
1363 data._updateViews();
1364 },
1365 onProgressChange: function DIVI_onProgressChange()
1366 {
1367 data._updateViews();
1368 }
1369 });
1370 },
1372 //////////////////////////////////////////////////////////////////////////////
1373 //// Propagation of properties to our views
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,
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,
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,
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 }
1416 this._refreshProperties();
1417 this._views.forEach(this._updateView, this);
1418 },
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 },
1435 //////////////////////////////////////////////////////////////////////////////
1436 //// Property updating based on current download status
1438 /**
1439 * Number of download items that are available to be displayed.
1440 */
1441 _itemCount: 0,
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,
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,
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 },
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());
1484 // Determine if the indicator should be shown or get attention.
1485 this._hasDownloads = (this._itemCount > 0);
1487 // If all downloads are paused, show the progress indicator as paused.
1488 this._paused = summary.numActive > 0 &&
1489 summary.numActive == summary.numPaused;
1491 this._percentComplete = summary.percentComplete;
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 };
1511 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() {
1512 return new DownloadsIndicatorDataCtor(true);
1513 });
1515 XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() {
1516 return new DownloadsIndicatorDataCtor(false);
1517 });
1519 ////////////////////////////////////////////////////////////////////////////////
1520 //// DownloadsSummaryData
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;
1543 this._dataItems = [];
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;
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;
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;
1565 this._isPrivate = aIsPrivate;
1566 this._views = [];
1567 }
1569 DownloadsSummaryData.prototype = {
1570 __proto__: DownloadsViewPrototype,
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);
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 },
1589 //////////////////////////////////////////////////////////////////////////////
1590 //// Callback functions from DownloadsData - see the documentation in
1591 //// DownloadsViewPrototype for more information on what these functions
1592 //// are used for.
1594 onDataLoadCompleted: function DSD_onDataLoadCompleted()
1595 {
1596 DownloadsViewPrototype.onDataLoadCompleted.call(this);
1597 this._updateViews();
1598 },
1600 onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest)
1601 {
1602 if (aNewest) {
1603 this._dataItems.unshift(aDataItem);
1604 } else {
1605 this._dataItems.push(aDataItem);
1606 }
1608 this._updateViews();
1609 },
1611 onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem)
1612 {
1613 let itemIndex = this._dataItems.indexOf(aDataItem);
1614 this._dataItems.splice(itemIndex, 1);
1615 this._updateViews();
1616 },
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 },
1636 //////////////////////////////////////////////////////////////////////////////
1637 //// Propagation of properties to our views
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 }
1649 this._refreshProperties();
1650 this._views.forEach(this._updateView, this);
1651 },
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 },
1667 //////////////////////////////////////////////////////////////////////////////
1668 //// Property updating based on current download status
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 },
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());
1695 this._description = DownloadsCommon.strings
1696 .otherDownloads2(summary.numActive);
1697 this._percentComplete = summary.percentComplete;
1699 // If all downloads are paused, show the progress indicator as paused.
1700 this._showingProgress = summary.numDownloading > 0 ||
1701 summary.numPaused > 0;
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 }