dom/downloads/src/DownloadsAPI.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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/. */
     5 "use strict";
     7 const Cc = Components.classes;
     8 const Ci = Components.interfaces;
     9 const Cu = Components.utils;
    10 const Cr = Components.results;
    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");
    17 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
    18                                    "@mozilla.org/childprocessmessagemanager;1",
    19                                    "nsIMessageSender");
    21 function debug(aStr) {
    22 #ifdef MOZ_DEBUG
    23   dump("-*- DownloadsAPI.js : " + aStr + "\n");
    24 #endif
    25 }
    27 function DOMDownloadManagerImpl() {
    28   debug("DOMDownloadManagerImpl constructor");
    29 }
    31 DOMDownloadManagerImpl.prototype = {
    32   __proto__: DOMRequestIpcHelper.prototype,
    34   // nsIDOMGlobalPropertyInitializer implementation
    35   init: function(aWindow) {
    36     debug("DownloadsManager init");
    37     this.initDOMRequestHelper(aWindow,
    38                               ["Downloads:Added",
    39                                "Downloads:Removed"]);
    40   },
    42   uninit: function() {
    43     debug("uninit");
    44     downloadsCache.evict(this._window);
    45   },
    47   set ondownloadstart(aHandler) {
    48     this.__DOM_IMPL__.setEventHandler("ondownloadstart", aHandler);
    49   },
    51   get ondownloadstart() {
    52     return this.__DOM_IMPL__.getEventHandler("ondownloadstart");
    53   },
    55   getDownloads: function() {
    56     debug("getDownloads()");
    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   },
    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   },
    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       }
   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   },
   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   },
   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   },
   149   classID: Components.ID("{c6587afa-0696-469f-9eff-9dac0dd727fe}"),
   150   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
   151                                          Ci.nsISupportsWeakReference,
   152                                          Ci.nsIObserver,
   153                                          Ci.nsIDOMGlobalPropertyInitializer]),
   155 };
   157 /**
   158   * Keep track of download objects per window.
   159   */
   160 let downloadsCache = {
   161   init: function() {
   162     this.cache = new WeakMap();
   163   },
   165   has: function(aWindow, aId) {
   166     let downloads = this.cache.get(aWindow);
   167     return !!(downloads && downloads[aId]);
   168   },
   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   },
   187   evict: function(aWindow) {
   188     this.cache.delete(aWindow);
   189   }
   190 };
   192 downloadsCache.init();
   194 /**
   195   * The DOM facade of a download object.
   196   */
   198 function createDOMDownloadObject(aWindow, aDownload) {
   199   return downloadsCache.get(aWindow, aDownload);
   200 }
   202 function DOMDownloadImpl() {
   203   debug("DOMDownloadImpl constructor ");
   205   this.wrappedJSObject = this;
   206   this.totalBytes = 0;
   207   this.currentBytes = 0;
   208   this.url = null;
   209   this.path = null;
   210   this.contentType = null;
   212   /* fields that require getters/setters */
   213   this._error = null;
   214   this._startTime = new Date();
   215   this._state = "stopped";
   217   /* private fields */
   218   this.id = null;
   219 }
   221 DOMDownloadImpl.prototype = {
   223   createPromise: function(aPromiseInit) {
   224     return new this._window.Promise(aPromiseInit);
   225   },
   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   },
   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   },
   245   set onstatechange(aHandler) {
   246     this.__DOM_IMPL__.setEventHandler("onstatechange", aHandler);
   247   },
   249   get onstatechange() {
   250     return this.__DOM_IMPL__.getEventHandler("onstatechange");
   251   },
   253   get error() {
   254     return this._error;
   255   },
   257   set error(aError) {
   258     this._error = aError;
   259   },
   261   get startTime() {
   262     return this._startTime;
   263   },
   265   set startTime(aStartTime) {
   266     if (aStartTime instanceof Date) {
   267       this._startTime = aStartTime;
   268     }
   269     else {
   270       this._startTime = new Date(aStartTime);
   271     }
   272   },
   274   get state() {
   275     return this._state;
   276   },
   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   },
   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   },
   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     }
   311     let props = ["totalBytes", "currentBytes", "url", "path", "state",
   312                  "contentType", "startTime"];
   313     let changed = false;
   315     props.forEach((prop) => {
   316       if (aDownload[prop] && (aDownload[prop] != this[prop])) {
   317         this[prop] = aDownload[prop];
   318         changed = true;
   319       }
   320     });
   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");
   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       }
   361       this.error =
   362         new this._window.DOMError("DownloadError", result);
   363     } else {
   364       this.error = null;
   365     }
   367     // The visible state has not changed, so no need to fire an event.
   368     if (!changed) {
   369       return;
   370     }
   372     this._sendStateChange();
   373   },
   375   _updateWithError: function(aDownload, aError) {
   376     this.error =
   377       new this._window.DOMError("DownloadError", aError);
   378     this._sendStateChange();
   379   },
   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   },
   392   observe: function(aSubject, aTopic, aData) {
   393     debug("DOMDownloadImpl observe " + aTopic);
   394     if (aTopic !== "downloads-state-change-" + this.id) {
   395       return;
   396     }
   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   },
   408   classID: Components.ID("{96b81b99-aa96-439d-8c59-92eeed34705f}"),
   409   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
   410                                          Ci.nsIObserver,
   411                                          Ci.nsISupportsWeakReference])
   412 };
   414 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DOMDownloadManagerImpl,
   415                                                      DOMDownloadImpl]);

mercurial