Bug 1117141 - Part 1 of 2 - Bypass all the DownloadsDataItem properties. r=mak

This commit is contained in:
Paolo Amadini 2015-02-16 18:49:49 +00:00
parent 91501caee2
commit 797528c9fe
5 changed files with 355 additions and 309 deletions

View File

@ -300,6 +300,55 @@ this.DownloadsCommon = {
_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;
},
/**
* Returns the highest number of bytes transferred or the known size of the
* given Download object, or -1 if the size is not available. Callers should
* use Download properties directly when possible.
*/
maxBytesOfDownload(download) {
if (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.
return download.hasProgress ? download.totalBytes : download.currentBytes;
} else if (download.hasProgress) {
// If the final size and progress are known, use them.
return download.totalBytes;
} else {
// The download final size and progress percentage is unknown.
return -1;
}
},
/**
* Given an iterable collection of DownloadDataItems, generates and returns
* statistics about that collection.
@ -339,8 +388,12 @@ this.DownloadsCommon = {
}
for (let dataItem of aDataItems) {
let download = dataItem.download;
let state = DownloadsCommon.stateOfDownload(download);
let maxBytes = DownloadsCommon.maxBytesOfDownload(download);
summary.numActive++;
switch (dataItem.state) {
switch (state) {
case nsIDM.DOWNLOAD_PAUSED:
summary.numPaused++;
break;
@ -349,21 +402,20 @@ this.DownloadsCommon = {
break;
case nsIDM.DOWNLOAD_DOWNLOADING:
summary.numDownloading++;
if (dataItem.maxBytes > 0 && dataItem.download.speed > 0) {
let sizeLeft = dataItem.maxBytes - dataItem.download.currentBytes;
if (maxBytes > 0 && download.speed > 0) {
let sizeLeft = maxBytes - download.currentBytes;
summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
sizeLeft / dataItem.download.speed);
sizeLeft / download.speed);
summary.slowestSpeed = Math.min(summary.slowestSpeed,
dataItem.download.speed);
download.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.download.currentBytes;
if (maxBytes > 0 && state != nsIDM.DOWNLOAD_CANCELED &&
state != nsIDM.DOWNLOAD_FAILED) {
summary.totalSize += maxBytes;
summary.totalTransferred += download.currentBytes;
}
}
@ -418,7 +470,7 @@ this.DownloadsCommon = {
/**
* Opens a downloaded file.
* If you've a dataItem, you should call dataItem.openLocalFile.
*
* @param aFile
* the downloaded file to be opened.
* @param aMimeInfo
@ -609,6 +661,7 @@ function DownloadsDataCtor(aPrivate) {
// Contains all the available DownloadsDataItem objects.
this.dataItems = new Set();
this.oldDownloadStates = new Map();
// Array of view objects that should be notified when the available download
// data changes.
@ -637,7 +690,9 @@ DownloadsDataCtor.prototype = {
*/
get canRemoveFinished() {
for (let dataItem of this.dataItems) {
if (!dataItem.inProgress) {
let download = dataItem.download;
// Stopped, paused, and failed downloads with partial data are removed.
if (download.stopped && !(download.canceled && download.hasPartialData)) {
return true;
}
}
@ -661,22 +716,81 @@ DownloadsDataCtor.prototype = {
let dataItem = new DownloadsDataItem(aDownload);
this._downloadToDataItemMap.set(aDownload, dataItem);
this.dataItems.add(dataItem);
this.oldDownloadStates.set(aDownload,
DownloadsCommon.stateOfDownload(aDownload));
for (let view of this._views) {
view.onDataItemAdded(dataItem, true);
}
this._updateDataItemState(dataItem);
},
onDownloadChanged(aDownload) {
let dataItem = this._downloadToDataItemMap.get(aDownload);
if (!dataItem) {
let aDataItem = this._downloadToDataItemMap.get(aDownload);
if (!aDataItem) {
Cu.reportError("Download doesn't exist.");
return;
}
this._updateDataItemState(dataItem);
let oldState = this.oldDownloadStates.get(aDownload);
let newState = DownloadsCommon.stateOfDownload(aDownload);
this.oldDownloadStates.set(aDownload, newState);
if (oldState != newState) {
if (aDownload.succeeded ||
(aDownload.canceled && !aDownload.hasPartialData) ||
aDownload.error) {
// Store the end time that may be displayed by the views.
aDownload.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(aDownload),
endTime: aDownload.endTime,
};
if (aDownload.succeeded ||
(aDownload.error && aDownload.error.becauseBlocked)) {
downloadMetaData.fileSize =
DownloadsCommon.maxBytesOfDownload(aDataItem.download);
}
PlacesUtils.annotations.setPageAnnotation(
NetUtil.newURI(aDownload.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.onDataItemStateChanged(aDataItem);
} catch (ex) {
Cu.reportError(ex);
}
}
if (aDownload.succeeded ||
(aDownload.error && aDownload.error.becauseBlocked)) {
this._notifyDownloadEvent("finish");
}
}
if (!aDownload.newDownloadNotified) {
aDownload.newDownloadNotified = true;
this._notifyDownloadEvent("start");
}
for (let view of this._views) {
view.onDataItemChanged(aDataItem);
}
},
onDownloadRemoved(aDownload) {
@ -688,71 +802,12 @@ DownloadsDataCtor.prototype = {
this._downloadToDataItemMap.delete(aDownload);
this.dataItems.delete(dataItem);
this.oldDownloadStates.delete(aDownload);
for (let view of this._views) {
view.onDataItemRemoved(dataItem);
}
},
/**
* Updates the given data item and sends related notifications.
*/
_updateDataItemState(aDataItem) {
let oldState = aDataItem.state;
let wasInProgress = aDataItem.inProgress;
let wasDone = aDataItem.done;
aDataItem.updateFromDownload();
if (wasInProgress && !aDataItem.inProgress) {
aDataItem.endTime = Date.now();
}
if (oldState != aDataItem.state) {
for (let view of this._views) {
try {
view.onDataItemStateChanged(aDataItem, 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;
}
PlacesUtils.annotations.setPageAnnotation(
NetUtil.newURI(aDataItem.download.source.url),
"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.onDataItemChanged(aDataItem);
}
},
//////////////////////////////////////////////////////////////////////////////
//// Registration of views
@ -871,54 +926,11 @@ XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
*/
function DownloadsDataItem(aDownload) {
this.download = aDownload;
this.endTime = Date.now();
this.updateFromDownload();
this.download.endTime = Date.now();
}
DownloadsDataItem.prototype = {
/**
* Updates this object from the underlying Download object.
*/
updateFromDownload() {
// 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.download.error.becauseBlockedByReputationCheck) {
this.state = nsIDM.DOWNLOAD_DIRTY;
} 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;
}
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;
}
},
get state() DownloadsCommon.stateOfDownload(this.download),
/**
* Indicates whether the download is proceeding normally, and not finished
@ -1259,9 +1271,10 @@ DownloadsIndicatorDataCtor.prototype = {
},
// DownloadsView
onDataItemStateChanged(aDataItem, aOldState) {
if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
onDataItemStateChanged(aDataItem) {
let download = aDataItem.download;
if (download.succeeded || download.error) {
this.attention = true;
}
@ -1368,7 +1381,11 @@ DownloadsIndicatorDataCtor.prototype = {
let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
: DownloadsData.dataItems;
for (let dataItem of dataItems) {
if (dataItem && dataItem.inProgress) {
if (!dataItem) {
continue;
}
let download = dataItem.download;
if (!download.stopped || (download.canceled && download.hasPartialData)) {
yield dataItem;
}
}
@ -1512,7 +1529,7 @@ DownloadsSummaryData.prototype = {
},
// DownloadsView
onDataItemStateChanged(aOldState) {
onDataItemStateChanged() {
// Since the state of a download changed, reset the estimated time left.
this._lastRawTimeLeft = -1;
this._lastTimeLeft = -1;

View File

@ -24,14 +24,90 @@ const DOWNLOAD_VIEW_SUPPORTED_COMMANDS =
*
* @param url
* URI string for the download source.
* @param endTime
* Timestamp with the end time for the download, used if there is no
* additional metadata available.
*/
function HistoryDownload(url) {
function HistoryDownload(aPlacesNode) {
// TODO (bug 829201): history downloads should get the referrer from Places.
this.source = { url };
this.source = { url: aPlacesNode.uri };
this.target = { path: undefined, size: undefined };
// In case this download cannot obtain its end time from the Places metadata,
// use the time from the Places node, that is the start time of the download.
this.endTime = aPlacesNode.time / 1000;
}
HistoryDownload.prototype = {
/**
* Pushes information from Places metadata into this object.
*/
updateFromMetaData(aPlacesMetaData) {
try {
this.target.path = Cc["@mozilla.org/network/protocol;1?name=file"]
.getService(Ci.nsIFileProtocolHandler)
.getFileFromURLSpec(aPlacesMetaData.
targetFileURISpec).path;
} catch (ex) {
this.target.path = undefined;
}
try {
let metaData = JSON.parse(aPlacesMetaData.jsonDetails);
this.succeeded = metaData.state == nsIDM.DOWNLOAD_FINISHED;
this.error = metaData.state == nsIDM.DOWNLOAD_FAILED
? { message: "History download failed." }
: metaData.state == nsIDM.DOWNLOAD_BLOCKED_PARENTAL
? { becauseBlockedByParentalControls: true }
: metaData.state == nsIDM.DOWNLOAD_DIRTY
? { becauseBlockedByReputationCheck: true }
: null;
this.canceled = metaData.state == nsIDM.DOWNLOAD_CANCELED ||
metaData.state == nsIDM.DOWNLOAD_PAUSED;
this.endTime = metaData.endTime;
this.target.size = metaData.fileSize;
} catch (ex) {
// Metadata might be missing from a download that has started but hasn't
// stopped already. Normally, this state is overridden with the one from
// the corresponding in-progress session download. But if the browser is
// terminated abruptly and additionally the file with information about
// in-progress downloads is lost, we may end up using this state. We use
// the failed state to allow the download to be restarted.
//
// On the other hand, if the download is missing the target file
// annotation as well, it is just a very old one, and we can assume it
// succeeded.
this.succeeded = !this.target.path;
this.error = this.target.path ? { message: "Unstarted download." } : null;
this.canceled = false;
this.target.size = -1;
}
// This property is currently used to get the size of downloads, but will be
// replaced by download.target.size when available for session downloads.
this.totalBytes = this.target.size;
this.currentBytes = this.target.size;
},
/**
* History downloads are never in progress.
*/
stopped: true,
/**
* No percentage indication is shown for history downloads.
*/
hasProgress: false,
/**
* History downloads cannot be restarted using their partial data, even if
* they are indicated as paused in their Places metadata. The only way is to
* use the information from a persisted session download, that will be shown
* instead of the history download. In case this session download is not
* available, we show the history download as canceled, not paused.
*/
hasPartialData: false,
/**
* This method mimicks the "start" method of session downloads, and is called
* when the user retries a history download.
@ -59,57 +135,11 @@ HistoryDownload.prototype = {
* The Places node for the history download.
*/
function DownloadsHistoryDataItem(aPlacesNode) {
this.download = new HistoryDownload(aPlacesNode.uri);
// In case this download cannot obtain its end time from the Places metadata,
// use the time from the Places node, that is the start time of the download.
this.endTime = aPlacesNode.time / 1000;
this.download = new HistoryDownload(aPlacesNode);
}
DownloadsHistoryDataItem.prototype = {
__proto__: DownloadsDataItem.prototype,
/**
* Pushes information from Places metadata into this object.
*/
updateFromMetaData(aPlacesMetaData) {
try {
let targetFile = Cc["@mozilla.org/network/protocol;1?name=file"]
.getService(Ci.nsIFileProtocolHandler)
.getFileFromURLSpec(aPlacesMetaData.targetFileURISpec);
this.download.target.path = targetFile.path;
} catch (ex) {
this.download.target.path = undefined;
}
try {
let metaData = JSON.parse(aPlacesMetaData.jsonDetails);
this.state = metaData.state;
this.endTime = metaData.endTime;
this.download.target.size = metaData.fileSize;
} catch (ex) {
// Metadata might be missing from a download that has started but hasn't
// stopped already. Normally, this state is overridden with the one from
// the corresponding in-progress session download. But if the browser is
// terminated abruptly and additionally the file with information about
// in-progress downloads is lost, we may end up using this state. We use
// the failed state to allow the download to be restarted.
//
// On the other hand, if the download is missing the target file
// annotation as well, it is just a very old one, and we can assume it
// succeeded.
this.state = this.download.target.path ? nsIDM.DOWNLOAD_FAILED
: nsIDM.DOWNLOAD_FINISHED;
this.download.target.size = undefined;
}
// This property is currently used to get the size of downloads, but will be
// replaced by download.target.size when available for session downloads.
this.maxBytes = this.download.target.size;
// This is not displayed for history downloads, that are never in progress.
this.percentComplete = 100;
},
};
/**
@ -223,7 +253,7 @@ HistoryDownloadElementShell.prototype = {
// The base object would show extended progress information in the tooltip,
// but we move this to the main view and never display a tooltip.
if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
if (!this.download.stopped) {
status.text = status.tip;
}
status.tip = "";
@ -232,18 +262,9 @@ HistoryDownloadElementShell.prototype = {
},
onStateChanged() {
// If a download just finished successfully, it means that the target file
// now exists and we can extract its specific icon. To ensure that the icon
// is reloaded, we must change the URI used by the XUL image element, for
// example by adding a query parameter. Since this URI has a "moz-icon"
// scheme, this only works if we add one of the parameters explicitly
// supported by the nsIMozIconURI interface.
if (this.dataItem.state == nsIDM.DOWNLOAD_FINISHED) {
this.element.setAttribute("image", this.image + "&state=normal");
}
// Update the user interface after switching states.
this.element.setAttribute("state", this.dataItem.state);
this.element.setAttribute("image", this.image);
this.element.setAttribute("state",
DownloadsCommon.stateOfDownload(this.download));
if (this.element.selected) {
goUpdateDownloadCommands();
@ -277,12 +298,14 @@ HistoryDownloadElementShell.prototype = {
// If the target file information is not yet fetched,
// temporarily assume that the file is in place.
return this.dataItem.state == nsIDM.DOWNLOAD_FINISHED;
return this.download.succeeded;
case "downloadsCmd_show":
// TODO: Bug 827010 - Handle part-file asynchronously.
if (this._sessionDataItem &&
this.dataItem.partFile && this.dataItem.partFile.exists()) {
return true;
if (this._sessionDataItem && this.download.target.partFilePath) {
let partFile = new FileUtils.File(this.download.target.partFilePath);
if (partFile.exists()) {
return true;
}
}
if (this._targetFileChecked) {
@ -291,17 +314,16 @@ HistoryDownloadElementShell.prototype = {
// If the target file information is not yet fetched,
// temporarily assume that the file is in place.
return this.dataItem.state == nsIDM.DOWNLOAD_FINISHED;
return this.download.succeeded;
case "downloadsCmd_pauseResume":
return this._sessionDataItem && this.dataItem.inProgress &&
this.dataItem.download.hasPartialData;
return this.download.hasPartialData && !this.download.error;
case "downloadsCmd_retry":
return this.dataItem.canRetry;
return this.download.canceled || this.download.error;
case "downloadsCmd_openReferrer":
return !!this.download.source.referrer;
case "cmd_delete":
// The behavior in this case is somewhat unexpected, so we disallow that.
return !this.dataItem.inProgress;
// We don't want in-progress downloads to be removed accidentally.
return this.download.stopped;
case "downloadsCmd_cancel":
return !!this._sessionDataItem;
}
@ -396,7 +418,8 @@ HistoryDownloadElementShell.prototype = {
}
return "";
}
let command = getDefaultCommandForState(this.dataItem.state);
let command = getDefaultCommandForState(
DownloadsCommon.stateOfDownload(this.download));
if (command && this.isCommandEnabled(command)) {
this.doCommand(command);
}
@ -625,8 +648,9 @@ DownloadsPlacesView.prototype = {
*/
_addDownloadData(aDataItem, aPlacesNode, aNewest = false,
aDocumentFragment = null) {
let sessionDownload = aDataItem && aDataItem.download;
let downloadURI = aPlacesNode ? aPlacesNode.uri
: aDataItem.download.source.url;
: sessionDownload.source.url;
let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
if (!shellsForURI) {
shellsForURI = new Set();
@ -678,7 +702,7 @@ DownloadsPlacesView.prototype = {
if (aPlacesNode) {
let metaData = this._getCachedPlacesMetaDataFor(aPlacesNode.uri);
historyDataItem = new DownloadsHistoryDataItem(aPlacesNode);
historyDataItem.updateFromMetaData(metaData);
historyDataItem.download.updateFromMetaData(metaData);
}
let shell = new HistoryDownloadElementShell(aDataItem, historyDataItem);
shell.element._placesNode = aPlacesNode;
@ -782,8 +806,9 @@ DownloadsPlacesView.prototype = {
},
_removeSessionDownloadFromView(aDataItem) {
let download = aDataItem.download;
let shells = this._downloadElementsShellsForURI
.get(aDataItem.download.source.url);
.get(download.source.url);
if (shells.size == 0) {
throw new Error("Should have had at leaat one shell for this uri");
}
@ -801,7 +826,7 @@ DownloadsPlacesView.prototype = {
this._removeElement(shell.element);
shells.delete(shell);
if (shells.size == 0) {
this._downloadElementsShellsForURI.delete(aDataItem.download.source.url);
this._downloadElementsShellsForURI.delete(download.source.url);
}
} else {
// We have one download element shell containing both a session download
@ -811,7 +836,7 @@ DownloadsPlacesView.prototype = {
// read the latest metadata before removing the session download.
let url = shell.historyDataItem.download.source.url;
let metaData = this._getPlacesMetaDataFor(url);
shell.historyDataItem.updateFromMetaData(metaData);
shell.historyDataItem.download.updateFromMetaData(metaData);
shell.sessionDataItem = null;
// Move it below the session-download items;
if (this._lastSessionDownloadElement == shell.element) {
@ -1146,7 +1171,9 @@ DownloadsPlacesView.prototype = {
// Because history downloads are always removable and are listed after the
// session downloads, check from bottom to top.
for (let elt = this._richlistbox.lastChild; elt; elt = elt.previousSibling) {
if (!elt._shell.dataItem.inProgress) {
// Stopped, paused, and failed downloads with partial data are removed.
let download = elt._shell.download;
if (download.stopped && !(download.canceled && download.hasPartialData)) {
return true;
}
}
@ -1243,14 +1270,16 @@ DownloadsPlacesView.prototype = {
// Set the state attribute so that only the appropriate items are displayed.
let contextMenu = document.getElementById("downloadsContextMenu");
let state = element._shell.dataItem.state;
contextMenu.setAttribute("state", state);
let download = element._shell.download;
contextMenu.setAttribute("state",
DownloadsCommon.stateOfDownload(download));
if (state == nsIDM.DOWNLOAD_DOWNLOADING) {
// The resumable property of a download may change at any time, so
// ensure we update the related command now.
if (!download.stopped) {
// The hasPartialData property of a download may change at any time after
// it has started, so ensure we update the related command now.
goUpdateCommand("downloadsCmd_pauseResume");
}
return true;
},

View File

@ -953,9 +953,10 @@ const DownloadsView = {
return;
}
let localFile = DownloadsView.controllerForElement(element)
.dataItem.localFile;
if (!localFile.exists()) {
// We must check for existence synchronously because this is a DOM event.
let file = new FileUtils.File(DownloadsView.controllerForElement(element)
.download.target.path);
if (!file.exists()) {
return;
}
@ -1009,24 +1010,17 @@ DownloadsViewItem.prototype = {
_element: null,
onStateChanged() {
// If a download just finished successfully, it means that the target file
// now exists and we can extract its specific icon. To ensure that the icon
// is reloaded, we must change the URI used by the XUL image element, for
// example by adding a query parameter. Since this URI has a "moz-icon"
// scheme, this only works if we add one of the parameters explicitly
// supported by the nsIMozIconURI interface.
if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
this.element.setAttribute("image", this.image + "&state=normal");
this.element.setAttribute("image", this.image);
this.element.setAttribute("state",
DownloadsCommon.stateOfDownload(this.download));
if (this.download.succeeded) {
// We assume the existence of the target of a download that just completed
// successfully, without checking the condition in the background. If the
// panel is already open, this will take effect immediately. If the panel
// is opened later, a new background existence check will be performed.
this.element.setAttribute("exists", "true");
}
// Update the user interface after switching states.
this.element.setAttribute("state", this.dataItem.state);
},
onChanged() {
@ -1167,23 +1161,37 @@ DownloadsViewItemController.prototype = {
*/
dataItem: null,
get download() this.dataItem.download,
isCommandEnabled(aCommand) {
switch (aCommand) {
case "downloadsCmd_open": {
return this.dataItem.download.succeeded &&
this.dataItem.localFile.exists();
if (!this.download.succeeded) {
return false;
}
let file = new FileUtils.File(this.download.target.path);
return file.exists();
}
case "downloadsCmd_show": {
return this.dataItem.localFile.exists() ||
this.dataItem.partFile.exists();
let file = new FileUtils.File(this.download.target.path);
if (file.exists()) {
return true;
}
if (!this.download.target.partFilePath) {
return false;
}
let partFile = new FileUtils.File(this.download.target.partFilePath);
return partFile.exists();
}
case "downloadsCmd_pauseResume":
return this.dataItem.inProgress &&
this.dataItem.download.hasPartialData;
return this.download.hasPartialData && !this.download.error;
case "downloadsCmd_retry":
return this.dataItem.canRetry;
return this.download.canceled || this.download.error;
case "downloadsCmd_openReferrer":
return !!this.dataItem.download.source.referrer;
return !!this.download.source.referrer;
case "cmd_delete":
case "downloadsCmd_cancel":
case "downloadsCmd_copyLocation":
@ -1210,20 +1218,20 @@ DownloadsViewItemController.prototype = {
commands: {
cmd_delete() {
Downloads.getList(Downloads.ALL)
.then(list => list.remove(this.dataItem.download))
.then(() => this.dataItem.download.finalize(true))
.then(list => list.remove(this.download))
.then(() => this.download.finalize(true))
.catch(Cu.reportError);
PlacesUtils.bhistory.removePage(
NetUtil.newURI(this.dataItem.download.source.url));
NetUtil.newURI(this.download.source.url));
},
downloadsCmd_cancel() {
this.dataItem.download.cancel().catch(() => {});
this.dataItem.download.removePartialData().catch(Cu.reportError);
this.download.cancel().catch(() => {});
this.download.removePartialData().catch(Cu.reportError);
},
downloadsCmd_open() {
this.dataItem.download.launch().catch(Cu.reportError);
this.download.launch().catch(Cu.reportError);
// We explicitly close the panel here to give the user the feedback that
// their click has been received, and we're handling the action.
@ -1234,7 +1242,8 @@ DownloadsViewItemController.prototype = {
},
downloadsCmd_show() {
DownloadsCommon.showDownloadedFile(this.dataItem.localFile);
let file = new FileUtils.File(this.download.target.path);
DownloadsCommon.showDownloadedFile(file);
// We explicitly close the panel here to give the user the feedback that
// their click has been received, and we're handling the action.
@ -1245,25 +1254,25 @@ DownloadsViewItemController.prototype = {
},
downloadsCmd_pauseResume() {
if (this.dataItem.download.stopped) {
this.dataItem.download.start();
if (this.download.stopped) {
this.download.start();
} else {
this.dataItem.download.cancel();
this.download.cancel();
}
},
downloadsCmd_retry() {
this.dataItem.download.start().catch(() => {});
this.download.start().catch(() => {});
},
downloadsCmd_openReferrer() {
openURL(this.dataItem.download.source.referrer);
openURL(this.download.source.referrer);
},
downloadsCmd_copyLocation() {
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper);
clipboard.copyString(this.dataItem.download.source.url, document);
clipboard.copyString(this.download.source.url, document);
},
downloadsCmd_doDefault() {
@ -1271,7 +1280,7 @@ DownloadsViewItemController.prototype = {
// Determine the default command for the current item.
let defaultCommand = function () {
switch (this.dataItem.state) {
switch (DownloadsCommon.stateOfDownload(this.download)) {
case nsIDM.DOWNLOAD_NOTSTARTED: return "downloadsCmd_cancel";
case nsIDM.DOWNLOAD_FINISHED: return "downloadsCmd_open";
case nsIDM.DOWNLOAD_FAILED: return "downloadsCmd_retry";

View File

@ -72,12 +72,19 @@ DownloadElementShell.prototype = {
* URI string for the file type icon displayed in the download element.
*/
get image() {
if (this.download.target.path) {
return "moz-icon://" + this.download.target.path + "?size=32";
if (!this.download.target.path) {
// Old history downloads may not have a target path.
return "moz-icon://.unknown?size=32";
}
// Old history downloads may not have a target path.
return "moz-icon://.unknown?size=32";
// When a download that was previously in progress finishes successfully, it
// means that the target file now exists and we can extract its specific
// icon, for example from a Windows executable. To ensure that the icon is
// reloaded, however, we must change the URI used by the XUL image element,
// for example by adding a query parameter. This only works if we add one of
// the parameters explicitly supported by the nsIMozIconURI interface.
return "moz-icon://" + this.download.target.path + "?size=32" +
(this.download.succeeded ? "&state=normal" : "");
},
/**
@ -111,9 +118,10 @@ DownloadElementShell.prototype = {
* update in order to improve performance.
*/
_updateState() {
this.element.setAttribute("state", this.dataItem.state);
this.element.setAttribute("displayName", this.displayName);
this.element.setAttribute("image", this.image);
this.element.setAttribute("state",
DownloadsCommon.stateOfDownload(this.download));
// Since state changed, reset the time left estimation.
this.lastEstimatedSecondsLeft = Infinity;
@ -126,19 +134,12 @@ DownloadElementShell.prototype = {
* namely the progress bar and the status line.
*/
_updateProgress() {
if (this.dataItem.starting) {
// Before the download starts, the progress meter has its initial value.
// The progress bar is only displayed for in-progress downloads.
if (this.download.hasProgress) {
this.element.setAttribute("progressmode", "normal");
this.element.setAttribute("progress", "0");
} else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING ||
this.dataItem.percentComplete == -1) {
// We might not know the progress of a running download, and we don't know
// the remaining time during the malware scanning phase.
this.element.setAttribute("progressmode", "undetermined");
this.element.setAttribute("progress", this.download.progress);
} else {
// This is a running download of which we know the progress.
this.element.setAttribute("progressmode", "normal");
this.element.setAttribute("progress", this.dataItem.percentComplete);
this.element.setAttribute("progressmode", "undetermined");
}
// Dispatch the ValueChange event for accessibility, if possible.
@ -172,71 +173,60 @@ DownloadElementShell.prototype = {
let text = "";
let tip = "";
if (this.dataItem.paused) {
let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes,
this.dataItem.maxBytes);
// We use the same XUL label to display both the state and the amount
// transferred, for example "Paused - 1.1 MB".
text = s.statusSeparatorBeforeNumber(s.statePaused, transfer);
} else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
if (!this.download.stopped) {
let maxBytes = DownloadsCommon.maxBytesOfDownload(this.download);
// By default, extended status information including the individual
// download rate is displayed in the tooltip. The history view overrides
// the getter and displays the detials in the main area instead.
[text] = DownloadUtils.getDownloadStatusNoRate(
this.download.currentBytes,
this.dataItem.maxBytes,
maxBytes,
this.download.speed,
this.lastEstimatedSecondsLeft);
let newEstimatedSecondsLeft;
[tip, newEstimatedSecondsLeft] = DownloadUtils.getDownloadStatus(
this.download.currentBytes,
this.dataItem.maxBytes,
maxBytes,
this.download.speed,
this.lastEstimatedSecondsLeft);
this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
} else if (this.dataItem.starting) {
} else if (this.download.canceled && this.download.hasPartialData) {
let maxBytes = DownloadsCommon.maxBytesOfDownload(this.download);
let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes,
maxBytes);
// We use the same XUL label to display both the state and the amount
// transferred, for example "Paused - 1.1 MB".
text = s.statusSeparatorBeforeNumber(s.statePaused, transfer);
} else if (!this.download.succeeded && !this.download.canceled &&
!this.download.error) {
text = s.stateStarting;
} else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
text = s.stateScanning;
} else {
let stateLabel;
switch (this.dataItem.state) {
case nsIDM.DOWNLOAD_FAILED:
stateLabel = s.stateFailed;
break;
case nsIDM.DOWNLOAD_CANCELED:
stateLabel = s.stateCanceled;
break;
case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
stateLabel = s.stateBlockedParentalControls;
break;
case nsIDM.DOWNLOAD_BLOCKED_POLICY:
stateLabel = s.stateBlockedPolicy;
break;
case nsIDM.DOWNLOAD_DIRTY:
stateLabel = s.stateDirty;
break;
case nsIDM.DOWNLOAD_FINISHED:
// For completed downloads, show the file size (e.g. "1.5 MB")
if (this.dataItem.maxBytes !== undefined &&
this.dataItem.maxBytes >= 0) {
let [size, unit] =
DownloadUtils.convertByteUnits(this.dataItem.maxBytes);
stateLabel = s.sizeWithUnits(size, unit);
break;
}
// Fallback to default unknown state.
default:
if (this.download.succeeded) {
// For completed downloads, show the file size (e.g. "1.5 MB")
let maxBytes = DownloadsCommon.maxBytesOfDownload(this.download);
if (maxBytes >= 0) {
let [size, unit] = DownloadUtils.convertByteUnits(maxBytes);
stateLabel = s.sizeWithUnits(size, unit);
} else {
stateLabel = s.sizeUnknown;
break;
}
} else if (this.download.canceled) {
stateLabel = s.stateCanceled;
} else if (this.download.error.becauseBlockedByParentalControls) {
stateLabel = s.stateBlockedParentalControls;
} else if (this.download.error.becauseBlockedByReputationCheck) {
stateLabel = s.stateDirty;
} else {
stateLabel = s.stateFailed;
}
let referrer = this.download.source.referrer ||
this.download.source.url;
let referrer = this.download.source.referrer || this.download.source.url;
let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer);
let date = new Date(this.dataItem.endTime);
let date = new Date(this.download.endTime);
let [displayDate, fullDate] = DownloadUtils.getReadableDates(date);
let firstPart = s.statusSeparator(stateLabel, displayHost);

View File

@ -49,7 +49,8 @@ add_task(function* test_basic_functionality() {
let itemCount = richlistbox.children.length;
for (let i = 0; i < itemCount; i++) {
let element = richlistbox.children[itemCount - i - 1];
let dataItem = DownloadsView.controllerForElement(element).dataItem;
is(dataItem.state, DownloadData[i].state, "Download states match up");
let download = DownloadsView.controllerForElement(element).download;
is(DownloadsCommon.stateOfDownload(download), DownloadData[i].state,
"Download states match up");
}
});