dom/downloads/src/DownloadsAPI.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:d6d301ff0870
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 "use strict";
6
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
10 const Cr = Components.results;
11
12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
13 Cu.import("resource://gre/modules/Services.jsm");
14 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
15 Cu.import("resource://gre/modules/DownloadsIPC.jsm");
16
17 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
18 "@mozilla.org/childprocessmessagemanager;1",
19 "nsIMessageSender");
20
21 function debug(aStr) {
22 #ifdef MOZ_DEBUG
23 dump("-*- DownloadsAPI.js : " + aStr + "\n");
24 #endif
25 }
26
27 function DOMDownloadManagerImpl() {
28 debug("DOMDownloadManagerImpl constructor");
29 }
30
31 DOMDownloadManagerImpl.prototype = {
32 __proto__: DOMRequestIpcHelper.prototype,
33
34 // nsIDOMGlobalPropertyInitializer implementation
35 init: function(aWindow) {
36 debug("DownloadsManager init");
37 this.initDOMRequestHelper(aWindow,
38 ["Downloads:Added",
39 "Downloads:Removed"]);
40 },
41
42 uninit: function() {
43 debug("uninit");
44 downloadsCache.evict(this._window);
45 },
46
47 set ondownloadstart(aHandler) {
48 this.__DOM_IMPL__.setEventHandler("ondownloadstart", aHandler);
49 },
50
51 get ondownloadstart() {
52 return this.__DOM_IMPL__.getEventHandler("ondownloadstart");
53 },
54
55 getDownloads: function() {
56 debug("getDownloads()");
57
58 return this.createPromise(function (aResolve, aReject) {
59 DownloadsIPC.getDownloads().then(
60 function(aDownloads) {
61 // Turn the list of download objects into DOM objects and
62 // send them.
63 let array = new this._window.Array();
64 for (let id in aDownloads) {
65 let dom = createDOMDownloadObject(this._window, aDownloads[id]);
66 array.push(this._prepareForContent(dom));
67 }
68 aResolve(array);
69 }.bind(this),
70 function() {
71 aReject("GetDownloadsError");
72 }
73 );
74 }.bind(this));
75 },
76
77 clearAllDone: function() {
78 debug("clearAllDone()");
79 return this.createPromise(function (aResolve, aReject) {
80 DownloadsIPC.clearAllDone().then(
81 function(aDownloads) {
82 // Turn the list of download objects into DOM objects and
83 // send them.
84 let array = new this._window.Array();
85 for (let id in aDownloads) {
86 let dom = createDOMDownloadObject(this._window, aDownloads[id]);
87 array.push(this._prepareForContent(dom));
88 }
89 aResolve(array);
90 }.bind(this),
91 function() {
92 aReject("ClearAllDoneError");
93 }
94 );
95 }.bind(this));
96 },
97
98 remove: function(aDownload) {
99 debug("remove " + aDownload.url + " " + aDownload.id);
100 return this.createPromise(function (aResolve, aReject) {
101 if (!downloadsCache.has(this._window, aDownload.id)) {
102 debug("no download " + aDownload.id);
103 aReject("InvalidDownload");
104 return;
105 }
106
107 DownloadsIPC.remove(aDownload.id).then(
108 function(aResult) {
109 let dom = createDOMDownloadObject(this._window, aResult);
110 // Change the state right away to not race against the update message.
111 dom.wrappedJSObject.state = "finalized";
112 aResolve(this._prepareForContent(dom));
113 }.bind(this),
114 function() {
115 aReject("RemoveError");
116 }
117 );
118 }.bind(this));
119 },
120
121 /**
122 * Turns a chrome download object into a content accessible one.
123 * When we have __DOM_IMPL__ available we just use that, otherwise
124 * we run _create() with the wrapped js object.
125 */
126 _prepareForContent: function(aChromeObject) {
127 if (aChromeObject.__DOM_IMPL__) {
128 return aChromeObject.__DOM_IMPL__;
129 }
130 let res = this._window.DOMDownload._create(this._window,
131 aChromeObject.wrappedJSObject);
132 return res;
133 },
134
135 receiveMessage: function(aMessage) {
136 let data = aMessage.data;
137 switch(aMessage.name) {
138 case "Downloads:Added":
139 debug("Adding " + uneval(data));
140 let event = new this._window.DownloadEvent("downloadstart", {
141 download:
142 this._prepareForContent(createDOMDownloadObject(this._window, data))
143 });
144 this.__DOM_IMPL__.dispatchEvent(event);
145 break;
146 }
147 },
148
149 classID: Components.ID("{c6587afa-0696-469f-9eff-9dac0dd727fe}"),
150 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
151 Ci.nsISupportsWeakReference,
152 Ci.nsIObserver,
153 Ci.nsIDOMGlobalPropertyInitializer]),
154
155 };
156
157 /**
158 * Keep track of download objects per window.
159 */
160 let downloadsCache = {
161 init: function() {
162 this.cache = new WeakMap();
163 },
164
165 has: function(aWindow, aId) {
166 let downloads = this.cache.get(aWindow);
167 return !!(downloads && downloads[aId]);
168 },
169
170 get: function(aWindow, aDownload) {
171 let downloads = this.cache.get(aWindow);
172 if (!(downloads && downloads[aDownload.id])) {
173 debug("Adding download " + aDownload.id + " to cache.");
174 if (!downloads) {
175 this.cache.set(aWindow, {});
176 downloads = this.cache.get(aWindow);
177 }
178 // Create the object and add it to the cache.
179 let impl = Cc["@mozilla.org/downloads/download;1"]
180 .createInstance(Ci.nsISupports);
181 impl.wrappedJSObject._init(aWindow, aDownload);
182 downloads[aDownload.id] = impl;
183 }
184 return downloads[aDownload.id];
185 },
186
187 evict: function(aWindow) {
188 this.cache.delete(aWindow);
189 }
190 };
191
192 downloadsCache.init();
193
194 /**
195 * The DOM facade of a download object.
196 */
197
198 function createDOMDownloadObject(aWindow, aDownload) {
199 return downloadsCache.get(aWindow, aDownload);
200 }
201
202 function DOMDownloadImpl() {
203 debug("DOMDownloadImpl constructor ");
204
205 this.wrappedJSObject = this;
206 this.totalBytes = 0;
207 this.currentBytes = 0;
208 this.url = null;
209 this.path = null;
210 this.contentType = null;
211
212 /* fields that require getters/setters */
213 this._error = null;
214 this._startTime = new Date();
215 this._state = "stopped";
216
217 /* private fields */
218 this.id = null;
219 }
220
221 DOMDownloadImpl.prototype = {
222
223 createPromise: function(aPromiseInit) {
224 return new this._window.Promise(aPromiseInit);
225 },
226
227 pause: function() {
228 debug("DOMDownloadImpl pause");
229 let id = this.id;
230 // We need to wrap the Promise.jsm promise in a "real" DOM promise...
231 return this.createPromise(function(aResolve, aReject) {
232 DownloadsIPC.pause(id).then(aResolve, aReject);
233 });
234 },
235
236 resume: function() {
237 debug("DOMDownloadImpl resume");
238 let id = this.id;
239 // We need to wrap the Promise.jsm promise in a "real" DOM promise...
240 return this.createPromise(function(aResolve, aReject) {
241 DownloadsIPC.resume(id).then(aResolve, aReject);
242 });
243 },
244
245 set onstatechange(aHandler) {
246 this.__DOM_IMPL__.setEventHandler("onstatechange", aHandler);
247 },
248
249 get onstatechange() {
250 return this.__DOM_IMPL__.getEventHandler("onstatechange");
251 },
252
253 get error() {
254 return this._error;
255 },
256
257 set error(aError) {
258 this._error = aError;
259 },
260
261 get startTime() {
262 return this._startTime;
263 },
264
265 set startTime(aStartTime) {
266 if (aStartTime instanceof Date) {
267 this._startTime = aStartTime;
268 }
269 else {
270 this._startTime = new Date(aStartTime);
271 }
272 },
273
274 get state() {
275 return this._state;
276 },
277
278 // We require a setter here to simplify the internals of the Download Manager
279 // since we actually pass dummy JSON objects to the child process and update
280 // them. This is the case for all other setters for read-only attributes
281 // implemented in this object.
282 set state(aState) {
283 // We need to ensure that XPCOM consumers of this API respect the enum
284 // values as well.
285 if (["downloading",
286 "stopped",
287 "succeeded",
288 "finalized"].indexOf(aState) != -1) {
289 this._state = aState;
290 }
291 },
292
293 _init: function(aWindow, aDownload) {
294 this._window = aWindow;
295 this.id = aDownload.id;
296 this._update(aDownload);
297 Services.obs.addObserver(this, "downloads-state-change-" + this.id,
298 /* ownsWeak */ true);
299 debug("observer set for " + this.id);
300 },
301
302 /**
303 * Updates the state of the object and fires the statechange event.
304 */
305 _update: function(aDownload) {
306 debug("update " + uneval(aDownload));
307 if (this.id != aDownload.id) {
308 return;
309 }
310
311 let props = ["totalBytes", "currentBytes", "url", "path", "state",
312 "contentType", "startTime"];
313 let changed = false;
314
315 props.forEach((prop) => {
316 if (aDownload[prop] && (aDownload[prop] != this[prop])) {
317 this[prop] = aDownload[prop];
318 changed = true;
319 }
320 });
321
322 if (aDownload.error) {
323 //
324 // When we get a generic error failure back from the js downloads api
325 // we will verify the status of device storage to see if we can't provide
326 // a better error result value.
327 //
328 // XXX If these checks expand further, consider moving them into their
329 // own function.
330 //
331 let result = aDownload.error.result;
332 let storage = this._window.navigator.getDeviceStorage("sdcard");
333
334 // If we don't have access to device storage we'll opt out of these
335 // extra checks as they are all dependent on the state of the storage.
336 if (result == Cr.NS_ERROR_FAILURE && storage) {
337 // We will delay sending the notification until we've inferred which
338 // error is really happening.
339 changed = false;
340 debug("Attempting to infer error via device storage sanity checks.");
341 // Get device storage and request availability status.
342 let available = storage.available();
343 available.onsuccess = (function() {
344 debug("Storage Status = '" + available.result + "'");
345 let inferredError = result;
346 switch (available.result) {
347 case "unavailable":
348 inferredError = Cr.NS_ERROR_FILE_NOT_FOUND;
349 break;
350 case "shared":
351 inferredError = Cr.NS_ERROR_FILE_ACCESS_DENIED;
352 break;
353 }
354 this._updateWithError(aDownload, inferredError);
355 }).bind(this);
356 available.onerror = (function() {
357 this._updateWithError(aDownload, result);
358 }).bind(this);
359 }
360
361 this.error =
362 new this._window.DOMError("DownloadError", result);
363 } else {
364 this.error = null;
365 }
366
367 // The visible state has not changed, so no need to fire an event.
368 if (!changed) {
369 return;
370 }
371
372 this._sendStateChange();
373 },
374
375 _updateWithError: function(aDownload, aError) {
376 this.error =
377 new this._window.DOMError("DownloadError", aError);
378 this._sendStateChange();
379 },
380
381 _sendStateChange: function() {
382 // __DOM_IMPL__ may not be available at first update.
383 if (this.__DOM_IMPL__) {
384 let event = new this._window.DownloadEvent("statechange", {
385 download: this.__DOM_IMPL__
386 });
387 debug("Dispatching statechange event. state=" + this.state);
388 this.__DOM_IMPL__.dispatchEvent(event);
389 }
390 },
391
392 observe: function(aSubject, aTopic, aData) {
393 debug("DOMDownloadImpl observe " + aTopic);
394 if (aTopic !== "downloads-state-change-" + this.id) {
395 return;
396 }
397
398 try {
399 let download = JSON.parse(aData);
400 // We get the start time as milliseconds, not as a Date object.
401 if (download.startTime) {
402 download.startTime = new Date(download.startTime);
403 }
404 this._update(download);
405 } catch(e) {}
406 },
407
408 classID: Components.ID("{96b81b99-aa96-439d-8c59-92eeed34705f}"),
409 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
410 Ci.nsIObserver,
411 Ci.nsISupportsWeakReference])
412 };
413
414 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DOMDownloadManagerImpl,
415 DOMDownloadImpl]);

mercurial