1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/alarm/AlarmService.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,563 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +/* static functions */ 1.11 +const DEBUG = false; 1.12 + 1.13 +function debug(aStr) { 1.14 + if (DEBUG) 1.15 + dump("AlarmService: " + aStr + "\n"); 1.16 +} 1.17 + 1.18 +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; 1.19 + 1.20 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.21 +Cu.import("resource://gre/modules/Services.jsm"); 1.22 +Cu.import("resource://gre/modules/AlarmDB.jsm"); 1.23 + 1.24 +this.EXPORTED_SYMBOLS = ["AlarmService"]; 1.25 + 1.26 +XPCOMUtils.defineLazyGetter(this, "appsService", function() { 1.27 + return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService); 1.28 +}); 1.29 + 1.30 +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", 1.31 + "@mozilla.org/parentprocessmessagemanager;1", 1.32 + "nsIMessageListenerManager"); 1.33 + 1.34 +XPCOMUtils.defineLazyGetter(this, "messenger", function() { 1.35 + return Cc["@mozilla.org/system-message-internal;1"].getService(Ci.nsISystemMessagesInternal); 1.36 +}); 1.37 + 1.38 +XPCOMUtils.defineLazyGetter(this, "powerManagerService", function() { 1.39 + return Cc["@mozilla.org/power/powermanagerservice;1"].getService(Ci.nsIPowerManagerService); 1.40 +}); 1.41 + 1.42 +/** 1.43 + * AlarmService provides an API to schedule alarms using the device's RTC. 1.44 + * 1.45 + * AlarmService is primarily used by the mozAlarms API (navigator.mozAlarms) 1.46 + * which uses IPC to communicate with the service. 1.47 + * 1.48 + * AlarmService can also be used by Gecko code by importing the module and then 1.49 + * using AlarmService.add() and AlarmService.remove(). Only Gecko code running 1.50 + * in the parent process should do this. 1.51 + */ 1.52 + 1.53 +this.AlarmService = { 1.54 + init: function init() { 1.55 + debug("init()"); 1.56 + Services.obs.addObserver(this, "profile-change-teardown", false); 1.57 + Services.obs.addObserver(this, "webapps-clear-data",false); 1.58 + 1.59 + this._currentTimezoneOffset = (new Date()).getTimezoneOffset(); 1.60 + 1.61 + let alarmHalService = 1.62 + this._alarmHalService = Cc["@mozilla.org/alarmHalService;1"] 1.63 + .getService(Ci.nsIAlarmHalService); 1.64 + 1.65 + alarmHalService.setAlarmFiredCb(this._onAlarmFired.bind(this)); 1.66 + alarmHalService.setTimezoneChangedCb(this._onTimezoneChanged.bind(this)); 1.67 + 1.68 + // Add the messages to be listened to. 1.69 + this._messages = ["AlarmsManager:GetAll", 1.70 + "AlarmsManager:Add", 1.71 + "AlarmsManager:Remove"]; 1.72 + this._messages.forEach(function addMessage(msgName) { 1.73 + ppmm.addMessageListener(msgName, this); 1.74 + }.bind(this)); 1.75 + 1.76 + // Set the indexeddb database. 1.77 + this._db = new AlarmDB(); 1.78 + this._db.init(); 1.79 + 1.80 + // Variable to save alarms waiting to be set. 1.81 + this._alarmQueue = []; 1.82 + 1.83 + this._restoreAlarmsFromDb(); 1.84 + }, 1.85 + 1.86 + // Getter/setter to access the current alarm set in system. 1.87 + _alarm: null, 1.88 + get _currentAlarm() { 1.89 + return this._alarm; 1.90 + }, 1.91 + set _currentAlarm(aAlarm) { 1.92 + this._alarm = aAlarm; 1.93 + if (!aAlarm) { 1.94 + return; 1.95 + } 1.96 + 1.97 + let alarmTimeInMs = this._getAlarmTime(aAlarm); 1.98 + let ns = (alarmTimeInMs % 1000) * 1000000; 1.99 + if (!this._alarmHalService.setAlarm(alarmTimeInMs / 1000, ns)) { 1.100 + throw Components.results.NS_ERROR_FAILURE; 1.101 + } 1.102 + }, 1.103 + 1.104 + receiveMessage: function receiveMessage(aMessage) { 1.105 + debug("receiveMessage(): " + aMessage.name); 1.106 + let json = aMessage.json; 1.107 + 1.108 + // To prevent the hacked child process from sending commands to parent 1.109 + // to schedule alarms, we need to check its permission and manifest URL. 1.110 + if (this._messages.indexOf(aMessage.name) != -1) { 1.111 + if (!aMessage.target.assertPermission("alarms")) { 1.112 + debug("Got message from a child process with no 'alarms' permission."); 1.113 + return null; 1.114 + } 1.115 + if (!aMessage.target.assertContainApp(json.manifestURL)) { 1.116 + debug("Got message from a child process containing illegal manifest URL."); 1.117 + return null; 1.118 + } 1.119 + } 1.120 + 1.121 + let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender); 1.122 + switch (aMessage.name) { 1.123 + case "AlarmsManager:GetAll": 1.124 + this._db.getAll( 1.125 + json.manifestURL, 1.126 + function getAllSuccessCb(aAlarms) { 1.127 + debug("Callback after getting alarms from database: " + 1.128 + JSON.stringify(aAlarms)); 1.129 + this._sendAsyncMessage(mm, "GetAll", true, json.requestId, aAlarms); 1.130 + }.bind(this), 1.131 + function getAllErrorCb(aErrorMsg) { 1.132 + this._sendAsyncMessage(mm, "GetAll", false, json.requestId, aErrorMsg); 1.133 + }.bind(this) 1.134 + ); 1.135 + break; 1.136 + 1.137 + case "AlarmsManager:Add": 1.138 + // Prepare a record for the new alarm to be added. 1.139 + let newAlarm = { 1.140 + date: json.date, 1.141 + ignoreTimezone: json.ignoreTimezone, 1.142 + data: json.data, 1.143 + pageURL: json.pageURL, 1.144 + manifestURL: json.manifestURL 1.145 + }; 1.146 + 1.147 + this.add(newAlarm, null, 1.148 + // Receives the alarm ID as the last argument. 1.149 + this._sendAsyncMessage.bind(this, mm, "Add", true, json.requestId), 1.150 + // Receives the error message as the last argument. 1.151 + this._sendAsyncMessage.bind(this, mm, "Add", false, json.requestId) 1.152 + ); 1.153 + break; 1.154 + 1.155 + case "AlarmsManager:Remove": 1.156 + this.remove(json.id, json.manifestURL); 1.157 + break; 1.158 + 1.159 + default: 1.160 + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; 1.161 + break; 1.162 + } 1.163 + }, 1.164 + 1.165 + _sendAsyncMessage: function _sendAsyncMessage(aMessageManager, aMessageName, 1.166 + aSuccess, aRequestId, aData) { 1.167 + debug("_sendAsyncMessage()"); 1.168 + 1.169 + if (!aMessageManager) { 1.170 + debug("Invalid message manager: null"); 1.171 + throw Components.results.NS_ERROR_FAILURE; 1.172 + } 1.173 + 1.174 + let json = null; 1.175 + switch (aMessageName) 1.176 + { 1.177 + case "Add": 1.178 + json = aSuccess ? 1.179 + { requestId: aRequestId, id: aData } : 1.180 + { requestId: aRequestId, errorMsg: aData }; 1.181 + break; 1.182 + 1.183 + case "GetAll": 1.184 + json = aSuccess ? 1.185 + { requestId: aRequestId, alarms: aData } : 1.186 + { requestId: aRequestId, errorMsg: aData }; 1.187 + break; 1.188 + 1.189 + default: 1.190 + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; 1.191 + break; 1.192 + } 1.193 + 1.194 + aMessageManager.sendAsyncMessage("AlarmsManager:" + aMessageName + 1.195 + ":Return:" + (aSuccess ? "OK" : "KO"), json); 1.196 + }, 1.197 + 1.198 + _removeAlarmFromDb: function _removeAlarmFromDb(aId, aManifestURL, 1.199 + aRemoveSuccessCb) { 1.200 + debug("_removeAlarmFromDb()"); 1.201 + 1.202 + // If the aRemoveSuccessCb is undefined or null, set a dummy callback for 1.203 + // it which is needed for _db.remove(). 1.204 + if (!aRemoveSuccessCb) { 1.205 + aRemoveSuccessCb = function removeSuccessCb() { 1.206 + debug("Remove alarm from DB successfully."); 1.207 + }; 1.208 + } 1.209 + 1.210 + this._db.remove( 1.211 + aId, 1.212 + aManifestURL, 1.213 + aRemoveSuccessCb, 1.214 + function removeErrorCb(aErrorMsg) { 1.215 + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; 1.216 + } 1.217 + ); 1.218 + }, 1.219 + 1.220 + /** 1.221 + * Create a copy of the alarm that does not expose internal fields to 1.222 + * receivers and sticks to the public |respectTimezone| API rather than the 1.223 + * boolean |ignoreTimezone| field. 1.224 + */ 1.225 + _publicAlarm: function _publicAlarm(aAlarm) { 1.226 + let alarm = { 1.227 + "id": aAlarm.id, 1.228 + "date": aAlarm.date, 1.229 + "respectTimezone": aAlarm.ignoreTimezone ? 1.230 + "ignoreTimezone" : "honorTimezone", 1.231 + "data": aAlarm.data 1.232 + }; 1.233 + 1.234 + return alarm; 1.235 + }, 1.236 + 1.237 + _fireSystemMessage: function _fireSystemMessage(aAlarm) { 1.238 + debug("Fire system message: " + JSON.stringify(aAlarm)); 1.239 + 1.240 + let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null); 1.241 + let pageURI = Services.io.newURI(aAlarm.pageURL, null, null); 1.242 + 1.243 + messenger.sendMessage("alarm", this._publicAlarm(aAlarm), 1.244 + pageURI, manifestURI); 1.245 + }, 1.246 + 1.247 + _notifyAlarmObserver: function _notifyAlarmObserver(aAlarm) { 1.248 + debug("_notifyAlarmObserver()"); 1.249 + 1.250 + if (aAlarm.manifestURL) { 1.251 + this._fireSystemMessage(aAlarm); 1.252 + } else if (typeof aAlarm.alarmFiredCb === "function") { 1.253 + aAlarm.alarmFiredCb(this._publicAlarm(aAlarm)); 1.254 + } 1.255 + }, 1.256 + 1.257 + _onAlarmFired: function _onAlarmFired() { 1.258 + debug("_onAlarmFired()"); 1.259 + 1.260 + if (this._currentAlarm) { 1.261 + this._removeAlarmFromDb(this._currentAlarm.id, null); 1.262 + this._notifyAlarmObserver(this._currentAlarm); 1.263 + this._currentAlarm = null; 1.264 + } 1.265 + 1.266 + // Reset the next alarm from the queue. 1.267 + let alarmQueue = this._alarmQueue; 1.268 + while (alarmQueue.length > 0) { 1.269 + let nextAlarm = alarmQueue.shift(); 1.270 + let nextAlarmTime = this._getAlarmTime(nextAlarm); 1.271 + 1.272 + // If the next alarm has been expired, directly notify the observer. 1.273 + // it instead of setting it. 1.274 + if (nextAlarmTime <= Date.now()) { 1.275 + this._removeAlarmFromDb(nextAlarm.id, null); 1.276 + this._notifyAlarmObserver(nextAlarm); 1.277 + } else { 1.278 + this._currentAlarm = nextAlarm; 1.279 + break; 1.280 + } 1.281 + } 1.282 + this._debugCurrentAlarm(); 1.283 + }, 1.284 + 1.285 + _onTimezoneChanged: function _onTimezoneChanged(aTimezoneOffset) { 1.286 + debug("_onTimezoneChanged()"); 1.287 + 1.288 + this._currentTimezoneOffset = aTimezoneOffset; 1.289 + this._restoreAlarmsFromDb(); 1.290 + }, 1.291 + 1.292 + _restoreAlarmsFromDb: function _restoreAlarmsFromDb() { 1.293 + debug("_restoreAlarmsFromDb()"); 1.294 + 1.295 + this._db.getAll( 1.296 + null, 1.297 + function getAllSuccessCb(aAlarms) { 1.298 + debug("Callback after getting alarms from database: " + 1.299 + JSON.stringify(aAlarms)); 1.300 + 1.301 + // Clear any alarms set or queued in the cache. 1.302 + let alarmQueue = this._alarmQueue; 1.303 + alarmQueue.length = 0; 1.304 + this._currentAlarm = null; 1.305 + 1.306 + // Only restore the alarm that's not yet expired; otherwise, remove it 1.307 + // from the database and notify the observer. 1.308 + aAlarms.forEach(function addAlarm(aAlarm) { 1.309 + if (this._getAlarmTime(aAlarm) > Date.now()) { 1.310 + alarmQueue.push(aAlarm); 1.311 + } else { 1.312 + this._removeAlarmFromDb(aAlarm.id, null); 1.313 + this._notifyAlarmObserver(aAlarm); 1.314 + } 1.315 + }.bind(this)); 1.316 + 1.317 + // Set the next alarm from queue. 1.318 + if (alarmQueue.length) { 1.319 + alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); 1.320 + this._currentAlarm = alarmQueue.shift(); 1.321 + } 1.322 + 1.323 + this._debugCurrentAlarm(); 1.324 + }.bind(this), 1.325 + function getAllErrorCb(aErrorMsg) { 1.326 + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; 1.327 + } 1.328 + ); 1.329 + }, 1.330 + 1.331 + _getAlarmTime: function _getAlarmTime(aAlarm) { 1.332 + // Avoid casting a Date object to a Date again to 1.333 + // preserve milliseconds. See bug 810973. 1.334 + let alarmTime; 1.335 + if (aAlarm.date instanceof Date) { 1.336 + alarmTime = aAlarm.date.getTime(); 1.337 + } else { 1.338 + alarmTime = (new Date(aAlarm.date)).getTime(); 1.339 + } 1.340 + 1.341 + // For an alarm specified with "ignoreTimezone", it must be fired respect 1.342 + // to the user's timezone. Supposing an alarm was set at 7:00pm at Tokyo, 1.343 + // it must be gone off at 7:00pm respect to Paris' local time when the user 1.344 + // is located at Paris. We can adjust the alarm UTC time by calculating 1.345 + // the difference of the orginal timezone and the current timezone. 1.346 + if (aAlarm.ignoreTimezone) { 1.347 + alarmTime += (this._currentTimezoneOffset - aAlarm.timezoneOffset) * 60000; 1.348 + } 1.349 + return alarmTime; 1.350 + }, 1.351 + 1.352 + _sortAlarmByTimeStamps: function _sortAlarmByTimeStamps(aAlarm1, aAlarm2) { 1.353 + return this._getAlarmTime(aAlarm1) - this._getAlarmTime(aAlarm2); 1.354 + }, 1.355 + 1.356 + _debugCurrentAlarm: function _debugCurrentAlarm() { 1.357 + debug("Current alarm: " + JSON.stringify(this._currentAlarm)); 1.358 + debug("Alarm queue: " + JSON.stringify(this._alarmQueue)); 1.359 + }, 1.360 + 1.361 + /** 1.362 + * 1.363 + * Add a new alarm. This will set the RTC to fire at the selected date and 1.364 + * notify the caller. Notifications are delivered via System Messages if the 1.365 + * alarm is added on behalf of a app. Otherwise aAlarmFiredCb is called. 1.366 + * 1.367 + * @param object aNewAlarm 1.368 + * Should contain the following literal properties: 1.369 + * - |date| date: when the alarm should timeout. 1.370 + * - |ignoreTimezone| boolean: See [1] for the details. 1.371 + * - |manifestURL| string: Manifest of app on whose behalf the alarm 1.372 + * is added. 1.373 + * - |pageURL| string: The page in the app that receives the system 1.374 + * message. 1.375 + * - |data| object [optional]: Data that can be stored in DB. 1.376 + * @param function aAlarmFiredCb 1.377 + * Callback function invoked when the alarm is fired. 1.378 + * It receives a single argument, the alarm object. 1.379 + * May be null. 1.380 + * @param function aSuccessCb 1.381 + * Callback function to receive an alarm ID (number). 1.382 + * @param function aErrorCb 1.383 + * Callback function to receive an error message (string). 1.384 + * @returns void 1.385 + * 1.386 + * Notes: 1.387 + * [1] https://wiki.mozilla.org/WebAPI/AlarmAPI#Proposed_API 1.388 + */ 1.389 + 1.390 + add: function(aNewAlarm, aAlarmFiredCb, aSuccessCb, aErrorCb) { 1.391 + debug("add(" + aNewAlarm.date + ")"); 1.392 + 1.393 + aSuccessCb = aSuccessCb || function() {}; 1.394 + aErrorCb = aErrorCb || function() {}; 1.395 + 1.396 + if (!aNewAlarm) { 1.397 + aErrorCb("alarm is null"); 1.398 + return; 1.399 + } 1.400 + 1.401 + if (!aNewAlarm.date) { 1.402 + aErrorCb("alarm.date is null"); 1.403 + return; 1.404 + } 1.405 + 1.406 + aNewAlarm['timezoneOffset'] = this._currentTimezoneOffset; 1.407 + 1.408 + this._db.add( 1.409 + aNewAlarm, 1.410 + function addSuccessCb(aNewId) { 1.411 + debug("Callback after adding alarm in database."); 1.412 + 1.413 + aNewAlarm['id'] = aNewId; 1.414 + 1.415 + // Now that the alarm has been added to the database, we can tack on 1.416 + // the non-serializable callback to the in-memory object. 1.417 + aNewAlarm['alarmFiredCb'] = aAlarmFiredCb; 1.418 + 1.419 + // If there is no alarm being set in system, set the new alarm. 1.420 + if (this._currentAlarm == null) { 1.421 + this._currentAlarm = aNewAlarm; 1.422 + this._debugCurrentAlarm(); 1.423 + aSuccessCb(aNewId); 1.424 + return; 1.425 + } 1.426 + 1.427 + // If the new alarm is earlier than the current alarm, swap them and 1.428 + // push the previous alarm back to queue. 1.429 + let alarmQueue = this._alarmQueue; 1.430 + let aNewAlarmTime = this._getAlarmTime(aNewAlarm); 1.431 + let currentAlarmTime = this._getAlarmTime(this._currentAlarm); 1.432 + if (aNewAlarmTime < currentAlarmTime) { 1.433 + alarmQueue.unshift(this._currentAlarm); 1.434 + this._currentAlarm = aNewAlarm; 1.435 + this._debugCurrentAlarm(); 1.436 + aSuccessCb(aNewId); 1.437 + return; 1.438 + } 1.439 + 1.440 + // Push the new alarm in the queue. 1.441 + alarmQueue.push(aNewAlarm); 1.442 + alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this)); 1.443 + this._debugCurrentAlarm(); 1.444 + aSuccessCb(aNewId); 1.445 + }.bind(this), 1.446 + function addErrorCb(aErrorMsg) { 1.447 + aErrorCb(aErrorMsg); 1.448 + }.bind(this) 1.449 + ); 1.450 + }, 1.451 + 1.452 + /* 1.453 + * Remove the alarm associated with an ID. 1.454 + * 1.455 + * @param number aAlarmId 1.456 + * The ID of the alarm to be removed. 1.457 + * @param string aManifestURL 1.458 + * Manifest URL for application which added the alarm. (Optional) 1.459 + * @returns void 1.460 + */ 1.461 + remove: function(aAlarmId, aManifestURL) { 1.462 + debug("remove(" + aAlarmId + ", " + aManifestURL + ")"); 1.463 + this._removeAlarmFromDb( 1.464 + aAlarmId, 1.465 + aManifestURL, 1.466 + function removeSuccessCb() { 1.467 + debug("Callback after removing alarm from database."); 1.468 + 1.469 + // If there are no alarms set, nothing to do. 1.470 + if (!this._currentAlarm) { 1.471 + debug("No alarms set."); 1.472 + return; 1.473 + } 1.474 + 1.475 + // Check if the alarm to be removed is in the queue and whether it 1.476 + // belongs to the requesting app. 1.477 + let alarmQueue = this._alarmQueue; 1.478 + if (this._currentAlarm.id != aAlarmId || 1.479 + this._currentAlarm.manifestURL != aManifestURL) { 1.480 + 1.481 + for (let i = 0; i < alarmQueue.length; i++) { 1.482 + if (alarmQueue[i].id == aAlarmId && 1.483 + alarmQueue[i].manifestURL == aManifestURL) { 1.484 + 1.485 + alarmQueue.splice(i, 1); 1.486 + break; 1.487 + } 1.488 + } 1.489 + this._debugCurrentAlarm(); 1.490 + return; 1.491 + } 1.492 + 1.493 + // The alarm to be removed is the current alarm reset the next alarm 1.494 + // from queue if any. 1.495 + if (alarmQueue.length) { 1.496 + this._currentAlarm = alarmQueue.shift(); 1.497 + this._debugCurrentAlarm(); 1.498 + return; 1.499 + } 1.500 + 1.501 + // No alarm waiting to be set in the queue. 1.502 + this._currentAlarm = null; 1.503 + this._debugCurrentAlarm(); 1.504 + }.bind(this) 1.505 + ); 1.506 + }, 1.507 + 1.508 + observe: function(aSubject, aTopic, aData) { 1.509 + switch (aTopic) { 1.510 + case "profile-change-teardown": 1.511 + this.uninit(); 1.512 + break; 1.513 + case "webapps-clear-data": 1.514 + let params = 1.515 + aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams); 1.516 + if (!params) { 1.517 + debug("Error! Fail to remove alarms for an uninstalled app."); 1.518 + return; 1.519 + } 1.520 + 1.521 + // Only remove alarms for apps. 1.522 + if (params.browserOnly) { 1.523 + return; 1.524 + } 1.525 + 1.526 + let manifestURL = appsService.getManifestURLByLocalId(params.appId); 1.527 + if (!manifestURL) { 1.528 + debug("Error! Fail to remove alarms for an uninstalled app."); 1.529 + return; 1.530 + } 1.531 + 1.532 + this._db.getAll( 1.533 + manifestURL, 1.534 + function getAllSuccessCb(aAlarms) { 1.535 + aAlarms.forEach(function removeAlarm(aAlarm) { 1.536 + this.remove(aAlarm.id, manifestURL); 1.537 + }, this); 1.538 + }.bind(this), 1.539 + function getAllErrorCb(aErrorMsg) { 1.540 + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; 1.541 + } 1.542 + ); 1.543 + break; 1.544 + } 1.545 + }, 1.546 + 1.547 + uninit: function uninit() { 1.548 + debug("uninit()"); 1.549 + Services.obs.removeObserver(this, "profile-change-teardown"); 1.550 + Services.obs.removeObserver(this, "webapps-clear-data"); 1.551 + 1.552 + this._messages.forEach(function(aMsgName) { 1.553 + ppmm.removeMessageListener(aMsgName, this); 1.554 + }.bind(this)); 1.555 + ppmm = null; 1.556 + 1.557 + if (this._db) { 1.558 + this._db.close(); 1.559 + } 1.560 + this._db = null; 1.561 + 1.562 + this._alarmHalService = null; 1.563 + } 1.564 +} 1.565 + 1.566 +AlarmService.init();