|
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 file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 /* static functions */ |
|
8 const DEBUG = false; |
|
9 |
|
10 function debug(aStr) { |
|
11 if (DEBUG) |
|
12 dump("AlarmService: " + aStr + "\n"); |
|
13 } |
|
14 |
|
15 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; |
|
16 |
|
17 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
18 Cu.import("resource://gre/modules/Services.jsm"); |
|
19 Cu.import("resource://gre/modules/AlarmDB.jsm"); |
|
20 |
|
21 this.EXPORTED_SYMBOLS = ["AlarmService"]; |
|
22 |
|
23 XPCOMUtils.defineLazyGetter(this, "appsService", function() { |
|
24 return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService); |
|
25 }); |
|
26 |
|
27 XPCOMUtils.defineLazyServiceGetter(this, "ppmm", |
|
28 "@mozilla.org/parentprocessmessagemanager;1", |
|
29 "nsIMessageListenerManager"); |
|
30 |
|
31 XPCOMUtils.defineLazyGetter(this, "messenger", function() { |
|
32 return Cc["@mozilla.org/system-message-internal;1"].getService(Ci.nsISystemMessagesInternal); |
|
33 }); |
|
34 |
|
35 XPCOMUtils.defineLazyGetter(this, "powerManagerService", function() { |
|
36 return Cc["@mozilla.org/power/powermanagerservice;1"].getService(Ci.nsIPowerManagerService); |
|
37 }); |
|
38 |
|
39 /** |
|
40 * AlarmService provides an API to schedule alarms using the device's RTC. |
|
41 * |
|
42 * AlarmService is primarily used by the mozAlarms API (navigator.mozAlarms) |
|
43 * which uses IPC to communicate with the service. |
|
44 * |
|
45 * AlarmService can also be used by Gecko code by importing the module and then |
|
46 * using AlarmService.add() and AlarmService.remove(). Only Gecko code running |
|
47 * in the parent process should do this. |
|
48 */ |
|
49 |
|
50 this.AlarmService = { |
|
51 init: function init() { |
|
52 debug("init()"); |
|
53 Services.obs.addObserver(this, "profile-change-teardown", false); |
|
54 Services.obs.addObserver(this, "webapps-clear-data",false); |
|
55 |
|
56 this._currentTimezoneOffset = (new Date()).getTimezoneOffset(); |
|
57 |
|
58 let alarmHalService = |
|
59 this._alarmHalService = Cc["@mozilla.org/alarmHalService;1"] |
|
60 .getService(Ci.nsIAlarmHalService); |
|
61 |
|
62 alarmHalService.setAlarmFiredCb(this._onAlarmFired.bind(this)); |
|
63 alarmHalService.setTimezoneChangedCb(this._onTimezoneChanged.bind(this)); |
|
64 |
|
65 // Add the messages to be listened to. |
|
66 this._messages = ["AlarmsManager:GetAll", |
|
67 "AlarmsManager:Add", |
|
68 "AlarmsManager:Remove"]; |
|
69 this._messages.forEach(function addMessage(msgName) { |
|
70 ppmm.addMessageListener(msgName, this); |
|
71 }.bind(this)); |
|
72 |
|
73 // Set the indexeddb database. |
|
74 this._db = new AlarmDB(); |
|
75 this._db.init(); |
|
76 |
|
77 // Variable to save alarms waiting to be set. |
|
78 this._alarmQueue = []; |
|
79 |
|
80 this._restoreAlarmsFromDb(); |
|
81 }, |
|
82 |
|
83 // Getter/setter to access the current alarm set in system. |
|
84 _alarm: null, |
|
85 get _currentAlarm() { |
|
86 return this._alarm; |
|
87 }, |
|
88 set _currentAlarm(aAlarm) { |
|
89 this._alarm = aAlarm; |
|
90 if (!aAlarm) { |
|
91 return; |
|
92 } |
|
93 |
|
94 let alarmTimeInMs = this._getAlarmTime(aAlarm); |
|
95 let ns = (alarmTimeInMs % 1000) * 1000000; |
|
96 if (!this._alarmHalService.setAlarm(alarmTimeInMs / 1000, ns)) { |
|
97 throw Components.results.NS_ERROR_FAILURE; |
|
98 } |
|
99 }, |
|
100 |
|
101 receiveMessage: function receiveMessage(aMessage) { |
|
102 debug("receiveMessage(): " + aMessage.name); |
|
103 let json = aMessage.json; |
|
104 |
|
105 // To prevent the hacked child process from sending commands to parent |
|
106 // to schedule alarms, we need to check its permission and manifest URL. |
|
107 if (this._messages.indexOf(aMessage.name) != -1) { |
|
108 if (!aMessage.target.assertPermission("alarms")) { |
|
109 debug("Got message from a child process with no 'alarms' permission."); |
|
110 return null; |
|
111 } |
|
112 if (!aMessage.target.assertContainApp(json.manifestURL)) { |
|
113 debug("Got message from a child process containing illegal manifest URL."); |
|
114 return null; |
|
115 } |
|
116 } |
|
117 |
|
118 let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender); |
|
119 switch (aMessage.name) { |
|
120 case "AlarmsManager:GetAll": |
|
121 this._db.getAll( |
|
122 json.manifestURL, |
|
123 function getAllSuccessCb(aAlarms) { |
|
124 debug("Callback after getting alarms from database: " + |
|
125 JSON.stringify(aAlarms)); |
|
126 this._sendAsyncMessage(mm, "GetAll", true, json.requestId, aAlarms); |
|
127 }.bind(this), |
|
128 function getAllErrorCb(aErrorMsg) { |
|
129 this._sendAsyncMessage(mm, "GetAll", false, json.requestId, aErrorMsg); |
|
130 }.bind(this) |
|
131 ); |
|
132 break; |
|
133 |
|
134 case "AlarmsManager:Add": |
|
135 // Prepare a record for the new alarm to be added. |
|
136 let newAlarm = { |
|
137 date: json.date, |
|
138 ignoreTimezone: json.ignoreTimezone, |
|
139 data: json.data, |
|
140 pageURL: json.pageURL, |
|
141 manifestURL: json.manifestURL |
|
142 }; |
|
143 |
|
144 this.add(newAlarm, null, |
|
145 // Receives the alarm ID as the last argument. |
|
146 this._sendAsyncMessage.bind(this, mm, "Add", true, json.requestId), |
|
147 // Receives the error message as the last argument. |
|
148 this._sendAsyncMessage.bind(this, mm, "Add", false, json.requestId) |
|
149 ); |
|
150 break; |
|
151 |
|
152 case "AlarmsManager:Remove": |
|
153 this.remove(json.id, json.manifestURL); |
|
154 break; |
|
155 |
|
156 default: |
|
157 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
158 break; |
|
159 } |
|
160 }, |
|
161 |
|
162 _sendAsyncMessage: function _sendAsyncMessage(aMessageManager, aMessageName, |
|
163 aSuccess, aRequestId, aData) { |
|
164 debug("_sendAsyncMessage()"); |
|
165 |
|
166 if (!aMessageManager) { |
|
167 debug("Invalid message manager: null"); |
|
168 throw Components.results.NS_ERROR_FAILURE; |
|
169 } |
|
170 |
|
171 let json = null; |
|
172 switch (aMessageName) |
|
173 { |
|
174 case "Add": |
|
175 json = aSuccess ? |
|
176 { requestId: aRequestId, id: aData } : |
|
177 { requestId: aRequestId, errorMsg: aData }; |
|
178 break; |
|
179 |
|
180 case "GetAll": |
|
181 json = aSuccess ? |
|
182 { requestId: aRequestId, alarms: aData } : |
|
183 { requestId: aRequestId, errorMsg: aData }; |
|
184 break; |
|
185 |
|
186 default: |
|
187 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
188 break; |
|
189 } |
|
190 |
|
191 aMessageManager.sendAsyncMessage("AlarmsManager:" + aMessageName + |
|
192 ":Return:" + (aSuccess ? "OK" : "KO"), json); |
|
193 }, |
|
194 |
|
195 _removeAlarmFromDb: function _removeAlarmFromDb(aId, aManifestURL, |
|
196 aRemoveSuccessCb) { |
|
197 debug("_removeAlarmFromDb()"); |
|
198 |
|
199 // If the aRemoveSuccessCb is undefined or null, set a dummy callback for |
|
200 // it which is needed for _db.remove(). |
|
201 if (!aRemoveSuccessCb) { |
|
202 aRemoveSuccessCb = function removeSuccessCb() { |
|
203 debug("Remove alarm from DB successfully."); |
|
204 }; |
|
205 } |
|
206 |
|
207 this._db.remove( |
|
208 aId, |
|
209 aManifestURL, |
|
210 aRemoveSuccessCb, |
|
211 function removeErrorCb(aErrorMsg) { |
|
212 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
213 } |
|
214 ); |
|
215 }, |
|
216 |
|
217 /** |
|
218 * Create a copy of the alarm that does not expose internal fields to |
|
219 * receivers and sticks to the public |respectTimezone| API rather than the |
|
220 * boolean |ignoreTimezone| field. |
|
221 */ |
|
222 _publicAlarm: function _publicAlarm(aAlarm) { |
|
223 let alarm = { |
|
224 "id": aAlarm.id, |
|
225 "date": aAlarm.date, |
|
226 "respectTimezone": aAlarm.ignoreTimezone ? |
|
227 "ignoreTimezone" : "honorTimezone", |
|
228 "data": aAlarm.data |
|
229 }; |
|
230 |
|
231 return alarm; |
|
232 }, |
|
233 |
|
234 _fireSystemMessage: function _fireSystemMessage(aAlarm) { |
|
235 debug("Fire system message: " + JSON.stringify(aAlarm)); |
|
236 |
|
237 let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null); |
|
238 let pageURI = Services.io.newURI(aAlarm.pageURL, null, null); |
|
239 |
|
240 messenger.sendMessage("alarm", this._publicAlarm(aAlarm), |
|
241 pageURI, manifestURI); |
|
242 }, |
|
243 |
|
244 _notifyAlarmObserver: function _notifyAlarmObserver(aAlarm) { |
|
245 debug("_notifyAlarmObserver()"); |
|
246 |
|
247 if (aAlarm.manifestURL) { |
|
248 this._fireSystemMessage(aAlarm); |
|
249 } else if (typeof aAlarm.alarmFiredCb === "function") { |
|
250 aAlarm.alarmFiredCb(this._publicAlarm(aAlarm)); |
|
251 } |
|
252 }, |
|
253 |
|
254 _onAlarmFired: function _onAlarmFired() { |
|
255 debug("_onAlarmFired()"); |
|
256 |
|
257 if (this._currentAlarm) { |
|
258 this._removeAlarmFromDb(this._currentAlarm.id, null); |
|
259 this._notifyAlarmObserver(this._currentAlarm); |
|
260 this._currentAlarm = null; |
|
261 } |
|
262 |
|
263 // Reset the next alarm from the queue. |
|
264 let alarmQueue = this._alarmQueue; |
|
265 while (alarmQueue.length > 0) { |
|
266 let nextAlarm = alarmQueue.shift(); |
|
267 let nextAlarmTime = this._getAlarmTime(nextAlarm); |
|
268 |
|
269 // If the next alarm has been expired, directly notify the observer. |
|
270 // it instead of setting it. |
|
271 if (nextAlarmTime <= Date.now()) { |
|
272 this._removeAlarmFromDb(nextAlarm.id, null); |
|
273 this._notifyAlarmObserver(nextAlarm); |
|
274 } else { |
|
275 this._currentAlarm = nextAlarm; |
|
276 break; |
|
277 } |
|
278 } |
|
279 this._debugCurrentAlarm(); |
|
280 }, |
|
281 |
|
282 _onTimezoneChanged: function _onTimezoneChanged(aTimezoneOffset) { |
|
283 debug("_onTimezoneChanged()"); |
|
284 |
|
285 this._currentTimezoneOffset = aTimezoneOffset; |
|
286 this._restoreAlarmsFromDb(); |
|
287 }, |
|
288 |
|
289 _restoreAlarmsFromDb: function _restoreAlarmsFromDb() { |
|
290 debug("_restoreAlarmsFromDb()"); |
|
291 |
|
292 this._db.getAll( |
|
293 null, |
|
294 function getAllSuccessCb(aAlarms) { |
|
295 debug("Callback after getting alarms from database: " + |
|
296 JSON.stringify(aAlarms)); |
|
297 |
|
298 // Clear any alarms set or queued in the cache. |
|
299 let alarmQueue = this._alarmQueue; |
|
300 alarmQueue.length = 0; |
|
301 this._currentAlarm = null; |
|
302 |
|
303 // Only restore the alarm that's not yet expired; otherwise, remove it |
|
304 // from the database and notify the observer. |
|
305 aAlarms.forEach(function addAlarm(aAlarm) { |
|
306 if (this._getAlarmTime(aAlarm) > Date.now()) { |
|
307 alarmQueue.push(aAlarm); |
|
308 } else { |
|
309 this._removeAlarmFromDb(aAlarm.id, null); |
|
310 this._notifyAlarmObserver(aAlarm); |
|
311 } |
|
312 }.bind(this)); |
|
313 |
|
314 // Set the next alarm from queue. |
|
315 if (alarmQueue.length) { |
|
316 alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); |
|
317 this._currentAlarm = alarmQueue.shift(); |
|
318 } |
|
319 |
|
320 this._debugCurrentAlarm(); |
|
321 }.bind(this), |
|
322 function getAllErrorCb(aErrorMsg) { |
|
323 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
324 } |
|
325 ); |
|
326 }, |
|
327 |
|
328 _getAlarmTime: function _getAlarmTime(aAlarm) { |
|
329 // Avoid casting a Date object to a Date again to |
|
330 // preserve milliseconds. See bug 810973. |
|
331 let alarmTime; |
|
332 if (aAlarm.date instanceof Date) { |
|
333 alarmTime = aAlarm.date.getTime(); |
|
334 } else { |
|
335 alarmTime = (new Date(aAlarm.date)).getTime(); |
|
336 } |
|
337 |
|
338 // For an alarm specified with "ignoreTimezone", it must be fired respect |
|
339 // to the user's timezone. Supposing an alarm was set at 7:00pm at Tokyo, |
|
340 // it must be gone off at 7:00pm respect to Paris' local time when the user |
|
341 // is located at Paris. We can adjust the alarm UTC time by calculating |
|
342 // the difference of the orginal timezone and the current timezone. |
|
343 if (aAlarm.ignoreTimezone) { |
|
344 alarmTime += (this._currentTimezoneOffset - aAlarm.timezoneOffset) * 60000; |
|
345 } |
|
346 return alarmTime; |
|
347 }, |
|
348 |
|
349 _sortAlarmByTimeStamps: function _sortAlarmByTimeStamps(aAlarm1, aAlarm2) { |
|
350 return this._getAlarmTime(aAlarm1) - this._getAlarmTime(aAlarm2); |
|
351 }, |
|
352 |
|
353 _debugCurrentAlarm: function _debugCurrentAlarm() { |
|
354 debug("Current alarm: " + JSON.stringify(this._currentAlarm)); |
|
355 debug("Alarm queue: " + JSON.stringify(this._alarmQueue)); |
|
356 }, |
|
357 |
|
358 /** |
|
359 * |
|
360 * Add a new alarm. This will set the RTC to fire at the selected date and |
|
361 * notify the caller. Notifications are delivered via System Messages if the |
|
362 * alarm is added on behalf of a app. Otherwise aAlarmFiredCb is called. |
|
363 * |
|
364 * @param object aNewAlarm |
|
365 * Should contain the following literal properties: |
|
366 * - |date| date: when the alarm should timeout. |
|
367 * - |ignoreTimezone| boolean: See [1] for the details. |
|
368 * - |manifestURL| string: Manifest of app on whose behalf the alarm |
|
369 * is added. |
|
370 * - |pageURL| string: The page in the app that receives the system |
|
371 * message. |
|
372 * - |data| object [optional]: Data that can be stored in DB. |
|
373 * @param function aAlarmFiredCb |
|
374 * Callback function invoked when the alarm is fired. |
|
375 * It receives a single argument, the alarm object. |
|
376 * May be null. |
|
377 * @param function aSuccessCb |
|
378 * Callback function to receive an alarm ID (number). |
|
379 * @param function aErrorCb |
|
380 * Callback function to receive an error message (string). |
|
381 * @returns void |
|
382 * |
|
383 * Notes: |
|
384 * [1] https://wiki.mozilla.org/WebAPI/AlarmAPI#Proposed_API |
|
385 */ |
|
386 |
|
387 add: function(aNewAlarm, aAlarmFiredCb, aSuccessCb, aErrorCb) { |
|
388 debug("add(" + aNewAlarm.date + ")"); |
|
389 |
|
390 aSuccessCb = aSuccessCb || function() {}; |
|
391 aErrorCb = aErrorCb || function() {}; |
|
392 |
|
393 if (!aNewAlarm) { |
|
394 aErrorCb("alarm is null"); |
|
395 return; |
|
396 } |
|
397 |
|
398 if (!aNewAlarm.date) { |
|
399 aErrorCb("alarm.date is null"); |
|
400 return; |
|
401 } |
|
402 |
|
403 aNewAlarm['timezoneOffset'] = this._currentTimezoneOffset; |
|
404 |
|
405 this._db.add( |
|
406 aNewAlarm, |
|
407 function addSuccessCb(aNewId) { |
|
408 debug("Callback after adding alarm in database."); |
|
409 |
|
410 aNewAlarm['id'] = aNewId; |
|
411 |
|
412 // Now that the alarm has been added to the database, we can tack on |
|
413 // the non-serializable callback to the in-memory object. |
|
414 aNewAlarm['alarmFiredCb'] = aAlarmFiredCb; |
|
415 |
|
416 // If there is no alarm being set in system, set the new alarm. |
|
417 if (this._currentAlarm == null) { |
|
418 this._currentAlarm = aNewAlarm; |
|
419 this._debugCurrentAlarm(); |
|
420 aSuccessCb(aNewId); |
|
421 return; |
|
422 } |
|
423 |
|
424 // If the new alarm is earlier than the current alarm, swap them and |
|
425 // push the previous alarm back to queue. |
|
426 let alarmQueue = this._alarmQueue; |
|
427 let aNewAlarmTime = this._getAlarmTime(aNewAlarm); |
|
428 let currentAlarmTime = this._getAlarmTime(this._currentAlarm); |
|
429 if (aNewAlarmTime < currentAlarmTime) { |
|
430 alarmQueue.unshift(this._currentAlarm); |
|
431 this._currentAlarm = aNewAlarm; |
|
432 this._debugCurrentAlarm(); |
|
433 aSuccessCb(aNewId); |
|
434 return; |
|
435 } |
|
436 |
|
437 // Push the new alarm in the queue. |
|
438 alarmQueue.push(aNewAlarm); |
|
439 alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); |
|
440 this._debugCurrentAlarm(); |
|
441 aSuccessCb(aNewId); |
|
442 }.bind(this), |
|
443 function addErrorCb(aErrorMsg) { |
|
444 aErrorCb(aErrorMsg); |
|
445 }.bind(this) |
|
446 ); |
|
447 }, |
|
448 |
|
449 /* |
|
450 * Remove the alarm associated with an ID. |
|
451 * |
|
452 * @param number aAlarmId |
|
453 * The ID of the alarm to be removed. |
|
454 * @param string aManifestURL |
|
455 * Manifest URL for application which added the alarm. (Optional) |
|
456 * @returns void |
|
457 */ |
|
458 remove: function(aAlarmId, aManifestURL) { |
|
459 debug("remove(" + aAlarmId + ", " + aManifestURL + ")"); |
|
460 this._removeAlarmFromDb( |
|
461 aAlarmId, |
|
462 aManifestURL, |
|
463 function removeSuccessCb() { |
|
464 debug("Callback after removing alarm from database."); |
|
465 |
|
466 // If there are no alarms set, nothing to do. |
|
467 if (!this._currentAlarm) { |
|
468 debug("No alarms set."); |
|
469 return; |
|
470 } |
|
471 |
|
472 // Check if the alarm to be removed is in the queue and whether it |
|
473 // belongs to the requesting app. |
|
474 let alarmQueue = this._alarmQueue; |
|
475 if (this._currentAlarm.id != aAlarmId || |
|
476 this._currentAlarm.manifestURL != aManifestURL) { |
|
477 |
|
478 for (let i = 0; i < alarmQueue.length; i++) { |
|
479 if (alarmQueue[i].id == aAlarmId && |
|
480 alarmQueue[i].manifestURL == aManifestURL) { |
|
481 |
|
482 alarmQueue.splice(i, 1); |
|
483 break; |
|
484 } |
|
485 } |
|
486 this._debugCurrentAlarm(); |
|
487 return; |
|
488 } |
|
489 |
|
490 // The alarm to be removed is the current alarm reset the next alarm |
|
491 // from queue if any. |
|
492 if (alarmQueue.length) { |
|
493 this._currentAlarm = alarmQueue.shift(); |
|
494 this._debugCurrentAlarm(); |
|
495 return; |
|
496 } |
|
497 |
|
498 // No alarm waiting to be set in the queue. |
|
499 this._currentAlarm = null; |
|
500 this._debugCurrentAlarm(); |
|
501 }.bind(this) |
|
502 ); |
|
503 }, |
|
504 |
|
505 observe: function(aSubject, aTopic, aData) { |
|
506 switch (aTopic) { |
|
507 case "profile-change-teardown": |
|
508 this.uninit(); |
|
509 break; |
|
510 case "webapps-clear-data": |
|
511 let params = |
|
512 aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams); |
|
513 if (!params) { |
|
514 debug("Error! Fail to remove alarms for an uninstalled app."); |
|
515 return; |
|
516 } |
|
517 |
|
518 // Only remove alarms for apps. |
|
519 if (params.browserOnly) { |
|
520 return; |
|
521 } |
|
522 |
|
523 let manifestURL = appsService.getManifestURLByLocalId(params.appId); |
|
524 if (!manifestURL) { |
|
525 debug("Error! Fail to remove alarms for an uninstalled app."); |
|
526 return; |
|
527 } |
|
528 |
|
529 this._db.getAll( |
|
530 manifestURL, |
|
531 function getAllSuccessCb(aAlarms) { |
|
532 aAlarms.forEach(function removeAlarm(aAlarm) { |
|
533 this.remove(aAlarm.id, manifestURL); |
|
534 }, this); |
|
535 }.bind(this), |
|
536 function getAllErrorCb(aErrorMsg) { |
|
537 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
538 } |
|
539 ); |
|
540 break; |
|
541 } |
|
542 }, |
|
543 |
|
544 uninit: function uninit() { |
|
545 debug("uninit()"); |
|
546 Services.obs.removeObserver(this, "profile-change-teardown"); |
|
547 Services.obs.removeObserver(this, "webapps-clear-data"); |
|
548 |
|
549 this._messages.forEach(function(aMsgName) { |
|
550 ppmm.removeMessageListener(aMsgName, this); |
|
551 }.bind(this)); |
|
552 ppmm = null; |
|
553 |
|
554 if (this._db) { |
|
555 this._db.close(); |
|
556 } |
|
557 this._db = null; |
|
558 |
|
559 this._alarmHalService = null; |
|
560 } |
|
561 } |
|
562 |
|
563 AlarmService.init(); |