From 541f47ab685678f64cd7d64f3d488e9c468daeb7 Mon Sep 17 00:00:00 2001 From: Nikhil Marathe Date: Wed, 1 May 2013 21:32:49 +0530 Subject: [PATCH] Bug 862322 - Allow AlarmService to be used from Gecko code. r=gene --- dom/alarm/AlarmService.jsm | 366 ++++++++++++++++++++++++------------- 1 file changed, 236 insertions(+), 130 deletions(-) diff --git a/dom/alarm/AlarmService.jsm b/dom/alarm/AlarmService.jsm index 9e21a7c4eac4..2927f0c640a3 100644 --- a/dom/alarm/AlarmService.jsm +++ b/dom/alarm/AlarmService.jsm @@ -34,17 +34,31 @@ XPCOMUtils.defineLazyGetter(this, "powerManagerService", function() { let myGlobal = this; +/** + * AlarmService provides an API to schedule alarms using the device's RTC. + * + * AlarmService is primarily used by the mozAlarms API (navigator.mozAlarms) + * which uses IPC to communicate with the service. + * + * AlarmService can also be used by Gecko code by importing the module and then + * using AlarmService.add() and AlarmService.remove(). Only Gecko code running + * in the parent process should do this. + */ + this.AlarmService = { init: function init() { debug("init()"); this._currentTimezoneOffset = (new Date()).getTimezoneOffset(); - let alarmHalService = this._alarmHalService = Cc["@mozilla.org/alarmHalService;1"].getService(Ci.nsIAlarmHalService); + let alarmHalService = + this._alarmHalService = Cc["@mozilla.org/alarmHalService;1"] + .getService(Ci.nsIAlarmHalService); + alarmHalService.setAlarmFiredCb(this._onAlarmFired.bind(this)); alarmHalService.setTimezoneChangedCb(this._onTimezoneChanged.bind(this)); - // add the messages to be listened + // Add the messages to be listened to. const messages = ["AlarmsManager:GetAll", "AlarmsManager:Add", "AlarmsManager:Remove"]; @@ -52,19 +66,20 @@ this.AlarmService = { ppmm.addMessageListener(msgName, this); }.bind(this)); - // set the indexeddb database - let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"].getService(Ci.nsIIndexedDatabaseManager); + // Set the indexeddb database. + let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"] + .getService(Ci.nsIIndexedDatabaseManager); idbManager.initWindowless(myGlobal); this._db = new AlarmDB(myGlobal); this._db.init(myGlobal); - // variable to save alarms waiting to be set + // Variable to save alarms waiting to be set. this._alarmQueue = []; this._restoreAlarmsFromDb(); }, - // getter/setter to access the current alarm set in system + // Getter/setter to access the current alarm set in system. _alarm: null, get _currentAlarm() { return this._alarm; @@ -102,7 +117,8 @@ this.AlarmService = { this._db.getAll( json.manifestURL, function getAllSuccessCb(aAlarms) { - debug("Callback after getting alarms from database: " + JSON.stringify(aAlarms)); + debug("Callback after getting alarms from database: " + + JSON.stringify(aAlarms)); this._sendAsyncMessage(mm, "GetAll", true, json.requestId, aAlarms); }.bind(this), function getAllErrorCb(aErrorMsg) { @@ -112,105 +128,25 @@ this.AlarmService = { break; case "AlarmsManager:Add": - // prepare a record for the new alarm to be added + // Prepare a record for the new alarm to be added. let newAlarm = { - date: json.date, - ignoreTimezone: json.ignoreTimezone, - timezoneOffset: this._currentTimezoneOffset, + date: json.date, + ignoreTimezone: json.ignoreTimezone, data: json.data, pageURL: json.pageURL, manifestURL: json.manifestURL }; - let newAlarmTime = this._getAlarmTime(newAlarm); - if (newAlarmTime <= Date.now()) { - debug("Adding a alarm that has past time. Return DOMError."); - this._debugCurrentAlarm(); - this._sendAsyncMessage(mm, "Add", false, json.requestId, "InvalidStateError"); - break; - } - - this._db.add( - newAlarm, - function addSuccessCb(aNewId) { - debug("Callback after adding alarm in database."); - - newAlarm['id'] = aNewId; - - // if there is no alarm being set in system, set the new alarm - if (this._currentAlarm == null) { - this._currentAlarm = newAlarm; - this._debugCurrentAlarm(); - this._sendAsyncMessage(mm, "Add", true, json.requestId, aNewId); - return; - } - - // if the new alarm is earlier than the current alarm - // swap them and push the previous alarm back to queue - let alarmQueue = this._alarmQueue; - let currentAlarmTime = this._getAlarmTime(this._currentAlarm); - if (newAlarmTime < currentAlarmTime) { - alarmQueue.unshift(this._currentAlarm); - this._currentAlarm = newAlarm; - this._debugCurrentAlarm(); - this._sendAsyncMessage(mm, "Add", true, json.requestId, aNewId); - return; - } - - //push the new alarm in the queue - alarmQueue.push(newAlarm); - alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); - this._debugCurrentAlarm(); - this._sendAsyncMessage(mm, "Add", true, json.requestId, aNewId); - }.bind(this), - function addErrorCb(aErrorMsg) { - this._sendAsyncMessage(mm, "Add", false, json.requestId, aErrorMsg); - }.bind(this) + this.add(newAlarm, null, + // Receives the alarm ID as the last argument. + this._sendAsyncMessage.bind(this, mm, "Add", true, json.requestId), + // Receives the error message as the last argument. + this._sendAsyncMessage.bind(this, mm, "Add", false, json.requestId) ); break; case "AlarmsManager:Remove": - this._removeAlarmFromDb( - json.id, - json.manifestURL, - function removeSuccessCb() { - debug("Callback after removing alarm from database."); - - // if there is no alarm being set - if (!this._currentAlarm) { - this._debugCurrentAlarm(); - return; - } - - // check if the alarm to be removed is in the queue - // by ID and whether it belongs to the requesting app - let alarmQueue = this._alarmQueue; - if (this._currentAlarm.id != json.id || - this._currentAlarm.manifestURL != json.manifestURL) { - for (let i = 0; i < alarmQueue.length; i++) { - if (alarmQueue[i].id == json.id && - alarmQueue[i].manifestURL == json.manifestURL) { - alarmQueue.splice(i, 1); - break; - } - } - this._debugCurrentAlarm(); - return; - } - - // the alarm to be removed is the current alarm - // reset the next alarm from queue if any - if (alarmQueue.length) { - this._currentAlarm = alarmQueue.shift(); - this._debugCurrentAlarm(); - return; - } - - // no alarm waiting to be set in the queue - this._currentAlarm = null; - this._debugCurrentAlarm(); - }.bind(this) - ); + this.remove(json.id, json.manifestURL); break; default: @@ -219,7 +155,8 @@ this.AlarmService = { } }, - _sendAsyncMessage: function _sendAsyncMessage(aMessageManager, aMessageName, aSuccess, aRequestId, aData) { + _sendAsyncMessage: function _sendAsyncMessage(aMessageManager, aMessageName, + aSuccess, aRequestId, aData) { debug("_sendAsyncMessage()"); if (!aMessageManager) { @@ -231,14 +168,14 @@ this.AlarmService = { switch (aMessageName) { case "Add": - json = aSuccess ? - { requestId: aRequestId, id: aData } : + json = aSuccess ? + { requestId: aRequestId, id: aData } : { requestId: aRequestId, errorMsg: aData }; break; case "GetAll": - json = aSuccess ? - { requestId: aRequestId, alarms: aData } : + json = aSuccess ? + { requestId: aRequestId, alarms: aData } : { requestId: aRequestId, errorMsg: aData }; break; @@ -247,14 +184,16 @@ this.AlarmService = { break; } - aMessageManager.sendAsyncMessage("AlarmsManager:" + aMessageName + ":Return:" + (aSuccess ? "OK" : "KO"), json); + aMessageManager.sendAsyncMessage("AlarmsManager:" + aMessageName + + ":Return:" + (aSuccess ? "OK" : "KO"), json); }, - _removeAlarmFromDb: function _removeAlarmFromDb(aId, aManifestURL, aRemoveSuccessCb) { + _removeAlarmFromDb: function _removeAlarmFromDb(aId, aManifestURL, + aRemoveSuccessCb) { debug("_removeAlarmFromDb()"); - // If the aRemoveSuccessCb is undefined or null, set a - // dummy callback for it which is needed for _db.remove() + // If the aRemoveSuccessCb is undefined or null, set a dummy callback for + // it which is needed for _db.remove(). if (!aRemoveSuccessCb) { aRemoveSuccessCb = function removeSuccessCb() { debug("Remove alarm from DB successfully."); @@ -271,28 +210,49 @@ this.AlarmService = { ); }, + /** + * Create a copy of the alarm that does not expose internal fields to + * receivers and sticks to the public |respectTimezone| API rather than the + * boolean |ignoreTimezone| field. + */ + _publicAlarm: function _publicAlarm(aAlarm) { + let alarm = { + "id": aAlarm.id, + "date": aAlarm.date, + "respectTimezone": aAlarm.ignoreTimezone ? + "ignoreTimezone" : "honorTimezone", + "data": aAlarm.data + }; + + return alarm; + }, + _fireSystemMessage: function _fireSystemMessage(aAlarm) { debug("Fire system message: " + JSON.stringify(aAlarm)); let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null); let pageURI = Services.io.newURI(aAlarm.pageURL, null, null); - // We don't need to expose everything to the web content. - let alarm = { "id": aAlarm.id, - "date": aAlarm.date, - "respectTimezone": aAlarm.ignoreTimezone ? - "ignoreTimezone" : "honorTimezone", - "data": aAlarm.data }; + messenger.sendMessage("alarm", this._publicAlarm(aAlarm), + pageURI, manifestURI); + }, - messenger.sendMessage("alarm", alarm, pageURI, manifestURI); + _notifyAlarmObserver: function _notifyAlarmObserver(aAlarm) { + debug("_notifyAlarmObserver()"); + + if (aAlarm.manifestURL) { + this._fireSystemMessage(aAlarm); + } else if (typeof aAlarm.alarmFiredCb === "function") { + aAlarm.alarmFiredCb(this._publicAlarm(aAlarm)); + } }, _onAlarmFired: function _onAlarmFired() { debug("_onAlarmFired()"); if (this._currentAlarm) { - this._fireSystemMessage(this._currentAlarm); this._removeAlarmFromDb(this._currentAlarm.id, null); + this._notifyAlarmObserver(this._currentAlarm); this._currentAlarm = null; } @@ -302,11 +262,11 @@ this.AlarmService = { let nextAlarm = alarmQueue.shift(); let nextAlarmTime = this._getAlarmTime(nextAlarm); - // If the next alarm has been expired, directly - // fire system message for it instead of setting it. + // If the next alarm has been expired, directly notify the observer. + // it instead of setting it. if (nextAlarmTime <= Date.now()) { - this._fireSystemMessage(nextAlarm); this._removeAlarmFromDb(nextAlarm.id, null); + this._notifyAlarmObserver(nextAlarm); } else { this._currentAlarm = nextAlarm; break; @@ -328,25 +288,26 @@ this.AlarmService = { this._db.getAll( null, function getAllSuccessCb(aAlarms) { - debug("Callback after getting alarms from database: " + JSON.stringify(aAlarms)); + debug("Callback after getting alarms from database: " + + JSON.stringify(aAlarms)); - // clear any alarms set or queued in the cache + // Clear any alarms set or queued in the cache. let alarmQueue = this._alarmQueue; alarmQueue.length = 0; this._currentAlarm = null; - // Only restore the alarm that's not yet expired; otherwise, - // fire a system message for it and remove it from database. + // Only restore the alarm that's not yet expired; otherwise, remove it + // from the database and notify the observer. aAlarms.forEach(function addAlarm(aAlarm) { if (this._getAlarmTime(aAlarm) > Date.now()) { alarmQueue.push(aAlarm); } else { - this._fireSystemMessage(aAlarm); this._removeAlarmFromDb(aAlarm.id, null); + this._notifyAlarmObserver(aAlarm); } }.bind(this)); - // set the next alarm from queue + // Set the next alarm from queue. if (alarmQueue.length) { alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); this._currentAlarm = alarmQueue.shift(); @@ -363,14 +324,11 @@ this.AlarmService = { _getAlarmTime: function _getAlarmTime(aAlarm) { let alarmTime = (new Date(aAlarm.date)).getTime(); - // For an alarm specified with "ignoreTimezone", - // it must be fired respect to the user's timezone. - // Supposing an alarm was set at 7:00pm at Tokyo, - // it must be gone off at 7:00pm respect to Paris' - // local time when the user is located at Paris. - // We can adjust the alarm UTC time by calculating - // the difference of the orginal timezone and the - // current timezone. + // For an alarm specified with "ignoreTimezone", it must be fired respect + // to the user's timezone. Supposing an alarm was set at 7:00pm at Tokyo, + // it must be gone off at 7:00pm respect to Paris' local time when the user + // is located at Paris. We can adjust the alarm UTC time by calculating + // the difference of the orginal timezone and the current timezone. if (aAlarm.ignoreTimezone) alarmTime += (this._currentTimezoneOffset - aAlarm.timezoneOffset) * 60000; @@ -385,6 +343,154 @@ this.AlarmService = { debug("Current alarm: " + JSON.stringify(this._currentAlarm)); debug("Alarm queue: " + JSON.stringify(this._alarmQueue)); }, + + /** + * + * Add a new alarm. This will set the RTC to fire at the selected date and + * notify the caller. Notifications are delivered via System Messages if the + * alarm is added on behalf of a app. Otherwise aAlarmFiredCb is called. + * + * @param object aNewAlarm + * Should contain the following literal properties: + * - |date| date: when the alarm should timeout. + * - |ignoreTimezone| boolean: See [1] for the details. + * - |manifestURL| string: Manifest of app on whose behalf the alarm + * is added. + * - |pageURL| string: The page in the app that receives the system + * message. + * - |data| object [optional]: Data that can be stored in DB. + * @param function aAlarmFiredCb + * Callback function invoked when the alarm is fired. + * It receives a single argument, the alarm object. + * May be null. + * @param function aSuccessCb + * Callback function to receive an alarm ID (number). + * @param function aErrorCb + * Callback function to receive an error message (string). + * @returns void + * + * Notes: + * [1] https://wiki.mozilla.org/WebAPI/AlarmAPI#Proposed_API + */ + + add: function(aNewAlarm, aAlarmFiredCb, aSuccessCb, aErrorCb) { + debug("add(" + aNewAlarm.date + ")"); + + aSuccessCb = aSuccessCb || function() {}; + aErrorCb = aErrorCb || function() {}; + + if (!aNewAlarm) { + aErrorCb("alarm is null"); + return; + } + + aNewAlarm['timezoneOffset'] = this._currentTimezoneOffset; + let aNewAlarmTime = this._getAlarmTime(aNewAlarm); + if (aNewAlarmTime <= Date.now()) { + debug("Adding a alarm that has past time."); + this._debugCurrentAlarm(); + aErrorCb("InvalidStateError"); + return; + } + + this._db.add( + aNewAlarm, + function addSuccessCb(aNewId) { + debug("Callback after adding alarm in database."); + + aNewAlarm['id'] = aNewId; + + // Now that the alarm has been added to the database, we can tack on + // the non-serializable callback to the in-memory object. + aNewAlarm['alarmFiredCb'] = aAlarmFiredCb; + + // If there is no alarm being set in system, set the new alarm. + if (this._currentAlarm == null) { + this._currentAlarm = aNewAlarm; + this._debugCurrentAlarm(); + aSuccessCb(aNewId); + return; + } + + // If the new alarm is earlier than the current alarm, swap them and + // push the previous alarm back to queue. + let alarmQueue = this._alarmQueue; + let currentAlarmTime = this._getAlarmTime(this._currentAlarm); + if (aNewAlarmTime < currentAlarmTime) { + alarmQueue.unshift(this._currentAlarm); + this._currentAlarm = aNewAlarm; + this._debugCurrentAlarm(); + aSuccessCb(aNewId); + return; + } + + // Push the new alarm in the queue. + alarmQueue.push(aNewAlarm); + alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); + this._debugCurrentAlarm(); + aSuccessCb(aNewId); + }.bind(this), + function addErrorCb(aErrorMsg) { + aErrorCb(aErrorMsg); + }.bind(this) + ); + }, + + /* + * Remove the alarm associated with an ID. + * + * @param number aAlarmId + * The ID of the alarm to be removed. + * @param string aManifestURL + * Manifest URL for application which added the alarm. (Optional) + * @returns void + */ + remove: function(aAlarmId, aManifestURL) { + debug("remove(" + aAlarmId + ", " + aManifestURL + ")"); + this._removeAlarmFromDb( + aAlarmId, + aManifestURL, + function removeSuccessCb() { + debug("Callback after removing alarm from database."); + + // If there are no alarms set, nothing to do. + if (!this._currentAlarm) { + debug("No alarms set."); + return; + } + + // Check if the alarm to be removed is in the queue and whether it + // belongs to the requesting app. + let alarmQueue = this._alarmQueue; + if (this._currentAlarm.id != aAlarmId || + this._currentAlarm.manifestURL != aManifestURL) { + + for (let i = 0; i < alarmQueue.length; i++) { + if (alarmQueue[i].id == aAlarmId && + alarmQueue[i].manifestURL == aManifestURL) { + + alarmQueue.splice(i, 1); + break; + } + } + this._debugCurrentAlarm(); + return; + } + + // The alarm to be removed is the current alarm reset the next alarm + // from queue if any. + if (alarmQueue.length) { + this._currentAlarm = alarmQueue.shift(); + this._debugCurrentAlarm(); + return; + } + + // No alarm waiting to be set in the queue. + this._currentAlarm = null; + this._debugCurrentAlarm(); + }.bind(this) + ); + } } AlarmService.init();