mirror of https://github.com/roytam1/kmeleon.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1461 lines
48 KiB
1461 lines
48 KiB
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ |
|
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ |
|
/* This Source Code Form is subject to the terms of the Mozilla Public |
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
|
|
"use strict"; |
|
|
|
this.EXPORTED_SYMBOLS = [ |
|
"DownloadsCommon", |
|
]; |
|
|
|
/** |
|
* Handles the Downloads panel shared methods and data access. |
|
* |
|
* This file includes the following constructors and global objects: |
|
* |
|
* DownloadsCommon |
|
* This object is exposed directly to the consumers of this JavaScript module, |
|
* and provides shared methods for all the instances of the user interface. |
|
* |
|
* DownloadsData |
|
* Retrieves the list of past and completed downloads from the underlying |
|
* Downloads API data, and provides asynchronous notifications allowing |
|
* to build a consistent view of the available data. |
|
* |
|
* DownloadsIndicatorData |
|
* This object registers itself with DownloadsData as a view, and transforms the |
|
* notifications it receives into overall status data, that is then broadcast to |
|
* the registered download status indicators. |
|
*/ |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
//// Globals |
|
|
|
const Cc = Components.classes; |
|
const Ci = Components.interfaces; |
|
const Cu = Components.utils; |
|
const Cr = Components.results; |
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
Cu.import("resource://gre/modules/Services.jsm"); |
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
"resource://gre/modules/NetUtil.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
"resource://gre/modules/PluralForm.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "Downloads", |
|
"resource://gre/modules/Downloads.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", |
|
"resource://gre/modules/DownloadUIHelper.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", |
|
"resource://gre/modules/DownloadUtils.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
|
"resource://gre/modules/FileUtils.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "OS", |
|
"resource://gre/modules/osfile.jsm") |
|
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
|
"resource://gre/modules/PlacesUtils.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", |
|
"resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", |
|
"resource:///modules/RecentWindow.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
"resource://gre/modules/Promise.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
"resource://gre/modules/Task.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger", |
|
"resource:///modules/DownloadsLogger.jsm"); |
|
|
|
const nsIDM = Ci.nsIDownloadManager; |
|
|
|
const kDownloadsStringBundleUrl = |
|
"chrome://browser/locale/downloads/downloads.properties"; |
|
|
|
const kDownloadsStringsRequiringFormatting = { |
|
sizeWithUnits: true, |
|
shortTimeLeftSeconds: true, |
|
shortTimeLeftMinutes: true, |
|
shortTimeLeftHours: true, |
|
shortTimeLeftDays: true, |
|
statusSeparator: true, |
|
statusSeparatorBeforeNumber: true, |
|
fileExecutableSecurityWarning: true |
|
}; |
|
|
|
const kDownloadsStringsRequiringPluralForm = { |
|
otherDownloads2: true |
|
}; |
|
|
|
const kPartialDownloadSuffix = ".part"; |
|
|
|
const kPrefBranch = Services.prefs.getBranch("browser.download."); |
|
|
|
let PrefObserver = { |
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
|
Ci.nsISupportsWeakReference]), |
|
getPref(name) { |
|
try { |
|
switch (typeof this.prefs[name]) { |
|
case "boolean": |
|
return kPrefBranch.getBoolPref(name); |
|
} |
|
} catch (ex) { } |
|
return this.prefs[name]; |
|
}, |
|
observe(aSubject, aTopic, aData) { |
|
if (this.prefs.hasOwnProperty(aData)) { |
|
delete this[aData]; |
|
return this[aData] = this.getPref(aData); |
|
} |
|
}, |
|
register(prefs) { |
|
this.prefs = prefs; |
|
kPrefBranch.addObserver("", this, true); |
|
for (let key in prefs) { |
|
let name = key; |
|
XPCOMUtils.defineLazyGetter(this, name, function () { |
|
return PrefObserver.getPref(name); |
|
}); |
|
} |
|
}, |
|
}; |
|
|
|
PrefObserver.register({ |
|
// prefName: defaultValue |
|
debug: false, |
|
animateNotifications: true |
|
}); |
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
//// DownloadsCommon |
|
|
|
/** |
|
* This object is exposed directly to the consumers of this JavaScript module, |
|
* and provides shared methods for all the instances of the user interface. |
|
*/ |
|
this.DownloadsCommon = { |
|
/** |
|
* Constants with the different types of unblock messages. |
|
*/ |
|
BLOCK_VERDICT_MALWARE: "Malware", |
|
BLOCK_VERDICT_POTENTIALLY_UNWANTED: "PotentiallyUnwanted", |
|
BLOCK_VERDICT_UNCOMMON: "Uncommon", |
|
|
|
log(...aMessageArgs) { |
|
if (!PrefObserver.debug) { |
|
return; |
|
} |
|
DownloadsLogger.log(...aMessageArgs); |
|
}, |
|
|
|
error(...aMessageArgs) { |
|
if (!PrefObserver.debug) { |
|
return; |
|
} |
|
DownloadsLogger.reportError(...aMessageArgs); |
|
}, |
|
|
|
/** |
|
* Returns an object whose keys are the string names from the downloads string |
|
* bundle, and whose values are either the translated strings or functions |
|
* returning formatted strings. |
|
*/ |
|
get strings() { |
|
let strings = {}; |
|
let sb = Services.strings.createBundle(kDownloadsStringBundleUrl); |
|
let enumerator = sb.getSimpleEnumeration(); |
|
while (enumerator.hasMoreElements()) { |
|
let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); |
|
let stringName = string.key; |
|
if (stringName in kDownloadsStringsRequiringFormatting) { |
|
strings[stringName] = function () { |
|
// Convert "arguments" to a real array before calling into XPCOM. |
|
return sb.formatStringFromName(stringName, |
|
Array.slice(arguments, 0), |
|
arguments.length); |
|
}; |
|
} else if (stringName in kDownloadsStringsRequiringPluralForm) { |
|
strings[stringName] = function (aCount) { |
|
// Convert "arguments" to a real array before calling into XPCOM. |
|
let formattedString = sb.formatStringFromName(stringName, |
|
Array.slice(arguments, 0), |
|
arguments.length); |
|
return PluralForm.get(aCount, formattedString); |
|
}; |
|
} else { |
|
strings[stringName] = string.value; |
|
} |
|
} |
|
delete this.strings; |
|
return this.strings = strings; |
|
}, |
|
|
|
/** |
|
* Generates a very short string representing the given time left. |
|
* |
|
* @param aSeconds |
|
* Value to be formatted. It represents the number of seconds, it must |
|
* be positive but does not need to be an integer. |
|
* |
|
* @return Formatted string, for example "30s" or "2h". The returned value is |
|
* maximum three characters long, at least in English. |
|
*/ |
|
formatTimeLeft(aSeconds) { |
|
// Decide what text to show for the time |
|
let seconds = Math.round(aSeconds); |
|
if (!seconds) { |
|
return ""; |
|
} else if (seconds <= 30) { |
|
return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds); |
|
} |
|
let minutes = Math.round(aSeconds / 60); |
|
if (minutes < 60) { |
|
return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes); |
|
} |
|
let hours = Math.round(minutes / 60); |
|
if (hours < 48) { // two days |
|
return DownloadsCommon.strings["shortTimeLeftHours"](hours); |
|
} |
|
let days = Math.round(hours / 24); |
|
return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99)); |
|
}, |
|
|
|
/** |
|
* Indicates whether we should show visual notification on the indicator |
|
* when a download event is triggered. |
|
*/ |
|
get animateNotifications() { |
|
return PrefObserver.animateNotifications; |
|
}, |
|
|
|
/** |
|
* Get access to one of the DownloadsData or PrivateDownloadsData objects, |
|
* depending on the privacy status of the window in question. |
|
* |
|
* @param aWindow |
|
* The browser window which owns the download button. |
|
*/ |
|
getData(aWindow) { |
|
if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { |
|
return PrivateDownloadsData; |
|
} else { |
|
return DownloadsData; |
|
} |
|
}, |
|
|
|
/** |
|
* Initializes the Downloads back-end and starts receiving events for both the |
|
* private and non-private downloads data objects. |
|
*/ |
|
initializeAllDataLinks() { |
|
DownloadsData.initializeDataLink(); |
|
PrivateDownloadsData.initializeDataLink(); |
|
}, |
|
|
|
/** |
|
* Get access to one of the DownloadsIndicatorData or |
|
* PrivateDownloadsIndicatorData objects, depending on the privacy status of |
|
* the window in question. |
|
*/ |
|
getIndicatorData(aWindow) { |
|
if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { |
|
return PrivateDownloadsIndicatorData; |
|
} else { |
|
return DownloadsIndicatorData; |
|
} |
|
}, |
|
|
|
/** |
|
* Returns a reference to the DownloadsSummaryData singleton - creating one |
|
* in the process if one hasn't been instantiated yet. |
|
* |
|
* @param aWindow |
|
* The browser window which owns the download button. |
|
* @param aNumToExclude |
|
* The number of items on the top of the downloads list to exclude |
|
* from the summary. |
|
*/ |
|
getSummary(aWindow, aNumToExclude) { |
|
if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) { |
|
if (this._privateSummary) { |
|
return this._privateSummary; |
|
} |
|
return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude); |
|
} else { |
|
if (this._summary) { |
|
return this._summary; |
|
} |
|
return this._summary = new DownloadsSummaryData(false, aNumToExclude); |
|
} |
|
}, |
|
_summary: null, |
|
_privateSummary: null, |
|
|
|
/** |
|
* Returns the legacy state integer value for the provided Download object. |
|
*/ |
|
stateOfDownload(download) { |
|
// Collapse state using the correct priority. |
|
if (!download.stopped) { |
|
return nsIDM.DOWNLOAD_DOWNLOADING; |
|
} |
|
if (download.succeeded) { |
|
return nsIDM.DOWNLOAD_FINISHED; |
|
} |
|
if (download.error) { |
|
if (download.error.becauseBlockedByParentalControls) { |
|
return nsIDM.DOWNLOAD_BLOCKED_PARENTAL; |
|
} |
|
if (download.error.becauseBlockedByReputationCheck) { |
|
return nsIDM.DOWNLOAD_DIRTY; |
|
} |
|
return nsIDM.DOWNLOAD_FAILED; |
|
} |
|
if (download.canceled) { |
|
if (download.hasPartialData) { |
|
return nsIDM.DOWNLOAD_PAUSED; |
|
} |
|
return nsIDM.DOWNLOAD_CANCELED; |
|
} |
|
return nsIDM.DOWNLOAD_NOTSTARTED; |
|
}, |
|
|
|
/** |
|
* Helper function required because the Downloads Panel and the Downloads View |
|
* don't share the controller yet. |
|
*/ |
|
removeAndFinalizeDownload(download) { |
|
Downloads.getList(Downloads.ALL) |
|
.then(list => list.remove(download)) |
|
.then(() => download.finalize(true)) |
|
.catch(Cu.reportError); |
|
}, |
|
|
|
/** |
|
* Given an iterable collection of Download objects, generates and returns |
|
* statistics about that collection. |
|
* |
|
* @param downloads An iterable collection of Download objects. |
|
* |
|
* @return Object whose properties are the generated statistics. Currently, |
|
* we return the following properties: |
|
* |
|
* numActive : The total number of downloads. |
|
* numPaused : The total number of paused downloads. |
|
* numDownloading : The total number of downloads being downloaded. |
|
* totalSize : The total size of all downloads once completed. |
|
* totalTransferred: The total amount of transferred data for these |
|
* downloads. |
|
* slowestSpeed : The slowest download rate. |
|
* rawTimeLeft : The estimated time left for the downloads to |
|
* complete. |
|
* percentComplete : The percentage of bytes successfully downloaded. |
|
*/ |
|
summarizeDownloads(downloads) { |
|
let summary = { |
|
numActive: 0, |
|
numPaused: 0, |
|
numDownloading: 0, |
|
totalSize: 0, |
|
totalTransferred: 0, |
|
// slowestSpeed is Infinity so that we can use Math.min to |
|
// find the slowest speed. We'll set this to 0 afterwards if |
|
// it's still at Infinity by the time we're done iterating all |
|
// download. |
|
slowestSpeed: Infinity, |
|
rawTimeLeft: -1, |
|
percentComplete: -1 |
|
} |
|
|
|
for (let download of downloads) { |
|
summary.numActive++; |
|
|
|
if (!download.stopped) { |
|
summary.numDownloading++; |
|
if (download.hasProgress && download.speed > 0) { |
|
let sizeLeft = download.totalBytes - download.currentBytes; |
|
summary.rawTimeLeft = Math.max(summary.rawTimeLeft, |
|
sizeLeft / download.speed); |
|
summary.slowestSpeed = Math.min(summary.slowestSpeed, |
|
download.speed); |
|
} |
|
} else if (download.canceled && download.hasPartialData) { |
|
summary.numPaused++; |
|
} |
|
|
|
// Only add to total values if we actually know the download size. |
|
if (download.succeeded) { |
|
summary.totalSize += download.target.size; |
|
summary.totalTransferred += download.target.size; |
|
} else if (download.hasProgress) { |
|
summary.totalSize += download.totalBytes; |
|
summary.totalTransferred += download.currentBytes; |
|
} |
|
} |
|
|
|
if (summary.totalSize != 0) { |
|
summary.percentComplete = (summary.totalTransferred / |
|
summary.totalSize) * 100; |
|
} |
|
|
|
if (summary.slowestSpeed == Infinity) { |
|
summary.slowestSpeed = 0; |
|
} |
|
|
|
return summary; |
|
}, |
|
|
|
/** |
|
* If necessary, smooths the estimated number of seconds remaining for one |
|
* or more downloads to complete. |
|
* |
|
* @param aSeconds |
|
* Current raw estimate on number of seconds left for one or more |
|
* downloads. This is a floating point value to help get sub-second |
|
* accuracy for current and future estimates. |
|
*/ |
|
smoothSeconds(aSeconds, aLastSeconds) { |
|
// We apply an algorithm similar to the DownloadUtils.getTimeLeft function, |
|
// though tailored to a single time estimation for all downloads. We never |
|
// apply something if the new value is less than half the previous value. |
|
let shouldApplySmoothing = aLastSeconds >= 0 && |
|
aSeconds > aLastSeconds / 2; |
|
if (shouldApplySmoothing) { |
|
// Apply hysteresis to favor downward over upward swings. Trust only 30% |
|
// of the new value if lower, and 10% if higher (exponential smoothing). |
|
let diff = aSeconds - aLastSeconds; |
|
aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff; |
|
|
|
// If the new time is similar, reuse something close to the last time |
|
// left, but subtract a little to provide forward progress. |
|
diff = aSeconds - aLastSeconds; |
|
let diffPercent = diff / aLastSeconds * 100; |
|
if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { |
|
aSeconds = aLastSeconds - (diff < 0 ? .4 : .2); |
|
} |
|
} |
|
|
|
// In the last few seconds of downloading, we are always subtracting and |
|
// never adding to the time left. Ensure that we never fall below one |
|
// second left until all downloads are actually finished. |
|
return aLastSeconds = Math.max(aSeconds, 1); |
|
}, |
|
|
|
/** |
|
* Opens a downloaded file. |
|
* |
|
* @param aFile |
|
* the downloaded file to be opened. |
|
* @param aMimeInfo |
|
* the mime type info object. May be null. |
|
* @param aOwnerWindow |
|
* the window with which this action is associated. |
|
*/ |
|
openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) { |
|
if (!(aFile instanceof Ci.nsIFile)) { |
|
throw new Error("aFile must be a nsIFile object"); |
|
} |
|
if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) { |
|
throw new Error("Invalid value passed for aMimeInfo"); |
|
} |
|
if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) { |
|
throw new Error("aOwnerWindow must be a dom-window object"); |
|
} |
|
|
|
let promiseShouldLaunch; |
|
if (aFile.isExecutable()) { |
|
// We get a prompter for the provided window here, even though anchoring |
|
// to the most recently active window should work as well. |
|
promiseShouldLaunch = |
|
DownloadUIHelper.getPrompter(aOwnerWindow) |
|
.confirmLaunchExecutable(aFile.path); |
|
} else { |
|
promiseShouldLaunch = Promise.resolve(true); |
|
} |
|
|
|
promiseShouldLaunch.then(shouldLaunch => { |
|
if (!shouldLaunch) { |
|
return; |
|
} |
|
|
|
// Actually open the file. |
|
try { |
|
if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) { |
|
aMimeInfo.launchWithFile(aFile); |
|
return; |
|
} |
|
} catch (ex) { } |
|
|
|
// If either we don't have the mime info, or the preferred action failed, |
|
// attempt to launch the file directly. |
|
try { |
|
aFile.launch(); |
|
} catch (ex) { |
|
// If launch fails, try sending it through the system's external "file:" |
|
// URL handler. |
|
Cc["@mozilla.org/uriloader/external-protocol-service;1"] |
|
.getService(Ci.nsIExternalProtocolService) |
|
.loadUrl(NetUtil.newURI(aFile)); |
|
} |
|
}).then(null, Cu.reportError); |
|
}, |
|
|
|
/** |
|
* Show a downloaded file in the system file manager. |
|
* |
|
* @param aFile |
|
* a downloaded file. |
|
*/ |
|
showDownloadedFile(aFile) { |
|
if (!(aFile instanceof Ci.nsIFile)) { |
|
throw new Error("aFile must be a nsIFile object"); |
|
} |
|
try { |
|
// Show the directory containing the file and select the file. |
|
aFile.reveal(); |
|
} catch (ex) { |
|
// If reveal fails for some reason (e.g., it's not implemented on unix |
|
// or the file doesn't exist), try using the parent if we have it. |
|
let parent = aFile.parent; |
|
if (parent) { |
|
try { |
|
// Open the parent directory to show where the file should be. |
|
parent.launch(); |
|
} catch (ex) { |
|
// If launch also fails (probably because it's not implemented), let |
|
// the OS handler try to open the parent. |
|
Cc["@mozilla.org/uriloader/external-protocol-service;1"] |
|
.getService(Ci.nsIExternalProtocolService) |
|
.loadUrl(NetUtil.newURI(parent)); |
|
} |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Displays an alert message box which asks the user if they want to |
|
* unblock the downloaded file or not. |
|
* |
|
* @param aType |
|
* The type of malware the downloaded file contains. |
|
* @param aOwnerWindow |
|
* The window with which this action is associated. |
|
* |
|
* @return True to unblock the file, false to keep the user safe and |
|
* cancel the operation. |
|
*/ |
|
confirmUnblockDownload: Task.async(function* (aType, aOwnerWindow) { |
|
let s = DownloadsCommon.strings; |
|
let title = s.unblockHeader; |
|
let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + |
|
(Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) + |
|
Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; |
|
let type = ""; |
|
let message = s.unblockTip; |
|
let okButton = s.unblockButtonContinue; |
|
let cancelButton = s.unblockButtonCancel; |
|
|
|
switch (aType) { |
|
case this.BLOCK_VERDICT_MALWARE: |
|
type = s.unblockTypeMalware; |
|
break; |
|
case this.BLOCK_VERDICT_POTENTIALLY_UNWANTED: |
|
type = s.unblockTypePotentiallyUnwanted; |
|
break; |
|
case this.BLOCK_VERDICT_UNCOMMON: |
|
type = s.unblockTypeUncommon; |
|
break; |
|
} |
|
|
|
if (type) { |
|
message = type + "\n\n" + message; |
|
} |
|
|
|
Services.ww.registerNotification(function onOpen(subj, topic) { |
|
if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) { |
|
// Make sure to listen for "DOMContentLoaded" because it is fired |
|
// before the "load" event. |
|
subj.addEventListener("DOMContentLoaded", function onLoad() { |
|
subj.removeEventListener("DOMContentLoaded", onLoad); |
|
if (subj.document.documentURI == |
|
"chrome://global/content/commonDialog.xul") { |
|
Services.ww.unregisterNotification(onOpen); |
|
let dialog = subj.document.getElementById("commonDialog"); |
|
if (dialog) { |
|
// Change the dialog to use a warning icon. |
|
dialog.classList.add("alert-dialog"); |
|
} |
|
} |
|
}); |
|
} |
|
}); |
|
|
|
// The ordering of the ok/cancel buttons is used this way to allow "cancel" |
|
// to have the same result as hitting the ESC or Close button (see bug 345067). |
|
let rv = Services.prompt.confirmEx(aOwnerWindow, title, message, buttonFlags, |
|
okButton, cancelButton, null, null, {}); |
|
return (rv == 0); |
|
}), |
|
}; |
|
|
|
/** |
|
* Returns true if we are executing on Windows Vista or a later version. |
|
*/ |
|
XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () { |
|
let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; |
|
if (os != "WINNT") { |
|
return false; |
|
} |
|
let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); |
|
return parseFloat(sysInfo.getProperty("version")) >= 6; |
|
}); |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
//// DownloadsData |
|
|
|
/** |
|
* Retrieves the list of past and completed downloads from the underlying |
|
* Download Manager data, and provides asynchronous notifications allowing to |
|
* build a consistent view of the available data. |
|
* |
|
* This object responds to real-time changes in the underlying Download Manager |
|
* data. For example, the deletion of one or more downloads is notified through |
|
* the nsIObserver interface, while any state or progress change is notified |
|
* through the nsIDownloadProgressListener interface. |
|
* |
|
* Note that using this object does not automatically start the Download Manager |
|
* service. Consumers will see an empty list of downloads until the service is |
|
* actually started. This is useful to display a neutral progress indicator in |
|
* the main browser window until the autostart timeout elapses. |
|
* |
|
* Note that DownloadsData and PrivateDownloadsData are two equivalent singleton |
|
* objects, one accessing non-private downloads, and the other accessing private |
|
* ones. |
|
*/ |
|
function DownloadsDataCtor(aPrivate) { |
|
this._isPrivate = aPrivate; |
|
|
|
// Contains all the available Download objects and their integer state. |
|
this.oldDownloadStates = new Map(); |
|
|
|
// Array of view objects that should be notified when the available download |
|
// data changes. |
|
this._views = []; |
|
} |
|
|
|
DownloadsDataCtor.prototype = { |
|
/** |
|
* Starts receiving events for current downloads. |
|
*/ |
|
initializeDataLink() { |
|
if (!this._dataLinkInitialized) { |
|
let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE |
|
: Downloads.PUBLIC); |
|
promiseList.then(list => list.addView(this)).then(null, Cu.reportError); |
|
this._dataLinkInitialized = true; |
|
} |
|
}, |
|
_dataLinkInitialized: false, |
|
|
|
/** |
|
* Iterator for all the available Download objects. This is empty until the |
|
* data has been loaded using the JavaScript API for downloads. |
|
*/ |
|
get downloads() this.oldDownloadStates.keys(), |
|
|
|
/** |
|
* True if there are finished downloads that can be removed from the list. |
|
*/ |
|
get canRemoveFinished() { |
|
for (let download of this.downloads) { |
|
// Stopped, paused, and failed downloads with partial data are removed. |
|
if (download.stopped && !(download.canceled && download.hasPartialData)) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
}, |
|
|
|
/** |
|
* Asks the back-end to remove finished downloads from the list. |
|
*/ |
|
removeFinished() { |
|
let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE |
|
: Downloads.PUBLIC); |
|
promiseList.then(list => list.removeFinished()) |
|
.then(null, Cu.reportError); |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Integration with the asynchronous Downloads back-end |
|
|
|
onDownloadAdded(download) { |
|
// Download objects do not store the end time of downloads, as the Downloads |
|
// API does not need to persist this information for all platforms. Once a |
|
// download terminates on a Desktop browser, it becomes a history download, |
|
// for which the end time is stored differently, as a Places annotation. |
|
download.endTime = Date.now(); |
|
|
|
this.oldDownloadStates.set(download, |
|
DownloadsCommon.stateOfDownload(download)); |
|
|
|
for (let view of this._views) { |
|
view.onDownloadAdded(download, true); |
|
} |
|
}, |
|
|
|
onDownloadChanged(download) { |
|
let oldState = this.oldDownloadStates.get(download); |
|
let newState = DownloadsCommon.stateOfDownload(download); |
|
this.oldDownloadStates.set(download, newState); |
|
|
|
if (oldState != newState) { |
|
if (download.succeeded || |
|
(download.canceled && !download.hasPartialData) || |
|
download.error) { |
|
// Store the end time that may be displayed by the views. |
|
download.endTime = Date.now(); |
|
|
|
// This state transition code should actually be located in a Downloads |
|
// API module (bug 941009). Moreover, the fact that state is stored as |
|
// annotations should be ideally hidden behind methods of |
|
// nsIDownloadHistory (bug 830415). |
|
if (!this._isPrivate) { |
|
try { |
|
let downloadMetaData = { |
|
state: DownloadsCommon.stateOfDownload(download), |
|
endTime: download.endTime, |
|
}; |
|
if (download.succeeded) { |
|
downloadMetaData.fileSize = download.target.size; |
|
} |
|
|
|
PlacesUtils.annotations.setPageAnnotation( |
|
NetUtil.newURI(download.source.url), |
|
"downloads/metaData", |
|
JSON.stringify(downloadMetaData), 0, |
|
PlacesUtils.annotations.EXPIRE_WITH_HISTORY); |
|
} catch (ex) { |
|
Cu.reportError(ex); |
|
} |
|
} |
|
} |
|
|
|
for (let view of this._views) { |
|
try { |
|
view.onDownloadStateChanged(download); |
|
} catch (ex) { |
|
Cu.reportError(ex); |
|
} |
|
} |
|
|
|
if (download.succeeded || |
|
(download.error && download.error.becauseBlocked)) { |
|
this._notifyDownloadEvent("finish"); |
|
} |
|
} |
|
|
|
if (!download.newDownloadNotified) { |
|
download.newDownloadNotified = true; |
|
this._notifyDownloadEvent("start"); |
|
} |
|
|
|
for (let view of this._views) { |
|
view.onDownloadChanged(download); |
|
} |
|
}, |
|
|
|
onDownloadRemoved(download) { |
|
this.oldDownloadStates.delete(download); |
|
|
|
for (let view of this._views) { |
|
view.onDownloadRemoved(download); |
|
} |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Registration of views |
|
|
|
/** |
|
* Adds an object to be notified when the available download data changes. |
|
* The specified object is initialized with the currently available downloads. |
|
* |
|
* @param aView |
|
* DownloadsView object to be added. This reference must be passed to |
|
* removeView before termination. |
|
*/ |
|
addView(aView) { |
|
this._views.push(aView); |
|
this._updateView(aView); |
|
}, |
|
|
|
/** |
|
* Removes an object previously added using addView. |
|
* |
|
* @param aView |
|
* DownloadsView object to be removed. |
|
*/ |
|
removeView(aView) { |
|
let index = this._views.indexOf(aView); |
|
if (index != -1) { |
|
this._views.splice(index, 1); |
|
} |
|
}, |
|
|
|
/** |
|
* Ensures that the currently loaded data is added to the specified view. |
|
* |
|
* @param aView |
|
* DownloadsView object to be initialized. |
|
*/ |
|
_updateView(aView) { |
|
// Indicate to the view that a batch loading operation is in progress. |
|
aView.onDataLoadStarting(); |
|
|
|
// Sort backwards by start time, ensuring that the most recent |
|
// downloads are added first regardless of their state. |
|
let downloadsArray = [...this.downloads]; |
|
downloadsArray.sort((a, b) => b.startTime - a.startTime); |
|
downloadsArray.forEach(download => aView.onDownloadAdded(download, false)); |
|
|
|
// Notify the view that all data is available. |
|
aView.onDataLoadCompleted(); |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Notifications sent to the most recent browser window only |
|
|
|
/** |
|
* Set to true after the first download causes the downloads panel to be |
|
* displayed. |
|
*/ |
|
get panelHasShownBefore() { |
|
try { |
|
return Services.prefs.getBoolPref("browser.download.panel.shown"); |
|
} catch (ex) { } |
|
return false; |
|
}, |
|
|
|
set panelHasShownBefore(aValue) { |
|
Services.prefs.setBoolPref("browser.download.panel.shown", aValue); |
|
return aValue; |
|
}, |
|
|
|
/** |
|
* Displays a new or finished download notification in the most recent browser |
|
* window, if one is currently available with the required privacy type. |
|
* |
|
* @param aType |
|
* Set to "start" for new downloads, "finish" for completed downloads. |
|
*/ |
|
_notifyDownloadEvent(aType) { |
|
DownloadsCommon.log("Attempting to notify that a new download has started or finished."); |
|
|
|
// Show the panel in the most recent browser window, if present. |
|
let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate }); |
|
if (!browserWin) { |
|
return; |
|
} |
|
|
|
if (this.panelHasShownBefore) { |
|
// For new downloads after the first one, don't show the panel |
|
// automatically, but provide a visible notification in the topmost |
|
// browser window, if the status indicator is already visible. |
|
DownloadsCommon.log("Showing new download notification."); |
|
browserWin.DownloadsIndicatorView.showEventNotification(aType); |
|
return; |
|
} |
|
this.panelHasShownBefore = true; |
|
browserWin.DownloadsPanel.showPanel(); |
|
} |
|
}; |
|
|
|
XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() { |
|
return new DownloadsDataCtor(true); |
|
}); |
|
|
|
XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() { |
|
return new DownloadsDataCtor(false); |
|
}); |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
//// DownloadsViewPrototype |
|
|
|
/** |
|
* A prototype for an object that registers itself with DownloadsData as soon |
|
* as a view is registered with it. |
|
*/ |
|
const DownloadsViewPrototype = { |
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Registration of views |
|
|
|
/** |
|
* Array of view objects that should be notified when the available status |
|
* data changes. |
|
* |
|
* SUBCLASSES MUST OVERRIDE THIS PROPERTY. |
|
*/ |
|
_views: null, |
|
|
|
/** |
|
* Determines whether this view object is over the private or non-private |
|
* downloads. |
|
* |
|
* SUBCLASSES MUST OVERRIDE THIS PROPERTY. |
|
*/ |
|
_isPrivate: false, |
|
|
|
/** |
|
* Adds an object to be notified when the available status data changes. |
|
* The specified object is initialized with the currently available status. |
|
* |
|
* @param aView |
|
* View object to be added. This reference must be |
|
* passed to removeView before termination. |
|
*/ |
|
addView(aView) { |
|
// Start receiving events when the first of our views is registered. |
|
if (this._views.length == 0) { |
|
if (this._isPrivate) { |
|
PrivateDownloadsData.addView(this); |
|
} else { |
|
DownloadsData.addView(this); |
|
} |
|
} |
|
|
|
this._views.push(aView); |
|
this.refreshView(aView); |
|
}, |
|
|
|
/** |
|
* Updates the properties of an object previously added using addView. |
|
* |
|
* @param aView |
|
* View object to be updated. |
|
*/ |
|
refreshView(aView) { |
|
// Update immediately even if we are still loading data asynchronously. |
|
// Subclasses must provide these two functions! |
|
this._refreshProperties(); |
|
this._updateView(aView); |
|
}, |
|
|
|
/** |
|
* Removes an object previously added using addView. |
|
* |
|
* @param aView |
|
* View object to be removed. |
|
*/ |
|
removeView(aView) { |
|
let index = this._views.indexOf(aView); |
|
if (index != -1) { |
|
this._views.splice(index, 1); |
|
} |
|
|
|
// Stop receiving events when the last of our views is unregistered. |
|
if (this._views.length == 0) { |
|
if (this._isPrivate) { |
|
PrivateDownloadsData.removeView(this); |
|
} else { |
|
DownloadsData.removeView(this); |
|
} |
|
} |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Callback functions from DownloadsData |
|
|
|
/** |
|
* Indicates whether we are still loading downloads data asynchronously. |
|
*/ |
|
_loading: false, |
|
|
|
/** |
|
* Called before multiple downloads are about to be loaded. |
|
*/ |
|
onDataLoadStarting() { |
|
this._loading = true; |
|
}, |
|
|
|
/** |
|
* Called after data loading finished. |
|
*/ |
|
onDataLoadCompleted() { |
|
this._loading = false; |
|
}, |
|
|
|
/** |
|
* Called when a new download data item is available, either during the |
|
* asynchronous data load or when a new download is started. |
|
* |
|
* @param download |
|
* Download object that was just added. |
|
* @param newest |
|
* When true, indicates that this item is the most recent and should be |
|
* added in the topmost position. This happens when a new download is |
|
* started. When false, indicates that the item is the least recent |
|
* with regard to the items that have been already added. The latter |
|
* generally happens during the asynchronous data load. |
|
* |
|
* @note Subclasses should override this. |
|
*/ |
|
onDownloadAdded(download, newest) { |
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
}, |
|
|
|
/** |
|
* Called when the overall state of a Download has changed. In particular, |
|
* this is called only once when the download succeeds or is blocked |
|
* permanently, and is never called if only the current progress changed. |
|
* |
|
* The onDownloadChanged notification will always be sent afterwards. |
|
* |
|
* @note Subclasses should override this. |
|
*/ |
|
onDownloadStateChanged(download) { |
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
}, |
|
|
|
/** |
|
* Called every time any state property of a Download may have changed, |
|
* including progress properties. |
|
* |
|
* Note that progress notification changes are throttled at the Downloads.jsm |
|
* API level, and there is no throttling mechanism in the front-end. |
|
* |
|
* @note Subclasses should override this. |
|
*/ |
|
onDownloadChanged(download) { |
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
}, |
|
|
|
/** |
|
* Called when a data item is removed, ensures that the widget associated with |
|
* the view item is removed from the user interface. |
|
* |
|
* @param download |
|
* Download object that is being removed. |
|
* |
|
* @note Subclasses should override this. |
|
*/ |
|
onDownloadRemoved(download) { |
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
}, |
|
|
|
/** |
|
* Private function used to refresh the internal properties being sent to |
|
* each registered view. |
|
* |
|
* @note Subclasses should override this. |
|
*/ |
|
_refreshProperties() { |
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
}, |
|
|
|
/** |
|
* Private function used to refresh an individual view. |
|
* |
|
* @note Subclasses should override this. |
|
*/ |
|
_updateView() { |
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
}, |
|
}; |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
//// DownloadsIndicatorData |
|
|
|
/** |
|
* This object registers itself with DownloadsData as a view, and transforms the |
|
* notifications it receives into overall status data, that is then broadcast to |
|
* the registered download status indicators. |
|
* |
|
* Note that using this object does not automatically start the Download Manager |
|
* service. Consumers will see an empty list of downloads until the service is |
|
* actually started. This is useful to display a neutral progress indicator in |
|
* the main browser window until the autostart timeout elapses. |
|
*/ |
|
function DownloadsIndicatorDataCtor(aPrivate) { |
|
this._isPrivate = aPrivate; |
|
this._views = []; |
|
} |
|
DownloadsIndicatorDataCtor.prototype = { |
|
__proto__: DownloadsViewPrototype, |
|
|
|
/** |
|
* Removes an object previously added using addView. |
|
* |
|
* @param aView |
|
* DownloadsIndicatorView object to be removed. |
|
*/ |
|
removeView(aView) { |
|
DownloadsViewPrototype.removeView.call(this, aView); |
|
|
|
if (this._views.length == 0) { |
|
this._itemCount = 0; |
|
} |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Callback functions from DownloadsData |
|
|
|
onDataLoadCompleted() { |
|
DownloadsViewPrototype.onDataLoadCompleted.call(this); |
|
this._updateViews(); |
|
}, |
|
|
|
onDownloadAdded(download, newest) { |
|
this._itemCount++; |
|
this._updateViews(); |
|
}, |
|
|
|
onDownloadStateChanged(download) { |
|
if (download.succeeded || download.error) { |
|
this.attention = true; |
|
} |
|
|
|
// Since the state of a download changed, reset the estimated time left. |
|
this._lastRawTimeLeft = -1; |
|
this._lastTimeLeft = -1; |
|
}, |
|
|
|
onDownloadChanged(download) { |
|
this._updateViews(); |
|
}, |
|
|
|
onDownloadRemoved(download) { |
|
this._itemCount--; |
|
this._updateViews(); |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Propagation of properties to our views |
|
|
|
// The following properties are updated by _refreshProperties and are then |
|
// propagated to the views. See _refreshProperties for details. |
|
_hasDownloads: false, |
|
_counter: "", |
|
_percentComplete: -1, |
|
_paused: false, |
|
|
|
/** |
|
* Indicates whether the download indicators should be highlighted. |
|
*/ |
|
set attention(aValue) { |
|
this._attention = aValue; |
|
this._updateViews(); |
|
return aValue; |
|
}, |
|
_attention: false, |
|
|
|
/** |
|
* Indicates whether the user is interacting with downloads, thus the |
|
* attention indication should not be shown even if requested. |
|
*/ |
|
set attentionSuppressed(aValue) { |
|
this._attentionSuppressed = aValue; |
|
this._attention = false; |
|
this._updateViews(); |
|
return aValue; |
|
}, |
|
_attentionSuppressed: false, |
|
|
|
/** |
|
* Computes aggregate values and propagates the changes to our views. |
|
*/ |
|
_updateViews() { |
|
// Do not update the status indicators during batch loads of download items. |
|
if (this._loading) { |
|
return; |
|
} |
|
|
|
this._refreshProperties(); |
|
this._views.forEach(this._updateView, this); |
|
}, |
|
|
|
/** |
|
* Updates the specified view with the current aggregate values. |
|
* |
|
* @param aView |
|
* DownloadsIndicatorView object to be updated. |
|
*/ |
|
_updateView(aView) { |
|
aView.hasDownloads = this._hasDownloads; |
|
aView.counter = this._counter; |
|
aView.percentComplete = this._percentComplete; |
|
aView.paused = this._paused; |
|
aView.attention = this._attention && !this._attentionSuppressed; |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Property updating based on current download status |
|
|
|
/** |
|
* Number of download items that are available to be displayed. |
|
*/ |
|
_itemCount: 0, |
|
|
|
/** |
|
* Floating point value indicating the last number of seconds estimated until |
|
* the longest download will finish. We need to store this value so that we |
|
* don't continuously apply smoothing if the actual download state has not |
|
* changed. This is set to -1 if the previous value is unknown. |
|
*/ |
|
_lastRawTimeLeft: -1, |
|
|
|
/** |
|
* Last number of seconds estimated until all in-progress downloads with a |
|
* known size and speed will finish. This value is stored to allow smoothing |
|
* in case of small variations. This is set to -1 if the previous value is |
|
* unknown. |
|
*/ |
|
_lastTimeLeft: -1, |
|
|
|
/** |
|
* A generator function for the Download objects this summary is currently |
|
* interested in. This generator is passed off to summarizeDownloads in order |
|
* to generate statistics about the downloads we care about - in this case, |
|
* it's all active downloads. |
|
*/ |
|
* _activeDownloads() { |
|
let downloads = this._isPrivate ? PrivateDownloadsData.downloads |
|
: DownloadsData.downloads; |
|
for (let download of downloads) { |
|
if (!download.stopped || (download.canceled && download.hasPartialData)) { |
|
yield download; |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Computes aggregate values based on the current state of downloads. |
|
*/ |
|
_refreshProperties() { |
|
let summary = |
|
DownloadsCommon.summarizeDownloads(this._activeDownloads()); |
|
|
|
// Determine if the indicator should be shown or get attention. |
|
this._hasDownloads = (this._itemCount > 0); |
|
|
|
// If all downloads are paused, show the progress indicator as paused. |
|
this._paused = summary.numActive > 0 && |
|
summary.numActive == summary.numPaused; |
|
|
|
this._percentComplete = summary.percentComplete; |
|
|
|
// Display the estimated time left, if present. |
|
if (summary.rawTimeLeft == -1) { |
|
// There are no downloads with a known time left. |
|
this._lastRawTimeLeft = -1; |
|
this._lastTimeLeft = -1; |
|
this._counter = ""; |
|
} else { |
|
// Compute the new time left only if state actually changed. |
|
if (this._lastRawTimeLeft != summary.rawTimeLeft) { |
|
this._lastRawTimeLeft = summary.rawTimeLeft; |
|
this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, |
|
this._lastTimeLeft); |
|
} |
|
this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft); |
|
} |
|
} |
|
}; |
|
|
|
XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() { |
|
return new DownloadsIndicatorDataCtor(true); |
|
}); |
|
|
|
XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() { |
|
return new DownloadsIndicatorDataCtor(false); |
|
}); |
|
|
|
//////////////////////////////////////////////////////////////////////////////// |
|
//// DownloadsSummaryData |
|
|
|
/** |
|
* DownloadsSummaryData is a view for DownloadsData that produces a summary |
|
* of all downloads after a certain exclusion point aNumToExclude. For example, |
|
* if there were 5 downloads in progress, and a DownloadsSummaryData was |
|
* constructed with aNumToExclude equal to 3, then that DownloadsSummaryData |
|
* would produce a summary of the last 2 downloads. |
|
* |
|
* @param aIsPrivate |
|
* True if the browser window which owns the download button is a private |
|
* window. |
|
* @param aNumToExclude |
|
* The number of items to exclude from the summary, starting from the |
|
* top of the list. |
|
*/ |
|
function DownloadsSummaryData(aIsPrivate, aNumToExclude) { |
|
this._numToExclude = aNumToExclude; |
|
// Since we can have multiple instances of DownloadsSummaryData, we |
|
// override these values from the prototype so that each instance can be |
|
// completely separated from one another. |
|
this._loading = false; |
|
|
|
this._downloads = []; |
|
|
|
// Floating point value indicating the last number of seconds estimated until |
|
// the longest download will finish. We need to store this value so that we |
|
// don't continuously apply smoothing if the actual download state has not |
|
// changed. This is set to -1 if the previous value is unknown. |
|
this._lastRawTimeLeft = -1; |
|
|
|
// Last number of seconds estimated until all in-progress downloads with a |
|
// known size and speed will finish. This value is stored to allow smoothing |
|
// in case of small variations. This is set to -1 if the previous value is |
|
// unknown. |
|
this._lastTimeLeft = -1; |
|
|
|
// The following properties are updated by _refreshProperties and are then |
|
// propagated to the views. |
|
this._showingProgress = false; |
|
this._details = ""; |
|
this._description = ""; |
|
this._numActive = 0; |
|
this._percentComplete = -1; |
|
|
|
this._isPrivate = aIsPrivate; |
|
this._views = []; |
|
} |
|
|
|
DownloadsSummaryData.prototype = { |
|
__proto__: DownloadsViewPrototype, |
|
|
|
/** |
|
* Removes an object previously added using addView. |
|
* |
|
* @param aView |
|
* DownloadsSummary view to be removed. |
|
*/ |
|
removeView(aView) { |
|
DownloadsViewPrototype.removeView.call(this, aView); |
|
|
|
if (this._views.length == 0) { |
|
// Clear out our collection of Download objects. If we ever have |
|
// another view registered with us, this will get re-populated. |
|
this._downloads = []; |
|
} |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Callback functions from DownloadsData - see the documentation in |
|
//// DownloadsViewPrototype for more information on what these functions |
|
//// are used for. |
|
|
|
onDataLoadCompleted() { |
|
DownloadsViewPrototype.onDataLoadCompleted.call(this); |
|
this._updateViews(); |
|
}, |
|
|
|
onDownloadAdded(download, newest) { |
|
if (newest) { |
|
this._downloads.unshift(download); |
|
} else { |
|
this._downloads.push(download); |
|
} |
|
|
|
this._updateViews(); |
|
}, |
|
|
|
onDownloadStateChanged() { |
|
// Since the state of a download changed, reset the estimated time left. |
|
this._lastRawTimeLeft = -1; |
|
this._lastTimeLeft = -1; |
|
}, |
|
|
|
onDownloadChanged() { |
|
this._updateViews(); |
|
}, |
|
|
|
onDownloadRemoved(download) { |
|
let itemIndex = this._downloads.indexOf(download); |
|
this._downloads.splice(itemIndex, 1); |
|
this._updateViews(); |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Propagation of properties to our views |
|
|
|
/** |
|
* Computes aggregate values and propagates the changes to our views. |
|
*/ |
|
_updateViews() { |
|
// Do not update the status indicators during batch loads of download items. |
|
if (this._loading) { |
|
return; |
|
} |
|
|
|
this._refreshProperties(); |
|
this._views.forEach(this._updateView, this); |
|
}, |
|
|
|
/** |
|
* Updates the specified view with the current aggregate values. |
|
* |
|
* @param aView |
|
* DownloadsIndicatorView object to be updated. |
|
*/ |
|
_updateView(aView) { |
|
aView.showingProgress = this._showingProgress; |
|
aView.percentComplete = this._percentComplete; |
|
aView.description = this._description; |
|
aView.details = this._details; |
|
}, |
|
|
|
////////////////////////////////////////////////////////////////////////////// |
|
//// Property updating based on current download status |
|
|
|
/** |
|
* A generator function for the Download objects this summary is currently |
|
* interested in. This generator is passed off to summarizeDownloads in order |
|
* to generate statistics about the downloads we care about - in this case, |
|
* it's the downloads in this._downloads after the first few to exclude, |
|
* which was set when constructing this DownloadsSummaryData instance. |
|
*/ |
|
* _downloadsForSummary() { |
|
if (this._downloads.length > 0) { |
|
for (let i = this._numToExclude; i < this._downloads.length; ++i) { |
|
yield this._downloads[i]; |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Computes aggregate values based on the current state of downloads. |
|
*/ |
|
_refreshProperties() { |
|
// Pre-load summary with default values. |
|
let summary = |
|
DownloadsCommon.summarizeDownloads(this._downloadsForSummary()); |
|
|
|
this._description = DownloadsCommon.strings |
|
.otherDownloads2(summary.numActive); |
|
this._percentComplete = summary.percentComplete; |
|
|
|
// If all downloads are paused, show the progress indicator as paused. |
|
this._showingProgress = summary.numDownloading > 0 || |
|
summary.numPaused > 0; |
|
|
|
// Display the estimated time left, if present. |
|
if (summary.rawTimeLeft == -1) { |
|
// There are no downloads with a known time left. |
|
this._lastRawTimeLeft = -1; |
|
this._lastTimeLeft = -1; |
|
this._details = ""; |
|
} else { |
|
// Compute the new time left only if state actually changed. |
|
if (this._lastRawTimeLeft != summary.rawTimeLeft) { |
|
this._lastRawTimeLeft = summary.rawTimeLeft; |
|
this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, |
|
this._lastTimeLeft); |
|
} |
|
[this._details] = DownloadUtils.getDownloadStatusNoRate( |
|
summary.totalTransferred, summary.totalSize, summary.slowestSpeed, |
|
this._lastTimeLeft); |
|
} |
|
}, |
|
}
|
|
|