gecko-dev/dom/alarm/AlarmService.jsm

634 lines
20 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AlarmDB.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
function getLogger() {
var logger = Log.repository.getLogger("AlarmsService");
logger.addAppender(new Log.DumpAppender(new Log.BasicFormatter()));
logger.level = Log.Level.Debug;
return logger;
}
const logger = getLogger();
/* Only log in B2G */
function debug(aStr) {
AppConstants.MOZ_B2G && logger.debug(aStr);
}
this.EXPORTED_SYMBOLS = ["AlarmService"];
XPCOMUtils.defineLazyGetter(this, "appsService", function() {
return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
});
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
"@mozilla.org/parentprocessmessagemanager;1",
"nsIMessageListenerManager");
XPCOMUtils.defineLazyGetter(this, "messenger", function() {
return Cc["@mozilla.org/system-message-internal;1"]
.getService(Ci.nsISystemMessagesInternal);
});
XPCOMUtils.defineLazyGetter(this, "powerManagerService", function() {
return Cc["@mozilla.org/power/powermanagerservice;1"]
.getService(Ci.nsIPowerManagerService);
});
/**
* 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 = {
lastChromeId: 0,
init: function init() {
debug("init()");
Services.obs.addObserver(this, "profile-change-teardown", false);
Services.obs.addObserver(this, "webapps-clear-data",false);
this._currentTimezoneOffset = (new Date()).getTimezoneOffset();
let alarmHalService = this._alarmHalService =
Cc["@mozilla.org/alarmHalService;1"].getService(Ci.nsIAlarmHalService);
alarmHalService.setAlarmFiredCb(this._onAlarmFired.bind(this));
alarmHalService.setTimezoneChangedCb(this._onTimezoneChanged.bind(this));
alarmHalService.setSystemClockChangedCb(
this._onSystemClockChanged.bind(this));
// Add the messages to be listened to.
this._messages = ["AlarmsManager:GetAll",
"AlarmsManager:Add",
"AlarmsManager:Remove"];
this._messages.forEach(function addMessage(msgName) {
ppmm.addMessageListener(msgName, this);
}.bind(this));
// Set the indexeddb database.
this._db = new AlarmDB();
this._db.init();
// Variable to save alarms waiting to be set.
this._alarmQueue = [];
this._restoreAlarmsFromDb();
},
// Getter/setter to access the current alarm set in system.
_alarm: null,
get _currentAlarm() {
return this._alarm;
},
set _currentAlarm(aAlarm) {
this._alarm = aAlarm;
if (!aAlarm) {
return;
}
let alarmTimeInMs = this._getAlarmTime(aAlarm);
let ns = (alarmTimeInMs % 1000) * 1000000;
if (!this._alarmHalService.setAlarm(alarmTimeInMs / 1000, ns)) {
throw Components.results.NS_ERROR_FAILURE;
}
},
receiveMessage: function receiveMessage(aMessage) {
debug("receiveMessage(): " + aMessage.name);
let json = aMessage.json;
// To prevent the hacked child process from sending commands to parent
// to schedule alarms, we need to check its permission and manifest URL.
if (this._messages.indexOf(aMessage.name) != -1) {
if (!aMessage.target.assertPermission("alarms")) {
debug("Got message from a child process with no 'alarms' permission.");
return null;
}
if (!aMessage.target.assertContainApp(json.manifestURL)) {
debug("Got message from a child process containing illegal manifest URL.");
return null;
}
}
let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender);
switch (aMessage.name) {
case "AlarmsManager:GetAll":
this._db.getAll(json.manifestURL,
function getAllSuccessCb(aAlarms) {
debug("Callback after getting alarms from database: " +
JSON.stringify(aAlarms));
this._sendAsyncMessage(mm, "GetAll", true, json.requestId, aAlarms);
}.bind(this),
function getAllErrorCb(aErrorMsg) {
this._sendAsyncMessage(mm, "GetAll", false, json.requestId, aErrorMsg);
}.bind(this));
break;
case "AlarmsManager:Add":
// Prepare a record for the new alarm to be added.
let newAlarm = { date: json.date,
ignoreTimezone: json.ignoreTimezone,
data: json.data,
pageURL: json.pageURL,
manifestURL: json.manifestURL };
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.remove(json.id, json.manifestURL);
break;
default:
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
break;
}
},
_sendAsyncMessage: function _sendAsyncMessage(aMessageManager, aMessageName,
aSuccess, aRequestId, aData) {
debug("_sendAsyncMessage()");
if (!aMessageManager) {
debug("Invalid message manager: null");
throw Components.results.NS_ERROR_FAILURE;
}
let json = null;
switch (aMessageName) {
case "Add":
json = aSuccess ?
{ requestId: aRequestId, id: aData } :
{ requestId: aRequestId, errorMsg: aData };
break;
case "GetAll":
json = aSuccess ?
{ requestId: aRequestId, alarms: aData } :
{ requestId: aRequestId, errorMsg: aData };
break;
default:
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
break;
}
aMessageManager.sendAsyncMessage("AlarmsManager:" + aMessageName +
":Return:" + (aSuccess ? "OK" : "KO"),
json);
},
_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 (!aRemoveSuccessCb) {
aRemoveSuccessCb = function removeSuccessCb() {
debug("Remove alarm from DB successfully.");
};
}
// Is this a chrome alarm?
if (aId < 0) {
aRemoveSuccessCb();
return;
}
this._db.remove(aId, aManifestURL, aRemoveSuccessCb,
function removeErrorCb(aErrorMsg) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
});
},
/**
* 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);
messenger.sendMessage("alarm",
this._publicAlarm(aAlarm),
pageURI,
manifestURI);
},
_notifyAlarmObserver: function _notifyAlarmObserver(aAlarm) {
debug("_notifyAlarmObserver()");
let wakeLock = powerManagerService.newWakeLock("cpu");
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(() => {
debug("_notifyAlarmObserver - timeout()");
if (aAlarm.manifestURL) {
this._fireSystemMessage(aAlarm);
} else if (typeof aAlarm.alarmFiredCb === "function") {
aAlarm.alarmFiredCb(this._publicAlarm(aAlarm));
}
wakeLock.unlock();
}, 0, Ci.nsITimer.TYPE_ONE_SHOT);
},
_onAlarmFired: function _onAlarmFired() {
debug("_onAlarmFired()");
if (this._currentAlarm) {
let currentAlarmTime = this._getAlarmTime(this._currentAlarm);
// If a alarm fired before the actual time that the current
// alarm should occur, we reset this current alarm.
if (currentAlarmTime > Date.now()) {
let currentAlarm = this._currentAlarm;
this._currentAlarm = currentAlarm;
this._debugCurrentAlarm();
return;
}
this._removeAlarmFromDb(this._currentAlarm.id, null);
// We need to clear the current alarm before notifying because chrome
// alarms may add a new alarm during their callback, and we do not want
// to clobber it.
let firingAlarm = this._currentAlarm;
this._currentAlarm = null;
this._notifyAlarmObserver(firingAlarm);
}
// Reset the next alarm from the queue.
let alarmQueue = this._alarmQueue;
while (alarmQueue.length > 0) {
let nextAlarm = alarmQueue.shift();
let nextAlarmTime = this._getAlarmTime(nextAlarm);
// If the next alarm has been expired, directly notify the observer.
// it instead of setting it.
if (nextAlarmTime <= Date.now()) {
this._removeAlarmFromDb(nextAlarm.id, null);
this._notifyAlarmObserver(nextAlarm);
} else {
this._currentAlarm = nextAlarm;
break;
}
}
this._debugCurrentAlarm();
},
_onTimezoneChanged: function _onTimezoneChanged(aTimezoneOffset) {
debug("_onTimezoneChanged()");
this._currentTimezoneOffset = aTimezoneOffset;
this._restoreAlarmsFromDb();
},
_onSystemClockChanged: function _onSystemClockChanged(aClockDeltaMS) {
debug("_onSystemClockChanged");
this._restoreAlarmsFromDb();
},
_restoreAlarmsFromDb: function _restoreAlarmsFromDb() {
debug("_restoreAlarmsFromDb()");
this._db.getAll(null,
function getAllSuccessCb(aAlarms) {
debug("Callback after getting alarms from database: " +
JSON.stringify(aAlarms));
// Clear any alarms set or queued in the cache if coming from db.
let alarmQueue = this._alarmQueue;
if (this._currentAlarm) {
alarmQueue.unshift(this._currentAlarm);
this._currentAlarm = null;
}
for (let i = 0; i < alarmQueue.length;) {
if (alarmQueue[i]['id'] < 0) {
++i;
continue;
}
alarmQueue.splice(i, 1);
}
// 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 ("manifestURL" in aAlarm && aAlarm.manifestURL &&
this._getAlarmTime(aAlarm) > Date.now()) {
alarmQueue.push(aAlarm);
} else {
this._removeAlarmFromDb(aAlarm.id, null);
this._notifyAlarmObserver(aAlarm);
}
}.bind(this));
// Set the next alarm from the queue.
if (alarmQueue.length) {
alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this));
this._currentAlarm = alarmQueue.shift();
}
this._debugCurrentAlarm();
}.bind(this),
function getAllErrorCb(aErrorMsg) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
});
},
_getAlarmTime: function _getAlarmTime(aAlarm) {
// Avoid casting a Date object to a Date again to
// preserve milliseconds. See bug 810973.
let alarmTime;
if (aAlarm.date instanceof Date) {
alarmTime = aAlarm.date.getTime();
} else {
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.
if (aAlarm.ignoreTimezone) {
alarmTime +=
(this._currentTimezoneOffset - aAlarm.timezoneOffset) * 60000;
}
return alarmTime;
},
_sortAlarmByTimeStamps: function _sortAlarmByTimeStamps(aAlarm1, aAlarm2) {
return this._getAlarmTime(aAlarm1) - this._getAlarmTime(aAlarm2);
},
_debugCurrentAlarm: function _debugCurrentAlarm() {
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;
}
if (!aNewAlarm.date) {
aErrorCb("alarm.date is null");
return;
}
aNewAlarm['timezoneOffset'] = this._currentTimezoneOffset;
if ("manifestURL" in aNewAlarm) {
this._db.add(aNewAlarm,
function addSuccessCb(aNewId) {
debug("Callback after adding alarm in database.");
this.processNewAlarm(aNewAlarm, aNewId, aAlarmFiredCb, aSuccessCb);
}.bind(this),
function addErrorCb(aErrorMsg) {
aErrorCb(aErrorMsg);
}.bind(this));
} else {
// alarms without manifests are managed by chrome code. For them we use
// negative IDs.
this.processNewAlarm(aNewAlarm, --this.lastChromeId, aAlarmFiredCb,
aSuccessCb);
}
},
processNewAlarm: function(aNewAlarm, aNewId, aAlarmFiredCb, aSuccessCb) {
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 the new alarm already expired at this moment, we directly
// notify this alarm
let newAlarmTime = this._getAlarmTime(aNewAlarm);
if (newAlarmTime < Date.now()) {
aSuccessCb(aNewId);
this._removeAlarmFromDb(aNewAlarm.id, null);
this._notifyAlarmObserver(aNewAlarm);
return;
}
// 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 the queue.
let alarmQueue = this._alarmQueue;
let currentAlarmTime = this._getAlarmTime(this._currentAlarm);
if (newAlarmTime < 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);
},
/*
* 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 the 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));
},
observe: function(aSubject, aTopic, aData) {
debug("observe(): " + aTopic);
switch (aTopic) {
case "profile-change-teardown":
this.uninit();
break;
case "webapps-clear-data":
let params =
aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
if (!params) {
debug("Error! Fail to remove alarms for an uninstalled app.");
return;
}
// Only remove alarms for apps.
if (params.browserOnly) {
return;
}
let manifestURL = appsService.getManifestURLByLocalId(params.appId);
if (!manifestURL) {
debug("Error! Fail to remove alarms for an uninstalled app.");
return;
}
this._db.getAll(manifestURL,
function getAllSuccessCb(aAlarms) {
aAlarms.forEach(function removeAlarm(aAlarm) {
this.remove(aAlarm.id, manifestURL);
}, this);
}.bind(this),
function getAllErrorCb(aErrorMsg) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
});
break;
}
},
uninit: function uninit() {
debug("uninit()");
Services.obs.removeObserver(this, "profile-change-teardown");
Services.obs.removeObserver(this, "webapps-clear-data");
this._messages.forEach(function(aMsgName) {
ppmm.removeMessageListener(aMsgName, this);
}.bind(this));
ppmm = null;
if (this._db) {
this._db.close();
}
this._db = null;
this._alarmHalService = null;
}
}
AlarmService.init();