1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/downloads/src/DownloadsAPI.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,415 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 +const Cr = Components.results; 1.14 + 1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.16 +Cu.import("resource://gre/modules/Services.jsm"); 1.17 +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); 1.18 +Cu.import("resource://gre/modules/DownloadsIPC.jsm"); 1.19 + 1.20 +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", 1.21 + "@mozilla.org/childprocessmessagemanager;1", 1.22 + "nsIMessageSender"); 1.23 + 1.24 +function debug(aStr) { 1.25 +#ifdef MOZ_DEBUG 1.26 + dump("-*- DownloadsAPI.js : " + aStr + "\n"); 1.27 +#endif 1.28 +} 1.29 + 1.30 +function DOMDownloadManagerImpl() { 1.31 + debug("DOMDownloadManagerImpl constructor"); 1.32 +} 1.33 + 1.34 +DOMDownloadManagerImpl.prototype = { 1.35 + __proto__: DOMRequestIpcHelper.prototype, 1.36 + 1.37 + // nsIDOMGlobalPropertyInitializer implementation 1.38 + init: function(aWindow) { 1.39 + debug("DownloadsManager init"); 1.40 + this.initDOMRequestHelper(aWindow, 1.41 + ["Downloads:Added", 1.42 + "Downloads:Removed"]); 1.43 + }, 1.44 + 1.45 + uninit: function() { 1.46 + debug("uninit"); 1.47 + downloadsCache.evict(this._window); 1.48 + }, 1.49 + 1.50 + set ondownloadstart(aHandler) { 1.51 + this.__DOM_IMPL__.setEventHandler("ondownloadstart", aHandler); 1.52 + }, 1.53 + 1.54 + get ondownloadstart() { 1.55 + return this.__DOM_IMPL__.getEventHandler("ondownloadstart"); 1.56 + }, 1.57 + 1.58 + getDownloads: function() { 1.59 + debug("getDownloads()"); 1.60 + 1.61 + return this.createPromise(function (aResolve, aReject) { 1.62 + DownloadsIPC.getDownloads().then( 1.63 + function(aDownloads) { 1.64 + // Turn the list of download objects into DOM objects and 1.65 + // send them. 1.66 + let array = new this._window.Array(); 1.67 + for (let id in aDownloads) { 1.68 + let dom = createDOMDownloadObject(this._window, aDownloads[id]); 1.69 + array.push(this._prepareForContent(dom)); 1.70 + } 1.71 + aResolve(array); 1.72 + }.bind(this), 1.73 + function() { 1.74 + aReject("GetDownloadsError"); 1.75 + } 1.76 + ); 1.77 + }.bind(this)); 1.78 + }, 1.79 + 1.80 + clearAllDone: function() { 1.81 + debug("clearAllDone()"); 1.82 + return this.createPromise(function (aResolve, aReject) { 1.83 + DownloadsIPC.clearAllDone().then( 1.84 + function(aDownloads) { 1.85 + // Turn the list of download objects into DOM objects and 1.86 + // send them. 1.87 + let array = new this._window.Array(); 1.88 + for (let id in aDownloads) { 1.89 + let dom = createDOMDownloadObject(this._window, aDownloads[id]); 1.90 + array.push(this._prepareForContent(dom)); 1.91 + } 1.92 + aResolve(array); 1.93 + }.bind(this), 1.94 + function() { 1.95 + aReject("ClearAllDoneError"); 1.96 + } 1.97 + ); 1.98 + }.bind(this)); 1.99 + }, 1.100 + 1.101 + remove: function(aDownload) { 1.102 + debug("remove " + aDownload.url + " " + aDownload.id); 1.103 + return this.createPromise(function (aResolve, aReject) { 1.104 + if (!downloadsCache.has(this._window, aDownload.id)) { 1.105 + debug("no download " + aDownload.id); 1.106 + aReject("InvalidDownload"); 1.107 + return; 1.108 + } 1.109 + 1.110 + DownloadsIPC.remove(aDownload.id).then( 1.111 + function(aResult) { 1.112 + let dom = createDOMDownloadObject(this._window, aResult); 1.113 + // Change the state right away to not race against the update message. 1.114 + dom.wrappedJSObject.state = "finalized"; 1.115 + aResolve(this._prepareForContent(dom)); 1.116 + }.bind(this), 1.117 + function() { 1.118 + aReject("RemoveError"); 1.119 + } 1.120 + ); 1.121 + }.bind(this)); 1.122 + }, 1.123 + 1.124 + /** 1.125 + * Turns a chrome download object into a content accessible one. 1.126 + * When we have __DOM_IMPL__ available we just use that, otherwise 1.127 + * we run _create() with the wrapped js object. 1.128 + */ 1.129 + _prepareForContent: function(aChromeObject) { 1.130 + if (aChromeObject.__DOM_IMPL__) { 1.131 + return aChromeObject.__DOM_IMPL__; 1.132 + } 1.133 + let res = this._window.DOMDownload._create(this._window, 1.134 + aChromeObject.wrappedJSObject); 1.135 + return res; 1.136 + }, 1.137 + 1.138 + receiveMessage: function(aMessage) { 1.139 + let data = aMessage.data; 1.140 + switch(aMessage.name) { 1.141 + case "Downloads:Added": 1.142 + debug("Adding " + uneval(data)); 1.143 + let event = new this._window.DownloadEvent("downloadstart", { 1.144 + download: 1.145 + this._prepareForContent(createDOMDownloadObject(this._window, data)) 1.146 + }); 1.147 + this.__DOM_IMPL__.dispatchEvent(event); 1.148 + break; 1.149 + } 1.150 + }, 1.151 + 1.152 + classID: Components.ID("{c6587afa-0696-469f-9eff-9dac0dd727fe}"), 1.153 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, 1.154 + Ci.nsISupportsWeakReference, 1.155 + Ci.nsIObserver, 1.156 + Ci.nsIDOMGlobalPropertyInitializer]), 1.157 + 1.158 +}; 1.159 + 1.160 +/** 1.161 + * Keep track of download objects per window. 1.162 + */ 1.163 +let downloadsCache = { 1.164 + init: function() { 1.165 + this.cache = new WeakMap(); 1.166 + }, 1.167 + 1.168 + has: function(aWindow, aId) { 1.169 + let downloads = this.cache.get(aWindow); 1.170 + return !!(downloads && downloads[aId]); 1.171 + }, 1.172 + 1.173 + get: function(aWindow, aDownload) { 1.174 + let downloads = this.cache.get(aWindow); 1.175 + if (!(downloads && downloads[aDownload.id])) { 1.176 + debug("Adding download " + aDownload.id + " to cache."); 1.177 + if (!downloads) { 1.178 + this.cache.set(aWindow, {}); 1.179 + downloads = this.cache.get(aWindow); 1.180 + } 1.181 + // Create the object and add it to the cache. 1.182 + let impl = Cc["@mozilla.org/downloads/download;1"] 1.183 + .createInstance(Ci.nsISupports); 1.184 + impl.wrappedJSObject._init(aWindow, aDownload); 1.185 + downloads[aDownload.id] = impl; 1.186 + } 1.187 + return downloads[aDownload.id]; 1.188 + }, 1.189 + 1.190 + evict: function(aWindow) { 1.191 + this.cache.delete(aWindow); 1.192 + } 1.193 +}; 1.194 + 1.195 +downloadsCache.init(); 1.196 + 1.197 +/** 1.198 + * The DOM facade of a download object. 1.199 + */ 1.200 + 1.201 +function createDOMDownloadObject(aWindow, aDownload) { 1.202 + return downloadsCache.get(aWindow, aDownload); 1.203 +} 1.204 + 1.205 +function DOMDownloadImpl() { 1.206 + debug("DOMDownloadImpl constructor "); 1.207 + 1.208 + this.wrappedJSObject = this; 1.209 + this.totalBytes = 0; 1.210 + this.currentBytes = 0; 1.211 + this.url = null; 1.212 + this.path = null; 1.213 + this.contentType = null; 1.214 + 1.215 + /* fields that require getters/setters */ 1.216 + this._error = null; 1.217 + this._startTime = new Date(); 1.218 + this._state = "stopped"; 1.219 + 1.220 + /* private fields */ 1.221 + this.id = null; 1.222 +} 1.223 + 1.224 +DOMDownloadImpl.prototype = { 1.225 + 1.226 + createPromise: function(aPromiseInit) { 1.227 + return new this._window.Promise(aPromiseInit); 1.228 + }, 1.229 + 1.230 + pause: function() { 1.231 + debug("DOMDownloadImpl pause"); 1.232 + let id = this.id; 1.233 + // We need to wrap the Promise.jsm promise in a "real" DOM promise... 1.234 + return this.createPromise(function(aResolve, aReject) { 1.235 + DownloadsIPC.pause(id).then(aResolve, aReject); 1.236 + }); 1.237 + }, 1.238 + 1.239 + resume: function() { 1.240 + debug("DOMDownloadImpl resume"); 1.241 + let id = this.id; 1.242 + // We need to wrap the Promise.jsm promise in a "real" DOM promise... 1.243 + return this.createPromise(function(aResolve, aReject) { 1.244 + DownloadsIPC.resume(id).then(aResolve, aReject); 1.245 + }); 1.246 + }, 1.247 + 1.248 + set onstatechange(aHandler) { 1.249 + this.__DOM_IMPL__.setEventHandler("onstatechange", aHandler); 1.250 + }, 1.251 + 1.252 + get onstatechange() { 1.253 + return this.__DOM_IMPL__.getEventHandler("onstatechange"); 1.254 + }, 1.255 + 1.256 + get error() { 1.257 + return this._error; 1.258 + }, 1.259 + 1.260 + set error(aError) { 1.261 + this._error = aError; 1.262 + }, 1.263 + 1.264 + get startTime() { 1.265 + return this._startTime; 1.266 + }, 1.267 + 1.268 + set startTime(aStartTime) { 1.269 + if (aStartTime instanceof Date) { 1.270 + this._startTime = aStartTime; 1.271 + } 1.272 + else { 1.273 + this._startTime = new Date(aStartTime); 1.274 + } 1.275 + }, 1.276 + 1.277 + get state() { 1.278 + return this._state; 1.279 + }, 1.280 + 1.281 + // We require a setter here to simplify the internals of the Download Manager 1.282 + // since we actually pass dummy JSON objects to the child process and update 1.283 + // them. This is the case for all other setters for read-only attributes 1.284 + // implemented in this object. 1.285 + set state(aState) { 1.286 + // We need to ensure that XPCOM consumers of this API respect the enum 1.287 + // values as well. 1.288 + if (["downloading", 1.289 + "stopped", 1.290 + "succeeded", 1.291 + "finalized"].indexOf(aState) != -1) { 1.292 + this._state = aState; 1.293 + } 1.294 + }, 1.295 + 1.296 + _init: function(aWindow, aDownload) { 1.297 + this._window = aWindow; 1.298 + this.id = aDownload.id; 1.299 + this._update(aDownload); 1.300 + Services.obs.addObserver(this, "downloads-state-change-" + this.id, 1.301 + /* ownsWeak */ true); 1.302 + debug("observer set for " + this.id); 1.303 + }, 1.304 + 1.305 + /** 1.306 + * Updates the state of the object and fires the statechange event. 1.307 + */ 1.308 + _update: function(aDownload) { 1.309 + debug("update " + uneval(aDownload)); 1.310 + if (this.id != aDownload.id) { 1.311 + return; 1.312 + } 1.313 + 1.314 + let props = ["totalBytes", "currentBytes", "url", "path", "state", 1.315 + "contentType", "startTime"]; 1.316 + let changed = false; 1.317 + 1.318 + props.forEach((prop) => { 1.319 + if (aDownload[prop] && (aDownload[prop] != this[prop])) { 1.320 + this[prop] = aDownload[prop]; 1.321 + changed = true; 1.322 + } 1.323 + }); 1.324 + 1.325 + if (aDownload.error) { 1.326 + // 1.327 + // When we get a generic error failure back from the js downloads api 1.328 + // we will verify the status of device storage to see if we can't provide 1.329 + // a better error result value. 1.330 + // 1.331 + // XXX If these checks expand further, consider moving them into their 1.332 + // own function. 1.333 + // 1.334 + let result = aDownload.error.result; 1.335 + let storage = this._window.navigator.getDeviceStorage("sdcard"); 1.336 + 1.337 + // If we don't have access to device storage we'll opt out of these 1.338 + // extra checks as they are all dependent on the state of the storage. 1.339 + if (result == Cr.NS_ERROR_FAILURE && storage) { 1.340 + // We will delay sending the notification until we've inferred which 1.341 + // error is really happening. 1.342 + changed = false; 1.343 + debug("Attempting to infer error via device storage sanity checks."); 1.344 + // Get device storage and request availability status. 1.345 + let available = storage.available(); 1.346 + available.onsuccess = (function() { 1.347 + debug("Storage Status = '" + available.result + "'"); 1.348 + let inferredError = result; 1.349 + switch (available.result) { 1.350 + case "unavailable": 1.351 + inferredError = Cr.NS_ERROR_FILE_NOT_FOUND; 1.352 + break; 1.353 + case "shared": 1.354 + inferredError = Cr.NS_ERROR_FILE_ACCESS_DENIED; 1.355 + break; 1.356 + } 1.357 + this._updateWithError(aDownload, inferredError); 1.358 + }).bind(this); 1.359 + available.onerror = (function() { 1.360 + this._updateWithError(aDownload, result); 1.361 + }).bind(this); 1.362 + } 1.363 + 1.364 + this.error = 1.365 + new this._window.DOMError("DownloadError", result); 1.366 + } else { 1.367 + this.error = null; 1.368 + } 1.369 + 1.370 + // The visible state has not changed, so no need to fire an event. 1.371 + if (!changed) { 1.372 + return; 1.373 + } 1.374 + 1.375 + this._sendStateChange(); 1.376 + }, 1.377 + 1.378 + _updateWithError: function(aDownload, aError) { 1.379 + this.error = 1.380 + new this._window.DOMError("DownloadError", aError); 1.381 + this._sendStateChange(); 1.382 + }, 1.383 + 1.384 + _sendStateChange: function() { 1.385 + // __DOM_IMPL__ may not be available at first update. 1.386 + if (this.__DOM_IMPL__) { 1.387 + let event = new this._window.DownloadEvent("statechange", { 1.388 + download: this.__DOM_IMPL__ 1.389 + }); 1.390 + debug("Dispatching statechange event. state=" + this.state); 1.391 + this.__DOM_IMPL__.dispatchEvent(event); 1.392 + } 1.393 + }, 1.394 + 1.395 + observe: function(aSubject, aTopic, aData) { 1.396 + debug("DOMDownloadImpl observe " + aTopic); 1.397 + if (aTopic !== "downloads-state-change-" + this.id) { 1.398 + return; 1.399 + } 1.400 + 1.401 + try { 1.402 + let download = JSON.parse(aData); 1.403 + // We get the start time as milliseconds, not as a Date object. 1.404 + if (download.startTime) { 1.405 + download.startTime = new Date(download.startTime); 1.406 + } 1.407 + this._update(download); 1.408 + } catch(e) {} 1.409 + }, 1.410 + 1.411 + classID: Components.ID("{96b81b99-aa96-439d-8c59-92eeed34705f}"), 1.412 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, 1.413 + Ci.nsIObserver, 1.414 + Ci.nsISupportsWeakReference]) 1.415 +}; 1.416 + 1.417 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DOMDownloadManagerImpl, 1.418 + DOMDownloadImpl]);