michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["DownloadsIPC"]; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: michael@0: /** michael@0: * This module lives in the child process and receives the ipc messages michael@0: * from the parent. It saves the download's state and redispatch changes michael@0: * to DOM objects using an observer notification. michael@0: * michael@0: * This module needs to be loaded once and only once per process. michael@0: */ michael@0: michael@0: function debug(aStr) { michael@0: #ifdef MOZ_DEBUG michael@0: dump("-*- DownloadsIPC.jsm : " + aStr + "\n"); michael@0: #endif michael@0: } michael@0: michael@0: const ipcMessages = ["Downloads:Added", michael@0: "Downloads:Removed", michael@0: "Downloads:Changed", michael@0: "Downloads:GetList:Return", michael@0: "Downloads:ClearAllDone:Return", michael@0: "Downloads:Remove:Return", michael@0: "Downloads:Pause:Return", michael@0: "Downloads:Resume:Return"]; michael@0: michael@0: this.DownloadsIPC = { michael@0: downloads: {}, michael@0: michael@0: init: function() { michael@0: debug("init"); michael@0: Services.obs.addObserver(this, "xpcom-shutdown", false); michael@0: ipcMessages.forEach((aMessage) => { michael@0: cpmm.addMessageListener(aMessage, this); michael@0: }); michael@0: michael@0: // We need to get the list of current downloads. michael@0: this.ready = false; michael@0: this.getListPromises = []; michael@0: this.clearAllPromises = []; michael@0: this.downloadPromises = {}; michael@0: cpmm.sendAsyncMessage("Downloads:GetList", {}); michael@0: this._promiseId = 0; michael@0: }, michael@0: michael@0: notifyChanges: function(aId) { michael@0: // TODO: use the subject instead of stringifying. michael@0: if (this.downloads[aId]) { michael@0: debug("notifyChanges notifying changes for " + aId); michael@0: Services.obs.notifyObservers(null, "downloads-state-change-" + aId, michael@0: JSON.stringify(this.downloads[aId])); michael@0: } else { michael@0: debug("notifyChanges failed for " + aId) michael@0: } michael@0: }, michael@0: michael@0: _updateDownloadsArray: function(aDownloads) { michael@0: this.downloads = []; michael@0: // We actually have an array of downloads. michael@0: aDownloads.forEach((aDownload) => { michael@0: this.downloads[aDownload.id] = aDownload; michael@0: }); michael@0: }, michael@0: michael@0: receiveMessage: function(aMessage) { michael@0: let download = aMessage.data; michael@0: debug("message: " + aMessage.name); michael@0: switch(aMessage.name) { michael@0: case "Downloads:GetList:Return": michael@0: this._updateDownloadsArray(download); michael@0: michael@0: if (!this.ready) { michael@0: this.getListPromises.forEach(aPromise => michael@0: aPromise.resolve(this.downloads)); michael@0: this.getListPromises.length = 0; michael@0: } michael@0: this.ready = true; michael@0: break; michael@0: case "Downloads:ClearAllDone:Return": michael@0: this._updateDownloadsArray(download); michael@0: this.clearAllPromises.forEach(aPromise => michael@0: aPromise.resolve(this.downloads)); michael@0: this.clearAllPromises.length = 0; michael@0: break; michael@0: case "Downloads:Added": michael@0: this.downloads[download.id] = download; michael@0: this.notifyChanges(download.id); michael@0: break; michael@0: case "Downloads:Removed": michael@0: if (this.downloads[download.id]) { michael@0: this.downloads[download.id] = download; michael@0: this.notifyChanges(download.id); michael@0: delete this.downloads[download.id]; michael@0: } michael@0: break; michael@0: case "Downloads:Changed": michael@0: // Only update properties that actually changed. michael@0: let cached = this.downloads[download.id]; michael@0: if (!cached) { michael@0: debug("No download found for " + download.id); michael@0: return; michael@0: } michael@0: let props = ["totalBytes", "currentBytes", "url", "path", "state", michael@0: "contentType", "startTime"]; michael@0: let changed = false; michael@0: michael@0: props.forEach((aProp) => { michael@0: if (download[aProp] && (download[aProp] != cached[aProp])) { michael@0: cached[aProp] = download[aProp]; michael@0: changed = true; michael@0: } michael@0: }); michael@0: michael@0: // Updating the error property. We always get a 'state' change as michael@0: // well. michael@0: cached.error = download.error; michael@0: michael@0: if (changed) { michael@0: this.notifyChanges(download.id); michael@0: } michael@0: break; michael@0: case "Downloads:Remove:Return": michael@0: case "Downloads:Pause:Return": michael@0: case "Downloads:Resume:Return": michael@0: if (this.downloadPromises[download.promiseId]) { michael@0: if (!download.error) { michael@0: this.downloadPromises[download.promiseId].resolve(download); michael@0: } else { michael@0: this.downloadPromises[download.promiseId].reject(download); michael@0: } michael@0: delete this.downloadPromises[download.promiseId]; michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns a promise that is resolved with the list of current downloads. michael@0: */ michael@0: getDownloads: function() { michael@0: debug("getDownloads()"); michael@0: let deferred = Promise.defer(); michael@0: if (this.ready) { michael@0: debug("Returning existing list."); michael@0: deferred.resolve(this.downloads); michael@0: } else { michael@0: this.getListPromises.push(deferred); michael@0: } michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a promise that is resolved with the list of current downloads. michael@0: */ michael@0: clearAllDone: function() { michael@0: debug("clearAllDone"); michael@0: let deferred = Promise.defer(); michael@0: this.clearAllPromises.push(deferred); michael@0: cpmm.sendAsyncMessage("Downloads:ClearAllDone", {}); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: promiseId: function() { michael@0: return this._promiseId++; michael@0: }, michael@0: michael@0: remove: function(aId) { michael@0: debug("remove " + aId); michael@0: let deferred = Promise.defer(); michael@0: let pId = this.promiseId(); michael@0: this.downloadPromises[pId] = deferred; michael@0: cpmm.sendAsyncMessage("Downloads:Remove", michael@0: { id: aId, promiseId: pId }); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: pause: function(aId) { michael@0: debug("pause " + aId); michael@0: let deferred = Promise.defer(); michael@0: let pId = this.promiseId(); michael@0: this.downloadPromises[pId] = deferred; michael@0: cpmm.sendAsyncMessage("Downloads:Pause", michael@0: { id: aId, promiseId: pId }); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: resume: function(aId) { michael@0: debug("resume " + aId); michael@0: let deferred = Promise.defer(); michael@0: let pId = this.promiseId(); michael@0: this.downloadPromises[pId] = deferred; michael@0: cpmm.sendAsyncMessage("Downloads:Resume", michael@0: { id: aId, promiseId: pId }); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic == "xpcom-shutdown") { michael@0: ipcMessages.forEach((aMessage) => { michael@0: cpmm.removeMessageListener(aMessage, this); michael@0: }); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: DownloadsIPC.init();