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