Bug 881174 - part2 - cdma 3way call RIL impl. r=vicamo
authorHsin-Yi Tsai <htsai@mozilla.com>
Fri, 21 Feb 2014 17:46:58 +0800
changeset 171391 f98abdde7107a993e9f8cb43139ed9ecf78341f3
parent 171390 761fb3484d0af3a7953da1bb1be5a0d3a9a9d30c
child 171392 d6a671e2ba43e02937c1c35848d2b728431d2867
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersvicamo
bugs881174
milestone30.0a1
Bug 881174 - part2 - cdma 3way call RIL impl. r=vicamo
dom/system/gonk/RadioInterfaceLayer.js
dom/system/gonk/ril_consts.js
dom/system/gonk/ril_worker.js
dom/telephony/gonk/TelephonyProvider.js
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -2147,20 +2147,16 @@ RadioInterface.prototype = {
       case "cdmaCallWaiting":
         gTelephonyProvider.notifyCdmaCallWaiting(this.clientId, message.number);
         break;
       case "suppSvcNotification":
         gTelephonyProvider.notifySupplementaryService(this.clientId,
                                                       message.callIndex,
                                                       message.notification);
         break;
-      case "conferenceError":
-        gTelephonyProvider.notifyConferenceError(message.errorName,
-                                                 message.errorMsg);
-        break;
       case "datacallerror":
         connHandler.handleDataCallError(message);
         break;
       case "datacallstatechange":
         message.ip = null;
         message.prefixLength = 0;
         message.broadcast = null;
         if (message.ipaddr) {
@@ -4174,19 +4170,23 @@ RadioInterface.prototype = {
   },
 
   deactivateDataCall: function(cid, reason) {
     this.workerMessenger.send("deactivateDataCall", { cid: cid,
                                                       reason: reason });
   },
 
   sendWorkerMessage: function(rilMessageType, message, callback) {
-    this.workerMessenger.send(rilMessageType, message, function(response) {
-      return callback.handleResponse(response);
-    });
+    if (callback) {
+      this.workerMessenger.send(rilMessageType, message, function(response) {
+        return callback.handleResponse(response);
+      });
+    } else {
+      this.workerMessenger.send(rilMessageType, message);
+    }
   }
 };
 
 function RILNetworkInterface(dataConnectionHandler, apnSetting) {
   this.dataConnectionHandler = dataConnectionHandler;
   this.apnSetting = apnSetting;
   this.connectedTypes = [];
 }
--- a/dom/system/gonk/ril_consts.js
+++ b/dom/system/gonk/ril_consts.js
@@ -31,17 +31,16 @@ this.REQUEST_CHANGE_SIM_PIN2 = 7;
 this.REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE = 8;
 this.REQUEST_GET_CURRENT_CALLS = 9;
 this.REQUEST_DIAL = 10;
 this.REQUEST_GET_IMSI = 11;
 this.REQUEST_HANGUP = 12;
 this.REQUEST_HANGUP_WAITING_OR_BACKGROUND = 13;
 this.REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND = 14;
 this.REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE = 15;
-this.REQUEST_SWITCH_HOLDING_AND_ACTIVE = 15;
 this.REQUEST_CONFERENCE = 16;
 this.REQUEST_UDUB = 17;
 this.REQUEST_LAST_CALL_FAIL_CAUSE = 18;
 this.REQUEST_SIGNAL_STRENGTH = 19;
 this.REQUEST_VOICE_REGISTRATION_STATE = 20;
 this.REQUEST_DATA_REGISTRATION_STATE = 21;
 this.REQUEST_OPERATOR = 22;
 this.REQUEST_RADIO_POWER = 23;
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -1461,53 +1461,73 @@ RilObject.prototype = {
       return;
     }
 
     // Exit emergency callback mode when user dial a non-emergency call.
     if (this._isInEmergencyCbMode) {
       this.exitEmergencyCbMode();
     }
 
-    options.request = REQUEST_DIAL;
-    this.sendDialRequest(options);
+    if (this._isCdma && Object.keys(this.currentCalls).length == 1) {
+      // Make a Cdma 3way call.
+      options.featureStr = options.number;
+      this.sendCdmaFlashCommand(options);
+    } else {
+      options.request = REQUEST_DIAL;
+      this.sendDialRequest(options);
+    }
   },
 
   dialEmergencyNumber: function(options, onerror) {
     options.request = RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL ?
                       REQUEST_DIAL_EMERGENCY_CALL : REQUEST_DIAL;
-
     if (this.radioState == GECKO_RADIOSTATE_OFF) {
       if (DEBUG) {
         this.context.debug("Automatically enable radio for an emergency call.");
       }
 
       if (!this.cachedDialRequest) {
         this.cachedDialRequest = {};
       }
       this.cachedDialRequest.onerror = onerror;
       this.cachedDialRequest.callback = this.sendDialRequest.bind(this, options);
       this.setRadioEnabled({enabled: true});
       return;
     }
 
-    this.sendDialRequest(options);
+    if (this._isCdma && Object.keys(this.currentCalls).length == 1) {
+      // Make a Cdma 3way call.
+      options.featureStr = options.number;
+      this.sendCdmaFlashCommand(options);
+    } else {
+      this.sendDialRequest(options);
+    }
   },
 
   sendDialRequest: function(options) {
     let Buf = this.context.Buf;
     Buf.newParcel(options.request, options);
     Buf.writeString(options.number);
     Buf.writeInt32(options.clirMode || 0);
     Buf.writeInt32(options.uusInfo || 0);
     // TODO Why do we need this extra 0? It was put it in to make this
     // match the format of the binary message.
     Buf.writeInt32(0);
     Buf.sendParcel();
   },
 
+  sendCdmaFlashCommand: function(options) {
+    let Buf = this.context.Buf;
+    options.isCdma = true;
+    options.request = REQUEST_CDMA_FLASH;
+    Buf.newParcel(options.request, options);
+    Buf.writeString(options.featureStr);
+    Buf.sendParcel();
+  },
+
   /**
    * Hang up all calls
    */
   hangUpAll: function() {
     for (let callIndex in this.currentCalls) {
       this.hangUp({callIndex: callIndex});
     }
   },
@@ -1613,57 +1633,100 @@ RilObject.prototype = {
         // Reject the waiting (second) call, and remain the first call.
         Buf.simpleRequest(REQUEST_HANGUP_WAITING_OR_BACKGROUND);
         break;
     }
   },
 
   holdCall: function(options) {
     let call = this.currentCalls[options.callIndex];
-    if (call && call.state == CALL_STATE_ACTIVE) {
-      let Buf = this.context.Buf;
-      if (this._isCdma) {
-        Buf.newParcel(REQUEST_CDMA_FLASH);
-        Buf.writeString("");
-        Buf.sendParcel();
-      } else {
-        Buf.simpleRequest(REQUEST_SWITCH_HOLDING_AND_ACTIVE);
-      }
-    }
-  },
+    if (!call) {
+      options.errorMsg = GECKO_ERROR_GENERIC_FAILURE;
+      options.success = false;
+      this.sendChromeMessage(options);
+      return;
+    }
+
+    let Buf = this.context.Buf;
+    if (this._isCdma) {
+      options.featureStr = "";
+      this.sendCdmaFlashCommand(options);
+    } else if (call.state == CALL_STATE_ACTIVE) {
+      Buf.simpleRequest(REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, options);
+    }
+ },
 
   resumeCall: function(options) {
     let call = this.currentCalls[options.callIndex];
-    if (call && call.state == CALL_STATE_HOLDING) {
-      this.context.Buf.simpleRequest(REQUEST_SWITCH_HOLDING_AND_ACTIVE);
+    if (!call) {
+      options.errorMsg = GECKO_ERROR_GENERIC_FAILURE;
+      options.success = false;
+      this.sendChromeMessage(options);
+      return;
+    }
+
+    let Buf = this.context.Buf;
+    if (this._isCdma) {
+      options.featureStr = "";
+      this.sendCdmaFlashCommand(options);
+    } else if (call.state == CALL_STATE_HOLDING) {
+      Buf.simpleRequest(REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, options);
     }
   },
 
   // Flag indicating whether user has requested making a conference call.
   _hasConferenceRequest: false,
 
   conferenceCall: function(options) {
-    this._hasConferenceRequest = true;
-    this.context.Buf.simpleRequest(REQUEST_CONFERENCE, options);
+    let Buf = this.context.Buf;
+    if (this._isCdma) {
+      options.featureStr = "";
+      this.sendCdmaFlashCommand(options);
+    } else {
+      this._hasConferenceRequest = true;
+      Buf.simpleRequest(REQUEST_CONFERENCE, options);
+    }
   },
 
   separateCall: function(options) {
-    let Buf = this.context.Buf;
-    Buf.newParcel(REQUEST_SEPARATE_CONNECTION, options);
-    Buf.writeInt32(1);
-    Buf.writeInt32(options.callIndex);
-    Buf.sendParcel();
-  },
+    let call = this.currentCalls[options.callIndex];
+    if (!call) {
+      options.errorName = "removeError";
+      options.errorMsg = GECKO_ERROR_GENERIC_FAILURE;
+      options.success = false;
+      this.sendChromeMessage(options);
+      return;
+    }
+
+    let Buf = this.context.Buf;
+    if (this._isCdma) {
+      options.featureStr = "";
+      this.sendCdmaFlashCommand(options);
+    } else {
+      Buf.newParcel(REQUEST_SEPARATE_CONNECTION, options);
+      Buf.writeInt32(1);
+      Buf.writeInt32(options.callIndex);
+      Buf.sendParcel();
+    }
+ },
 
   holdConference: function() {
-    this.context.Buf.simpleRequest(REQUEST_SWITCH_HOLDING_AND_ACTIVE);
+    if (this._isCdma) {
+      return;
+    }
+
+    this.context.Buf.simpleRequest(REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE);
   },
 
   resumeConference: function() {
-    this.context.Buf.simpleRequest(REQUEST_SWITCH_HOLDING_AND_ACTIVE);
+    if (this._isCdma) {
+      return;
+    }
+
+    this.context.Buf.simpleRequest(REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE);
   },
 
   /**
    * Send an SMS.
    *
    * The `options` parameter object should contain the following attributes:
    *
    * @param number
@@ -5287,41 +5350,37 @@ RilObject.prototype[REQUEST_HANGUP_WAITI
 RilObject.prototype[REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND] = function REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND(length, options) {
   if (options.rilRequestError) {
     return;
   }
 
   this.getCurrentCalls();
 };
 RilObject.prototype[REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE] = function REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE(length, options) {
-  if (options.rilRequestError) {
+  options.success = (options.rilRequestError === 0);
+  if (!options.success) {
+    options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
+    this.sendChromeMessage(options);
     return;
   }
 
-  this.getCurrentCalls();
-};
-RilObject.prototype[REQUEST_SWITCH_HOLDING_AND_ACTIVE] = function REQUEST_SWITCH_HOLDING_AND_ACTIVE(length, options) {
-  if (options.rilRequestError) {
-    return;
-  }
-
-  // XXX Normally we should get a UNSOLICITED_RESPONSE_CALL_STATE_CHANGED parcel
-  // notifying us of call state changes, but sometimes we don't (have no idea why).
-  // this.getCurrentCalls() helps update the call state actively.
+  this.sendChromeMessage(options);
   this.getCurrentCalls();
 };
 RilObject.prototype[REQUEST_CONFERENCE] = function REQUEST_CONFERENCE(length, options) {
-  if (options.rilRequestError) {
+  options.success = (options.rilRequestError === 0);
+  if (!options.success) {
     this._hasConferenceRequest = false;
-    options = {rilMessageType: "conferenceError",
-               errorName: "addError",
-               errorMsg: RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError]};
+    options.errorName = "addError";
+    options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
     this.sendChromeMessage(options);
     return;
   }
+
+  this.sendChromeMessage(options);
 };
 RilObject.prototype[REQUEST_UDUB] = null;
 RilObject.prototype[REQUEST_LAST_CALL_FAIL_CAUSE] = function REQUEST_LAST_CALL_FAIL_CAUSE(length, options) {
   let Buf = this.context.Buf;
   let num = length ? Buf.readInt32() : 0;
   let failCause = num ? RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[Buf.readInt32()] : null;
   if (options.callback) {
     options.callback(failCause);
@@ -5898,23 +5957,25 @@ RilObject.prototype[REQUEST_BASEBAND_VER
   if (options.rilRequestError) {
     return;
   }
 
   this.basebandVersion = this.context.Buf.readString();
   if (DEBUG) this.context.debug("Baseband version: " + this.basebandVersion);
 };
 RilObject.prototype[REQUEST_SEPARATE_CONNECTION] = function REQUEST_SEPARATE_CONNECTION(length, options) {
-  if (options.rilRequestError) {
-    options = {rilMessageType: "conferenceError",
-               errorName: "removeError",
-               errorMsg: RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError]};
+  options.success = (options.rilRequestError === 0);
+  if (!options.success) {
+    options.errorName = "removeError";
+    options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
     this.sendChromeMessage(options);
     return;
   }
+
+  this.sendChromeMessage(options);
 };
 RilObject.prototype[REQUEST_SET_MUTE] = null;
 RilObject.prototype[REQUEST_GET_MUTE] = null;
 RilObject.prototype[REQUEST_QUERY_CLIP] = function REQUEST_QUERY_CLIP(length, options) {
   options.success = (options.rilRequestError === 0);
   if (!options.success) {
     options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
     this.sendChromeMessage(options);
@@ -6120,17 +6181,29 @@ RilObject.prototype[REQUEST_CDMA_QUERY_P
     this.sendChromeMessage(options);
     return;
   }
 
   let enabled = this.context.Buf.readInt32List();
   options.enabled = enabled[0] ? true : false;
   this.sendChromeMessage(options);
 };
-RilObject.prototype[REQUEST_CDMA_FLASH] = null;
+RilObject.prototype[REQUEST_CDMA_FLASH] = function REQUEST_CDMA_FLASH(length, options) {
+  options.success = (options.rilRequestError === 0);
+  if (!options.success) {
+    if (options.rilMessageType === "conferenceCall") {
+      options.errorName = "addError";
+    } else if (options.rilMessageType === "separateCall") {
+      options.errorName = "removeError";
+    }
+    options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[options.rilRequestError];
+  }
+
+  this.sendChromeMessage(options);
+};
 RilObject.prototype[REQUEST_CDMA_BURST_DTMF] = null;
 RilObject.prototype[REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY] = null;
 RilObject.prototype[REQUEST_CDMA_SEND_SMS] = function REQUEST_CDMA_SEND_SMS(length, options) {
   this._processSmsSendResult(length, options);
 };
 RilObject.prototype[REQUEST_CDMA_SMS_ACKNOWLEDGE] = null;
 RilObject.prototype[REQUEST_GSM_GET_BROADCAST_SMS_CONFIG] = null;
 RilObject.prototype[REQUEST_GSM_SET_BROADCAST_SMS_CONFIG] = function REQUEST_GSM_SET_BROADCAST_SMS_CONFIG(length, options) {
--- a/dom/telephony/gonk/TelephonyProvider.js
+++ b/dom/telephony/gonk/TelephonyProvider.js
@@ -27,16 +27,19 @@ const kPrefRilNumRadioInterfaces = "ril.
 const kPrefRilDebuggingEnabled = "ril.debugging.enabled";
 const kPrefDefaultServiceId = "dom.telephony.defaultServiceId";
 
 const nsIAudioManager = Ci.nsIAudioManager;
 const nsITelephonyProvider = Ci.nsITelephonyProvider;
 
 const CALL_WAKELOCK_TIMEOUT = 5000;
 
+// Index of the CDMA second call which isn't held in RIL but only in TelephoyProvider.
+const CDMA_SECOND_CALL_INDEX = 2;
+
 let DEBUG;
 function debug(s) {
   dump("TelephonyProvider: " + s + "\n");
 }
 
 XPCOMUtils.defineLazyGetter(this, "gAudioManager", function getAudioManager() {
   try {
     return Cc["@mozilla.org/telephony/audiomanager;1"]
@@ -107,23 +110,28 @@ function ConferenceCall(state){
 }
 ConferenceCall.prototype = {
   state: null
 };
 
 function TelephonyProvider() {
   this._numClients = gRadioInterfaceLayer.numRadioInterfaces;
   this._listeners = [];
+  this._currentCalls = {};
   this._updateDebugFlag();
   this.defaultServiceId = this._getDefaultServiceId();
 
   Services.prefs.addObserver(kPrefRilDebuggingEnabled, this, false);
   Services.prefs.addObserver(kPrefDefaultServiceId, this, false);
 
   Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+
+  for (let i = 0; i < this._numClients; ++i) {
+    this._enumerateCallsForClient(i);
+  }
 }
 TelephonyProvider.prototype = {
   classID: GONK_TELEPHONYPROVIDER_CID,
   classInfo: XPCOMUtils.generateCI({classID: GONK_TELEPHONYPROVIDER_CID,
                                     contractID: GONK_TELEPHONYPROVIDER_CONTRACTID,
                                     classDescription: "TelephonyProvider",
                                     interfaces: [Ci.nsITelephonyProvider,
                                                  Ci.nsIGonkTelephonyProvider],
@@ -335,16 +343,39 @@ TelephonyProvider.prototype = {
 
     if (id >= numRil || id < 0) {
       id = 0;
     }
 
     return id;
   },
 
+  _currentCalls: null,
+  _enumerateCallsForClient: function(aClientId) {
+    if (DEBUG) debug("Enumeration of calls for client " + aClientId);
+
+    this._getClient(aClientId).sendWorkerMessage("enumerateCalls", null,
+                                                 (function(response) {
+      if (!this._currentCalls[aClientId]) {
+        this._currentCalls[aClientId] = {};
+      }
+      for (let call of response.calls) {
+        call.clientId = aClientId;
+        call.state = this._convertRILCallState(call.state);
+        call.isActive = this._matchActiveSingleCall(call);
+        call.isSwitchable = true;
+        call.isMergeable = true;
+
+        this._currentCalls[aClientId][call.callIndex] = call;
+      }
+
+      return false;
+    }).bind(this));
+  },
+
   /**
    * nsITelephonyProvider interface.
    */
 
   defaultServiceId: 0,
 
   registerListener: function(aListener) {
     if (this._listeners.indexOf(aListener) >= 0) {
@@ -358,51 +389,34 @@ TelephonyProvider.prototype = {
     let index = this._listeners.indexOf(aListener);
     if (index < 0) {
       throw Cr.NS_ERROR_UNEXPECTED;
     }
 
     this._listeners.splice(index, 1);
   },
 
-  _enumerateCallsForClient: function(aClientId, aListener) {
-    if (DEBUG) debug("Enumeration of calls for client " + aClientId);
-
-    let deferred = Promise.defer();
+  enumerateCalls: function(aListener) {
+    if (DEBUG) debug("Requesting enumeration of calls for callback");
 
-    this._getClient(aClientId).sendWorkerMessage("enumerateCalls", null,
-                                                 (function(response) {
-      for (let call of response.calls) {
-        call.clientId = aClientId;
-        call.state = this._convertRILCallState(call.state);
-        call.isActive = this._matchActiveSingleCall(call);
-
+    for (let cid = 0; cid < this._numClients; ++cid) {
+      let calls = this._currentCalls[cid];
+      if (!calls) {
+        continue;
+      }
+      for (let i = 0, indexes = Object.keys(calls); i < indexes.length; ++i) {
+        let call = calls[indexes[i]];
         aListener.enumerateCallState(call.clientId, call.callIndex,
                                      call.state, call.number,
                                      call.isActive, call.isOutgoing,
-                                     call.isEmergency, call.isConference);
+                                     call.isEmergency, call.isConference,
+                                     call.isSwitchable, call.isMergeable);
       }
-      deferred.resolve();
-
-      return false;
-    }).bind(this));
-
-    return deferred.promise;
-  },
-
-  enumerateCalls: function(aListener) {
-    if (DEBUG) debug("Requesting enumeration of calls for callback");
-
-    let promise = Promise.resolve();
-    for (let i = 0; i < this._numClients; ++i) {
-      promise = promise.then(this._enumerateCallsForClient.bind(this, i, aListener));
     }
-    promise.then(function() {
-      aListener.enumerateCallStateComplete();
-    });
+    aListener.enumerateCallStateComplete();
   },
 
   isDialing: false,
   dial: function(aClientId, aNumber, aIsEmergency, aTelephonyCallback) {
     if (DEBUG) debug("Dialing " + (aIsEmergency ? "emergency " : "") + aNumber);
 
     if (this.isDialing) {
       if (DEBUG) debug("Already has a dialing call. Drop.");
@@ -420,33 +434,80 @@ TelephonyProvider.prototype = {
     if (!gPhoneNumberUtils.isPlainPhoneNumber(aNumber)) {
       // Note: isPlainPhoneNumber also accepts USSD and SS numbers
       if (DEBUG) debug("Number '" + aNumber + "' is not viable. Drop.");
       let errorMsg = RIL.RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[RIL.CALL_FAIL_UNOBTAINABLE_NUMBER];
       aTelephonyCallback.notifyDialError(errorMsg);
       return;
     }
 
+    function onCdmaDialSuccess() {
+      let indexes = Object.keys(this._currentCalls[aClientId]);
+      if (indexes.length != 1 ) {
+        aTelephonyCallback.notifyDialSuccess();
+        return;
+      }
+
+      // RIL doesn't hold the 2nd call. We create one by ourselves.
+      let childCall = {
+        callIndex: CDMA_SECOND_CALL_INDEX,
+        state: RIL.CALL_STATE_DIALING,
+        number: aNumber,
+        isOutgoing: true,
+        isEmergency: false,
+        isConference: false,
+        isSwitchable: false,
+        isMergeable: true,
+        parentId: indexes[0]
+      };
+      aTelephonyCallback.notifyDialSuccess();
+
+      // Manual update call state according to the request response.
+      this.notifyCallStateChanged(aClientId, childCall);
+
+      childCall.state = RIL.CALL_STATE_ACTIVE;
+      this.notifyCallStateChanged(aClientId, childCall);
+
+      let parentCall = this._currentCalls[aClientId][childCall.parentId];
+      parentCall.childId = CDMA_SECOND_CALL_INDEX;
+      parentCall.state = RIL.CALL_STATE_HOLDING;
+      parentCall.isSwitchable = false;
+      parentCall.isMergeable = true;
+      this.notifyCallStateChanged(aClientId, parentCall);
+    };
+
     this.isDialing = true;
     this._getClient(aClientId).sendWorkerMessage("dial", {
       number: aNumber,
       isDialEmergency: aIsEmergency
     }, (function(response) {
       this.isDialing = false;
-      if (response.success) {
+      if (!response.success) {
+        aTelephonyCallback.notifyDialError(response.errorMsg);
+        return false;
+      }
+
+      if (response.isCdma) {
+        onCdmaDialSuccess.call(this);
+      } else {
         aTelephonyCallback.notifyDialSuccess();
-      } else {
-        aTelephonyCallback.notifyDialError(response.errorMsg);
       }
       return false;
     }).bind(this));
   },
 
   hangUp: function(aClientId, aCallIndex) {
-    this._getClient(aClientId).sendWorkerMessage("hangUp", { callIndex: aCallIndex });
+    let parentId = this._currentCalls[aClientId][aCallIndex].parentId;
+    if (parentId) {
+      // Should release both, child and parent, together. Since RIL holds only
+      // the parent call, we send 'parentId' to RIL.
+      this.hangUp(aClientId, parentId);
+    } else {
+      this._getClient(aClientId).sendWorkerMessage("hangUp", { callIndex: aCallIndex });
+    }
   },
 
   startTone: function(aClientId, aDtmfChar) {
     this._getClient(aClientId).sendWorkerMessage("startTone", { dtmfChar: aDtmfChar });
   },
 
   stopTone: function(aClientId) {
     this._getClient(aClientId).sendWorkerMessage("stopTone");
@@ -456,29 +517,197 @@ TelephonyProvider.prototype = {
     this._getClient(aClientId).sendWorkerMessage("answerCall", { callIndex: aCallIndex });
   },
 
   rejectCall: function(aClientId, aCallIndex) {
     this._getClient(aClientId).sendWorkerMessage("rejectCall", { callIndex: aCallIndex });
   },
 
   holdCall: function(aClientId, aCallIndex) {
-    this._getClient(aClientId).sendWorkerMessage("holdCall", { callIndex: aCallIndex });
+    let call = this._currentCalls[aClientId][aCallIndex];
+    if (!call || !call.isSwitchable) {
+      // TODO: Bug 975949 - [B2G] Telephony should throw exceptions when some
+      // operations aren't allowed instead of simply ignoring them.
+      return;
+    }
+
+    let parentId = this._currentCalls[aClientId][aCallIndex].parentId;
+    if (parentId) {
+      this.resumeCall(aClientId, parentId);
+      return;
+    }
+
+    function onCdmaHoldCallSuccess() {
+      let call = this._currentCalls[aClientId][aCallIndex];
+      if (!call) {
+        return;
+      }
+
+      call.state = RIL.CALL_STATE_HOLDING;
+      this.notifyCallStateChanged(aClientId, call);
+
+      if (!call.childId) {
+        return;
+      }
+
+      let childCall = this._currentCalls[aClientId][call.childId];
+      childCall.state = RIL.CALL_STATE_ACTIVE;
+      this.notifyCallStateChanged(aClientId, childCall);
+    };
+
+    this._getClient(aClientId).sendWorkerMessage("holdCall", {
+      callIndex: aCallIndex
+    },(function(response) {
+        if (!response.success) {
+          return false;
+        }
+
+        if (response.isCdma) {
+          onCdmaHoldCallSuccess.call(this);
+        }
+        return false;
+    }).bind(this));
   },
 
   resumeCall: function(aClientId, aCallIndex) {
-    this._getClient(aClientId).sendWorkerMessage("resumeCall", { callIndex: aCallIndex });
+    let call = this._currentCalls[aClientId][aCallIndex];
+    if (!call || !call.isSwitchable) {
+      // TODO: Bug 975949 - [B2G] Telephony should throw exceptions when some
+      // operations aren't allowed instead of simply ignoring them.
+      return;
+    }
+
+    let parentId = this._currentCalls[aClientId][aCallIndex].parentId;
+    if (parentId) {
+      this.holdCall(aClientId, parentId);
+      return;
+    }
+
+    function onCdmaResumeCallSuccess() {
+      let call = this._currentCalls[aClientId][aCallIndex];
+      if (!call) {
+        return;
+      }
+
+      call.state = RIL.CALL_STATE_ACTIVE;
+      this.notifyCallStateChanged(aClientId, call);
+
+      let childId = call.childId;
+      if (!childId) {
+        return;
+      }
+
+      let childCall = this._currentCalls[aClientId][childId];
+      childCall.state = RIL.CALL_STATE_HOLDING;
+      this.notifyCallStateChanged(aClientId, childCall);
+    };
+
+    this._getClient(aClientId).sendWorkerMessage("resumeCall", {
+      callIndex: aCallIndex
+    },(function(response) {
+      if (!response.success) {
+        return false;
+      }
+
+      if (response.isCdma) {
+        onCdmaResumeCallSuccess.call(this);
+      }
+      return false;
+    }).bind(this));
   },
 
   conferenceCall: function(aClientId) {
-    this._getClient(aClientId).sendWorkerMessage("conferenceCall");
+    let indexes = Object.keys(this._currentCalls[aClientId]);
+    if (indexes.length < 2) {
+      // TODO: Bug 975949 - [B2G] Telephony should throw exceptions when some
+      // operations aren't allowed instead of simply ignoring them.
+      return;
+    }
+
+    for (let i = 0; i < indexes.length; ++i) {
+      let call = this._currentCalls[aClientId][indexes[i]];
+      if (!call.isMergeable) {
+        return;
+      }
+    }
+
+    function onCdmaConferenceCallSuccess() {
+      let indexes = Object.keys(this._currentCalls[aClientId]);
+      if (indexes.length < 2) {
+        return;
+      }
+
+      for (let i = 0; i < indexes.length; ++i) {
+        let call = this._currentCalls[aClientId][indexes[i]];
+        call.state = RIL.CALL_STATE_ACTIVE;
+        call.isConference = true;
+        this.notifyCallStateChanged(aClientId, call);
+      }
+      this.notifyConferenceCallStateChanged(RIL.CALL_STATE_ACTIVE);
+    };
+
+    this._getClient(aClientId).sendWorkerMessage("conferenceCall", null,
+                                                 (function(response) {
+      if (!response.success) {
+        this._notifyAllListeners("notifyConferenceError", [response.errorName,
+                                                           response.errorMsg]);
+        return false;
+      }
+
+      if (response.isCdma) {
+        onCdmaConferenceCallSuccess.call(this);
+      }
+      return false;
+    }).bind(this));
   },
 
   separateCall: function(aClientId, aCallIndex) {
-    this._getClient(aClientId).sendWorkerMessage("separateCall", { callIndex: aCallIndex });
+    let call = this._currentCalls[aClientId][aCallIndex];
+    if (!call || !call.isConference) {
+      // TODO: Bug 975949 - [B2G] Telephony should throw exceptions when some
+      // operations aren't allowed instead of simply ignoring them.
+      return;
+    }
+
+    let parentId = call.parentId;
+    if (parentId) {
+      this.separateCall(aClientId, parentId);
+      return;
+    }
+
+    function onCdmaSeparateCallSuccess() {
+      // See 3gpp2, S.R0006-522-A v1.0. Table 4, XID 6S.
+      let call = this._currentCalls[aClientId][aCallIndex];
+      if (!call || !call.isConference) {
+        return;
+      }
+
+      let childId = call.childId;
+      if (!childId) {
+        return;
+      }
+
+      let childCall = this._currentCalls[aClientId][childId];
+      this.notifyCallDisconnected(aClientId, childCall);
+    };
+
+    this._getClient(aClientId).sendWorkerMessage("separateCall", {
+      callIndex: aCallIndex
+    }, (function(response) {
+      if (!response.success) {
+        this._notifyAllListeners("notifyConferenceError", [response.errorName,
+                                                           response.errorMsg]);
+        return false;
+      }
+
+      if (response.isCdma) {
+        onCdmaSeparateCallSuccess.call(this);
+      }
+      return false;
+    }).bind(this));
   },
 
   holdConference: function(aClientId) {
     this._getClient(aClientId).sendWorkerMessage("holdConference");
   },
 
   resumeConference: function(aClientId) {
     this._getClient(aClientId).sendWorkerMessage("resumeConference");
@@ -535,30 +764,61 @@ TelephonyProvider.prototype = {
       duration: duration,
       direction: aCall.isOutgoing ? "outgoing" : "incoming"
     };
     gSystemMessenger.broadcastMessage("telephony-call-ended", data);
 
     aCall.clientId = aClientId;
     this._updateCallAudioState(aCall, null);
 
+    let manualConfStateChange = false;
+    let childId = this._currentCalls[aClientId][aCall.callIndex].childId;
+    if (childId) {
+      // Child cannot live without parent.
+      let childCall = this._currentCalls[aClientId][childId];
+      this.notifyCallDisconnected(aClientId, childCall);
+    } else {
+      let parentId = this._currentCalls[aClientId][aCall.callIndex].parentId;
+      if (parentId) {
+        let parentCall = this._currentCalls[aClientId][parentId];
+        // The child is going to be released.
+        delete parentCall.childId;
+        if (parentCall.isConference) {
+          // As the child is going to be gone, the parent should be moved out
+          // of conference accordingly.
+          manualConfStateChange = true;
+          parentCall.isConference = false;
+          parentCall.isSwitchable = true;
+          parentCall.isMergeable = true;
+          aCall.isConference = false;
+          this.notifyCallStateChanged(aClientId, parentCall, true);
+        }
+      }
+    }
+
     if (!aCall.failCause ||
         aCall.failCause === RIL.GECKO_CALL_ERROR_NORMAL_CALL_CLEARING) {
       this._notifyAllListeners("callStateChanged", [aClientId,
                                                     aCall.callIndex,
                                                     aCall.state,
                                                     aCall.number,
                                                     aCall.isActive,
                                                     aCall.isOutgoing,
                                                     aCall.isEmergency,
-                                                    aCall.isConference]);
-      return;
+                                                    aCall.isConference,
+                                                    aCall.isSwitchable,
+                                                    aCall.isMergeable]);
+    } else {
+      this.notifyCallError(aClientId, aCall.callIndex, aCall.failCause);
     }
+    delete this._currentCalls[aClientId][aCall.callIndex];
 
-    this.notifyCallError(aClientId, aCall.callIndex, aCall.failCause);
+    if (manualConfStateChange) {
+      this.notifyConferenceCallStateChanged(RIL.CALL_STATE_UNKNOWN);
+    }
   },
 
   /**
    * Handle call error.
    */
   notifyCallError: function(aClientId, aCallIndex, aErrorMsg) {
     this._notifyAllListeners("notifyError", [aClientId, aCallIndex, aErrorMsg]);
   },
@@ -576,42 +836,72 @@ TelephonyProvider.prototype = {
 
     gSystemMessenger.broadcastMessage("telephony-new-call", {});
   },
 
   /**
    * Handle call state changes by updating our current state and the audio
    * system.
    */
-  notifyCallStateChanged: function(aClientId, aCall) {
+  notifyCallStateChanged: function(aClientId, aCall, aSkipStateConversion) {
     if (DEBUG) debug("handleCallStateChange: " + JSON.stringify(aCall));
 
-    aCall.state = this._convertRILCallState(aCall.state);
+    if (!aSkipStateConversion) {
+      aCall.state = this._convertRILCallState(aCall.state);
+    }
+
     if (aCall.state == nsITelephonyProvider.CALL_STATE_DIALING) {
       gSystemMessenger.broadcastMessage("telephony-new-call", {});
     }
 
     aCall.clientId = aClientId;
     this._updateCallAudioState(aCall, null);
 
+    let call = this._currentCalls[aClientId][aCall.callIndex];
+    if (call) {
+      call.state = aCall.state;
+      call.isConference = aCall.isConference;
+      call.isEmergency = aCall.isEmergency;
+      call.isActive = aCall.isActive;
+      call.isSwitchable = aCall.isSwitchable != null ?
+                          aCall.isSwitchable : call.isSwitchable;
+      call.isMergeable = aCall.isMergeable != null ?
+                         aCall.isMergeable : call.isMergeable;
+    } else {
+      call = aCall;
+      call.isSwitchable = aCall.isSwitchable != null ?
+                          aCall.isSwitchable : true;
+      call.isMergeable = aCall.isMergeable != null ?
+                         aCall.isMergeable : true;
+      this._currentCalls[aClientId][aCall.callIndex] = call;
+    }
+
     this._notifyAllListeners("callStateChanged", [aClientId,
-                                                  aCall.callIndex,
-                                                  aCall.state,
-                                                  aCall.number,
-                                                  aCall.isActive,
-                                                  aCall.isOutgoing,
-                                                  aCall.isEmergency,
-                                                  aCall.isConference]);
+                                                  call.callIndex,
+                                                  call.state,
+                                                  call.number,
+                                                  call.isActive,
+                                                  call.isOutgoing,
+                                                  call.isEmergency,
+                                                  call.isConference,
+                                                  call.isSwitchable,
+                                                  call.isMergeable]);
   },
 
   notifyCdmaCallWaiting: function(aClientId, aNumber) {
     // We need to acquire a CPU wake lock to avoid the system falling into
     // the sleep mode when the RIL handles the incoming call.
     this._acquireCallRingWakeLock();
 
+    let call = this._currentCalls[aClientId][CDMA_SECOND_CALL_INDEX];
+    if (call) {
+      // TODO: Bug 977503 - B2G RIL: [CDMA] update callNumber when a waiting
+      // call comes after a 3way call.
+      this.notifyCallDisconnected(aClientId, call);
+    }
     this._notifyAllListeners("notifyCdmaCallWaiting", [aClientId, aNumber]);
   },
 
   notifySupplementaryService: function(aClientId, aCallIndex, aNotification) {
     let notification = this._convertRILSuppSvcNotification(aNotification);
     this._notifyAllListeners("supplementaryServiceNotification",
                              [aClientId, aCallIndex, notification]);
   },
@@ -619,32 +909,26 @@ TelephonyProvider.prototype = {
   notifyConferenceCallStateChanged: function(aState) {
     if (DEBUG) debug("handleConferenceCallStateChanged: " + aState);
     aState = this._convertRILCallState(aState);
     this._updateCallAudioState(null, aState);
 
     this._notifyAllListeners("conferenceCallStateChanged", [aState]);
   },
 
-  notifyConferenceError: function(aName, aMessage) {
-    if (DEBUG) debug("handleConferenceError: " + aName + "." +
-                     " Error details: " + aMessage);
-    this._notifyAllListeners("notifyConferenceError", [aName, aMessage]);
-  },
-
   /**
    * nsIObserver interface.
    */
 
   observe: function(aSubject, aTopic, aData) {
     switch (aTopic) {
       case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
         if (aData === kPrefRilDebuggingEnabled) {
           this._updateDebugFlag();
-	} else if (aData === kPrefDefaultServiceId) {
+        } else if (aData === kPrefDefaultServiceId) {
           this.defaultServiceId = this._getDefaultServiceId();
         }
         break;
 
       case NS_XPCOM_SHUTDOWN_OBSERVER_ID:
         // Release the CPU wake lock for handling the incoming call.
         this._releaseCallRingWakeLock();