Bug 899107 - Allow using the JavaScript API as the back-end for the Downloads Panel. r=mak

This commit is contained in:
Paolo Amadini 2013-08-03 13:04:44 +02:00
Родитель ceabe32d38
Коммит 9441f6c0be
5 изменённых файлов: 278 добавлений и 13 удалений

Просмотреть файл

@ -335,6 +335,9 @@ pref("browser.download.manager.quitBehavior", 0);
pref("browser.download.manager.scanWhenDone", true);
pref("browser.download.manager.resumeOnWakeDelay", 10000);
// Enables the asynchronous Downloads API in the Downloads Panel.
pref("browser.download.useJSTransfer", false);
// This allows disabling the Downloads Panel in favor of the old interface.
pref("browser.download.useToolkitUI", false);

Просмотреть файл

@ -1106,7 +1106,15 @@ var gBrowserInit = {
// If the user manually opens the download manager before the timeout, the
// downloads will start right away, and getting the service again won't hurt.
setTimeout(function() {
Services.downloads;
let DownloadsCommon =
Cu.import("resource:///modules/DownloadsCommon.jsm", {}).DownloadsCommon;
if (DownloadsCommon.useJSTransfer) {
// Open the data link without initalizing nsIDownloadManager.
DownloadsCommon.initializeAllDataLinks();
} else {
// Initalizing nsIDownloadManager will trigger the data link.
Services.downloads;
}
let DownloadTaskbarProgress =
Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
DownloadTaskbarProgress.onBrowserWindowLoad(window);

Просмотреть файл

@ -51,8 +51,12 @@ 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, "DownloadUtils",
"resource://gre/modules/DownloadUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm")
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
@ -582,6 +586,20 @@ XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () {
return parseFloat(sysInfo.getProperty("version")) >= 6;
});
/**
* Returns true if we should hook the panel to the JavaScript API for downloads
* instead of the nsIDownloadManager back-end. In order for the logic to work
* properly, this value never changes during the execution of the application,
* even if the underlying preference value has changed. A restart is required
* for the change to take effect.
*/
XPCOMUtils.defineLazyGetter(DownloadsCommon, "useJSTransfer", function () {
try {
return Services.prefs.getBoolPref("browser.download.useJSTransfer");
} catch (ex) { }
return false;
});
////////////////////////////////////////////////////////////////////////////////
//// DownloadsData
@ -617,6 +635,11 @@ function DownloadsDataCtor(aPrivate) {
// Array of view objects that should be notified when the available download
// data changes.
this._views = [];
if (DownloadsCommon.useJSTransfer) {
// Maps Download objects to DownloadDataItem objects.
this._downloadToDataItemMap = new Map();
}
}
DownloadsDataCtor.prototype = {
@ -632,8 +655,15 @@ DownloadsDataCtor.prototype = {
initializeDataLink: function DD_initializeDataLink(aDownloadManagerService)
{
// Start receiving real-time events.
aDownloadManagerService.addPrivacyAwareListener(this);
Services.obs.addObserver(this, "download-manager-remove-download-guid", false);
if (DownloadsCommon.useJSTransfer) {
let promiseList = this._isPrivate ? Downloads.getPrivateDownloadList()
: Downloads.getPublicDownloadList();
promiseList.then(list => list.addView(this)).then(null, Cu.reportError);
} else {
aDownloadManagerService.addPrivacyAwareListener(this);
Services.obs.addObserver(this, "download-manager-remove-download-guid",
false);
}
},
/**
@ -641,6 +671,11 @@ DownloadsDataCtor.prototype = {
*/
terminateDataLink: function DD_terminateDataLink()
{
if (DownloadsCommon.useJSTransfer) {
Cu.reportError("terminateDataLink not applicable with useJSTransfer");
return;
}
this._terminateDataAccess();
// Stop receiving real-time events.
@ -648,6 +683,84 @@ DownloadsDataCtor.prototype = {
Services.downloads.removeListener(this);
},
//////////////////////////////////////////////////////////////////////////////
//// 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.remove(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 wasInProgress = aDataItem.inProgress;
let wasDone = aDataItem.done;
aDataItem.updateFromJSDownload();
if (wasInProgress && !aDataItem.inProgress) {
aDataItem.endTime = Date.now();
}
for (let view of this._views) {
try {
view.getViewItem(aDataItem).onStateChange({});
} 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
@ -1160,11 +1273,14 @@ XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
*
* @param aSource
* Object containing the data with which the item should be initialized.
* This should implement either nsIDownload or mozIStorageRow.
* This should implement either nsIDownload or mozIStorageRow. If the
* JavaScript API for downloads is enabled, this is a Download object.
*/
function DownloadsDataItem(aSource)
{
if (aSource instanceof Ci.nsIDownload) {
if (DownloadsCommon.useJSTransfer) {
this._initFromJSDownload(aSource);
} else if (aSource instanceof Ci.nsIDownload) {
this._initFromDownload(aSource);
} else {
this._initFromDataRow(aSource);
@ -1172,6 +1288,66 @@ function DownloadsDataItem(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.maxBytes = this._download.totalBytes;
this.resumable = this._download.hasPartialData;
this.speed = 0;
this.percentComplete = this._download.progress;
},
/**
* Initializes this object from a download object of the Download Manager.
*
@ -1408,6 +1584,11 @@ DownloadsDataItem.prototype = {
* @throws if the file cannot be opened.
*/
openLocalFile: function DDI_openLocalFile(aOwnerWindow) {
if (DownloadsCommon.useJSTransfer) {
this._download.launch().then(null, Cu.reportError);
return;
}
this.getDownload(function(aDownload) {
DownloadsCommon.openDownloadedFile(this.localFile,
aDownload.MIMEInfo,
@ -1427,6 +1608,15 @@ DownloadsDataItem.prototype = {
* @throws if the download is not resumable or if has already done.
*/
togglePauseResume: function DDI_togglePauseResume() {
if (DownloadsCommon.useJSTransfer) {
if (this._download.stopped) {
this._download.start();
} else {
this._download.cancel();
}
return;
}
if (!this.inProgress || !this.resumable)
throw new Error("The given download cannot be paused or resumed");
@ -1445,8 +1635,13 @@ DownloadsDataItem.prototype = {
* @throws if we cannot.
*/
retry: function DDI_retry() {
if (DownloadsCommon.useJSTransfer) {
this._download.start();
return;
}
if (!this.canRetry)
throw new Error("Cannot rerty this download");
throw new Error("Cannot retry this download");
this.getDownload(function(aDownload) {
aDownload.retry();
@ -1473,6 +1668,12 @@ DownloadsDataItem.prototype = {
* @throws if the download is already done.
*/
cancel: function() {
if (DownloadsCommon.useJSTransfer) {
this._download.cancel();
this._download.removePartialData().then(null, Cu.reportError);
return;
}
if (!this.inProgress)
throw new Error("Cannot cancel this download");
@ -1486,6 +1687,16 @@ DownloadsDataItem.prototype = {
* Remove the download.
*/
remove: function DDI_remove() {
if (DownloadsCommon.useJSTransfer) {
let promiseList = this._download.source.isPrivate
? Downloads.getPrivateDownloadList()
: Downloads.getPublicDownloadList();
promiseList.then(list => list.remove(this._download))
.then(() => this._download.finalize(true))
.then(null, Cu.reportError);
return;
}
this.getDownload(function (aDownload) {
if (this.inProgress) {
aDownload.cancel();

Просмотреть файл

@ -86,10 +86,6 @@ DownloadsStartup.prototype = {
{
switch (aTopic) {
case "profile-after-change":
kObservedTopics.forEach(
function (topic) Services.obs.addObserver(this, topic, true),
this);
// Override Toolkit's nsIDownloadManagerUI implementation with our own.
// This must be done at application startup and not in the manifest to
// ensure that our implementation overrides the original one.
@ -104,15 +100,21 @@ DownloadsStartup.prototype = {
// when nsIDownloadManager will not be available anymore (bug 851471).
let useJSTransfer = false;
try {
// For performance reasons, we don't want to load the DownloadsCommon
// module during startup, so we read the preference value directly.
useJSTransfer =
Services.prefs.getBoolPref("browser.download.useJSTransfer");
} catch (ex) {
// This is a hidden preference that does not exist by default.
}
} catch (ex) { }
if (useJSTransfer) {
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(kTransferCid, "",
kTransferContractId, null);
} else {
// The other notifications are handled internally by the JavaScript
// API for downloads, no need to observe when that API is enabled.
for (let topic of kObservedTopics) {
Services.obs.addObserver(this, topic, true);
}
}
break;

Просмотреть файл

@ -159,6 +159,47 @@ add_task(function test_notifications_change()
do_check_false(receivedOnDownloadChanged);
});
/**
* Checks that the reference to "this" is correct in the view callbacks.
*/
add_task(function test_notifications_this()
{
let list = yield promiseNewDownloadList();
// Check that we receive change notifications.
let receivedOnDownloadAdded = false;
let receivedOnDownloadChanged = false;
let receivedOnDownloadRemoved = false;
let view = {
onDownloadAdded: function () {
do_check_eq(this, view);
receivedOnDownloadAdded = true;
},
onDownloadChanged: function () {
// Only do this check once.
if (!receivedOnDownloadChanged) {
do_check_eq(this, view);
receivedOnDownloadChanged = true;
}
},
onDownloadRemoved: function () {
do_check_eq(this, view);
receivedOnDownloadRemoved = true;
},
};
list.addView(view);
let download = yield promiseNewDownload();
list.add(download);
yield download.start();
list.remove(download);
// Verify that we executed the checks.
do_check_true(receivedOnDownloadAdded);
do_check_true(receivedOnDownloadChanged);
do_check_true(receivedOnDownloadRemoved);
});
/**
* Checks that download is removed on history expiration.
*/