|
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 |
|
11 this.EXPORTED_SYMBOLS = ["DownloadsIPC"]; |
|
12 |
|
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
14 Cu.import("resource://gre/modules/Services.jsm"); |
|
15 Cu.import("resource://gre/modules/Promise.jsm"); |
|
16 |
|
17 XPCOMUtils.defineLazyServiceGetter(this, "cpmm", |
|
18 "@mozilla.org/childprocessmessagemanager;1", |
|
19 "nsIMessageSender"); |
|
20 |
|
21 /** |
|
22 * This module lives in the child process and receives the ipc messages |
|
23 * from the parent. It saves the download's state and redispatch changes |
|
24 * to DOM objects using an observer notification. |
|
25 * |
|
26 * This module needs to be loaded once and only once per process. |
|
27 */ |
|
28 |
|
29 function debug(aStr) { |
|
30 #ifdef MOZ_DEBUG |
|
31 dump("-*- DownloadsIPC.jsm : " + aStr + "\n"); |
|
32 #endif |
|
33 } |
|
34 |
|
35 const ipcMessages = ["Downloads:Added", |
|
36 "Downloads:Removed", |
|
37 "Downloads:Changed", |
|
38 "Downloads:GetList:Return", |
|
39 "Downloads:ClearAllDone:Return", |
|
40 "Downloads:Remove:Return", |
|
41 "Downloads:Pause:Return", |
|
42 "Downloads:Resume:Return"]; |
|
43 |
|
44 this.DownloadsIPC = { |
|
45 downloads: {}, |
|
46 |
|
47 init: function() { |
|
48 debug("init"); |
|
49 Services.obs.addObserver(this, "xpcom-shutdown", false); |
|
50 ipcMessages.forEach((aMessage) => { |
|
51 cpmm.addMessageListener(aMessage, this); |
|
52 }); |
|
53 |
|
54 // We need to get the list of current downloads. |
|
55 this.ready = false; |
|
56 this.getListPromises = []; |
|
57 this.clearAllPromises = []; |
|
58 this.downloadPromises = {}; |
|
59 cpmm.sendAsyncMessage("Downloads:GetList", {}); |
|
60 this._promiseId = 0; |
|
61 }, |
|
62 |
|
63 notifyChanges: function(aId) { |
|
64 // TODO: use the subject instead of stringifying. |
|
65 if (this.downloads[aId]) { |
|
66 debug("notifyChanges notifying changes for " + aId); |
|
67 Services.obs.notifyObservers(null, "downloads-state-change-" + aId, |
|
68 JSON.stringify(this.downloads[aId])); |
|
69 } else { |
|
70 debug("notifyChanges failed for " + aId) |
|
71 } |
|
72 }, |
|
73 |
|
74 _updateDownloadsArray: function(aDownloads) { |
|
75 this.downloads = []; |
|
76 // We actually have an array of downloads. |
|
77 aDownloads.forEach((aDownload) => { |
|
78 this.downloads[aDownload.id] = aDownload; |
|
79 }); |
|
80 }, |
|
81 |
|
82 receiveMessage: function(aMessage) { |
|
83 let download = aMessage.data; |
|
84 debug("message: " + aMessage.name); |
|
85 switch(aMessage.name) { |
|
86 case "Downloads:GetList:Return": |
|
87 this._updateDownloadsArray(download); |
|
88 |
|
89 if (!this.ready) { |
|
90 this.getListPromises.forEach(aPromise => |
|
91 aPromise.resolve(this.downloads)); |
|
92 this.getListPromises.length = 0; |
|
93 } |
|
94 this.ready = true; |
|
95 break; |
|
96 case "Downloads:ClearAllDone:Return": |
|
97 this._updateDownloadsArray(download); |
|
98 this.clearAllPromises.forEach(aPromise => |
|
99 aPromise.resolve(this.downloads)); |
|
100 this.clearAllPromises.length = 0; |
|
101 break; |
|
102 case "Downloads:Added": |
|
103 this.downloads[download.id] = download; |
|
104 this.notifyChanges(download.id); |
|
105 break; |
|
106 case "Downloads:Removed": |
|
107 if (this.downloads[download.id]) { |
|
108 this.downloads[download.id] = download; |
|
109 this.notifyChanges(download.id); |
|
110 delete this.downloads[download.id]; |
|
111 } |
|
112 break; |
|
113 case "Downloads:Changed": |
|
114 // Only update properties that actually changed. |
|
115 let cached = this.downloads[download.id]; |
|
116 if (!cached) { |
|
117 debug("No download found for " + download.id); |
|
118 return; |
|
119 } |
|
120 let props = ["totalBytes", "currentBytes", "url", "path", "state", |
|
121 "contentType", "startTime"]; |
|
122 let changed = false; |
|
123 |
|
124 props.forEach((aProp) => { |
|
125 if (download[aProp] && (download[aProp] != cached[aProp])) { |
|
126 cached[aProp] = download[aProp]; |
|
127 changed = true; |
|
128 } |
|
129 }); |
|
130 |
|
131 // Updating the error property. We always get a 'state' change as |
|
132 // well. |
|
133 cached.error = download.error; |
|
134 |
|
135 if (changed) { |
|
136 this.notifyChanges(download.id); |
|
137 } |
|
138 break; |
|
139 case "Downloads:Remove:Return": |
|
140 case "Downloads:Pause:Return": |
|
141 case "Downloads:Resume:Return": |
|
142 if (this.downloadPromises[download.promiseId]) { |
|
143 if (!download.error) { |
|
144 this.downloadPromises[download.promiseId].resolve(download); |
|
145 } else { |
|
146 this.downloadPromises[download.promiseId].reject(download); |
|
147 } |
|
148 delete this.downloadPromises[download.promiseId]; |
|
149 } |
|
150 break; |
|
151 } |
|
152 }, |
|
153 |
|
154 /** |
|
155 * Returns a promise that is resolved with the list of current downloads. |
|
156 */ |
|
157 getDownloads: function() { |
|
158 debug("getDownloads()"); |
|
159 let deferred = Promise.defer(); |
|
160 if (this.ready) { |
|
161 debug("Returning existing list."); |
|
162 deferred.resolve(this.downloads); |
|
163 } else { |
|
164 this.getListPromises.push(deferred); |
|
165 } |
|
166 return deferred.promise; |
|
167 }, |
|
168 |
|
169 /** |
|
170 * Returns a promise that is resolved with the list of current downloads. |
|
171 */ |
|
172 clearAllDone: function() { |
|
173 debug("clearAllDone"); |
|
174 let deferred = Promise.defer(); |
|
175 this.clearAllPromises.push(deferred); |
|
176 cpmm.sendAsyncMessage("Downloads:ClearAllDone", {}); |
|
177 return deferred.promise; |
|
178 }, |
|
179 |
|
180 promiseId: function() { |
|
181 return this._promiseId++; |
|
182 }, |
|
183 |
|
184 remove: function(aId) { |
|
185 debug("remove " + aId); |
|
186 let deferred = Promise.defer(); |
|
187 let pId = this.promiseId(); |
|
188 this.downloadPromises[pId] = deferred; |
|
189 cpmm.sendAsyncMessage("Downloads:Remove", |
|
190 { id: aId, promiseId: pId }); |
|
191 return deferred.promise; |
|
192 }, |
|
193 |
|
194 pause: function(aId) { |
|
195 debug("pause " + aId); |
|
196 let deferred = Promise.defer(); |
|
197 let pId = this.promiseId(); |
|
198 this.downloadPromises[pId] = deferred; |
|
199 cpmm.sendAsyncMessage("Downloads:Pause", |
|
200 { id: aId, promiseId: pId }); |
|
201 return deferred.promise; |
|
202 }, |
|
203 |
|
204 resume: function(aId) { |
|
205 debug("resume " + aId); |
|
206 let deferred = Promise.defer(); |
|
207 let pId = this.promiseId(); |
|
208 this.downloadPromises[pId] = deferred; |
|
209 cpmm.sendAsyncMessage("Downloads:Resume", |
|
210 { id: aId, promiseId: pId }); |
|
211 return deferred.promise; |
|
212 }, |
|
213 |
|
214 observe: function(aSubject, aTopic, aData) { |
|
215 if (aTopic == "xpcom-shutdown") { |
|
216 ipcMessages.forEach((aMessage) => { |
|
217 cpmm.removeMessageListener(aMessage, this); |
|
218 }); |
|
219 } |
|
220 } |
|
221 }; |
|
222 |
|
223 DownloadsIPC.init(); |