Bug 862322 - Allow AlarmService to be used from Gecko code. r=gene

This commit is contained in:
Nikhil Marathe 2013-05-01 21:32:49 +05:30
parent 5a4ff80d3f
commit 541f47ab68

View File

@ -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();