Bug 862322 - Allow AlarmService to be used from Gecko code. r=gene
authorNikhil Marathe <nsm.nikhil@gmail.com>
Wed, 01 May 2013 21:32:49 +0530
changeset 130489 241e3e556766b06c3fa588de5f1d872d5751105d
parent 130488 68869162af7c3b977c4ea461e70a702c333c0a18
child 130490 6982875b0dfd301f6ee5f7681c5b6c8744b76b89
push id1579
push userphilringnalda@gmail.com
push dateSat, 04 May 2013 04:38:04 +0000
treeherderfx-team@a56432a42a41 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgene
bugs862322
milestone23.0a1
Bug 862322 - Allow AlarmService to be used from Gecko code. r=gene
dom/alarm/AlarmService.jsm
--- a/dom/alarm/AlarmService.jsm
+++ b/dom/alarm/AlarmService.jsm
@@ -29,47 +29,62 @@ XPCOMUtils.defineLazyGetter(this, "messe
 });
 
 XPCOMUtils.defineLazyGetter(this, "powerManagerService", function() {
   return Cc["@mozilla.org/power/powermanagerservice;1"].getService(Ci.nsIPowerManagerService);
 });
 
 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"];
     messages.forEach(function addMessage(msgName) {
         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;
   },
   set _currentAlarm(aAlarm) {
     this._alarm = aAlarm;
     if (!aAlarm)
       return;
@@ -97,221 +112,166 @@ this.AlarmService = {
     }
 
     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));
+            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
+        // 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:
         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
         break;
     }
   },
 
-  _sendAsyncMessage: function _sendAsyncMessage(aMessageManager, aMessageName, aSuccess, aRequestId, aData) {
+  _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 } : 
+        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;
 
       default:
         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
         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.");
       };
     }
 
     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);
 
-    // 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;
     }
 
     // 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 
-      // 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;
       }
     }
     this._debugCurrentAlarm();
   },
 
@@ -323,68 +283,214 @@ this.AlarmService = {
   },
 
   _restoreAlarmsFromDb: function _restoreAlarmsFromDb() {
     debug("_restoreAlarmsFromDb()");
 
     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();
         }
 
         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.
+    // 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;
+    }
+
+    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();