Bug 749551 - Alarm API (AlarmsManager and AlarmService). r=vivien
☠☠ backed out by c86aa20289a4 ☠ ☠
authorGene Lian <clian@mozilla.com>
Fri, 22 Jun 2012 15:39:07 +0800
changeset 102981 1dba66cfad9ab1aac43a7a1dec8f219192f9566b
parent 102980 ed768b821da17bed5d0f932fa3b81b43f29ed9fb
child 102982 dbd41e244549492fbc354c7c1667c406db6fd781
push id1316
push userakeybl@mozilla.com
push dateMon, 27 Aug 2012 22:37:00 +0000
treeherdermozilla-beta@db4b09302ee2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvivien
bugs749551
milestone16.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 749551 - Alarm API (AlarmsManager and AlarmService). r=vivien
b2g/chrome/content/shell.js
dom/alarm/AlarmService.jsm
dom/alarm/AlarmsManager.js
dom/alarm/Makefile.in
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -9,16 +9,17 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/ContactService.jsm');
 Cu.import('resource://gre/modules/SettingsChangeNotifier.jsm');
 Cu.import('resource://gre/modules/Webapps.jsm');
+Cu.import('resource://gre/modules/AlarmService.jsm');
 
 XPCOMUtils.defineLazyServiceGetter(Services, 'env',
                                    '@mozilla.org/process/environment;1',
                                    'nsIEnvironment');
 
 XPCOMUtils.defineLazyServiceGetter(Services, 'ss',
                                    '@mozilla.org/content/style-sheet-service;1',
                                    'nsIStyleSheetService');
new file mode 100644
--- /dev/null
+++ b/dom/alarm/AlarmService.jsm
@@ -0,0 +1,299 @@
+/* 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";
+
+/* static functions */
+const DEBUG = false;
+
+function debug(aStr) {
+  if (DEBUG)
+    dump("AlarmService: " + aStr + "\n");
+}
+
+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");
+
+let EXPORTED_SYMBOLS = ["AlarmService"];
+
+XPCOMUtils.defineLazyGetter(this, "ppmm", function() {
+  return Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIFrameMessageManager);
+});
+
+let myGlobal = this;
+
+let AlarmService = {
+  init: function init() {
+    debug("init()");
+
+    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));
+
+    // add the messages to be listened
+    const messages = ["AlarmsManager:GetAll", "AlarmsManager:Add", "AlarmsManager:Remove"];
+    messages.forEach(function addMessage(msgName) {
+        ppmm.addMessageListener(msgName, this);
+    }.bind(this));
+
+    // set the indexeddb database
+    let idbManager = Components.classes["@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
+    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;
+
+    if (!this._alarmHalService.setAlarm(this._getAlarmTime(aAlarm) / 1000, 0))
+      throw Components.results.NS_ERROR_FAILURE;
+  },
+
+  receiveMessage: function receiveMessage(aMessage) {
+    debug("receiveMessage(): " + aMessage.name);
+
+    let json = aMessage.json;
+    switch (aMessage.name) {
+      case "AlarmsManager:GetAll":
+        this._db.getAll(
+          function getAllSuccessCb(aAlarms) {
+            debug("Callback after getting alarms from database: " + JSON.stringify(aAlarms));
+            ppmm.sendAsyncMessage(
+              "AlarmsManager:GetAll:Return:OK", 
+              { requestID: json.requestID, alarms: aAlarms }
+            ); 
+          }.bind(this),
+          function getAllErrorCb(aErrorMsg) {
+            ppmm.sendAsyncMessage(
+              "AlarmsManager:GetAll:Return:KO", 
+              { requestID: json.requestID, errorMsg: 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, 
+          timezoneOffset: this._currentTimezoneOffset, 
+          data: json.data
+        };
+
+        this._db.add(
+          newAlarm,
+          function addSuccessCb(aNewId) {
+            debug("Callback after adding alarm in database.");
+
+            newAlarm['id'] = aNewId;
+            let newAlarmTime = this._getAlarmTime(newAlarm);
+
+            if (newAlarmTime <= Date.now()) {
+              debug("Adding a alarm that has past time. Don't set it in system.");
+              this._debugCurrentAlarm();
+              return;
+            }
+
+            // if there is no alarm being set in system, set the new alarm
+            if (this._currentAlarm == null) {
+              this._currentAlarm = newAlarm;
+              this._debugCurrentAlarm();
+              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();
+              return;
+            }
+
+            //push the new alarm in the queue
+            alarmQueue.push(newAlarm);
+            alarmQueue.sort(this._sortAlarmByTimeStamps.bind(this));
+            this._debugCurrentAlarm();
+
+            ppmm.sendAsyncMessage(
+              "AlarmsManager:Add:Return:OK", 
+              { requestID: json.requestID, id: aNewId }
+            );
+          }.bind(this),
+          function addErrorCb(aErrorMsg) {
+            ppmm.sendAsyncMessage(
+              "AlarmsManager:Add:Return:KO", 
+              { requestID: json.requestID, errorMsg: aErrorMsg }
+            );
+          }.bind(this)
+        );
+        break;
+
+      case "AlarmsManager:Remove":
+        this._db.remove(
+          json.id,
+          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
+            let alarmQueue = this._alarmQueue;
+            if (this._currentAlarm.id != json.id) {
+              for (let i = 0; i < alarmQueue.length; i++) {
+                if (alarmQueue[i].id == json.id) {
+                  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),
+          function removeErrorCb(aErrorMsg) {
+            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+          }
+        );
+        break;
+
+      default:
+        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        break;
+    }
+  },
+
+  _onAlarmFired: function _onAlarmFired() {
+    debug("_onAlarmFired()");
+
+    if (this._currentAlarm) {
+      debug("Fire system intent: " + JSON.stringify(this._currentAlarm));
+      // TODO Fire a system message, see bug 755245
+      this._currentAlarm = null;
+    }
+
+    // reset the next alarm from the queue
+    let nowTime = Date.now();
+    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 
+      // fire system intent for it instead of setting it
+      if (nextAlarmTime <= nowTime) {
+        debug("Fire system intent: " + JSON.stringify(nextAlarm));
+        // TODO Fire a system message, see bug 755245
+      } else {
+        this._currentAlarm = nextAlarm;
+        break;
+      }
+    }
+    this._debugCurrentAlarm();
+  },
+
+  _onTimezoneChanged: function _onTimezoneChanged(aTimezoneOffset) {
+    debug("_onTimezoneChanged()");
+
+    this._currentTimezoneOffset = aTimezoneOffset;
+    this._restoreAlarmsFromDb();
+  },
+
+  _restoreAlarmsFromDb: function _restoreAlarmsFromDb() {
+    debug("_restoreAlarmsFromDb()");
+
+    this._db.getAll(
+      function getAllSuccessCb(aAlarms) {
+        debug("Callback after getting alarms from database: " + JSON.stringify(aAlarms));
+
+        // clear any alarms set or queued in the cache
+        let alarmQueue = this._alarmQueue;
+        alarmQueue.length = 0;
+        this._currentAlarm = null;
+        
+        // only add the alarm that is valid
+        let nowTime = Date.now();
+        aAlarms.forEach(function addAlarm(aAlarm) {
+          if (this._getAlarmTime(aAlarm) > nowTime)
+            alarmQueue.push(aAlarm);
+        }.bind(this));
+
+        // set the next alarm from 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) {
+    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.
+    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));
+  },
+}
+
+AlarmService.init();
--- a/dom/alarm/AlarmsManager.js
+++ b/dom/alarm/AlarmsManager.js
@@ -1,27 +1,36 @@
 /* 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";
 
+/* static functions */
+const DEBUG = false;
+
+function debug(aStr) {
+  if (DEBUG)
+    dump("AlarmsManager: " + aStr + "\n");
+}
+
 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/DOMRequestHelper.jsm");
 
 const ALARMSMANAGER_CONTRACTID = "@mozilla.org/alarmsManager;1";
 const ALARMSMANAGER_CID        = Components.ID("{fea1e884-9b05-11e1-9b64-87a7016c3860}");
 const nsIDOMMozAlarmsManager   = Ci.nsIDOMMozAlarmsManager;
 const nsIClassInfo             = Ci.nsIClassInfo;
 
 function AlarmsManager()
 {
+  debug("Constructor");
 }
 
 AlarmsManager.prototype = {
 
   __proto__: DOMRequestIpcHelper.prototype,
 
   classID : ALARMSMANAGER_CID,
 
@@ -29,47 +38,122 @@ AlarmsManager.prototype = {
 
   classInfo : XPCOMUtils.generateCI({ classID: ALARMSMANAGER_CID,
                                       contractID: ALARMSMANAGER_CONTRACTID,
                                       classDescription: "AlarmsManager",
                                       interfaces: [nsIDOMMozAlarmsManager],
                                       flags: nsIClassInfo.DOM_OBJECT }),
 
   add: function add(aDate, aRespectTimezone, aData) {
-    return null;
+    debug("add()");
+
+    let isIgnoreTimezone = true;
+    switch (aRespectTimezone) {
+      case "honorTimezone":
+        isIgnoreTimezone = false;
+        break;
+
+      case "ignoreTimezone":
+        isIgnoreTimezone = true;
+        break;
+
+      default:
+        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        break;
+    }
+
+    let request = this.createRequest();
+    this._cpmm.sendAsyncMessage(
+      "AlarmsManager:Add", 
+      { requestID: this.getRequestId(request), date: aDate, ignoreTimezone: isIgnoreTimezone, data: aData }
+    );
+    return request;
   },
 
   remove: function remove(aId) {
-    return;
+    debug("remove()");
+
+    return this._cpmm.sendSyncMessage(
+      "AlarmsManager:Remove", 
+      { id: aId }
+    );
   },
 
   getAll: function getAll() {
-    return null;
+    debug("getAll()");
+
+    let request = this.createRequest();
+    this._cpmm.sendAsyncMessage(
+      "AlarmsManager:GetAll", 
+      { requestID: this.getRequestId(request) }
+    );
+    return request;
   },
 
+  receiveMessage: function receiveMessage(aMessage) {
+    debug("receiveMessage(): " + aMessage.name);
+
+    let json = aMessage.json;
+    let request = this.getRequest(json.requestID);
+
+    if (!request) {
+      debug("No request stored! " + json.requestID);
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "AlarmsManager:Add:Return:OK":
+        Services.DOMRequest.fireSuccess(request, json.id);
+        break;
+
+      case "AlarmsManager:GetAll:Return:OK":
+        Services.DOMRequest.fireSuccess(request, json.alarms);
+        break;
+
+      case "AlarmsManager:Add:Return:KO":
+        Services.DOMRequest.fireError(request, json.errorMsg);
+        break;
+
+      case "AlarmsManager:GetAll:Return:KO":
+        Services.DOMRequest.fireError(request, json.errorMsg);
+        break;
+
+      default:
+        debug("Wrong message: " + aMessage.name);
+        break;
+    }
+    this.removeRequest(json.requestID);
+   },
+
   // nsIDOMGlobalPropertyInitializer implementation
   init: function init(aWindow) {
+    debug("init()");
+
     // Set navigator.mozAlarms to null.
     if (!Services.prefs.getBoolPref("dom.mozAlarms.enabled"))
       return null;
 
     let principal = aWindow.document.nodePrincipal;
     let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager);
 
     let perm = principal == secMan.getSystemPrincipal() ? 
       Ci.nsIPermissionManager.ALLOW_ACTION : Services.perms.testExactPermission(principal.URI, "alarms");
 
     // Only pages with perm set can use the alarms.
     this.hasPrivileges = perm == Ci.nsIPermissionManager.ALLOW_ACTION;
 
     if (!this.hasPrivileges)
       return null;
 
+    this._cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender);
+
     // Add the valid messages to be listened.
-    this.initHelper(aWindow, []);
+    this.initHelper(aWindow, ["AlarmsManager:Add:Return:OK", "AlarmsManager:Add:Return:KO", 
+                              "AlarmsManager:GetAll:Return:OK", "AlarmsManager:GetAll:Return:KO"]);
   },
 
   // Called from DOMRequestIpcHelper.
   uninit: function uninit() {
+    debug("uninit()");
   },
 }
 
 const NSGetFactory = XPCOMUtils.generateNSGetFactory([AlarmsManager])
--- a/dom/alarm/Makefile.in
+++ b/dom/alarm/Makefile.in
@@ -22,16 +22,17 @@ EXPORTS_NAMESPACES = mozilla/dom/alarm
 
 EXTRA_COMPONENTS =       \
   AlarmsManager.js       \
   AlarmsManager.manifest \
   $(NULL)
 
 EXTRA_JS_MODULES = \
   AlarmDB.jsm      \
+  AlarmService.jsm \
   $(NULL)
 
 XPIDLSRCS =               \
   nsIDOMAlarmsManager.idl \
   nsIAlarmHalService.idl  \
   $(NULL)
 
 EXPORTS_mozilla/dom/alarm = \