mirror of
https://github.com/roytam1/palemoon27.git
synced 2026-06-10 02:18:57 +00:00
2402 lines
80 KiB
JavaScript
2402 lines
80 KiB
JavaScript
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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
|
|
* Download Manager data, and provides asynchronous notifications allowing
|
|
* to build a consistent view of the available data.
|
|
*
|
|
* DownloadsDataItem
|
|
* Represents a single item in the list of downloads. This object either wraps
|
|
* an existing nsIDownload from the Download Manager, or provides the same
|
|
* information read directly from the downloads database, with the possibility
|
|
* of querying the nsIDownload lazily, for performance reasons.
|
|
*
|
|
* 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, "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, "PlacesUtils",
|
|
"resource://gre/modules/PlacesUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
|
"resource://gre/modules/Promise.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
|
|
"resource:///modules/DownloadsLogger.jsm");
|
|
|
|
const nsIDM = Ci.nsIDownloadManager;
|
|
|
|
const kDownloadsStringBundleUrl =
|
|
"chrome://browser/locale/downloads/downloads.properties";
|
|
|
|
const kPrefBdmScanWhenDone = "browser.download.manager.scanWhenDone";
|
|
const kPrefBdmAlertOnExeOpen = "browser.download.manager.alertOnEXEOpen";
|
|
|
|
const kDownloadsStringsRequiringFormatting = {
|
|
sizeWithUnits: true,
|
|
shortTimeLeftSeconds: true,
|
|
shortTimeLeftMinutes: true,
|
|
shortTimeLeftHours: true,
|
|
shortTimeLeftDays: true,
|
|
statusSeparator: true,
|
|
statusSeparatorBeforeNumber: true,
|
|
fileExecutableSecurityWarning: true
|
|
};
|
|
|
|
const kDownloadsStringsRequiringPluralForm = {
|
|
otherDownloads2: true
|
|
};
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
|
|
return Components.Constructor("@mozilla.org/file/local;1",
|
|
"nsILocalFile", "initWithPath");
|
|
});
|
|
|
|
const kPartialDownloadSuffix = ".part";
|
|
|
|
const kPrefBranch = Services.prefs.getBranch("browser.download.");
|
|
|
|
let PrefObserver = {
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
|
Ci.nsISupportsWeakReference]),
|
|
getPref: function PO_getPref(name) {
|
|
try {
|
|
switch (typeof this.prefs[name]) {
|
|
case "boolean":
|
|
return kPrefBranch.getBoolPref(name);
|
|
}
|
|
} catch (ex) { }
|
|
return this.prefs[name];
|
|
},
|
|
observe: function PO_observe(aSubject, aTopic, aData) {
|
|
if (this.prefs.hasOwnProperty(aData)) {
|
|
return this[aData] = this.getPref(aData);
|
|
}
|
|
},
|
|
register: function PO_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 = {
|
|
log: function DC_log(...aMessageArgs) {
|
|
delete this.log;
|
|
this.log = function DC_log(...aMessageArgs) {
|
|
if (!PrefObserver.debug) {
|
|
return;
|
|
}
|
|
DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs);
|
|
}
|
|
this.log.apply(this, aMessageArgs);
|
|
},
|
|
|
|
error: function DC_error(...aMessageArgs) {
|
|
delete this.error;
|
|
this.error = function DC_error(...aMessageArgs) {
|
|
if (!PrefObserver.debug) {
|
|
return;
|
|
}
|
|
DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs);
|
|
}
|
|
this.error.apply(this, 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: function DC_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 the full Download Manager window interface
|
|
* instead of the simplified panel interface. The behavior of downloads
|
|
* across browsing session is consistent with the selected interface.
|
|
*/
|
|
get useToolkitUI()
|
|
{
|
|
/* Toolkit UI is currently incompatible.
|
|
* FIXME: Either fix the toolkitUI (make DBConnection work) or remove
|
|
* the unused code altogether
|
|
*/
|
|
//try {
|
|
// return Services.prefs.getBoolPref("browser.download.useToolkitUI");
|
|
//} catch (ex) { }
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* 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: function DC_getData(aWindow) {
|
|
if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
|
|
return PrivateDownloadsData;
|
|
} else {
|
|
return DownloadsData;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Initializes the data link for both the private and non-private downloads
|
|
* data objects.
|
|
*
|
|
* @param aDownloadManagerService
|
|
* Reference to the service implementing nsIDownloadManager. We need
|
|
* this because getService isn't available for us when this method is
|
|
* called, and we must ensure to register our listeners before the
|
|
* getService call for the Download Manager returns.
|
|
*/
|
|
initializeAllDataLinks: function DC_initializeAllDataLinks(aDownloadManagerService) {
|
|
DownloadsData.initializeDataLink(aDownloadManagerService);
|
|
PrivateDownloadsData.initializeDataLink(aDownloadManagerService);
|
|
},
|
|
|
|
/**
|
|
* Terminates the data link for both the private and non-private downloads
|
|
* data objects.
|
|
*/
|
|
terminateAllDataLinks: function DC_terminateAllDataLinks() {
|
|
DownloadsData.terminateDataLink();
|
|
PrivateDownloadsData.terminateDataLink();
|
|
},
|
|
|
|
/**
|
|
* Reloads the specified kind of downloads from the non-private store.
|
|
* This method must only be called when Private Browsing Mode is disabled.
|
|
*
|
|
* @param aActiveOnly
|
|
* True to load only active downloads from the database.
|
|
*/
|
|
ensureAllPersistentDataLoaded:
|
|
function DC_ensureAllPersistentDataLoaded(aActiveOnly) {
|
|
DownloadsData.ensurePersistentDataLoaded(aActiveOnly);
|
|
},
|
|
|
|
/**
|
|
* Get access to one of the DownloadsIndicatorData or
|
|
* PrivateDownloadsIndicatorData objects, depending on the privacy status of
|
|
* the window in question.
|
|
*/
|
|
getIndicatorData: function DC_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: function DC_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;
|
|
},
|
|
|
|
/**
|
|
* Given an iterable collection of DownloadDataItems, generates and returns
|
|
* statistics about that collection.
|
|
*
|
|
* @param aDataItems An iterable collection of DownloadDataItems.
|
|
*
|
|
* @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.
|
|
* numScanning : The total number of downloads being scanned.
|
|
* 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: function DC_summarizeDownloads(aDataItems)
|
|
{
|
|
let summary = {
|
|
numActive: 0,
|
|
numPaused: 0,
|
|
numScanning: 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
|
|
// dataItems.
|
|
slowestSpeed: Infinity,
|
|
rawTimeLeft: -1,
|
|
percentComplete: -1
|
|
}
|
|
|
|
for (let dataItem of aDataItems) {
|
|
summary.numActive++;
|
|
switch (dataItem.state) {
|
|
case nsIDM.DOWNLOAD_PAUSED:
|
|
summary.numPaused++;
|
|
break;
|
|
case nsIDM.DOWNLOAD_SCANNING:
|
|
summary.numScanning++;
|
|
break;
|
|
case nsIDM.DOWNLOAD_DOWNLOADING:
|
|
summary.numDownloading++;
|
|
if (dataItem.maxBytes > 0 && dataItem.speed > 0) {
|
|
let sizeLeft = dataItem.maxBytes - dataItem.currBytes;
|
|
summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
|
|
sizeLeft / dataItem.speed);
|
|
summary.slowestSpeed = Math.min(summary.slowestSpeed,
|
|
dataItem.speed);
|
|
}
|
|
break;
|
|
}
|
|
// Only add to total values if we actually know the download size.
|
|
if (dataItem.maxBytes > 0 &&
|
|
dataItem.state != nsIDM.DOWNLOAD_CANCELED &&
|
|
dataItem.state != nsIDM.DOWNLOAD_FAILED) {
|
|
summary.totalSize += dataItem.maxBytes;
|
|
summary.totalTransferred += dataItem.currBytes;
|
|
}
|
|
}
|
|
|
|
if (summary.numActive != 0 && summary.totalSize != 0 &&
|
|
summary.numActive != summary.numScanning) {
|
|
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: function DC_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.
|
|
* If you've a dataItem, you should call dataItem.openLocalFile.
|
|
* @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: function DC_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");
|
|
|
|
// Confirm opening executable files if required.
|
|
if (aFile.isExecutable()) {
|
|
let showAlert = true;
|
|
try {
|
|
showAlert = Services.prefs.getBoolPref(kPrefBdmAlertOnExeOpen);
|
|
} catch (ex) { }
|
|
|
|
// On Vista and above, we rely on native security prompting for
|
|
// downloaded content unless it's disabled.
|
|
if (DownloadsCommon.isWinVistaOrHigher) {
|
|
try {
|
|
if (Services.prefs.getBoolPref(kPrefBdmScanWhenDone)) {
|
|
showAlert = false;
|
|
}
|
|
} catch (ex) { }
|
|
}
|
|
|
|
if (showAlert) {
|
|
let name = aFile.leafName;
|
|
let message =
|
|
DownloadsCommon.strings.fileExecutableSecurityWarning(name, name);
|
|
let title =
|
|
DownloadsCommon.strings.fileExecutableSecurityWarningTitle;
|
|
let dontAsk =
|
|
DownloadsCommon.strings.fileExecutableSecurityWarningDontAsk;
|
|
|
|
let checkbox = { value: false };
|
|
let open = Services.prompt.confirmCheck(aOwnerWindow, title, message,
|
|
dontAsk, checkbox);
|
|
if (!open) {
|
|
return;
|
|
}
|
|
|
|
Services.prefs.setBoolPref(kPrefBdmAlertOnExeOpen,
|
|
!checkbox.value);
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Show a downloaded file in the system file manager.
|
|
* If you have a dataItem, use dataItem.showLocalFile.
|
|
*
|
|
* @param aFile
|
|
* a downloaded file.
|
|
*/
|
|
showDownloadedFile: function DC_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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
});
|
|
|
|
/**
|
|
* Returns true to indicate that we should hook the panel to the JavaScript API
|
|
* for downloads instead of the nsIDownloadManager back-end.
|
|
* This is kept for compatibility/leftovers and should be removed later.
|
|
*/
|
|
XPCOMUtils.defineLazyGetter(DownloadsCommon, "useJSTransfer", function () {
|
|
return true;
|
|
});
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
//// 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;
|
|
|
|
// This Object contains all the available DownloadsDataItem objects, indexed by
|
|
// their globally unique identifier. The identifiers of downloads that have
|
|
// been removed from the Download Manager data are still present, however the
|
|
// associated objects are replaced with the value "null". This is required to
|
|
// prevent race conditions when populating the list asynchronously.
|
|
this.dataItems = {};
|
|
|
|
// Array of view objects that should be notified when the available download
|
|
// data changes.
|
|
this._views = [];
|
|
|
|
// Maps Download objects to DownloadDataItem objects.
|
|
this._downloadToDataItemMap = new Map();
|
|
}
|
|
|
|
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,
|
|
|
|
/**
|
|
* Stops receiving events for current downloads and cancels any pending read.
|
|
*/
|
|
terminateDataLink: function DD_terminateDataLink()
|
|
{
|
|
Cu.reportError("terminateDataLink not applicable with JS Transfers");
|
|
return;
|
|
},
|
|
|
|
/**
|
|
* True if there are finished downloads that can be removed from the list.
|
|
*/
|
|
get canRemoveFinished()
|
|
{
|
|
for (let [, dataItem] of Iterator(this.dataItems)) {
|
|
if (dataItem && !dataItem.inProgress) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Asks the back-end to remove finished downloads from the list.
|
|
*/
|
|
removeFinished: function DD_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: function (aDownload)
|
|
{
|
|
let dataItem = new DownloadsDataItem(aDownload);
|
|
this._downloadToDataItemMap.set(aDownload, dataItem);
|
|
this.dataItems[dataItem.downloadGuid] = dataItem;
|
|
|
|
for (let view of this._views) {
|
|
view.onDataItemAdded(dataItem, true);
|
|
}
|
|
|
|
this._updateDataItemState(dataItem);
|
|
},
|
|
|
|
onDownloadChanged: function (aDownload)
|
|
{
|
|
let dataItem = this._downloadToDataItemMap.get(aDownload);
|
|
if (!dataItem) {
|
|
Cu.reportError("Download doesn't exist.");
|
|
return;
|
|
}
|
|
|
|
this._updateDataItemState(dataItem);
|
|
},
|
|
|
|
onDownloadRemoved: function (aDownload)
|
|
{
|
|
let dataItem = this._downloadToDataItemMap.get(aDownload);
|
|
if (!dataItem) {
|
|
Cu.reportError("Download doesn't exist.");
|
|
return;
|
|
}
|
|
|
|
this._downloadToDataItemMap.delete(aDownload);
|
|
this.dataItems[dataItem.downloadGuid] = null;
|
|
for (let view of this._views) {
|
|
view.onDataItemRemoved(dataItem);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Updates the given data item and sends related notifications.
|
|
*/
|
|
_updateDataItemState: function (aDataItem)
|
|
{
|
|
let oldState = aDataItem.state;
|
|
let wasInProgress = aDataItem.inProgress;
|
|
let wasDone = aDataItem.done;
|
|
|
|
aDataItem.updateFromJSDownload();
|
|
|
|
if (wasInProgress && !aDataItem.inProgress) {
|
|
aDataItem.endTime = Date.now();
|
|
}
|
|
|
|
if (oldState != aDataItem.state) {
|
|
for (let view of this._views) {
|
|
try {
|
|
view.getViewItem(aDataItem).onStateChange(oldState);
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
|
|
// 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 && !aDataItem.inProgress) {
|
|
try {
|
|
let downloadMetaData = { state: aDataItem.state,
|
|
endTime: aDataItem.endTime };
|
|
if (aDataItem.done) {
|
|
downloadMetaData.fileSize = aDataItem.maxBytes;
|
|
}
|
|
|
|
// RRR: Annotation service throws here. commented out for now.
|
|
/*PlacesUtils.annotations.setPageAnnotation(
|
|
NetUtil.newURI(aDataItem.uri), "downloads/metaData",
|
|
JSON.stringify(downloadMetaData), 0,
|
|
PlacesUtils.annotations.EXPIRE_WITH_HISTORY);*/
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!aDataItem.newDownloadNotified) {
|
|
aDataItem.newDownloadNotified = true;
|
|
this._notifyDownloadEvent("start");
|
|
}
|
|
|
|
if (!wasDone && aDataItem.done) {
|
|
this._notifyDownloadEvent("finish");
|
|
}
|
|
|
|
for (let view of this._views) {
|
|
view.getViewItem(aDataItem).onProgressChange();
|
|
}
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// 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: function DD_addView(aView)
|
|
{
|
|
this._views.push(aView);
|
|
this._updateView(aView);
|
|
},
|
|
|
|
/**
|
|
* Removes an object previously added using addView.
|
|
*
|
|
* @param aView
|
|
* DownloadsView object to be removed.
|
|
*/
|
|
removeView: function DD_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: function DD_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 loadedItemsArray = [dataItem
|
|
for each (dataItem in this.dataItems)
|
|
if (dataItem)];
|
|
loadedItemsArray.sort(function(a, b) b.startTime - a.startTime);
|
|
loadedItemsArray.forEach(
|
|
function (dataItem) aView.onDataItemAdded(dataItem, false)
|
|
);
|
|
|
|
// Notify the view that all data is available unless loading is in progress.
|
|
if (!this._pendingStatement) {
|
|
aView.onDataLoadCompleted();
|
|
}
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// In-memory downloads data store
|
|
|
|
/**
|
|
* Clears the loaded data.
|
|
*/
|
|
clear: function DD_clear()
|
|
{
|
|
this._terminateDataAccess();
|
|
this.dataItems = {};
|
|
},
|
|
|
|
/**
|
|
* Returns the data item associated with the provided source object. The
|
|
* source can be a download object that we received from the Download Manager
|
|
* because of a real-time notification, or a row from the downloads database,
|
|
* during the asynchronous data load.
|
|
*
|
|
* In case we receive download status notifications while we are still
|
|
* populating the list of downloads from the database, we want the real-time
|
|
* status to take precedence over the state that is read from the database,
|
|
* which might be older. This is achieved by creating the download item if
|
|
* it's not already in the list, but never updating the returned object using
|
|
* the data from the database, if the object already exists.
|
|
*
|
|
* @param aSource
|
|
* Object containing the data with which the item should be initialized
|
|
* if it doesn't already exist in the list. This should implement
|
|
* either nsIDownload or mozIStorageRow. If the item exists, this
|
|
* argument is only used to retrieve the download identifier.
|
|
* @param aMayReuseGUID
|
|
* If false, indicates that the download should not be added if a
|
|
* download with the same identifier was removed in the meantime. This
|
|
* ensures that, while loading the list asynchronously, downloads that
|
|
* have been removed in the meantime do no reappear inadvertently.
|
|
*
|
|
* @return New or existing data item, or null if the item was deleted from the
|
|
* list of available downloads.
|
|
*/
|
|
_getOrAddDataItem: function DD_getOrAddDataItem(aSource, aMayReuseGUID)
|
|
{
|
|
let downloadGuid = (aSource instanceof Ci.nsIDownload)
|
|
? aSource.guid
|
|
: aSource.getResultByName("guid");
|
|
if (downloadGuid in this.dataItems) {
|
|
let existingItem = this.dataItems[downloadGuid];
|
|
if (existingItem || !aMayReuseGUID) {
|
|
// Returns null if the download was removed and we can't reuse the item.
|
|
return existingItem;
|
|
}
|
|
}
|
|
DownloadsCommon.log("Creating a new DownloadsDataItem with downloadGuid =",
|
|
downloadGuid);
|
|
let dataItem = new DownloadsDataItem(aSource);
|
|
this.dataItems[downloadGuid] = dataItem;
|
|
|
|
// Create the view items before returning.
|
|
let addToStartOfList = aSource instanceof Ci.nsIDownload;
|
|
this._views.forEach(
|
|
function (view) view.onDataItemAdded(dataItem, addToStartOfList)
|
|
);
|
|
return dataItem;
|
|
},
|
|
|
|
/**
|
|
* Removes the data item with the specified identifier.
|
|
*
|
|
* This method can be called at most once per download identifier.
|
|
*/
|
|
_removeDataItem: function DD_removeDataItem(aDownloadId)
|
|
{
|
|
if (aDownloadId in this.dataItems) {
|
|
let dataItem = this.dataItems[aDownloadId];
|
|
this.dataItems[aDownloadId] = null;
|
|
this._views.forEach(
|
|
function (view) view.onDataItemRemoved(dataItem)
|
|
);
|
|
}
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// Persistent data loading
|
|
|
|
/**
|
|
* Represents an executing statement, allowing its cancellation.
|
|
*/
|
|
_pendingStatement: null,
|
|
|
|
/**
|
|
* Indicates which kind of items from the persistent downloads database have
|
|
* been fully loaded in memory and are available to the views. This can
|
|
* assume the value of one of the kLoad constants.
|
|
*/
|
|
_loadState: 0,
|
|
|
|
/** No downloads have been fully loaded yet. */
|
|
get kLoadNone() 0,
|
|
/** All the active downloads in the database are loaded in memory. */
|
|
get kLoadActive() 1,
|
|
/** All the downloads in the database are loaded in memory. */
|
|
get kLoadAll() 2,
|
|
|
|
/**
|
|
* Reloads the specified kind of downloads from the persistent database. This
|
|
* method must only be called when Private Browsing Mode is disabled.
|
|
*
|
|
* @param aActiveOnly
|
|
* True to load only active downloads from the database.
|
|
*/
|
|
ensurePersistentDataLoaded:
|
|
function DD_ensurePersistentDataLoaded(aActiveOnly)
|
|
{
|
|
if (this == PrivateDownloadsData) {
|
|
Cu.reportError("ensurePersistentDataLoaded should not be called on PrivateDownloadsData");
|
|
return;
|
|
}
|
|
|
|
if (this._pendingStatement) {
|
|
// We are already in the process of reloading all downloads.
|
|
return;
|
|
}
|
|
|
|
if (aActiveOnly) {
|
|
if (this._loadState == this.kLoadNone) {
|
|
DownloadsCommon.log("Loading only active downloads from the persistence database");
|
|
// Indicate to the views that a batch loading operation is in progress.
|
|
this._views.forEach(
|
|
function (view) view.onDataLoadStarting()
|
|
);
|
|
|
|
// Reload the list using the Download Manager service. The list is
|
|
// returned in no particular order.
|
|
let downloads = Services.downloads.activeDownloads;
|
|
while (downloads.hasMoreElements()) {
|
|
let download = downloads.getNext().QueryInterface(Ci.nsIDownload);
|
|
this._getOrAddDataItem(download, true);
|
|
}
|
|
this._loadState = this.kLoadActive;
|
|
|
|
// Indicate to the views that the batch loading operation is complete.
|
|
this._views.forEach(
|
|
function (view) view.onDataLoadCompleted()
|
|
);
|
|
DownloadsCommon.log("Active downloads done loading.");
|
|
}
|
|
} else {
|
|
if (this._loadState != this.kLoadAll) {
|
|
// Load only the relevant columns from the downloads database. The
|
|
// columns are read in the _initFromDataRow method of DownloadsDataItem.
|
|
// Order by descending download identifier so that the most recent
|
|
// downloads are notified first to the listening views.
|
|
DownloadsCommon.log("Loading all downloads from the persistence database.");
|
|
let dbConnection = Services.downloads.DBConnection;
|
|
let statement = dbConnection.createAsyncStatement(
|
|
"SELECT guid, target, name, source, referrer, state, "
|
|
+ "startTime, endTime, currBytes, maxBytes "
|
|
+ "FROM moz_downloads "
|
|
+ "ORDER BY startTime DESC"
|
|
);
|
|
try {
|
|
this._pendingStatement = statement.executeAsync(this);
|
|
} finally {
|
|
statement.finalize();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Cancels any pending data access and ensures views are notified.
|
|
*/
|
|
_terminateDataAccess: function DD_terminateDataAccess()
|
|
{
|
|
if (this._pendingStatement) {
|
|
this._pendingStatement.cancel();
|
|
this._pendingStatement = null;
|
|
}
|
|
|
|
// Close all the views on the current data. Create a copy of the array
|
|
// because some views might unregister while processing this event.
|
|
Array.slice(this._views, 0).forEach(
|
|
function (view) view.onDataInvalidated()
|
|
);
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// mozIStorageStatementCallback
|
|
|
|
handleResult: function DD_handleResult(aResultSet)
|
|
{
|
|
for (let row = aResultSet.getNextRow();
|
|
row;
|
|
row = aResultSet.getNextRow()) {
|
|
// Add the download to the list and initialize it with the data we read,
|
|
// unless we already received a notification providing more reliable
|
|
// information for this download.
|
|
this._getOrAddDataItem(row, false);
|
|
}
|
|
},
|
|
|
|
handleError: function DD_handleError(aError)
|
|
{
|
|
DownloadsCommon.error("Database statement execution error (",
|
|
aError.result, "): ", aError.message);
|
|
},
|
|
|
|
handleCompletion: function DD_handleCompletion(aReason)
|
|
{
|
|
DownloadsCommon.log("Loading all downloads from database completed with reason:",
|
|
aReason);
|
|
this._pendingStatement = null;
|
|
|
|
// To ensure that we don't inadvertently delete more downloads from the
|
|
// database than needed on shutdown, we should update the load state only if
|
|
// the operation completed successfully.
|
|
if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
|
this._loadState = this.kLoadAll;
|
|
}
|
|
|
|
// Indicate to the views that the batch loading operation is complete, even
|
|
// if the lookup failed or was canceled. The only possible glitch happens
|
|
// in case the database backend changes while loading data, when the views
|
|
// would open and immediately close. This case is rare enough not to need a
|
|
// special treatment.
|
|
this._views.forEach(
|
|
function (view) view.onDataLoadCompleted()
|
|
);
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// nsIObserver
|
|
|
|
observe: function DD_observe(aSubject, aTopic, aData)
|
|
{
|
|
switch (aTopic) {
|
|
case "download-manager-remove-download-guid":
|
|
// If a single download was removed, remove the corresponding data item.
|
|
if (aSubject) {
|
|
let downloadGuid = aSubject.QueryInterface(Ci.nsISupportsCString).data;
|
|
DownloadsCommon.log("A single download with id",
|
|
downloadGuid, "was removed.");
|
|
this._removeDataItem(downloadGuid);
|
|
break;
|
|
}
|
|
|
|
// Multiple downloads have been removed. Iterate over known downloads
|
|
// and remove those that don't exist anymore.
|
|
DownloadsCommon.log("Multiple downloads were removed.");
|
|
for each (let dataItem in this.dataItems) {
|
|
if (dataItem) {
|
|
// Bug 449811 - We have to bind to the dataItem because Javascript
|
|
// doesn't do fresh let-bindings per loop iteration.
|
|
let dataItemBinding = dataItem;
|
|
Services.downloads.getDownloadByGUID(dataItemBinding.downloadGuid,
|
|
function(aStatus, aResult) {
|
|
if (aStatus == Components.results.NS_ERROR_NOT_AVAILABLE) {
|
|
DownloadsCommon.log("Removing download with id",
|
|
dataItemBinding.downloadGuid);
|
|
this._removeDataItem(dataItemBinding.downloadGuid);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// nsIDownloadProgressListener
|
|
|
|
onDownloadStateChange: function DD_onDownloadStateChange(aOldState, aDownload)
|
|
{
|
|
if (aDownload.isPrivate != this._isPrivate) {
|
|
// Ignore the downloads with a privacy status other than what we are
|
|
// tracking.
|
|
return;
|
|
}
|
|
|
|
// When a new download is added, it may have the same identifier of a
|
|
// download that we previously deleted during this session, and we also
|
|
// want to provide a visible indication that the download started.
|
|
let isNew = aOldState == nsIDM.DOWNLOAD_NOTSTARTED ||
|
|
aOldState == nsIDM.DOWNLOAD_QUEUED;
|
|
|
|
let dataItem = this._getOrAddDataItem(aDownload, isNew);
|
|
if (!dataItem) {
|
|
return;
|
|
}
|
|
|
|
let wasInProgress = dataItem.inProgress;
|
|
|
|
DownloadsCommon.log("A download changed its state to:", aDownload.state);
|
|
dataItem.state = aDownload.state;
|
|
dataItem.referrer = aDownload.referrer && aDownload.referrer.spec;
|
|
dataItem.resumable = aDownload.resumable;
|
|
dataItem.startTime = Math.round(aDownload.startTime / 1000);
|
|
dataItem.currBytes = aDownload.amountTransferred;
|
|
dataItem.maxBytes = aDownload.size;
|
|
|
|
if (wasInProgress && !dataItem.inProgress) {
|
|
dataItem.endTime = Date.now();
|
|
}
|
|
|
|
// When a download is retried, we create a different download object from
|
|
// the database with the same ID as before. This means that the nsIDownload
|
|
// that the dataItem holds might now need updating.
|
|
//
|
|
// We only overwrite this in the event that _download exists, because if it
|
|
// doesn't, that means that no caller ever tried to get the nsIDownload,
|
|
// which means it was never retrieved and doesn't need to be overwritten.
|
|
if (dataItem._download) {
|
|
dataItem._download = aDownload;
|
|
}
|
|
|
|
for (let view of this._views) {
|
|
try {
|
|
view.getViewItem(dataItem).onStateChange(aOldState);
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
|
|
if (isNew && !dataItem.newDownloadNotified) {
|
|
dataItem.newDownloadNotified = true;
|
|
this._notifyDownloadEvent("start");
|
|
}
|
|
|
|
// This is a final state of which we are only notified once.
|
|
if (dataItem.done) {
|
|
this._notifyDownloadEvent("finish");
|
|
}
|
|
|
|
// TODO Bug 830415: this isn't the right place to set these annotation.
|
|
// It should be set it in places' nsIDownloadHistory implementation.
|
|
if (!this._isPrivate && !dataItem.inProgress) {
|
|
let downloadMetaData = { state: dataItem.state,
|
|
endTime: dataItem.endTime };
|
|
if (dataItem.done)
|
|
downloadMetaData.fileSize = dataItem.maxBytes;
|
|
|
|
try {
|
|
PlacesUtils.annotations.setPageAnnotation(
|
|
NetUtil.newURI(dataItem.uri), "downloads/metaData", JSON.stringify(downloadMetaData), 0,
|
|
PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
|
|
}
|
|
catch(ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
},
|
|
|
|
onProgressChange: function DD_onProgressChange(aWebProgress, aRequest,
|
|
aCurSelfProgress,
|
|
aMaxSelfProgress,
|
|
aCurTotalProgress,
|
|
aMaxTotalProgress, aDownload)
|
|
{
|
|
if (aDownload.isPrivate != this._isPrivate) {
|
|
// Ignore the downloads with a privacy status other than what we are
|
|
// tracking.
|
|
return;
|
|
}
|
|
|
|
let dataItem = this._getOrAddDataItem(aDownload, false);
|
|
if (!dataItem) {
|
|
return;
|
|
}
|
|
|
|
dataItem.currBytes = aDownload.amountTransferred;
|
|
dataItem.maxBytes = aDownload.size;
|
|
dataItem.speed = aDownload.speed;
|
|
dataItem.percentComplete = aDownload.percentComplete;
|
|
|
|
this._views.forEach(
|
|
function (view) view.getViewItem(dataItem).onProgressChange()
|
|
);
|
|
},
|
|
|
|
onStateChange: function () { },
|
|
|
|
onSecurityChange: function () { },
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// 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: function DD_notifyDownloadEvent(aType)
|
|
{
|
|
DownloadsCommon.log("Attempting to notify that a new download has started or finished.");
|
|
if (DownloadsCommon.useToolkitUI) {
|
|
DownloadsCommon.log("Cancelling notification - we're using the toolkit downloads manager.");
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
//// DownloadsDataItem
|
|
|
|
/**
|
|
* Represents a single item in the list of downloads. This object either wraps
|
|
* an existing nsIDownload from the Download Manager, or provides the same
|
|
* information read directly from the downloads database, with the possibility
|
|
* of querying the nsIDownload lazily, for performance reasons.
|
|
*
|
|
* @param aSource
|
|
* Object containing the data with which the item should be initialized.
|
|
* This should implement either nsIDownload or mozIStorageRow. If the
|
|
* JavaScript API for downloads is enabled, this is a Download object.
|
|
*/
|
|
function DownloadsDataItem(aSource)
|
|
{
|
|
this._initFromJSDownload(aSource);
|
|
}
|
|
|
|
DownloadsDataItem.prototype = {
|
|
/**
|
|
* The JavaScript API does not need identifiers for Download objects, so they
|
|
* are generated sequentially for the corresponding DownloadDataItem.
|
|
*/
|
|
get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId,
|
|
__lastId: 0,
|
|
|
|
/**
|
|
* Initializes this object from the JavaScript API for downloads.
|
|
*
|
|
* The endTime property is initialized to the current date and time.
|
|
*
|
|
* @param aDownload
|
|
* The Download object with the current state.
|
|
*/
|
|
_initFromJSDownload: function (aDownload)
|
|
{
|
|
this._download = aDownload;
|
|
|
|
this.downloadGuid = "id:" + this._autoIncrementId;
|
|
this.file = aDownload.target.path;
|
|
this.target = OS.Path.basename(aDownload.target.path);
|
|
this.uri = aDownload.source.url;
|
|
this.endTime = Date.now();
|
|
|
|
this.updateFromJSDownload();
|
|
},
|
|
|
|
/**
|
|
* Updates this object from the JavaScript API for downloads.
|
|
*/
|
|
updateFromJSDownload: function ()
|
|
{
|
|
// Collapse state using the correct priority.
|
|
if (this._download.succeeded) {
|
|
this.state = nsIDM.DOWNLOAD_FINISHED;
|
|
} else if (this._download.error &&
|
|
this._download.error.becauseBlockedByParentalControls) {
|
|
this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
|
|
} else if (this._download.error) {
|
|
this.state = nsIDM.DOWNLOAD_FAILED;
|
|
} else if (this._download.canceled && this._download.hasPartialData) {
|
|
this.state = nsIDM.DOWNLOAD_PAUSED;
|
|
} else if (this._download.canceled) {
|
|
this.state = nsIDM.DOWNLOAD_CANCELED;
|
|
} else if (this._download.stopped) {
|
|
this.state = nsIDM.DOWNLOAD_NOTSTARTED;
|
|
} else {
|
|
this.state = nsIDM.DOWNLOAD_DOWNLOADING;
|
|
}
|
|
|
|
this.referrer = this._download.source.referrer;
|
|
this.startTime = this._download.startTime;
|
|
this.currBytes = this._download.currentBytes;
|
|
this.resumable = this._download.hasPartialData;
|
|
this.speed = this._download.speed;
|
|
|
|
if (this._download.succeeded) {
|
|
// If the download succeeded, show the final size if available, otherwise
|
|
// use the last known number of bytes transferred. The final size on disk
|
|
// will be available when bug 941063 is resolved.
|
|
this.maxBytes = this._download.hasProgress ?
|
|
this._download.totalBytes :
|
|
this._download.currentBytes;
|
|
this.percentComplete = 100;
|
|
} else if (this._download.hasProgress) {
|
|
// If the final size and progress are known, use them.
|
|
this.maxBytes = this._download.totalBytes;
|
|
this.percentComplete = this._download.progress;
|
|
} else {
|
|
// The download final size and progress percentage is unknown.
|
|
this.maxBytes = -1;
|
|
this.percentComplete = -1;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Initializes this object from a download object of the Download Manager.
|
|
*
|
|
* The endTime property is initialized to the current date and time.
|
|
*
|
|
* @param aDownload
|
|
* The nsIDownload with the current state.
|
|
*/
|
|
_initFromDownload: function DDI_initFromDownload(aDownload)
|
|
{
|
|
this._download = aDownload;
|
|
|
|
// Fetch all the download properties eagerly.
|
|
this.downloadGuid = aDownload.guid;
|
|
this.file = aDownload.target.spec;
|
|
this.target = aDownload.displayName;
|
|
this.uri = aDownload.source.spec;
|
|
this.referrer = aDownload.referrer && aDownload.referrer.spec;
|
|
this.state = aDownload.state;
|
|
this.startTime = Math.round(aDownload.startTime / 1000);
|
|
this.endTime = Date.now();
|
|
this.currBytes = aDownload.amountTransferred;
|
|
this.maxBytes = aDownload.size;
|
|
this.resumable = aDownload.resumable;
|
|
this.speed = aDownload.speed;
|
|
this.percentComplete = aDownload.percentComplete;
|
|
},
|
|
|
|
/**
|
|
* Initializes this object from a data row in the downloads database, without
|
|
* querying the associated nsIDownload object, to improve performance when
|
|
* loading the list of downloads asynchronously.
|
|
*
|
|
* When this object is initialized in this way, accessing the "download"
|
|
* property loads the underlying nsIDownload object synchronously, and should
|
|
* be avoided unless the object is really required.
|
|
*
|
|
* @param aStorageRow
|
|
* The mozIStorageRow from the downloads database.
|
|
*/
|
|
_initFromDataRow: function DDI_initFromDataRow(aStorageRow)
|
|
{
|
|
// Get the download properties from the data row.
|
|
this._download = null;
|
|
this.downloadGuid = aStorageRow.getResultByName("guid");
|
|
this.file = aStorageRow.getResultByName("target");
|
|
this.target = aStorageRow.getResultByName("name");
|
|
this.uri = aStorageRow.getResultByName("source");
|
|
this.referrer = aStorageRow.getResultByName("referrer");
|
|
this.state = aStorageRow.getResultByName("state");
|
|
this.startTime = Math.round(aStorageRow.getResultByName("startTime") / 1000);
|
|
this.endTime = Math.round(aStorageRow.getResultByName("endTime") / 1000);
|
|
this.currBytes = aStorageRow.getResultByName("currBytes");
|
|
this.maxBytes = aStorageRow.getResultByName("maxBytes");
|
|
|
|
// Now we have to determine if the download is resumable, but don't want to
|
|
// access the underlying download object unnecessarily. The only case where
|
|
// the property is relevant is when we are currently downloading data, and
|
|
// in this case the download object is already loaded in memory or will be
|
|
// loaded very soon in any case. In all the other cases, including a paused
|
|
// download, we assume that the download is resumable. The property will be
|
|
// updated as soon as the underlying download state changes.
|
|
|
|
// We'll start by assuming we're resumable, and then if we're downloading,
|
|
// update resumable property in case we were wrong.
|
|
this.resumable = true;
|
|
|
|
if (this.state == nsIDM.DOWNLOAD_DOWNLOADING) {
|
|
this.getDownload(function(aDownload) {
|
|
this.resumable = aDownload.resumable;
|
|
}.bind(this));
|
|
}
|
|
|
|
// Compute the other properties without accessing the download object.
|
|
this.speed = 0;
|
|
this.percentComplete = this.maxBytes <= 0
|
|
? -1
|
|
: Math.round(this.currBytes / this.maxBytes * 100);
|
|
},
|
|
|
|
/**
|
|
* Asynchronous getter for the download object corresponding to this data item.
|
|
*
|
|
* @param aCallback
|
|
* A callback function which will be called when the download object is
|
|
* available. It should accept one argument which will be the download
|
|
* object.
|
|
*/
|
|
getDownload: function DDI_getDownload(aCallback) {
|
|
if (this._download) {
|
|
// Return the download object asynchronously to the caller
|
|
let download = this._download;
|
|
Services.tm.mainThread.dispatch(function () aCallback(download),
|
|
Ci.nsIThread.DISPATCH_NORMAL);
|
|
} else {
|
|
Services.downloads.getDownloadByGUID(this.downloadGuid,
|
|
function(aStatus, aResult) {
|
|
if (!Components.isSuccessCode(aStatus)) {
|
|
Cu.reportError(
|
|
new Components.Exception("Cannot retrieve download for GUID: " +
|
|
this.downloadGuid));
|
|
} else {
|
|
this._download = aResult;
|
|
aCallback(aResult);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Indicates whether the download is proceeding normally, and not finished
|
|
* yet. This includes paused downloads. When this property is true, the
|
|
* "progress" property represents the current progress of the download.
|
|
*/
|
|
get inProgress()
|
|
{
|
|
return [
|
|
nsIDM.DOWNLOAD_NOTSTARTED,
|
|
nsIDM.DOWNLOAD_QUEUED,
|
|
nsIDM.DOWNLOAD_DOWNLOADING,
|
|
nsIDM.DOWNLOAD_PAUSED,
|
|
nsIDM.DOWNLOAD_SCANNING,
|
|
].indexOf(this.state) != -1;
|
|
},
|
|
|
|
/**
|
|
* This is true during the initial phases of a download, before the actual
|
|
* download of data bytes starts.
|
|
*/
|
|
get starting()
|
|
{
|
|
return this.state == nsIDM.DOWNLOAD_NOTSTARTED ||
|
|
this.state == nsIDM.DOWNLOAD_QUEUED;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether the download is paused.
|
|
*/
|
|
get paused()
|
|
{
|
|
return this.state == nsIDM.DOWNLOAD_PAUSED;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether the download is in a final state, either because it
|
|
* completed successfully or because it was blocked.
|
|
*/
|
|
get done()
|
|
{
|
|
return [
|
|
nsIDM.DOWNLOAD_FINISHED,
|
|
nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
|
|
nsIDM.DOWNLOAD_BLOCKED_POLICY,
|
|
nsIDM.DOWNLOAD_DIRTY,
|
|
].indexOf(this.state) != -1;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether the download is finished and can be opened.
|
|
*/
|
|
get openable()
|
|
{
|
|
return this.state == nsIDM.DOWNLOAD_FINISHED;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether the download stopped because of an error, and can be
|
|
* resumed manually.
|
|
*/
|
|
get canRetry()
|
|
{
|
|
return this.state == nsIDM.DOWNLOAD_CANCELED ||
|
|
this.state == nsIDM.DOWNLOAD_FAILED;
|
|
},
|
|
|
|
/**
|
|
* Returns the nsILocalFile for the download target.
|
|
*
|
|
* @throws if the native path is not valid. This can happen if the same
|
|
* profile is used on different platforms, for example if a native
|
|
* Windows path is stored and then the item is accessed on a Mac.
|
|
*/
|
|
get localFile()
|
|
{
|
|
return this._getFile(this.file);
|
|
},
|
|
|
|
/**
|
|
* Returns the nsILocalFile for the partially downloaded target.
|
|
*
|
|
* @throws if the native path is not valid. This can happen if the same
|
|
* profile is used on different platforms, for example if a native
|
|
* Windows path is stored and then the item is accessed on a Mac.
|
|
*/
|
|
get partFile()
|
|
{
|
|
return this._getFile(this.file + kPartialDownloadSuffix);
|
|
},
|
|
|
|
/**
|
|
* Returns an nsILocalFile for aFilename. aFilename might be a file URL or
|
|
* a native path.
|
|
*
|
|
* @param aFilename the filename of the file to retrieve.
|
|
* @return an nsILocalFile for the file.
|
|
* @throws if the native path is not valid. This can happen if the same
|
|
* profile is used on different platforms, for example if a native
|
|
* Windows path is stored and then the item is accessed on a Mac.
|
|
* @note This function makes no guarantees about the file's existence -
|
|
* callers should check that the returned file exists.
|
|
*/
|
|
_getFile: function DDI__getFile(aFilename)
|
|
{
|
|
// The download database may contain targets stored as file URLs or native
|
|
// paths. This can still be true for previously stored items, even if new
|
|
// items are stored using their file URL. See also bug 239948 comment 12.
|
|
if (aFilename.startsWith("file:")) {
|
|
// Assume the file URL we obtained from the downloads database or from the
|
|
// "spec" property of the target has the UTF-8 charset.
|
|
let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL);
|
|
return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
|
|
} else {
|
|
// The downloads database contains a native path. Try to create a local
|
|
// file, though this may throw an exception if the path is invalid.
|
|
return new DownloadsLocalFileCtor(aFilename);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open the target file for this download.
|
|
*
|
|
* @param aOwnerWindow
|
|
* The window with which the required action is associated.
|
|
* @throws if the file cannot be opened.
|
|
*/
|
|
openLocalFile: function DDI_openLocalFile(aOwnerWindow) {
|
|
this._download.launch().then(null, Cu.reportError);
|
|
return;
|
|
},
|
|
|
|
/**
|
|
* Show the downloaded file in the system file manager.
|
|
*/
|
|
showLocalFile: function DDI_showLocalFile() {
|
|
DownloadsCommon.showDownloadedFile(this.localFile);
|
|
},
|
|
|
|
/**
|
|
* Resumes the download if paused, pauses it if active.
|
|
* @throws if the download is not resumable or if has already done.
|
|
*/
|
|
togglePauseResume: function DDI_togglePauseResume() {
|
|
if (this._download.stopped) {
|
|
this._download.start();
|
|
} else {
|
|
this._download.cancel();
|
|
}
|
|
return;
|
|
},
|
|
|
|
/**
|
|
* Attempts to retry the download.
|
|
* @throws if we cannot.
|
|
*/
|
|
retry: function DDI_retry() {
|
|
this._download.start();
|
|
return;
|
|
},
|
|
|
|
/**
|
|
* Support function that deletes the local file for a download. This is
|
|
* used in cases where the Download Manager service doesn't delete the file
|
|
* from disk when cancelling. See bug 732924.
|
|
*/
|
|
_ensureLocalFileRemoved: function DDI__ensureLocalFileRemoved()
|
|
{
|
|
try {
|
|
let localFile = this.localFile;
|
|
if (localFile.exists()) {
|
|
localFile.remove(false);
|
|
}
|
|
} catch (ex) { }
|
|
},
|
|
|
|
/**
|
|
* Cancels the download.
|
|
* @throws if the download is already done.
|
|
*/
|
|
cancel: function() {
|
|
this._download.cancel();
|
|
this._download.removePartialData().then(null, Cu.reportError);
|
|
return;
|
|
},
|
|
|
|
/**
|
|
* Remove the download.
|
|
*/
|
|
remove: function DDI_remove() {
|
|
let promiseList = this._download.source.isPrivate
|
|
? Downloads.getList(Downloads.PUBLIC)
|
|
: Downloads.getList(Downloads.PRIVATE);
|
|
promiseList.then(list => list.remove(this._download))
|
|
.then(() => this._download.finalize(true))
|
|
.then(null, Cu.reportError);
|
|
return;
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
//// 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: function DVP_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: function DVP_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: function DVP_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: function DVP_onDataLoadStarting()
|
|
{
|
|
this._loading = true;
|
|
},
|
|
|
|
/**
|
|
* Called after data loading finished.
|
|
*/
|
|
onDataLoadCompleted: function DVP_onDataLoadCompleted()
|
|
{
|
|
this._loading = false;
|
|
},
|
|
|
|
/**
|
|
* Called when the downloads database becomes unavailable (for example, we
|
|
* entered Private Browsing Mode and the database backend changed).
|
|
* References to existing data should be discarded.
|
|
*
|
|
* @note Subclasses should override this.
|
|
*/
|
|
onDataInvalidated: function DVP_onDataInvalidated()
|
|
{
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
},
|
|
|
|
/**
|
|
* Called when a new download data item is available, either during the
|
|
* asynchronous data load or when a new download is started.
|
|
*
|
|
* @param aDataItem
|
|
* DownloadsDataItem object that was just added.
|
|
* @param aNewest
|
|
* 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.
|
|
*/
|
|
onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest)
|
|
{
|
|
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 aDataItem
|
|
* DownloadsDataItem object that is being removed.
|
|
*
|
|
* @note Subclasses should override this.
|
|
*/
|
|
onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem)
|
|
{
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
},
|
|
|
|
/**
|
|
* Returns the view item associated with the provided data item for this view.
|
|
*
|
|
* @param aDataItem
|
|
* DownloadsDataItem object for which the view item is requested.
|
|
*
|
|
* @return Object that can be used to notify item status events.
|
|
*
|
|
* @note Subclasses should override this.
|
|
*/
|
|
getViewItem: function DID_getViewItem(aDataItem)
|
|
{
|
|
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: function DID_refreshProperties()
|
|
{
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
},
|
|
|
|
/**
|
|
* Private function used to refresh an individual view.
|
|
*
|
|
* @note Subclasses should override this.
|
|
*/
|
|
_updateView: function DID_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: function DID_removeView(aView)
|
|
{
|
|
DownloadsViewPrototype.removeView.call(this, aView);
|
|
|
|
if (this._views.length == 0) {
|
|
this._itemCount = 0;
|
|
}
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// Callback functions from DownloadsData
|
|
|
|
/**
|
|
* Called after data loading finished.
|
|
*/
|
|
onDataLoadCompleted: function DID_onDataLoadCompleted()
|
|
{
|
|
DownloadsViewPrototype.onDataLoadCompleted.call(this);
|
|
this._updateViews();
|
|
},
|
|
|
|
/**
|
|
* Called when the downloads database becomes unavailable (for example, we
|
|
* entered Private Browsing Mode and the database backend changed).
|
|
* References to existing data should be discarded.
|
|
*/
|
|
onDataInvalidated: function DID_onDataInvalidated()
|
|
{
|
|
this._itemCount = 0;
|
|
},
|
|
|
|
/**
|
|
* Called when a new download data item is available, either during the
|
|
* asynchronous data load or when a new download is started.
|
|
*
|
|
* @param aDataItem
|
|
* DownloadsDataItem object that was just added.
|
|
* @param aNewest
|
|
* 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.
|
|
*/
|
|
onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest)
|
|
{
|
|
this._itemCount++;
|
|
this._updateViews();
|
|
},
|
|
|
|
/**
|
|
* Called when a data item is removed, ensures that the widget associated with
|
|
* the view item is removed from the user interface.
|
|
*
|
|
* @param aDataItem
|
|
* DownloadsDataItem object that is being removed.
|
|
*/
|
|
onDataItemRemoved: function DID_onDataItemRemoved(aDataItem)
|
|
{
|
|
this._itemCount--;
|
|
this._updateViews();
|
|
},
|
|
|
|
/**
|
|
* Returns the view item associated with the provided data item for this view.
|
|
*
|
|
* @param aDataItem
|
|
* DownloadsDataItem object for which the view item is requested.
|
|
*
|
|
* @return Object that can be used to notify item status events.
|
|
*/
|
|
getViewItem: function DID_getViewItem(aDataItem)
|
|
{
|
|
let data = this._isPrivate ? PrivateDownloadsIndicatorData
|
|
: DownloadsIndicatorData;
|
|
return Object.freeze({
|
|
onStateChange: function DIVI_onStateChange(aOldState)
|
|
{
|
|
if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
|
|
aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
|
|
data.attention = true;
|
|
}
|
|
|
|
// Since the state of a download changed, reset the estimated time left.
|
|
data._lastRawTimeLeft = -1;
|
|
data._lastTimeLeft = -1;
|
|
|
|
data._updateViews();
|
|
},
|
|
onProgressChange: function DIVI_onProgressChange()
|
|
{
|
|
data._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: function DID_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: function DID_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 dataItems that this summary is currently
|
|
* interested in. This generator is passed off to summarizeDownloads in order
|
|
* to generate statistics about the dataItems we care about - in this case,
|
|
* it's all dataItems for active downloads.
|
|
*/
|
|
_activeDataItems: function DID_activeDataItems()
|
|
{
|
|
let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
|
|
: DownloadsData.dataItems;
|
|
for each (let dataItem in dataItems) {
|
|
if (dataItem && dataItem.inProgress) {
|
|
yield dataItem;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Computes aggregate values based on the current state of downloads.
|
|
*/
|
|
_refreshProperties: function DID_refreshProperties()
|
|
{
|
|
let summary =
|
|
DownloadsCommon.summarizeDownloads(this._activeDataItems());
|
|
|
|
// 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._dataItems = [];
|
|
|
|
// 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: function DSD_removeView(aView)
|
|
{
|
|
DownloadsViewPrototype.removeView.call(this, aView);
|
|
|
|
if (this._views.length == 0) {
|
|
// Clear out our collection of DownloadDataItems. If we ever have
|
|
// another view registered with us, this will get re-populated.
|
|
this._dataItems = [];
|
|
}
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// Callback functions from DownloadsData - see the documentation in
|
|
//// DownloadsViewPrototype for more information on what these functions
|
|
//// are used for.
|
|
|
|
onDataLoadCompleted: function DSD_onDataLoadCompleted()
|
|
{
|
|
DownloadsViewPrototype.onDataLoadCompleted.call(this);
|
|
this._updateViews();
|
|
},
|
|
|
|
onDataInvalidated: function DSD_onDataInvalidated()
|
|
{
|
|
this._dataItems = [];
|
|
},
|
|
|
|
onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest)
|
|
{
|
|
if (aNewest) {
|
|
this._dataItems.unshift(aDataItem);
|
|
} else {
|
|
this._dataItems.push(aDataItem);
|
|
}
|
|
|
|
this._updateViews();
|
|
},
|
|
|
|
onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem)
|
|
{
|
|
let itemIndex = this._dataItems.indexOf(aDataItem);
|
|
this._dataItems.splice(itemIndex, 1);
|
|
this._updateViews();
|
|
},
|
|
|
|
getViewItem: function DSD_getViewItem(aDataItem)
|
|
{
|
|
let self = this;
|
|
return Object.freeze({
|
|
onStateChange: function DIVI_onStateChange(aOldState)
|
|
{
|
|
// Since the state of a download changed, reset the estimated time left.
|
|
self._lastRawTimeLeft = -1;
|
|
self._lastTimeLeft = -1;
|
|
self._updateViews();
|
|
},
|
|
onProgressChange: function DIVI_onProgressChange()
|
|
{
|
|
self._updateViews();
|
|
}
|
|
});
|
|
},
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//// Propagation of properties to our views
|
|
|
|
/**
|
|
* Computes aggregate values and propagates the changes to our views.
|
|
*/
|
|
_updateViews: function DSD_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: function DSD_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 dataItems that this summary is currently
|
|
* interested in. This generator is passed off to summarizeDownloads in order
|
|
* to generate statistics about the dataItems we care about - in this case,
|
|
* it's the dataItems in this._dataItems after the first few to exclude,
|
|
* which was set when constructing this DownloadsSummaryData instance.
|
|
*/
|
|
_dataItemsForSummary: function DSD_dataItemsForSummary()
|
|
{
|
|
if (this._dataItems.length > 0) {
|
|
for (let i = this._numToExclude; i < this._dataItems.length; ++i) {
|
|
yield this._dataItems[i];
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Computes aggregate values based on the current state of downloads.
|
|
*/
|
|
_refreshProperties: function DSD_refreshProperties()
|
|
{
|
|
// Pre-load summary with default values.
|
|
let summary =
|
|
DownloadsCommon.summarizeDownloads(this._dataItemsForSummary());
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|