Bug 707629 - Part 2: Complete most of the mozTelephony API. r=bent
authorPhilipp von Weitershausen <philipp@weitershausen.de>
Wed, 07 Dec 2011 14:57:19 +0800
changeset 82954 40d760a401001241d3b624306832c241515f4c12
parent 82953 c634c79dd8293bd20d48adca0d30a59db75f0fa5
child 82955 6c59889f16b09277ca870923da55907f631bf7e8
push idunknown
push userunknown
push dateunknown
reviewersbent
bugs707629
milestone11.0a1
Bug 707629 - Part 2: Complete most of the mozTelephony API. r=bent
dom/telephony/Telephony.js
dom/telephony/mozIDOMTelephony.idl
dom/telephony/nsITelephone.idl
dom/telephony/nsTelephonyWorker.js
--- a/dom/telephony/Telephony.js
+++ b/dom/telephony/Telephony.js
@@ -41,16 +41,41 @@ Cu.import("resource://gre/modules/XPCOMU
 
 const TELEPHONY_CID = Components.ID("{37e248d2-02ff-469b-bb31-eef5a4a4bee3}");
 const TELEPHONY_CONTRACTID = "@mozilla.org/telephony;1";
 
 const TELEPHONY_CALL_CID = Components.ID("{6b9b3daf-e5ea-460b-89a5-641ee20dd577}");
 const TELEPHONY_CALL_CONTRACTID = "@mozilla.org/telephony-call;1";
 
 
+const DOM_RADIOSTATE_UNAVAILABLE   = "unavailable";
+const DOM_RADIOSTATE_OFF           = "off";
+const DOM_RADIOSTATE_READY         = "ready";
+
+const DOM_CARDSTATE_UNAVAILABLE    = "unavailable";
+const DOM_CARDSTATE_ABSENT         = "absent";
+const DOM_CARDSTATE_PIN_REQUIRED   = "pin_required";
+const DOM_CARDSTATE_PUK_REQUIRED   = "puk_required";
+const DOM_CARDSTATE_NETWORK_LOCKED = "network_locked";
+const DOM_CARDSTATE_NOT_READY      = "not_ready";
+const DOM_CARDSTATE_READY          = "ready";
+
+const DOM_CALL_READYSTATE_DIALING        = "dialing";
+const DOM_CALL_READYSTATE_RINGING        = "ringing";
+const DOM_CALL_READYSTATE_BUSY           = "busy";
+const DOM_CALL_READYSTATE_CONNECTING     = "connecting";
+const DOM_CALL_READYSTATE_CONNECTED      = "connected";
+const DOM_CALL_READYSTATE_DISCONNECTING  = "disconnecting";
+const DOM_CALL_READYSTATE_DISCONNECTED   = "disconnected";
+const DOM_CALL_READYSTATE_INCOMING       = "incoming";
+const DOM_CALL_READYSTATE_HOLDING        = "holding";
+const DOM_CALL_READYSTATE_HELD           = "held";
+
+const CALLINDEX_TEMPORARY_DIALING = -1;
+
 /**
  * Define an event listener slot on an object, e.g.
  * 
  *   obj.onerror = function () {...}
  * 
  * will register the function as an event handler for the "error" event
  * if the "error" slot was defined on 'obj' or its prototype.
  */
@@ -105,93 +130,84 @@ EventTarget.prototype = {
        return;
      }
   },
 
   dispatchEvent: function dispatchEvent(event) {
     //TODO this does not deal with bubbling, defaultPrevented, canceling, etc.
     //TODO disallow re-dispatch of the same event if it's already being
     // dispatched (recursion).
+    if (!this._listeners) {
+      return;
+    }
     let handlerList = this._listeners[event.type];
     if (!handlerList) {
       return;
     }
     event.target = this;
 
     // We need to worry about event handler mutations during the event firing.
     // The correct behaviour is to *not* call any listeners that are added
     // during the firing and to *not* call any listeners that are removed
     // during the firing. To address this, we make a copy of the listener list
     // before dispatching and then double-check that each handler is still
     // registered before firing it.
     let handlers = handlerList.slice();
     handlers.forEach(function (handler) {
-      if (handerList.indexOf(handler) == -1) {
+      if (handlerList.indexOf(handler) == -1) {
         return;
       }
       switch (typeof handler) {
         case "function":
           handler(event);
           break;
         case "object":
           handler.handleEvent(event);
           break;
       }
     });
   }
 };
 
-const DOM_RADIOSTATE_UNAVAILABLE = "unavailable";
-const DOM_RADIOSTATE_OFF         = "off";
-const DOM_RADIOSTATE_READY       = "ready";
-   
-const DOM_CARDSTATE_UNAVAILABLE    = "unavailable";
-const DOM_CARDSTATE_ABSENT         = "absent";
-const DOM_CARDSTATE_PIN_REQUIRED   = "pin_required";
-const DOM_CARDSTATE_PUK_REQUIRED   = "puk_required";
-const DOM_CARDSTATE_NETWORK_LOCKED = "network_locked";
-const DOM_CARDSTATE_NOT_READY      = "not_ready";
-const DOM_CARDSTATE_READY          = "ready";
-
 /**
  * Callback object that Telephony registers with nsITelephone.
  * Telephony can't use itself because that might overload event handler
  * attributes ('onfoobar').
  */
-function TelephonyRadioCallback(telephony) {
+function TelephoneCallback(telephony) {
   this.telephony = telephony;
 }
-TelephonyRadioCallback.prototype = {
+TelephoneCallback.prototype = {
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephoneCallback]),
 
   // nsITelephoneCallback
 
-  onsignalstrengthchange: function onsignalstrengthchange(signalStrength) {
-    this.telephony.signalStrength = signalStrength;
+  onsignalstrengthchange: function onsignalstrengthchange(event) {
+    this.telephony.signalStrength = event.signalStrength;
     this.telephony._dispatchEventByType("signalstrengthchange");
   },
 
-  onoperatorchange: function onoperatorchange(operator) {
-    this.telephony.operator = operator;
+  onoperatorchange: function onoperatorchange(event) {
+    this.telephony.operator = event.operator;
     this.telephony._dispatchEventByType("operatorchange");
   },
 
-  onradiostatechange: function onradiostatechange(radioState) {
-    this.telephony.radioState = radioState;
+  onradiostatechange: function onradiostatechange(event) {
+    this.telephony.radioState = event.radioState;
     this.telephony._dispatchEventByType("radiostatechange");
   },
 
-  oncardstatechange: function oncardstatechange(cardState) {
-    this.telephony.cardState = cardState;
+  oncardstatechange: function oncardstatechange(event) {
+    this.telephony.cardState = event.cardState;
     this.telephony._dispatchEventByType("cardstatechange");
   },
 
-  oncallstatechange: function oncallstatechange(callState) {
-    this.telephony._processCallState(callState);
+  oncallstatechange: function oncallstatechange(event) {
+    this.telephony._processCallState(event);
   },
 
 };
 
 /**
  * The navigator.mozTelephony object.
  */
 function Telephony() {}
@@ -209,105 +225,202 @@ Telephony.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.mozIDOMTelephony,
                                          Ci.nsIDOMEventTarget,
                                          Ci.nsIDOMGlobalPropertyInitializer]),
 
   // nsIDOMGlobalPropertyInitializer
 
   init: function init(window) {
     this.window = window;
-    this.radioInterface = Cc["@mozilla.org/telephony/radio-interface;1"]
-                            .createInstance(Ci.nsITelephone);
-    this.radioCallback = new TelephonyRadioCallback(this);
+    this.telephone = Cc["@mozilla.org/telephony/radio-interface;1"]
+                       .createInstance(Ci.nsITelephone);
+    this.telephoneCallback = new TelephoneCallback(this);
+    //TODO switch to method suggested by bz in bug 707507
     window.addEventListener("unload", function onunload(event) {
-      this.radioInterface.unregisterCallback(this.radioCallback);
-      this.radioCallback = null;
+      this.telephone.unregisterCallback(this.telephoneCallback);
+      this.telephoneCallback = null;
       this.window = null;
     }.bind(this));
-    this.radioInterface.registerCallback(this.radioCallback);
+    this.telephone.registerCallback(this.telephoneCallback);
+    this.callsByIndex = {};
     this.liveCalls = [];
 
-    let initialState = this.radioInterface.initialState;
-    this.operator        = initialState.operator;
-    this.radioState      = initialState.radioState;
-    this.cardState       = initialState.cardState;
-    this.signalStrength  = initialState.signalStrength;
-    this._processCallState(initialState.callState);
+    // Populate existing state.
+    let currentState = this.telephone.currentState;
+    let states = currentState.currentCalls;
+    for (let i = 0; i < states.length; i++) {
+      let state = states[i];
+      let call = new TelephonyCall(this.telephone, state.callIndex);
+      call.readyState = state.callState;
+      call.number = state.number;
+      this.liveCalls.push(call);
+      this.callsByIndex[state.callIndex] = call;
+    }
+
+    this.operator        = currentState.operator;
+    this.radioState      = currentState.radioState;
+    this.cardState       = currentState.cardState;
+    this.signalStrength  = currentState.signalStrength;
   },
 
   _dispatchEventByType: function _dispatchEventByType(type) {
     let event = this.window.document.createEvent("Event");
     event.initEvent(type, false, false);
     //event.isTrusted = true;
     this.dispatchEvent(event);
   },
 
-  _processCallState: function _processCallState(callState) {
-    //TODO
+  _dispatchCallEvent: function _dispatchCallEvent(call, type, target) {
+    let event = this.window.document.createEvent("Event");
+    event.initEvent(type, false, false);
+    event.call = call; //XXX this is probably not going to work
+    //event.isTrusted = true;
+    target = target || call;
+    target.dispatchEvent(event);    
   },
 
+  _processCallState: function _processCallState(state) {
+    // If the call is dialing, chances are good that we just kicked that off
+    // so there's a call object without a callIndex. Let's fix that.
+    if (state.callState == DOM_CALL_READYSTATE_DIALING) {
+      let call = this.callsByIndex[CALLINDEX_TEMPORARY_DIALING];
+      if (call) {
+        call.callIndex = state.callIndex;
+        delete this.callsByIndex[CALLINDEX_TEMPORARY_DIALING];
+        this.callsByIndex[call.callIndex] = call;
+        // Nothing else to do, since the initial call state will already be
+        // DOM_CALL_READYSTATE_DIALING, so there's no event to dispatch.
+        return;
+      }
+    }
+
+    // If there is an existing call object, update state and dispatch event
+    // on it.
+    let call = this.callsByIndex[state.callIndex];
+    if (call) {
+      if (call.readyState == state.callState) {
+        // No change in ready state, don't dispatch an event.
+        return;
+      }
+      if (state.readyState == DOM_CALL_READYSTATE_DISCONNECTED) {
+        let index = this.liveCalls.indexOf(call);
+        if (index != -1) {
+          this.liveCalls.splice(index, 1);
+        }
+        delete this.callsByIndex[call.callIndex];
+      }
+      call.readyState = state.callState;
+      this._dispatchCallEvent(call, "readystatechange");
+      this._dispatchCallEvent(call, state.callState);
+      return;
+    }
+
+    // There's no call object yet, so let's create a new one, except when
+    // the state notified means that the call is over.
+    if (state.readyState == DOM_CALL_READYSTATE_DISCONNECTED) {
+      return;
+    }
+    call = new TelephonyCall(this.telephone, state.callIndex);
+    call.number = state.number;
+    call.readyState = state.callState;
+    this.callsByIndex[state.callIndex] = call;
+    this.liveCalls.push(call);
+
+    let target;
+    if (call.readyState == DOM_CALL_READYSTATE_INCOMING) {
+      target = this;
+    } else {
+      target = call;
+      this._dispatchCallEvent(call, "readystatechange");
+    }
+    this._dispatchCallEvent(call, state.callState, target);
+  },
+
+  callsByIndex: null,
+
   // mozIDOMTelephony
 
   liveCalls: null,
 
   dial: function dial(number) {
-    this.radioInterface.dial(number);
-    return new TelephonyCall(number, DOM_CALL_READYSTATE_DIALING);
+    this.telephone.dial(number);
+
+    // We don't know ahead of time what callIndex the call is going to have
+    // so let's assign a temp value for now and sort it out on the first
+    // 'callstatechange' event.
+    //TODO ensure there isn't already an outgoing call
+    let callIndex = CALLINDEX_TEMPORARY_DIALING;
+    let call = new TelephonyCall(this.telephone, callIndex);
+    call.readyState = DOM_CALL_READYSTATE_DIALING;
+    call.number = number;
+    this.callsByIndex[callIndex] = call;
+    this.liveCalls.push(call);
+    return call;
   },
 
   // Additional stuff that's useful.
 
   signalStrength: null,
   operator: null,
   radioState: DOM_RADIOSTATE_UNAVAILABLE,
   cardState: DOM_CARDSTATE_UNAVAILABLE,
 
 };
+defineEventListenerSlot(Telephony.prototype, DOM_CALL_READYSTATE_INCOMING);
+//XXX philikon's additions
 defineEventListenerSlot(Telephony.prototype, "radiostatechange");
 defineEventListenerSlot(Telephony.prototype, "cardstatechange");
 defineEventListenerSlot(Telephony.prototype, "signalstrengthchange");
 defineEventListenerSlot(Telephony.prototype, "operatorchange");
-defineEventListenerSlot(Telephony.prototype, "incoming");
 
 
-const DOM_CALL_READYSTATE_DIALING   = "dialing";
-const DOM_CALL_READYSTATE_DOM_CALLING   = "calling";
-const DOM_CALL_READYSTATE_INCOMING  = "incoming";
-const DOM_CALL_READYSTATE_CONNECTED = "connected";
-const DOM_CALL_READYSTATE_CLOSED    = "closed";
-const DOM_CALL_READYSTATE_BUSY      = "busy";
-
-function TelephonyCall(number, initialState) {
-  this.number = number;
-  this.readyState = initialState;
+function TelephonyCall(telephone, callIndex) {
+  this.telephone = telephone;
+  this.callIndex = callIndex;
 }
 TelephonyCall.prototype = {
 
   __proto__: EventTarget.prototype,
 
   classID: TELEPHONY_CALL_CID,
   classInfo: XPCOMUtils.generateCI({classID: TELEPHONY_CALL_CID,
                                     contractID: TELEPHONY_CALL_CONTRACTID,
                                     interfaces: [Ci.mozIDOMTelephonyCall,
                                                  Ci.nsIDOMEventTarget],
                                     flags: Ci.nsIClassInfo.DOM_OBJECT,
                                     classDescription: "TelephonyCall"}),
   QueryInterface: XPCOMUtils.generateQI([Ci.mozIDOMTelephonyCall,
                                          Ci.nsIDOMEventTarget]),
 
+
+  callIndex: null,
+
+  // mozIDOMTelephonyCall
+
   number: null,
   readyState: null,
 
   answer: function answer() {
-    //TODO
+    if (this.readyState != DOM_CALL_READYSTATE_INCOMING) {
+      throw "Can only answer an incoming call!";
+    }
+    this.telephone.answerCall();
   },
 
   disconnect: function disconnect() {
-    //TODO
+    if (this.readyState == DOM_CALL_READYSTATE_INCOMING) {
+      this.telephone.rejectCall();
+    } else {
+      this.telephone.hangUp(this.callIndex);
+    }
   },
 
 };
-defineEventListenerSlot(TelephonyCall.prototype, "connect");
-defineEventListenerSlot(TelephonyCall.prototype, "disconnect");
-defineEventListenerSlot(TelephonyCall.prototype, "busy");
+defineEventListenerSlot(TelephonyCall.prototype, "readystatechange");
+defineEventListenerSlot(TelephonyCall.prototype, DOM_CALL_READYSTATE_RINGING);
+defineEventListenerSlot(TelephonyCall.prototype, DOM_CALL_READYSTATE_BUSY);
+defineEventListenerSlot(TelephonyCall.prototype, DOM_CALL_READYSTATE_CONNECTING);
+defineEventListenerSlot(TelephonyCall.prototype, DOM_CALL_READYSTATE_CONNECTED);
+defineEventListenerSlot(TelephonyCall.prototype, DOM_CALL_READYSTATE_DISCONNECTING);
+defineEventListenerSlot(TelephonyCall.prototype, DOM_CALL_READYSTATE_DISCONNECTED);
 
 
 const NSGetFactory = XPCOMUtils.generateNSGetFactory([Telephony]);
--- a/dom/telephony/mozIDOMTelephony.idl
+++ b/dom/telephony/mozIDOMTelephony.idl
@@ -31,16 +31,17 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsIDOMEventTarget.idl"
+#include "nsIDOMEvent.idl"
 interface nsIDOMEventListener;
 interface mozIDOMTelephonyCall;
 
 [scriptable, uuid(24371c72-1631-477d-b26e-f14db369bc22)]
 interface mozIDOMTelephony : nsIDOMEventTarget {
 
   readonly attribute jsval liveCalls;
   mozIDOMTelephonyCall dial(in DOMString number);
@@ -69,8 +70,13 @@ interface mozIDOMTelephonyCall : nsIDOME
   attribute nsIDOMEventListener onreadystatechange;
   attribute nsIDOMEventListener onringing;
   attribute nsIDOMEventListener onbusy;
   attribute nsIDOMEventListener onconnecting;
   attribute nsIDOMEventListener onconnected;
   attribute nsIDOMEventListener ondisconnecting;
   attribute nsIDOMEventListener ondisconnected;
 };
+
+[scriptable, uuid(c8c42b0c-a0dd-4702-9425-a7a80b2075c3)]
+interface mozIDOMTelephonyCallEvent : nsIDOMEvent {
+  readonly attribute mozIDOMTelephonyCall call;
+};
--- a/dom/telephony/nsITelephone.idl
+++ b/dom/telephony/nsITelephone.idl
@@ -32,30 +32,32 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
 
-[scriptable, uuid(e21cdf40-c511-442b-8c7d-aa75471b0423)]
+[scriptable, uuid(9b7e3a01-9c45-4af3-81bb-1bf08a842226)]
 interface nsITelephoneCallback : nsISupports {
-  void oncallstatechange(in jsval callState);
+  void oncallstatechange(in jsval event);
 
   //XXX philikon's additions
-  void onoperatorchange(in jsval operatorInfo);
-  void onradiostatechange(in jsval radioState);
-  void oncardstatechange(in jsval cardState);
-  void onsignalstrengthchange(in jsval signalStrength);
+  void onoperatorchange(in jsval event);
+  void onradiostatechange(in jsval event);
+  void oncardstatechange(in jsval event);
+  void onsignalstrengthchange(in jsval event);
 };
 
-[scriptable, uuid(dda49485-5887-49bb-ba0b-10c5d116eb64)]
+[scriptable, uuid(3d3deb80-fa5e-4e05-9153-91ee614f67d5)]
 interface nsITelephone : nsISupports {
 
-  readonly attribute jsval initialState;
+  readonly attribute jsval currentState;
 
   void dial(in DOMString number);
-  void hangup(in long callIndex);
+  void hangUp(in long callIndex);
+  void answerCall();
+  void rejectCall();
 
   void registerCallback(in nsITelephoneCallback callback);
   void unregisterCallback(in nsITelephoneCallback callback);
 };
--- a/dom/telephony/nsTelephonyWorker.js
+++ b/dom/telephony/nsTelephonyWorker.js
@@ -40,24 +40,31 @@ const {classes: Cc, interfaces: Ci, util
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const DEBUG = true; // set to false to suppress debug messages
 
 const TELEPHONYWORKER_CONTRACTID = "@mozilla.org/telephony/worker;1";
 const TELEPHONYWORKER_CID        = Components.ID("{2d831c8d-6017-435b-a80c-e5d422810cea}");
 
+const DOM_CALL_READYSTATE_DISCONNECTED = "disconnected";
 
 function nsTelephonyWorker() {
   this.worker = new ChromeWorker("resource://gre/modules/ril_worker.js");
   this.worker.onerror = this.onerror.bind(this);
   this.worker.onmessage = this.onmessage.bind(this);
 
   this._callbacks = [];
-  this.initialState = {};
+  this.currentState = {
+    signalStrength: null,
+    operator:       null,
+    radioState:     null,
+    cardState:      null,
+    currentCalls:   {}
+  };
 }
 nsTelephonyWorker.prototype = {
 
   classID:   TELEPHONYWORKER_CID,
   classInfo: XPCOMUtils.generateCI({classID: TELEPHONYWORKER_CID,
                                     contractID: TELEPHONYWORKER_CONTRACTID,
                                     classDescription: "TelephonyWorker",
                                     interfaces: [Ci.nsIRadioWorker,
@@ -81,61 +88,78 @@ nsTelephonyWorker.prototype = {
           event.lineno + ": " + event.message + "\n");
   },
 
   onmessage: function onmessage(event) {
     let message = event.data;
     debug("Received message: " + JSON.stringify(message));
     let value;
     switch (message.type) {
+      case "callstatechange":
       case "signalstrengthchange":
-        this.initialState.signalStrength = message.signalStrength;
-        value = message.signalStrength;
+        this.currentState.signalStrength = message.signalStrength;
         break;
       case "operatorchange":
-        this.initialState.operator = message.operator;
-        value = message.operator;
+        this.currentState.operator = message.operator;
         break;
-      case "onradiostatechange":
-        this.initialState.radioState = message.radioState;
-        value = message.radioState;
+      case "radiostatechange":
+        this.currentState.radioState = message.radioState;
         break;
       case "cardstatechange":
-        this.initialState.cardState = message.cardState;
-        value = message.cardState;
+        this.currentState.cardState = message.cardState;
         break;
       case "callstatechange":
-        this.initialState.callState = message.callState;
-        value = message.callState;
+        // Reuse the message object as the value here since there's more to
+        // the call state than just the callState integer.
+        if (message.callState == DOM_CALL_READYSTATE_DISCONNECTED) {
+          delete this.currentState.callState[message.callIndex];
+        } else {
+          this.currentState.callState[value.callIndex] = message;
+        }
         break;
       default:
         // Got some message from the RIL worker that we don't know about.
+        return;
     }
+    let methodname = "on" + message.type;
     this._callbacks.forEach(function (callback) {
       let method = callback[methodname];
       if (typeof method != "function") {
         return;
       }
-      method.call(callback, value);
+      method.call(callback, message);
     });
   },
 
   // nsIRadioWorker
 
   worker: null,
 
   // nsITelephone
 
-  initialState: null,
+  currentState: null,
 
   dial: function dial(number) {
     debug("Dialing " + number);
     this.worker.postMessage({type: "dial", number: number});
   },
 
+  hangUp: function hangUp(callIndex) {
+    debug("Hanging up call no. " + callIndex);
+    this.worker.postMessage({type: "hangUp", callIndex: callIndex});
+  },
+
+  answerCall: function answerCall() {
+    this.worker.postMessage({type: "answerCall"});
+  },
+
+  rejectCall: function rejectCall() {
+    this.worker.postMessage({type: "rejectCall"});
+  },
+
   _callbacks: null,
 
   registerCallback: function registerCallback(callback) {
     this._callbacks.push(callback);
   },
 
   unregisterCallback: function unregisterCallback(callback) {
     let index = this._callbacks.indexOf(callback);