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