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/. */
10 function debug(aStr) {
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);
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);
36 XPCOMUtils.defineLazyGetter(this, "powerManagerService", function() {
37 return Cc["@mozilla.org/power/powermanagerservice;1"]
38 .getService(Ci.nsIPowerManagerService);
42 * AlarmService provides an API to schedule alarms using the device's RTC.
44 * AlarmService is primarily used by the mozAlarms API (navigator.mozAlarms)
45 * which uses IPC to communicate with the service.
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.
53 init: function 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",
70 "AlarmsManager:Remove"];
71 this._messages.forEach(function addMessage(msgName) {
72 ppmm.addMessageListener(msgName, this);
75 // Set the indexeddb database.
76 this._db = new AlarmDB();
79 // Variable to save alarms waiting to be set.
80 this._alarmQueue = [];
82 this._restoreAlarmsFromDb();
85 // Getter/setter to access the current alarm set in system.
90 set _currentAlarm(aAlarm) {
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;
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.");
115 if (!aMessage.target.assertContainApp(json.manifestURL)) {
116 debug("Got message from a child process containing illegal manifest URL.");
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);
132 function getAllErrorCb(aErrorMsg) {
133 this._sendAsyncMessage(mm, "GetAll", false, json.requestId, aErrorMsg);
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,
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));
152 case "AlarmsManager:Remove":
153 this.remove(json.id, json.manifestURL);
157 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
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;
172 switch (aMessageName) {
175 { requestId: aRequestId, id: aData } :
176 { requestId: aRequestId, errorMsg: aData };
181 { requestId: aRequestId, alarms: aData } :
182 { requestId: aRequestId, errorMsg: aData };
186 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
190 aMessageManager.sendAsyncMessage("AlarmsManager:" + aMessageName +
191 ":Return:" + (aSuccess ? "OK" : "KO"),
195 _removeAlarmFromDb: function _removeAlarmFromDb(aId, aManifestURL,
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.");
207 this._db.remove(aId, aManifestURL, aRemoveSuccessCb,
208 function removeErrorCb(aErrorMsg) {
209 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
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.
218 _publicAlarm: function _publicAlarm(aAlarm) {
219 let alarm = { "id": aAlarm.id,
221 "respectTimezone": aAlarm.ignoreTimezone ?
222 "ignoreTimezone" : "honorTimezone",
223 "data": aAlarm.data };
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),
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));
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;
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);
271 this._currentAlarm = nextAlarm;
276 this._debugCurrentAlarm();
279 _onTimezoneChanged: function _onTimezoneChanged(aTimezoneOffset) {
280 debug("_onTimezoneChanged()");
282 this._currentTimezoneOffset = aTimezoneOffset;
283 this._restoreAlarmsFromDb();
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);
305 this._removeAlarmFromDb(aAlarm.id, null);
306 this._notifyAlarmObserver(aAlarm);
310 // Set the next alarm from the queue.
311 if (alarmQueue.length) {
312 alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this));
313 this._currentAlarm = alarmQueue.shift();
316 this._debugCurrentAlarm();
318 function getAllErrorCb(aErrorMsg) {
319 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
323 _getAlarmTime: function _getAlarmTime(aAlarm) {
324 // Avoid casting a Date object to a Date again to
325 // preserve milliseconds. See bug 810973.
327 if (aAlarm.date instanceof Date) {
328 alarmTime = aAlarm.date.getTime();
330 alarmTime = (new Date(aAlarm.date)).getTime();
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) {
340 (this._currentTimezoneOffset - aAlarm.timezoneOffset) * 60000;
345 _sortAlarmByTimeStamps: function _sortAlarmByTimeStamps(aAlarm1, aAlarm2) {
346 return this._getAlarmTime(aAlarm1) - this._getAlarmTime(aAlarm2);
349 _debugCurrentAlarm: function _debugCurrentAlarm() {
350 debug("Current alarm: " + JSON.stringify(this._currentAlarm));
351 debug("Alarm queue: " + JSON.stringify(this._alarmQueue));
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.
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
366 * - |pageURL| string: The page in the app that receives the system
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.
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).
380 * [1] https://wiki.mozilla.org/WebAPI/AlarmAPI#Proposed_API
383 add: function(aNewAlarm, aAlarmFiredCb, aSuccessCb, aErrorCb) {
384 debug("add(" + aNewAlarm.date + ")");
386 aSuccessCb = aSuccessCb || function() {};
387 aErrorCb = aErrorCb || function() {};
390 aErrorCb("alarm is null");
394 if (!aNewAlarm.date) {
395 aErrorCb("alarm.date is null");
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();
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();
432 // Push the new alarm in the queue.
433 alarmQueue.push(aNewAlarm);
434 alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this));
435 this._debugCurrentAlarm();
438 function addErrorCb(aErrorMsg) {
444 * Remove the alarm associated with an ID.
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)
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.");
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);
479 this._debugCurrentAlarm();
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();
491 // No alarm waiting to be set in the queue.
492 this._currentAlarm = null;
493 this._debugCurrentAlarm();
497 observe: function(aSubject, aTopic, aData) {
498 debug("observe(): " + aTopic);
501 case "profile-change-teardown":
505 case "webapps-clear-data":
507 aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
509 debug("Error! Fail to remove alarms for an uninstalled app.");
513 // Only remove alarms for apps.
514 if (params.browserOnly) {
518 let manifestURL = appsService.getManifestURLByLocalId(params.appId);
520 debug("Error! Fail to remove alarms for an uninstalled app.");
524 this._db.getAll(manifestURL,
525 function getAllSuccessCb(aAlarms) {
526 aAlarms.forEach(function removeAlarm(aAlarm) {
527 this.remove(aAlarm.id, manifestURL);
530 function getAllErrorCb(aErrorMsg) {
531 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
537 uninit: function 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);
553 this._alarmHalService = null;