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: const Cr = Components.results; 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/DOMRequestHelper.jsm"); michael@0: Cu.import("resource://gre/modules/DownloadsIPC.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: michael@0: function debug(aStr) { michael@0: #ifdef MOZ_DEBUG michael@0: dump("-*- DownloadsAPI.js : " + aStr + "\n"); michael@0: #endif michael@0: } michael@0: michael@0: function DOMDownloadManagerImpl() { michael@0: debug("DOMDownloadManagerImpl constructor"); michael@0: } michael@0: michael@0: DOMDownloadManagerImpl.prototype = { michael@0: __proto__: DOMRequestIpcHelper.prototype, michael@0: michael@0: // nsIDOMGlobalPropertyInitializer implementation michael@0: init: function(aWindow) { michael@0: debug("DownloadsManager init"); michael@0: this.initDOMRequestHelper(aWindow, michael@0: ["Downloads:Added", michael@0: "Downloads:Removed"]); michael@0: }, michael@0: michael@0: uninit: function() { michael@0: debug("uninit"); michael@0: downloadsCache.evict(this._window); michael@0: }, michael@0: michael@0: set ondownloadstart(aHandler) { michael@0: this.__DOM_IMPL__.setEventHandler("ondownloadstart", aHandler); michael@0: }, michael@0: michael@0: get ondownloadstart() { michael@0: return this.__DOM_IMPL__.getEventHandler("ondownloadstart"); michael@0: }, michael@0: michael@0: getDownloads: function() { michael@0: debug("getDownloads()"); michael@0: michael@0: return this.createPromise(function (aResolve, aReject) { michael@0: DownloadsIPC.getDownloads().then( michael@0: function(aDownloads) { michael@0: // Turn the list of download objects into DOM objects and michael@0: // send them. michael@0: let array = new this._window.Array(); michael@0: for (let id in aDownloads) { michael@0: let dom = createDOMDownloadObject(this._window, aDownloads[id]); michael@0: array.push(this._prepareForContent(dom)); michael@0: } michael@0: aResolve(array); michael@0: }.bind(this), michael@0: function() { michael@0: aReject("GetDownloadsError"); michael@0: } michael@0: ); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: clearAllDone: function() { michael@0: debug("clearAllDone()"); michael@0: return this.createPromise(function (aResolve, aReject) { michael@0: DownloadsIPC.clearAllDone().then( michael@0: function(aDownloads) { michael@0: // Turn the list of download objects into DOM objects and michael@0: // send them. michael@0: let array = new this._window.Array(); michael@0: for (let id in aDownloads) { michael@0: let dom = createDOMDownloadObject(this._window, aDownloads[id]); michael@0: array.push(this._prepareForContent(dom)); michael@0: } michael@0: aResolve(array); michael@0: }.bind(this), michael@0: function() { michael@0: aReject("ClearAllDoneError"); michael@0: } michael@0: ); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: remove: function(aDownload) { michael@0: debug("remove " + aDownload.url + " " + aDownload.id); michael@0: return this.createPromise(function (aResolve, aReject) { michael@0: if (!downloadsCache.has(this._window, aDownload.id)) { michael@0: debug("no download " + aDownload.id); michael@0: aReject("InvalidDownload"); michael@0: return; michael@0: } michael@0: michael@0: DownloadsIPC.remove(aDownload.id).then( michael@0: function(aResult) { michael@0: let dom = createDOMDownloadObject(this._window, aResult); michael@0: // Change the state right away to not race against the update message. michael@0: dom.wrappedJSObject.state = "finalized"; michael@0: aResolve(this._prepareForContent(dom)); michael@0: }.bind(this), michael@0: function() { michael@0: aReject("RemoveError"); michael@0: } michael@0: ); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Turns a chrome download object into a content accessible one. michael@0: * When we have __DOM_IMPL__ available we just use that, otherwise michael@0: * we run _create() with the wrapped js object. michael@0: */ michael@0: _prepareForContent: function(aChromeObject) { michael@0: if (aChromeObject.__DOM_IMPL__) { michael@0: return aChromeObject.__DOM_IMPL__; michael@0: } michael@0: let res = this._window.DOMDownload._create(this._window, michael@0: aChromeObject.wrappedJSObject); michael@0: return res; michael@0: }, michael@0: michael@0: receiveMessage: function(aMessage) { michael@0: let data = aMessage.data; michael@0: switch(aMessage.name) { michael@0: case "Downloads:Added": michael@0: debug("Adding " + uneval(data)); michael@0: let event = new this._window.DownloadEvent("downloadstart", { michael@0: download: michael@0: this._prepareForContent(createDOMDownloadObject(this._window, data)) michael@0: }); michael@0: this.__DOM_IMPL__.dispatchEvent(event); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: classID: Components.ID("{c6587afa-0696-469f-9eff-9dac0dd727fe}"), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, michael@0: Ci.nsISupportsWeakReference, michael@0: Ci.nsIObserver, michael@0: Ci.nsIDOMGlobalPropertyInitializer]), michael@0: michael@0: }; michael@0: michael@0: /** michael@0: * Keep track of download objects per window. michael@0: */ michael@0: let downloadsCache = { michael@0: init: function() { michael@0: this.cache = new WeakMap(); michael@0: }, michael@0: michael@0: has: function(aWindow, aId) { michael@0: let downloads = this.cache.get(aWindow); michael@0: return !!(downloads && downloads[aId]); michael@0: }, michael@0: michael@0: get: function(aWindow, aDownload) { michael@0: let downloads = this.cache.get(aWindow); michael@0: if (!(downloads && downloads[aDownload.id])) { michael@0: debug("Adding download " + aDownload.id + " to cache."); michael@0: if (!downloads) { michael@0: this.cache.set(aWindow, {}); michael@0: downloads = this.cache.get(aWindow); michael@0: } michael@0: // Create the object and add it to the cache. michael@0: let impl = Cc["@mozilla.org/downloads/download;1"] michael@0: .createInstance(Ci.nsISupports); michael@0: impl.wrappedJSObject._init(aWindow, aDownload); michael@0: downloads[aDownload.id] = impl; michael@0: } michael@0: return downloads[aDownload.id]; michael@0: }, michael@0: michael@0: evict: function(aWindow) { michael@0: this.cache.delete(aWindow); michael@0: } michael@0: }; michael@0: michael@0: downloadsCache.init(); michael@0: michael@0: /** michael@0: * The DOM facade of a download object. michael@0: */ michael@0: michael@0: function createDOMDownloadObject(aWindow, aDownload) { michael@0: return downloadsCache.get(aWindow, aDownload); michael@0: } michael@0: michael@0: function DOMDownloadImpl() { michael@0: debug("DOMDownloadImpl constructor "); michael@0: michael@0: this.wrappedJSObject = this; michael@0: this.totalBytes = 0; michael@0: this.currentBytes = 0; michael@0: this.url = null; michael@0: this.path = null; michael@0: this.contentType = null; michael@0: michael@0: /* fields that require getters/setters */ michael@0: this._error = null; michael@0: this._startTime = new Date(); michael@0: this._state = "stopped"; michael@0: michael@0: /* private fields */ michael@0: this.id = null; michael@0: } michael@0: michael@0: DOMDownloadImpl.prototype = { michael@0: michael@0: createPromise: function(aPromiseInit) { michael@0: return new this._window.Promise(aPromiseInit); michael@0: }, michael@0: michael@0: pause: function() { michael@0: debug("DOMDownloadImpl pause"); michael@0: let id = this.id; michael@0: // We need to wrap the Promise.jsm promise in a "real" DOM promise... michael@0: return this.createPromise(function(aResolve, aReject) { michael@0: DownloadsIPC.pause(id).then(aResolve, aReject); michael@0: }); michael@0: }, michael@0: michael@0: resume: function() { michael@0: debug("DOMDownloadImpl resume"); michael@0: let id = this.id; michael@0: // We need to wrap the Promise.jsm promise in a "real" DOM promise... michael@0: return this.createPromise(function(aResolve, aReject) { michael@0: DownloadsIPC.resume(id).then(aResolve, aReject); michael@0: }); michael@0: }, michael@0: michael@0: set onstatechange(aHandler) { michael@0: this.__DOM_IMPL__.setEventHandler("onstatechange", aHandler); michael@0: }, michael@0: michael@0: get onstatechange() { michael@0: return this.__DOM_IMPL__.getEventHandler("onstatechange"); michael@0: }, michael@0: michael@0: get error() { michael@0: return this._error; michael@0: }, michael@0: michael@0: set error(aError) { michael@0: this._error = aError; michael@0: }, michael@0: michael@0: get startTime() { michael@0: return this._startTime; michael@0: }, michael@0: michael@0: set startTime(aStartTime) { michael@0: if (aStartTime instanceof Date) { michael@0: this._startTime = aStartTime; michael@0: } michael@0: else { michael@0: this._startTime = new Date(aStartTime); michael@0: } michael@0: }, michael@0: michael@0: get state() { michael@0: return this._state; michael@0: }, michael@0: michael@0: // We require a setter here to simplify the internals of the Download Manager michael@0: // since we actually pass dummy JSON objects to the child process and update michael@0: // them. This is the case for all other setters for read-only attributes michael@0: // implemented in this object. michael@0: set state(aState) { michael@0: // We need to ensure that XPCOM consumers of this API respect the enum michael@0: // values as well. michael@0: if (["downloading", michael@0: "stopped", michael@0: "succeeded", michael@0: "finalized"].indexOf(aState) != -1) { michael@0: this._state = aState; michael@0: } michael@0: }, michael@0: michael@0: _init: function(aWindow, aDownload) { michael@0: this._window = aWindow; michael@0: this.id = aDownload.id; michael@0: this._update(aDownload); michael@0: Services.obs.addObserver(this, "downloads-state-change-" + this.id, michael@0: /* ownsWeak */ true); michael@0: debug("observer set for " + this.id); michael@0: }, michael@0: michael@0: /** michael@0: * Updates the state of the object and fires the statechange event. michael@0: */ michael@0: _update: function(aDownload) { michael@0: debug("update " + uneval(aDownload)); michael@0: if (this.id != aDownload.id) { michael@0: return; michael@0: } 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((prop) => { michael@0: if (aDownload[prop] && (aDownload[prop] != this[prop])) { michael@0: this[prop] = aDownload[prop]; michael@0: changed = true; michael@0: } michael@0: }); michael@0: michael@0: if (aDownload.error) { michael@0: // michael@0: // When we get a generic error failure back from the js downloads api michael@0: // we will verify the status of device storage to see if we can't provide michael@0: // a better error result value. michael@0: // michael@0: // XXX If these checks expand further, consider moving them into their michael@0: // own function. michael@0: // michael@0: let result = aDownload.error.result; michael@0: let storage = this._window.navigator.getDeviceStorage("sdcard"); michael@0: michael@0: // If we don't have access to device storage we'll opt out of these michael@0: // extra checks as they are all dependent on the state of the storage. michael@0: if (result == Cr.NS_ERROR_FAILURE && storage) { michael@0: // We will delay sending the notification until we've inferred which michael@0: // error is really happening. michael@0: changed = false; michael@0: debug("Attempting to infer error via device storage sanity checks."); michael@0: // Get device storage and request availability status. michael@0: let available = storage.available(); michael@0: available.onsuccess = (function() { michael@0: debug("Storage Status = '" + available.result + "'"); michael@0: let inferredError = result; michael@0: switch (available.result) { michael@0: case "unavailable": michael@0: inferredError = Cr.NS_ERROR_FILE_NOT_FOUND; michael@0: break; michael@0: case "shared": michael@0: inferredError = Cr.NS_ERROR_FILE_ACCESS_DENIED; michael@0: break; michael@0: } michael@0: this._updateWithError(aDownload, inferredError); michael@0: }).bind(this); michael@0: available.onerror = (function() { michael@0: this._updateWithError(aDownload, result); michael@0: }).bind(this); michael@0: } michael@0: michael@0: this.error = michael@0: new this._window.DOMError("DownloadError", result); michael@0: } else { michael@0: this.error = null; michael@0: } michael@0: michael@0: // The visible state has not changed, so no need to fire an event. michael@0: if (!changed) { michael@0: return; michael@0: } michael@0: michael@0: this._sendStateChange(); michael@0: }, michael@0: michael@0: _updateWithError: function(aDownload, aError) { michael@0: this.error = michael@0: new this._window.DOMError("DownloadError", aError); michael@0: this._sendStateChange(); michael@0: }, michael@0: michael@0: _sendStateChange: function() { michael@0: // __DOM_IMPL__ may not be available at first update. michael@0: if (this.__DOM_IMPL__) { michael@0: let event = new this._window.DownloadEvent("statechange", { michael@0: download: this.__DOM_IMPL__ michael@0: }); michael@0: debug("Dispatching statechange event. state=" + this.state); michael@0: this.__DOM_IMPL__.dispatchEvent(event); michael@0: } michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: debug("DOMDownloadImpl observe " + aTopic); michael@0: if (aTopic !== "downloads-state-change-" + this.id) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: let download = JSON.parse(aData); michael@0: // We get the start time as milliseconds, not as a Date object. michael@0: if (download.startTime) { michael@0: download.startTime = new Date(download.startTime); michael@0: } michael@0: this._update(download); michael@0: } catch(e) {} michael@0: }, michael@0: michael@0: classID: Components.ID("{96b81b99-aa96-439d-8c59-92eeed34705f}"), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, michael@0: Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference]) michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DOMDownloadManagerImpl, michael@0: DOMDownloadImpl]);