diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 091c4c5fa5c3..2a143c12d6a6 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -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); diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index e4ad74c35164..dd016964b1aa 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -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); diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm index 2ceb4b0766af..992317704dd4 100644 --- a/browser/components/downloads/src/DownloadsCommon.jsm +++ b/browser/components/downloads/src/DownloadsCommon.jsm @@ -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(); diff --git a/browser/components/downloads/src/DownloadsStartup.js b/browser/components/downloads/src/DownloadsStartup.js index 5ed4a44e6a78..c46039d73956 100644 --- a/browser/components/downloads/src/DownloadsStartup.js +++ b/browser/components/downloads/src/DownloadsStartup.js @@ -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; diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js index 4a50fee055eb..aeaca97f53c2 100644 --- a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js @@ -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. */