michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: /* static functions */ michael@0: const DEBUG = false; michael@0: michael@0: function debug(aStr) { michael@0: if (DEBUG) michael@0: dump("AlarmService: " + aStr + "\n"); michael@0: } michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/AlarmDB.jsm"); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["AlarmService"]; michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "appsService", function() { michael@0: return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "ppmm", michael@0: "@mozilla.org/parentprocessmessagemanager;1", michael@0: "nsIMessageListenerManager"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "messenger", function() { michael@0: return Cc["@mozilla.org/system-message-internal;1"].getService(Ci.nsISystemMessagesInternal); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "powerManagerService", function() { michael@0: return Cc["@mozilla.org/power/powermanagerservice;1"].getService(Ci.nsIPowerManagerService); michael@0: }); michael@0: michael@0: /** michael@0: * AlarmService provides an API to schedule alarms using the device's RTC. michael@0: * michael@0: * AlarmService is primarily used by the mozAlarms API (navigator.mozAlarms) michael@0: * which uses IPC to communicate with the service. michael@0: * michael@0: * AlarmService can also be used by Gecko code by importing the module and then michael@0: * using AlarmService.add() and AlarmService.remove(). Only Gecko code running michael@0: * in the parent process should do this. michael@0: */ michael@0: michael@0: this.AlarmService = { michael@0: init: function init() { michael@0: debug("init()"); michael@0: Services.obs.addObserver(this, "profile-change-teardown", false); michael@0: Services.obs.addObserver(this, "webapps-clear-data",false); michael@0: michael@0: this._currentTimezoneOffset = (new Date()).getTimezoneOffset(); michael@0: michael@0: let alarmHalService = michael@0: this._alarmHalService = Cc["@mozilla.org/alarmHalService;1"] michael@0: .getService(Ci.nsIAlarmHalService); michael@0: michael@0: alarmHalService.setAlarmFiredCb(this._onAlarmFired.bind(this)); michael@0: alarmHalService.setTimezoneChangedCb(this._onTimezoneChanged.bind(this)); michael@0: michael@0: // Add the messages to be listened to. michael@0: this._messages = ["AlarmsManager:GetAll", michael@0: "AlarmsManager:Add", michael@0: "AlarmsManager:Remove"]; michael@0: this._messages.forEach(function addMessage(msgName) { michael@0: ppmm.addMessageListener(msgName, this); michael@0: }.bind(this)); michael@0: michael@0: // Set the indexeddb database. michael@0: this._db = new AlarmDB(); michael@0: this._db.init(); michael@0: michael@0: // Variable to save alarms waiting to be set. michael@0: this._alarmQueue = []; michael@0: michael@0: this._restoreAlarmsFromDb(); michael@0: }, michael@0: michael@0: // Getter/setter to access the current alarm set in system. michael@0: _alarm: null, michael@0: get _currentAlarm() { michael@0: return this._alarm; michael@0: }, michael@0: set _currentAlarm(aAlarm) { michael@0: this._alarm = aAlarm; michael@0: if (!aAlarm) { michael@0: return; michael@0: } michael@0: michael@0: let alarmTimeInMs = this._getAlarmTime(aAlarm); michael@0: let ns = (alarmTimeInMs % 1000) * 1000000; michael@0: if (!this._alarmHalService.setAlarm(alarmTimeInMs / 1000, ns)) { michael@0: throw Components.results.NS_ERROR_FAILURE; michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function receiveMessage(aMessage) { michael@0: debug("receiveMessage(): " + aMessage.name); michael@0: let json = aMessage.json; michael@0: michael@0: // To prevent the hacked child process from sending commands to parent michael@0: // to schedule alarms, we need to check its permission and manifest URL. michael@0: if (this._messages.indexOf(aMessage.name) != -1) { michael@0: if (!aMessage.target.assertPermission("alarms")) { michael@0: debug("Got message from a child process with no 'alarms' permission."); michael@0: return null; michael@0: } michael@0: if (!aMessage.target.assertContainApp(json.manifestURL)) { michael@0: debug("Got message from a child process containing illegal manifest URL."); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender); michael@0: switch (aMessage.name) { michael@0: case "AlarmsManager:GetAll": michael@0: this._db.getAll( michael@0: json.manifestURL, michael@0: function getAllSuccessCb(aAlarms) { michael@0: debug("Callback after getting alarms from database: " + michael@0: JSON.stringify(aAlarms)); michael@0: this._sendAsyncMessage(mm, "GetAll", true, json.requestId, aAlarms); michael@0: }.bind(this), michael@0: function getAllErrorCb(aErrorMsg) { michael@0: this._sendAsyncMessage(mm, "GetAll", false, json.requestId, aErrorMsg); michael@0: }.bind(this) michael@0: ); michael@0: break; michael@0: michael@0: case "AlarmsManager:Add": michael@0: // Prepare a record for the new alarm to be added. michael@0: let newAlarm = { michael@0: date: json.date, michael@0: ignoreTimezone: json.ignoreTimezone, michael@0: data: json.data, michael@0: pageURL: json.pageURL, michael@0: manifestURL: json.manifestURL michael@0: }; michael@0: michael@0: this.add(newAlarm, null, michael@0: // Receives the alarm ID as the last argument. michael@0: this._sendAsyncMessage.bind(this, mm, "Add", true, json.requestId), michael@0: // Receives the error message as the last argument. michael@0: this._sendAsyncMessage.bind(this, mm, "Add", false, json.requestId) michael@0: ); michael@0: break; michael@0: michael@0: case "AlarmsManager:Remove": michael@0: this.remove(json.id, json.manifestURL); michael@0: break; michael@0: michael@0: default: michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _sendAsyncMessage: function _sendAsyncMessage(aMessageManager, aMessageName, michael@0: aSuccess, aRequestId, aData) { michael@0: debug("_sendAsyncMessage()"); michael@0: michael@0: if (!aMessageManager) { michael@0: debug("Invalid message manager: null"); michael@0: throw Components.results.NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: let json = null; michael@0: switch (aMessageName) michael@0: { michael@0: case "Add": michael@0: json = aSuccess ? michael@0: { requestId: aRequestId, id: aData } : michael@0: { requestId: aRequestId, errorMsg: aData }; michael@0: break; michael@0: michael@0: case "GetAll": michael@0: json = aSuccess ? michael@0: { requestId: aRequestId, alarms: aData } : michael@0: { requestId: aRequestId, errorMsg: aData }; michael@0: break; michael@0: michael@0: default: michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: break; michael@0: } michael@0: michael@0: aMessageManager.sendAsyncMessage("AlarmsManager:" + aMessageName + michael@0: ":Return:" + (aSuccess ? "OK" : "KO"), json); michael@0: }, michael@0: michael@0: _removeAlarmFromDb: function _removeAlarmFromDb(aId, aManifestURL, michael@0: aRemoveSuccessCb) { michael@0: debug("_removeAlarmFromDb()"); michael@0: michael@0: // If the aRemoveSuccessCb is undefined or null, set a dummy callback for michael@0: // it which is needed for _db.remove(). michael@0: if (!aRemoveSuccessCb) { michael@0: aRemoveSuccessCb = function removeSuccessCb() { michael@0: debug("Remove alarm from DB successfully."); michael@0: }; michael@0: } michael@0: michael@0: this._db.remove( michael@0: aId, michael@0: aManifestURL, michael@0: aRemoveSuccessCb, michael@0: function removeErrorCb(aErrorMsg) { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: /** michael@0: * Create a copy of the alarm that does not expose internal fields to michael@0: * receivers and sticks to the public |respectTimezone| API rather than the michael@0: * boolean |ignoreTimezone| field. michael@0: */ michael@0: _publicAlarm: function _publicAlarm(aAlarm) { michael@0: let alarm = { michael@0: "id": aAlarm.id, michael@0: "date": aAlarm.date, michael@0: "respectTimezone": aAlarm.ignoreTimezone ? michael@0: "ignoreTimezone" : "honorTimezone", michael@0: "data": aAlarm.data michael@0: }; michael@0: michael@0: return alarm; michael@0: }, michael@0: michael@0: _fireSystemMessage: function _fireSystemMessage(aAlarm) { michael@0: debug("Fire system message: " + JSON.stringify(aAlarm)); michael@0: michael@0: let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null); michael@0: let pageURI = Services.io.newURI(aAlarm.pageURL, null, null); michael@0: michael@0: messenger.sendMessage("alarm", this._publicAlarm(aAlarm), michael@0: pageURI, manifestURI); michael@0: }, michael@0: michael@0: _notifyAlarmObserver: function _notifyAlarmObserver(aAlarm) { michael@0: debug("_notifyAlarmObserver()"); michael@0: michael@0: if (aAlarm.manifestURL) { michael@0: this._fireSystemMessage(aAlarm); michael@0: } else if (typeof aAlarm.alarmFiredCb === "function") { michael@0: aAlarm.alarmFiredCb(this._publicAlarm(aAlarm)); michael@0: } michael@0: }, michael@0: michael@0: _onAlarmFired: function _onAlarmFired() { michael@0: debug("_onAlarmFired()"); michael@0: michael@0: if (this._currentAlarm) { michael@0: this._removeAlarmFromDb(this._currentAlarm.id, null); michael@0: this._notifyAlarmObserver(this._currentAlarm); michael@0: this._currentAlarm = null; michael@0: } michael@0: michael@0: // Reset the next alarm from the queue. michael@0: let alarmQueue = this._alarmQueue; michael@0: while (alarmQueue.length > 0) { michael@0: let nextAlarm = alarmQueue.shift(); michael@0: let nextAlarmTime = this._getAlarmTime(nextAlarm); michael@0: michael@0: // If the next alarm has been expired, directly notify the observer. michael@0: // it instead of setting it. michael@0: if (nextAlarmTime <= Date.now()) { michael@0: this._removeAlarmFromDb(nextAlarm.id, null); michael@0: this._notifyAlarmObserver(nextAlarm); michael@0: } else { michael@0: this._currentAlarm = nextAlarm; michael@0: break; michael@0: } michael@0: } michael@0: this._debugCurrentAlarm(); michael@0: }, michael@0: michael@0: _onTimezoneChanged: function _onTimezoneChanged(aTimezoneOffset) { michael@0: debug("_onTimezoneChanged()"); michael@0: michael@0: this._currentTimezoneOffset = aTimezoneOffset; michael@0: this._restoreAlarmsFromDb(); michael@0: }, michael@0: michael@0: _restoreAlarmsFromDb: function _restoreAlarmsFromDb() { michael@0: debug("_restoreAlarmsFromDb()"); michael@0: michael@0: this._db.getAll( michael@0: null, michael@0: function getAllSuccessCb(aAlarms) { michael@0: debug("Callback after getting alarms from database: " + michael@0: JSON.stringify(aAlarms)); michael@0: michael@0: // Clear any alarms set or queued in the cache. michael@0: let alarmQueue = this._alarmQueue; michael@0: alarmQueue.length = 0; michael@0: this._currentAlarm = null; michael@0: michael@0: // Only restore the alarm that's not yet expired; otherwise, remove it michael@0: // from the database and notify the observer. michael@0: aAlarms.forEach(function addAlarm(aAlarm) { michael@0: if (this._getAlarmTime(aAlarm) > Date.now()) { michael@0: alarmQueue.push(aAlarm); michael@0: } else { michael@0: this._removeAlarmFromDb(aAlarm.id, null); michael@0: this._notifyAlarmObserver(aAlarm); michael@0: } michael@0: }.bind(this)); michael@0: michael@0: // Set the next alarm from queue. michael@0: if (alarmQueue.length) { michael@0: alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); michael@0: this._currentAlarm = alarmQueue.shift(); michael@0: } michael@0: michael@0: this._debugCurrentAlarm(); michael@0: }.bind(this), michael@0: function getAllErrorCb(aErrorMsg) { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: _getAlarmTime: function _getAlarmTime(aAlarm) { michael@0: // Avoid casting a Date object to a Date again to michael@0: // preserve milliseconds. See bug 810973. michael@0: let alarmTime; michael@0: if (aAlarm.date instanceof Date) { michael@0: alarmTime = aAlarm.date.getTime(); michael@0: } else { michael@0: alarmTime = (new Date(aAlarm.date)).getTime(); michael@0: } michael@0: michael@0: // For an alarm specified with "ignoreTimezone", it must be fired respect michael@0: // to the user's timezone. Supposing an alarm was set at 7:00pm at Tokyo, michael@0: // it must be gone off at 7:00pm respect to Paris' local time when the user michael@0: // is located at Paris. We can adjust the alarm UTC time by calculating michael@0: // the difference of the orginal timezone and the current timezone. michael@0: if (aAlarm.ignoreTimezone) { michael@0: alarmTime += (this._currentTimezoneOffset - aAlarm.timezoneOffset) * 60000; michael@0: } michael@0: return alarmTime; michael@0: }, michael@0: michael@0: _sortAlarmByTimeStamps: function _sortAlarmByTimeStamps(aAlarm1, aAlarm2) { michael@0: return this._getAlarmTime(aAlarm1) - this._getAlarmTime(aAlarm2); michael@0: }, michael@0: michael@0: _debugCurrentAlarm: function _debugCurrentAlarm() { michael@0: debug("Current alarm: " + JSON.stringify(this._currentAlarm)); michael@0: debug("Alarm queue: " + JSON.stringify(this._alarmQueue)); michael@0: }, michael@0: michael@0: /** michael@0: * michael@0: * Add a new alarm. This will set the RTC to fire at the selected date and michael@0: * notify the caller. Notifications are delivered via System Messages if the michael@0: * alarm is added on behalf of a app. Otherwise aAlarmFiredCb is called. michael@0: * michael@0: * @param object aNewAlarm michael@0: * Should contain the following literal properties: michael@0: * - |date| date: when the alarm should timeout. michael@0: * - |ignoreTimezone| boolean: See [1] for the details. michael@0: * - |manifestURL| string: Manifest of app on whose behalf the alarm michael@0: * is added. michael@0: * - |pageURL| string: The page in the app that receives the system michael@0: * message. michael@0: * - |data| object [optional]: Data that can be stored in DB. michael@0: * @param function aAlarmFiredCb michael@0: * Callback function invoked when the alarm is fired. michael@0: * It receives a single argument, the alarm object. michael@0: * May be null. michael@0: * @param function aSuccessCb michael@0: * Callback function to receive an alarm ID (number). michael@0: * @param function aErrorCb michael@0: * Callback function to receive an error message (string). michael@0: * @returns void michael@0: * michael@0: * Notes: michael@0: * [1] https://wiki.mozilla.org/WebAPI/AlarmAPI#Proposed_API michael@0: */ michael@0: michael@0: add: function(aNewAlarm, aAlarmFiredCb, aSuccessCb, aErrorCb) { michael@0: debug("add(" + aNewAlarm.date + ")"); michael@0: michael@0: aSuccessCb = aSuccessCb || function() {}; michael@0: aErrorCb = aErrorCb || function() {}; michael@0: michael@0: if (!aNewAlarm) { michael@0: aErrorCb("alarm is null"); michael@0: return; michael@0: } michael@0: michael@0: if (!aNewAlarm.date) { michael@0: aErrorCb("alarm.date is null"); michael@0: return; michael@0: } michael@0: michael@0: aNewAlarm['timezoneOffset'] = this._currentTimezoneOffset; michael@0: michael@0: this._db.add( michael@0: aNewAlarm, michael@0: function addSuccessCb(aNewId) { michael@0: debug("Callback after adding alarm in database."); michael@0: michael@0: aNewAlarm['id'] = aNewId; michael@0: michael@0: // Now that the alarm has been added to the database, we can tack on michael@0: // the non-serializable callback to the in-memory object. michael@0: aNewAlarm['alarmFiredCb'] = aAlarmFiredCb; michael@0: michael@0: // If there is no alarm being set in system, set the new alarm. michael@0: if (this._currentAlarm == null) { michael@0: this._currentAlarm = aNewAlarm; michael@0: this._debugCurrentAlarm(); michael@0: aSuccessCb(aNewId); michael@0: return; michael@0: } michael@0: michael@0: // If the new alarm is earlier than the current alarm, swap them and michael@0: // push the previous alarm back to queue. michael@0: let alarmQueue = this._alarmQueue; michael@0: let aNewAlarmTime = this._getAlarmTime(aNewAlarm); michael@0: let currentAlarmTime = this._getAlarmTime(this._currentAlarm); michael@0: if (aNewAlarmTime < currentAlarmTime) { michael@0: alarmQueue.unshift(this._currentAlarm); michael@0: this._currentAlarm = aNewAlarm; michael@0: this._debugCurrentAlarm(); michael@0: aSuccessCb(aNewId); michael@0: return; michael@0: } michael@0: michael@0: // Push the new alarm in the queue. michael@0: alarmQueue.push(aNewAlarm); michael@0: alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); michael@0: this._debugCurrentAlarm(); michael@0: aSuccessCb(aNewId); michael@0: }.bind(this), michael@0: function addErrorCb(aErrorMsg) { michael@0: aErrorCb(aErrorMsg); michael@0: }.bind(this) michael@0: ); michael@0: }, michael@0: michael@0: /* michael@0: * Remove the alarm associated with an ID. michael@0: * michael@0: * @param number aAlarmId michael@0: * The ID of the alarm to be removed. michael@0: * @param string aManifestURL michael@0: * Manifest URL for application which added the alarm. (Optional) michael@0: * @returns void michael@0: */ michael@0: remove: function(aAlarmId, aManifestURL) { michael@0: debug("remove(" + aAlarmId + ", " + aManifestURL + ")"); michael@0: this._removeAlarmFromDb( michael@0: aAlarmId, michael@0: aManifestURL, michael@0: function removeSuccessCb() { michael@0: debug("Callback after removing alarm from database."); michael@0: michael@0: // If there are no alarms set, nothing to do. michael@0: if (!this._currentAlarm) { michael@0: debug("No alarms set."); michael@0: return; michael@0: } michael@0: michael@0: // Check if the alarm to be removed is in the queue and whether it michael@0: // belongs to the requesting app. michael@0: let alarmQueue = this._alarmQueue; michael@0: if (this._currentAlarm.id != aAlarmId || michael@0: this._currentAlarm.manifestURL != aManifestURL) { michael@0: michael@0: for (let i = 0; i < alarmQueue.length; i++) { michael@0: if (alarmQueue[i].id == aAlarmId && michael@0: alarmQueue[i].manifestURL == aManifestURL) { michael@0: michael@0: alarmQueue.splice(i, 1); michael@0: break; michael@0: } michael@0: } michael@0: this._debugCurrentAlarm(); michael@0: return; michael@0: } michael@0: michael@0: // The alarm to be removed is the current alarm reset the next alarm michael@0: // from queue if any. michael@0: if (alarmQueue.length) { michael@0: this._currentAlarm = alarmQueue.shift(); michael@0: this._debugCurrentAlarm(); michael@0: return; michael@0: } michael@0: michael@0: // No alarm waiting to be set in the queue. michael@0: this._currentAlarm = null; michael@0: this._debugCurrentAlarm(); michael@0: }.bind(this) michael@0: ); michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "profile-change-teardown": michael@0: this.uninit(); michael@0: break; michael@0: case "webapps-clear-data": michael@0: let params = michael@0: aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams); michael@0: if (!params) { michael@0: debug("Error! Fail to remove alarms for an uninstalled app."); michael@0: return; michael@0: } michael@0: michael@0: // Only remove alarms for apps. michael@0: if (params.browserOnly) { michael@0: return; michael@0: } michael@0: michael@0: let manifestURL = appsService.getManifestURLByLocalId(params.appId); michael@0: if (!manifestURL) { michael@0: debug("Error! Fail to remove alarms for an uninstalled app."); michael@0: return; michael@0: } michael@0: michael@0: this._db.getAll( michael@0: manifestURL, michael@0: function getAllSuccessCb(aAlarms) { michael@0: aAlarms.forEach(function removeAlarm(aAlarm) { michael@0: this.remove(aAlarm.id, manifestURL); michael@0: }, this); michael@0: }.bind(this), michael@0: function getAllErrorCb(aErrorMsg) { michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: ); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: uninit: function uninit() { michael@0: debug("uninit()"); michael@0: Services.obs.removeObserver(this, "profile-change-teardown"); michael@0: Services.obs.removeObserver(this, "webapps-clear-data"); michael@0: michael@0: this._messages.forEach(function(aMsgName) { michael@0: ppmm.removeMessageListener(aMsgName, this); michael@0: }.bind(this)); michael@0: ppmm = null; michael@0: michael@0: if (this._db) { michael@0: this._db.close(); michael@0: } michael@0: this._db = null; michael@0: michael@0: this._alarmHalService = null; michael@0: } michael@0: } michael@0: michael@0: AlarmService.init();