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
7const Cc = Components.classes;
8const Ci = Components.interfaces;
9const Cu = Components.utils;
10
11this.EXPORTED_SYMBOLS = [];
12
13Cu.import("resource://gre/modules/XPCOMUtils.jsm");
14Cu.import("resource://gre/modules/Downloads.jsm");
15Cu.import("resource://gre/modules/Task.jsm");
16Cu.import("resource://gre/modules/osfile.jsm");
17
18XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
19                                   "@mozilla.org/parentprocessmessagemanager;1",
20                                   "nsIMessageBroadcaster");
21
22/**
23  * Parent process logic that services download API requests from the
24  * DownloadAPI.js instances in content processeses.  The actual work of managing
25  * downloads is done by Toolkit's Downloads.jsm.  This module is loaded by B2G's
26  * shell.js
27  */
28
29function debug(aStr) {
30#ifdef MOZ_DEBUG
31  dump("-*- DownloadsAPI.jsm : " + aStr + "\n");
32#endif
33}
34
35function sendPromiseMessage(aMm, aMessageName, aData, aError) {
36  debug("sendPromiseMessage " + aMessageName);
37  let msg = {
38    id: aData.id,
39    promiseId: aData.promiseId
40  };
41
42  if (aError) {
43    msg.error = aError;
44  }
45
46  aMm.sendAsyncMessage(aMessageName, msg);
47}
48
49var DownloadsAPI = {
50  init: function() {
51    debug("init");
52
53    this._ids = new WeakMap(); // Maps toolkit download objects to ids.
54    this._index = {};          // Maps ids to downloads.
55
56    ["Downloads:GetList",
57     "Downloads:ClearAllDone",
58     "Downloads:Remove",
59     "Downloads:Pause",
60     "Downloads:Resume",
61     "Downloads:Adopt"].forEach((msgName) => {
62      ppmm.addMessageListener(msgName, this);
63    });
64
65    let self = this;
66    Task.spawn(function () {
67      let list = yield Downloads.getList(Downloads.ALL);
68      yield list.addView(self);
69
70      debug("view added to download list.");
71    }).then(null, Components.utils.reportError);
72
73    this._currentId = 0;
74  },
75
76  /**
77    * Returns a unique id for each download, hashing the url and the path.
78    */
79  downloadId: function(aDownload) {
80    let id = this._ids.get(aDownload, null);
81    if (!id) {
82      id = "download-" + this._currentId++;
83      this._ids.set(aDownload, id);
84      this._index[id] = aDownload;
85    }
86    return id;
87  },
88
89  getDownloadById: function(aId) {
90    return this._index[aId];
91  },
92
93  /**
94    * Converts a download object into a plain json object that we'll
95    * send to the DOM side.
96    */
97  jsonDownload: function(aDownload) {
98    let res = {
99      totalBytes: aDownload.totalBytes,
100      currentBytes: aDownload.currentBytes,
101      url: aDownload.source.url,
102      path: aDownload.target.path,
103      contentType: aDownload.contentType,
104      startTime: aDownload.startTime.getTime(),
105      sourceAppManifestURL: aDownload._unknownProperties &&
106                              aDownload._unknownProperties.sourceAppManifestURL
107    };
108
109    if (aDownload.error) {
110      res.error = aDownload.error;
111    }
112
113    res.id = this.downloadId(aDownload);
114
115    // The state of the download. Can be any of "downloading", "stopped",
116    // "succeeded", finalized".
117
118    // Default to "stopped"
119    res.state = "stopped";
120    if (!aDownload.stopped &&
121        !aDownload.canceled &&
122        !aDownload.succeeded &&
123        !aDownload.DownloadError) {
124      res.state = "downloading";
125    } else if (aDownload.succeeded) {
126      res.state = "succeeded";
127    }
128    return res;
129  },
130
131  /**
132    * download view methods.
133    */
134  onDownloadAdded: function(aDownload) {
135    let download = this.jsonDownload(aDownload);
136    debug("onDownloadAdded " + uneval(download));
137    ppmm.broadcastAsyncMessage("Downloads:Added", download);
138  },
139
140  onDownloadRemoved: function(aDownload) {
141    let download = this.jsonDownload(aDownload);
142    download.state = "finalized";
143    debug("onDownloadRemoved " + uneval(download));
144    ppmm.broadcastAsyncMessage("Downloads:Removed", download);
145    this._index[this._ids.get(aDownload)] = null;
146    this._ids.delete(aDownload);
147  },
148
149  onDownloadChanged: function(aDownload) {
150    let download = this.jsonDownload(aDownload);
151    debug("onDownloadChanged " + uneval(download));
152    ppmm.broadcastAsyncMessage("Downloads:Changed", download);
153  },
154
155  receiveMessage: function(aMessage) {
156    if (!aMessage.target.assertPermission("downloads")) {
157      debug("No 'downloads' permission!");
158      return;
159    }
160
161    debug("message: " + aMessage.name);
162
163    switch (aMessage.name) {
164    case "Downloads:GetList":
165      this.getList(aMessage.data, aMessage.target);
166      break;
167    case "Downloads:ClearAllDone":
168      this.clearAllDone(aMessage.data, aMessage.target);
169      break;
170    case "Downloads:Remove":
171      this.remove(aMessage.data, aMessage.target);
172      break;
173    case "Downloads:Pause":
174      this.pause(aMessage.data, aMessage.target);
175      break;
176    case "Downloads:Resume":
177      this.resume(aMessage.data, aMessage.target);
178      break;
179    case "Downloads:Adopt":
180      this.adoptDownload(aMessage.data, aMessage.target);
181      break;
182    default:
183      debug("Invalid message: " + aMessage.name);
184    }
185  },
186
187  getList: function(aData, aMm) {
188    debug("getList called!");
189    let self = this;
190    Task.spawn(function () {
191      let list = yield Downloads.getList(Downloads.ALL);
192      let downloads = yield list.getAll();
193      let res = [];
194      downloads.forEach((aDownload) => {
195        res.push(self.jsonDownload(aDownload));
196      });
197      aMm.sendAsyncMessage("Downloads:GetList:Return", res);
198    }).then(null, Components.utils.reportError);
199  },
200
201  clearAllDone: function(aData, aMm) {
202    debug("clearAllDone called!");
203    Task.spawn(function () {
204      let list = yield Downloads.getList(Downloads.ALL);
205      list.removeFinished();
206    }).then(null, Components.utils.reportError);
207  },
208
209  remove: function(aData, aMm) {
210    debug("remove id " + aData.id);
211    let download = this.getDownloadById(aData.id);
212    if (!download) {
213      sendPromiseMessage(aMm, "Downloads:Remove:Return",
214                         aData, "NoSuchDownload");
215      return;
216    }
217
218    Task.spawn(function() {
219      yield download.finalize(true);
220      let list = yield Downloads.getList(Downloads.ALL);
221      yield list.remove(download);
222    }).then(
223      function() {
224        sendPromiseMessage(aMm, "Downloads:Remove:Return", aData);
225      },
226      function() {
227        sendPromiseMessage(aMm, "Downloads:Remove:Return",
228                           aData, "RemoveError");
229      }
230    );
231  },
232
233  pause: function(aData, aMm) {
234    debug("pause id " + aData.id);
235    let download = this.getDownloadById(aData.id);
236    if (!download) {
237      sendPromiseMessage(aMm, "Downloads:Pause:Return",
238                         aData, "NoSuchDownload");
239      return;
240    }
241
242    download.cancel().then(
243      function() {
244        sendPromiseMessage(aMm, "Downloads:Pause:Return", aData);
245      },
246      function() {
247        sendPromiseMessage(aMm, "Downloads:Pause:Return",
248                           aData, "PauseError");
249      }
250    );
251  },
252
253  resume: function(aData, aMm) {
254    debug("resume id " + aData.id);
255    let download = this.getDownloadById(aData.id);
256    if (!download) {
257      sendPromiseMessage(aMm, "Downloads:Resume:Return",
258                         aData, "NoSuchDownload");
259      return;
260    }
261
262    download.start().then(
263      function() {
264        sendPromiseMessage(aMm, "Downloads:Resume:Return", aData);
265      },
266      function() {
267        sendPromiseMessage(aMm, "Downloads:Resume:Return",
268                           aData, "ResumeError");
269      }
270    );
271  },
272
273  /**
274    * Receive a download to adopt in the same representation we produce from
275    * our "jsonDownload" normalizer and add it to the list of downloads.
276    */
277  adoptDownload: function(aData, aMm) {
278    let adoptJsonRep = aData.jsonDownload;
279    debug("adoptDownload " + uneval(adoptJsonRep));
280
281    Task.spawn(function* () {
282      // Verify that the file exists on disk.  This will result in a rejection
283      // if the file does not exist.  We will also use this information for the
284      // file size to avoid weird inconsistencies.  We ignore the filesystem
285      // timestamp in favor of whatever the caller is telling us.
286      let fileInfo = yield OS.File.stat(adoptJsonRep.path);
287
288      // We also require that the file is not a directory.
289      if (fileInfo.isDir) {
290        throw new Error("AdoptFileIsDirectory");
291      }
292
293      // We need to create a Download instance to add to the list.  Create a
294      // serialized representation and then from there the instance.
295      let serializedRep = {
296        // explicit initializations in toSerializable
297        source: {
298          url: adoptJsonRep.url
299          // This is where isPrivate would go if adoption supported private
300          // browsing.
301        },
302        target: {
303          path: adoptJsonRep.path,
304        },
305        startTime: adoptJsonRep.startTime,
306        // kPlainSerializableDownloadProperties propagations
307        succeeded: true, // (all adopted downloads are required to be completed)
308        totalBytes: fileInfo.size,
309        contentType: adoptJsonRep.contentType,
310        // unknown properties added/used by the DownloadsAPI
311        currentBytes: fileInfo.size,
312        sourceAppManifestURL: adoptJsonRep.sourceAppManifestURL
313      };
314
315      let download = yield Downloads.createDownload(serializedRep);
316
317      // The ALL list is a DownloadCombinedList instance that combines the
318      // PUBLIC (persisted to disk) and PRIVATE (ephemeral) download lists..
319      // When we call add on it, it dispatches to the appropriate list based on
320      // the 'isPrivate' field of the source.  (Which we don't initialize and
321      // defaults to false.)
322      let allDownloadList = yield Downloads.getList(Downloads.ALL);
323
324      // This add will automatically notify all views of the added download,
325      // including DownloadsAPI instances and the DownloadAutoSaveView that's
326      // subscribed to the PUBLIC list and will save the download.
327      yield allDownloadList.add(download);
328
329      debug("download adopted");
330      // The notification above occurred synchronously, and so we will have
331      // already dispatched an added notification for our download to the child
332      // process in question.  As such, we only need to relay the download id
333      // since the download will already have been cached.
334      return download;
335    }.bind(this)).then(
336      (download) => {
337        sendPromiseMessage(aMm, "Downloads:Adopt:Return",
338                           {
339                             id: this.downloadId(download),
340                             promiseId: aData.promiseId
341                           });
342      },
343      (ex) => {
344        let reportAs = "AdoptError";
345        // Provide better error codes for expected errors.
346        if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
347          reportAs = "AdoptNoSuchFile";
348        } else if (ex.message === "AdoptFileIsDirectory") {
349          reportAs = ex.message;
350        } else {
351          // Anything else is unexpected and should be reported to help track
352          // down what's going wrong.
353          debug("unexpected download error: " + ex);
354          Cu.reportError(ex);
355        }
356        sendPromiseMessage(aMm, "Downloads:Adopt:Return",
357                           {
358                             promiseId: aData.promiseId
359                           },
360                           reportAs);
361    });
362  }
363};
364
365DownloadsAPI.init();
366