/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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";

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/Promise.jsm");

/* global RIL */
XPCOMUtils.defineLazyGetter(this, "RIL", function () {
  let obj = {};
  Cu.import("resource://gre/modules/ril_consts.js", obj);
  return obj;
});

const GONK_TELEPHONYSERVICE_CONTRACTID =
  "@mozilla.org/telephony/gonktelephonyservice;1";

const GONK_TELEPHONYSERVICE_CID =
  Components.ID("{67d26434-d063-4d28-9f48-5b3189788155}");
const TELEPHONYCALLINFO_CID =
  Components.ID("{d9e8b358-a02c-4cf3-9fc7-816c2e8d46e4}");

const NS_XPCOM_SHUTDOWN_OBSERVER_ID = "xpcom-shutdown";

const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";

const kPrefRilNumRadioInterfaces = "ril.numRadioInterfaces";
const kPrefRilDebuggingEnabled = "ril.debugging.enabled";
const kPrefDefaultServiceId = "dom.telephony.defaultServiceId";

const nsITelephonyAudioService = Ci.nsITelephonyAudioService;
const nsITelephonyService = Ci.nsITelephonyService;
const nsIMobileConnection = Ci.nsIMobileConnection;

const CALL_WAKELOCK_TIMEOUT = 5000;

// In CDMA, RIL only hold one call index. We need to fake a second call index
// in TelephonyService for 3-way calling.
const CDMA_FIRST_CALL_INDEX = 1;
const CDMA_SECOND_CALL_INDEX = 2;

const DIAL_ERROR_INVALID_STATE_ERROR = "InvalidStateError";
const DIAL_ERROR_OTHER_CONNECTION_IN_USE = "OtherConnectionInUse";
const DIAL_ERROR_BAD_NUMBER = RIL.GECKO_CALL_ERROR_BAD_NUMBER;
const DIAL_ERROR_RADIO_NOT_AVAILABLE = RIL.GECKO_ERROR_RADIO_NOT_AVAILABLE;

const TONES_GAP_DURATION = 70;

// Consts for MMI.
// MMI procedure as defined in TS.22.030 6.5.2
const MMI_PROCEDURE_ACTIVATION = "*";
const MMI_PROCEDURE_DEACTIVATION = "#";
const MMI_PROCEDURE_INTERROGATION = "*#";
const MMI_PROCEDURE_REGISTRATION = "**";
const MMI_PROCEDURE_ERASURE = "##";

XPCOMUtils.defineConstant(this, "MMI_PROCEDURE_ACTIVATION", MMI_PROCEDURE_ACTIVATION);
XPCOMUtils.defineConstant(this, "MMI_PROCEDURE_DEACTIVATION", MMI_PROCEDURE_DEACTIVATION);
XPCOMUtils.defineConstant(this, "MMI_PROCEDURE_INTERROGATION", MMI_PROCEDURE_INTERROGATION);
XPCOMUtils.defineConstant(this, "MMI_PROCEDURE_REGISTRATION", MMI_PROCEDURE_REGISTRATION);
XPCOMUtils.defineConstant(this, "MMI_PROCEDURE_ERASURE", MMI_PROCEDURE_ERASURE);

// MMI call forwarding service codes as defined in TS.22.030 Annex B
const MMI_SC_CFU = "21";
const MMI_SC_CF_BUSY = "67";
const MMI_SC_CF_NO_REPLY = "61";
const MMI_SC_CF_NOT_REACHABLE = "62";
const MMI_SC_CF_ALL = "002";
const MMI_SC_CF_ALL_CONDITIONAL = "004";

// MMI service codes for PIN/PIN2/PUK/PUK2 management as defined in TS.22.030
// sec 6.6
const MMI_SC_PIN = "04";
const MMI_SC_PIN2 = "042";
const MMI_SC_PUK = "05";
const MMI_SC_PUK2 = "052";

// MMI service code for IMEI presentation as defined in TS.22.030 sec 6.7
const MMI_SC_IMEI = "06";

// MMI call waiting service code
const MMI_SC_CALL_WAITING = "43";

// MMI service code for registration new password as defined in TS 22.030 6.5.4
const MMI_SC_CHANGE_PASSWORD = "03";
const MMI_ZZ_BARRING_SERVICE = "330";

// MMI call barring service codes
const MMI_SC_BAOC = "33";
const MMI_SC_BAOIC = "331";
const MMI_SC_BAOICxH = "332";
const MMI_SC_BAIC = "35";
const MMI_SC_BAICr = "351";
const MMI_SC_BA_ALL = "330";
const MMI_SC_BA_MO = "333";
const MMI_SC_BA_MT = "353";

// MMI called line presentation service codes
const MMI_SC_CLIP = "30";
const MMI_SC_CLIR = "31";

// MMI service code key strings.
const MMI_KS_SC_CALL_BARRING = "scCallBarring";
const MMI_KS_SC_CALL_FORWARDING = "scCallForwarding";
const MMI_KS_SC_CLIP = "scClip";
const MMI_KS_SC_CLIR = "scClir";
const MMI_KS_SC_PWD = "scPwd";
const MMI_KS_SC_CALL_WAITING = "scCallWaiting";
const MMI_KS_SC_PIN = "scPin";
const MMI_KS_SC_PIN2 = "scPin2";
const MMI_KS_SC_PUK = "scPuk";
const MMI_KS_SC_PUK2 = "scPuk2";
const MMI_KS_SC_CHANGE_PASSWORD = "scChangePassword";
const MMI_KS_SC_IMEI = "scImei";
const MMI_KS_SC_USSD = "scUssd";
const MMI_KS_SC_CALL = "scCall";

// MMI error messages key strings.
const MMI_ERROR_KS_ERROR = "emMmiError";
const MMI_ERROR_KS_NOT_SUPPORTED = "emMmiErrorNotSupported";
const MMI_ERROR_KS_INVALID_ACTION = "emMmiErrorInvalidAction";
const MMI_ERROR_KS_MISMATCH_PIN = "emMmiErrorMismatchPin";
const MMI_ERROR_KS_MISMATCH_PASSWORD = "emMmiErrorMismatchPassword";
const MMI_ERROR_KS_BAD_PIN = "emMmiErrorBadPin";
const MMI_ERROR_KS_BAD_PUK = "emMmiErrorBadPuk";
const MMI_ERROR_KS_INVALID_PIN = "emMmiErrorInvalidPin";
const MMI_ERROR_KS_INVALID_PASSWORD = "emMmiErrorInvalidPassword";
const MMI_ERROR_KS_NEEDS_PUK = "emMmiErrorNeedsPuk";
const MMI_ERROR_KS_SIM_BLOCKED = "emMmiErrorSimBlocked";

// MMI status message.
const MMI_SM_KS_PASSWORD_CHANGED = "smPasswordChanged";
const MMI_SM_KS_PIN_CHANGED = "smPinChanged";
const MMI_SM_KS_PIN2_CHANGED = "smPin2Changed";
const MMI_SM_KS_PIN_UNBLOCKED = "smPinUnblocked";
const MMI_SM_KS_PIN2_UNBLOCKED = "smPin2Unblocked";
const MMI_SM_KS_SERVICE_ENABLED = "smServiceEnabled";
const MMI_SM_KS_SERVICE_ENABLED_FOR = "smServiceEnabledFor";
const MMI_SM_KS_SERVICE_DISABLED = "smServiceDisabled";
const MMI_SM_KS_SERVICE_REGISTERED = "smServiceRegistered";
const MMI_SM_KS_SERVICE_ERASED = "smServiceErased";
const MMI_SM_KS_SERVICE_INTERROGATED = "smServiceInterrogated";
const MMI_SM_KS_SERVICE_NOT_PROVISIONED = "smServiceNotProvisioned";
const MMI_SM_KS_CLIR_PERMANENT = "smClirPermanent";
const MMI_SM_KS_CLIR_DEFAULT_ON_NEXT_CALL_ON = "smClirDefaultOnNextCallOn";
const MMI_SM_KS_CLIR_DEFAULT_ON_NEXT_CALL_OFF = "smClirDefaultOnNextCallOff";
const MMI_SM_KS_CLIR_DEFAULT_OFF_NEXT_CALL_ON = "smClirDefaultOffNextCallOn";
const MMI_SM_KS_CLIR_DEFAULT_OFF_NEXT_CALL_OFF = "smClirDefaultOffNextCallOff";
const MMI_SM_KS_CALL_CONTROL = "smCallControl";

// MMI Service class
const MMI_KS_SERVICE_CLASS_VOICE = "serviceClassVoice";
const MMI_KS_SERVICE_CLASS_DATA = "serviceClassData";
const MMI_KS_SERVICE_CLASS_FAX = "serviceClassFax";
const MMI_KS_SERVICE_CLASS_SMS = "serviceClassSms";
const MMI_KS_SERVICE_CLASS_DATA_SYNC = "serviceClassDataSync";
const MMI_KS_SERVICE_CLASS_DATA_ASYNC = "serviceClassDataAsync";
const MMI_KS_SERVICE_CLASS_PACKET = "serviceClassPacket";
const MMI_KS_SERVICE_CLASS_PAD = "serviceClassPad";

// States of USSD Session : DONE -> ONGOING [-> CANCELLING] -> DONE
const USSD_SESSION_DONE = "DONE";
const USSD_SESSION_ONGOING = "ONGOING";
const USSD_SESSION_CANCELLING = "CANCELLING";

const MMI_PROC_TO_CF_ACTION = {};
MMI_PROC_TO_CF_ACTION[MMI_PROCEDURE_ACTIVATION] = Ci.nsIMobileConnection.CALL_FORWARD_ACTION_ENABLE;
MMI_PROC_TO_CF_ACTION[MMI_PROCEDURE_DEACTIVATION] = Ci.nsIMobileConnection.CALL_FORWARD_ACTION_DISABLE;
MMI_PROC_TO_CF_ACTION[MMI_PROCEDURE_INTERROGATION] = Ci.nsIMobileConnection.CALL_FORWARD_ACTION_QUERY_STATUS;
MMI_PROC_TO_CF_ACTION[MMI_PROCEDURE_REGISTRATION] = Ci.nsIMobileConnection.CALL_FORWARD_ACTION_REGISTRATION;
MMI_PROC_TO_CF_ACTION[MMI_PROCEDURE_ERASURE] = Ci.nsIMobileConnection.CALL_FORWARD_ACTION_ERASURE;

const MMI_SC_TO_CF_REASON = {};
MMI_SC_TO_CF_REASON[MMI_SC_CFU] = Ci.nsIMobileConnection.CALL_FORWARD_REASON_UNCONDITIONAL;
MMI_SC_TO_CF_REASON[MMI_SC_CF_BUSY] = Ci.nsIMobileConnection.CALL_FORWARD_REASON_MOBILE_BUSY;
MMI_SC_TO_CF_REASON[MMI_SC_CF_NO_REPLY] = Ci.nsIMobileConnection.CALL_FORWARD_REASON_NO_REPLY;
MMI_SC_TO_CF_REASON[MMI_SC_CF_NOT_REACHABLE] = Ci.nsIMobileConnection.CALL_FORWARD_REASON_NOT_REACHABLE;
MMI_SC_TO_CF_REASON[MMI_SC_CF_ALL] = Ci.nsIMobileConnection.CALL_FORWARD_REASON_ALL_CALL_FORWARDING;
MMI_SC_TO_CF_REASON[MMI_SC_CF_ALL_CONDITIONAL] = Ci.nsIMobileConnection.CALL_FORWARD_REASON_ALL_CONDITIONAL_CALL_FORWARDING;

const MMI_SC_TO_LOCK_TYPE = {};
MMI_SC_TO_LOCK_TYPE[MMI_SC_PIN] = Ci.nsIIcc.CARD_LOCK_TYPE_PIN;
MMI_SC_TO_LOCK_TYPE[MMI_SC_PIN2] = Ci.nsIIcc.CARD_LOCK_TYPE_PIN2;
MMI_SC_TO_LOCK_TYPE[MMI_SC_PUK] = Ci.nsIIcc.CARD_LOCK_TYPE_PUK;
MMI_SC_TO_LOCK_TYPE[MMI_SC_PUK2] = Ci.nsIIcc.CARD_LOCK_TYPE_PUK2;

const MMI_PROC_TO_CLIR_ACTION = {};
MMI_PROC_TO_CLIR_ACTION[MMI_PROCEDURE_ACTIVATION] = Ci.nsIMobileConnection.CLIR_INVOCATION;
MMI_PROC_TO_CLIR_ACTION[MMI_PROCEDURE_DEACTIVATION] = Ci.nsIMobileConnection.CLIR_SUPPRESSION;

const MMI_SC_TO_CB_PROGRAM = {};
MMI_SC_TO_CB_PROGRAM[MMI_SC_BAOC] = Ci.nsIMobileConnection.CALL_BARRING_PROGRAM_ALL_OUTGOING;
MMI_SC_TO_CB_PROGRAM[MMI_SC_BAOIC] = Ci.nsIMobileConnection.CALL_BARRING_PROGRAM_OUTGOING_INTERNATIONAL;
MMI_SC_TO_CB_PROGRAM[MMI_SC_BAOICxH] = Ci.nsIMobileConnection.CALL_BARRING_PROGRAM_OUTGOING_INTERNATIONAL_EXCEPT_HOME;
MMI_SC_TO_CB_PROGRAM[MMI_SC_BAIC] = Ci.nsIMobileConnection.CALL_BARRING_PROGRAM_ALL_INCOMING;
MMI_SC_TO_CB_PROGRAM[MMI_SC_BAICr] = Ci.nsIMobileConnection.CALL_BARRING_PROGRAM_INCOMING_ROAMING;
MMI_SC_TO_CB_PROGRAM[MMI_SC_BA_ALL] = Ci.nsIMobileConnection.CALL_BARRING_PROGRAM_ALL_SERVICE;
MMI_SC_TO_CB_PROGRAM[MMI_SC_BA_MO] = Ci.nsIMobileConnection.CALL_BARRING_PROGRAM_OUTGOING_SERVICE;
MMI_SC_TO_CB_PROGRAM[MMI_SC_BA_MT] = Ci.nsIMobileConnection.CALL_BARRING_PROGRAM_INCOMING_SERVICE;

const CF_ACTION_TO_STATUS_MESSAGE = {};
CF_ACTION_TO_STATUS_MESSAGE[Ci.nsIMobileConnection.CALL_FORWARD_ACTION_ENABLE] = MMI_SM_KS_SERVICE_ENABLED;
CF_ACTION_TO_STATUS_MESSAGE[Ci.nsIMobileConnection.CALL_FORWARD_ACTION_DISABLE] = MMI_SM_KS_SERVICE_DISABLED;
CF_ACTION_TO_STATUS_MESSAGE[Ci.nsIMobileConnection.CALL_FORWARD_ACTION_REGISTRATION] = MMI_SM_KS_SERVICE_REGISTERED;
CF_ACTION_TO_STATUS_MESSAGE[Ci.nsIMobileConnection.CALL_FORWARD_ACTION_ERASURE] = MMI_SM_KS_SERVICE_ERASED;

const LOCK_TYPE_TO_STATUS_MESSAGE = {};
LOCK_TYPE_TO_STATUS_MESSAGE[Ci.nsIIcc.CARD_LOCK_TYPE_PIN] = MMI_SM_KS_PIN_CHANGED;
LOCK_TYPE_TO_STATUS_MESSAGE[Ci.nsIIcc.CARD_LOCK_TYPE_PIN2] = MMI_SM_KS_PIN2_CHANGED;
LOCK_TYPE_TO_STATUS_MESSAGE[Ci.nsIIcc.CARD_LOCK_TYPE_PUK] = MMI_SM_KS_PIN_UNBLOCKED;
LOCK_TYPE_TO_STATUS_MESSAGE[Ci.nsIIcc.CARD_LOCK_TYPE_PUK2] = MMI_SM_KS_PIN2_UNBLOCKED;

const CLIR_ACTION_TO_STATUS_MESSAGE = {};
CLIR_ACTION_TO_STATUS_MESSAGE[Ci.nsIMobileConnection.CLIR_INVOCATION] = MMI_SM_KS_SERVICE_ENABLED;
CLIR_ACTION_TO_STATUS_MESSAGE[Ci.nsIMobileConnection.CLIR_SUPPRESSION] = MMI_SM_KS_SERVICE_DISABLED;

const MMI_KS_SERVICE_CLASS_MAPPING = {};
MMI_KS_SERVICE_CLASS_MAPPING[Ci.nsIMobileConnection.ICC_SERVICE_CLASS_VOICE] = MMI_KS_SERVICE_CLASS_VOICE;
MMI_KS_SERVICE_CLASS_MAPPING[Ci.nsIMobileConnection.ICC_SERVICE_CLASS_DATA] = MMI_KS_SERVICE_CLASS_DATA;
MMI_KS_SERVICE_CLASS_MAPPING[Ci.nsIMobileConnection.ICC_SERVICE_CLASS_FAX] = MMI_KS_SERVICE_CLASS_FAX;
MMI_KS_SERVICE_CLASS_MAPPING[Ci.nsIMobileConnection.ICC_SERVICE_CLASS_SMS] = MMI_KS_SERVICE_CLASS_SMS;
MMI_KS_SERVICE_CLASS_MAPPING[Ci.nsIMobileConnection.ICC_SERVICE_CLASS_DATA_SYNC] = MMI_KS_SERVICE_CLASS_DATA_SYNC;
MMI_KS_SERVICE_CLASS_MAPPING[Ci.nsIMobileConnection.ICC_SERVICE_CLASS_DATA_ASYNC] = MMI_KS_SERVICE_CLASS_DATA_ASYNC;
MMI_KS_SERVICE_CLASS_MAPPING[Ci.nsIMobileConnection.ICC_SERVICE_CLASS_PACKET] = MMI_KS_SERVICE_CLASS_PACKET;
MMI_KS_SERVICE_CLASS_MAPPING[Ci.nsIMobileConnection.ICC_SERVICE_CLASS_PAD] = MMI_KS_SERVICE_CLASS_PAD;

var DEBUG;
function debug(s) {
  dump("TelephonyService: " + s + "\n");
}

/* global gRadioInterfaceLayer */
XPCOMUtils.defineLazyGetter(this, "gRadioInterfaceLayer", function() {
  let ril = { numRadioInterfaces: 0 };
  try {
    ril = Cc["@mozilla.org/ril;1"].getService(Ci.nsIRadioInterfaceLayer);
  } catch(e) {}
  return ril;
});

/* global gPowerManagerService */
XPCOMUtils.defineLazyServiceGetter(this, "gPowerManagerService",
                                   "@mozilla.org/power/powermanagerservice;1",
                                   "nsIPowerManagerService");

/* global gTelephonyMessenger */
XPCOMUtils.defineLazyServiceGetter(this, "gTelephonyMessenger",
                                   "@mozilla.org/ril/system-messenger-helper;1",
                                   "nsITelephonyMessenger");

/* global gAudioService */
XPCOMUtils.defineLazyServiceGetter(this, "gAudioService",
                                   "@mozilla.org/telephony/audio-service;1",
                                   "nsITelephonyAudioService");

/* global gGonkMobileConnectionService */
XPCOMUtils.defineLazyServiceGetter(this, "gGonkMobileConnectionService",
                                   "@mozilla.org/mobileconnection/mobileconnectionservice;1",
                                   "nsIGonkMobileConnectionService");

/* global gIccService */
XPCOMUtils.defineLazyServiceGetter(this, "gIccService",
                                   "@mozilla.org/icc/iccservice;1",
                                   "nsIIccService");


/* global PhoneNumberUtils */
XPCOMUtils.defineLazyModuleGetter(this, "PhoneNumberUtils",
                                  "resource://gre/modules/PhoneNumberUtils.jsm");

/* global DialNumberUtils */
XPCOMUtils.defineLazyModuleGetter(this, "DialNumberUtils",
                                  "resource://gre/modules/DialNumberUtils.jsm");

function TelephonyCallInfo(aCall) {
  this.clientId = aCall.clientId;
  this.callIndex = aCall.callIndex;
  this.callState = aCall.state;
  this.disconnectedReason = aCall.disconnectedReason || "";

  this.number = aCall.number;
  this.numberPresentation = aCall.numberPresentation;
  this.name = aCall.name;
  this.namePresentation = aCall.namePresentation;

  this.isOutgoing = aCall.isOutgoing;
  this.isEmergency = aCall.isEmergency;
  this.isConference = aCall.isConference;
  this.isSwitchable = aCall.isSwitchable;
  this.isMergeable = aCall.isMergeable;
}
TelephonyCallInfo.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyCallInfo]),
  classID: TELEPHONYCALLINFO_CID,
  classInfo: XPCOMUtils.generateCI({
    classID:          TELEPHONYCALLINFO_CID,
    classDescription: "TelephonyCallInfo",
    interfaces:       [Ci.nsITelephonyCallInfo]
  }),

  // nsITelephonyCallInfo

  clientId: 0,
  callIndex: 0,
  callState: nsITelephonyService.CALL_STATE_UNKNOWN,
  disconnectedReason: "",

  number: "",
  numberPresentation: nsITelephonyService.CALL_PRESENTATION_ALLOWED,
  name: "",
  namePresentation: nsITelephonyService.CALL_PRESENTATION_ALLOWED,

  isOutgoing: true,
  isEmergency: false,
  isConference: false,
  isSwitchable: true,
  isMergeable: true
};

function Call(aClientId, aCallIndex) {
  this.clientId = aClientId;
  this.callIndex = aCallIndex;
}
Call.prototype = {
  clientId: 0,
  callIndex: 0,
  state: nsITelephonyService.CALL_STATE_UNKNOWN,
  number: "",
  numberPresentation: nsITelephonyService.CALL_PRESENTATION_ALLOWED,
  name: "",
  namePresentation: nsITelephonyService.CALL_PRESENTATION_ALLOWED,
  isOutgoing: true,
  isEmergency: false,
  isConference: false,
  isSwitchable: true,
  isMergeable: true,
  started: null
};

function MobileConnectionListener() {}
MobileConnectionListener.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionListener]),

  // nsIMobileConnectionListener

  notifyVoiceChanged: function() {},
  notifyDataChanged: function() {},
  notifyDataError: function(message) {},
  notifyCFStateChanged: function(action, reason, number, timeSeconds, serviceClass) {},
  notifyEmergencyCbModeChanged: function(active, timeoutMs) {},
  notifyOtaStatusChanged: function(status) {},
  notifyRadioStateChanged: function() {},
  notifyClirModeChanged: function(mode) {},
  notifyLastKnownNetworkChanged: function() {},
  notifyLastKnownHomeNetworkChanged: function() {},
  notifyNetworkSelectionModeChanged: function() {},
  notifyDeviceIdentitiesChanged: function() {}
};

function TelephonyService() {
  this._numClients = gRadioInterfaceLayer.numRadioInterfaces;
  this._listeners = [];

  this._isDialing = false;
  this._cachedDialRequest = null;
  this._currentCalls = {};
  this._audioStates = [];
  this._ussdSessions = [];

  this._cdmaCallWaitingNumber = null;

  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._audioStates[i] = nsITelephonyAudioService.PHONE_STATE_NORMAL;
    this._ussdSessions[i] = USSD_SESSION_DONE;
    this._currentCalls[i] = {};
    this._enumerateCallsForClient(i);
  }
}
TelephonyService.prototype = {
  classID: GONK_TELEPHONYSERVICE_CID,
  classInfo: XPCOMUtils.generateCI({classID: GONK_TELEPHONYSERVICE_CID,
                                    contractID: GONK_TELEPHONYSERVICE_CONTRACTID,
                                    classDescription: "TelephonyService",
                                    interfaces: [Ci.nsITelephonyService,
                                                 Ci.nsIGonkTelephonyService],
                                    flags: Ci.nsIClassInfo.SINGLETON}),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyService,
                                         Ci.nsIGonkTelephonyService,
                                         Ci.nsIObserver]),

  // The following attributes/functions are used for acquiring/releasing the
  // CPU wake lock when the RIL handles the incoming call. Note that we need
  // a timer to bound the lock's life cycle to avoid exhausting the battery.
  _callRingWakeLock: null,
  _callRingWakeLockTimer: null,

  /**
   * USSD session flags.
   * Only one USSD session may exist at a time, and the session is assumed
   * to exist until:
   *    a) There's a call to cancelUSSD()
   *    b) Receiving a session end unsolicited event.
   */
  _ussdSessions: null,

  _acquireCallRingWakeLock: function() {
    if (!this._callRingWakeLock) {
      if (DEBUG) debug("Acquiring a CPU wake lock for handling incoming call.");
      this._callRingWakeLock = gPowerManagerService.newWakeLock("cpu");
    }
    if (!this._callRingWakeLockTimer) {
      if (DEBUG) debug("Creating a timer for releasing the CPU wake lock.");
      this._callRingWakeLockTimer =
        Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    }
    if (DEBUG) debug("Setting the timer for releasing the CPU wake lock.");
    this._callRingWakeLockTimer
        .initWithCallback(this._releaseCallRingWakeLock.bind(this),
                          CALL_WAKELOCK_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
  },

  _releaseCallRingWakeLock: function() {
    if (DEBUG) debug("Releasing the CPU wake lock for handling incoming call.");
    if (this._callRingWakeLockTimer) {
      this._callRingWakeLockTimer.cancel();
    }
    if (this._callRingWakeLock) {
      this._callRingWakeLock.unlock();
      this._callRingWakeLock = null;
    }
  },

  _getClient: function(aClientId) {
    return gRadioInterfaceLayer.getRadioInterface(aClientId);
  },

  _sendToRilWorker: function(aClientId, aType, aMessage, aCallback) {
    this._getClient(aClientId).sendWorkerMessage(aType, aMessage, aCallback);
  },

  _isGsmTechGroup: function(aType) {
    switch (aType) {
      case null:  // Handle unknown as gsm.
      case "gsm":
      case "gprs":
      case "edge":
      case "umts":
      case "hsdpa":
      case "hsupa":
      case "hspa":
      case "hspa+":
      case "lte":
        return true;
      default:
        return false;
    }
  },

  _isCdmaClient: function(aClientId) {
    let type = gGonkMobileConnectionService.getItemByServiceId(aClientId).voice.type;
    return !this._isGsmTechGroup(type);
  },

  _isEmergencyOnly: function(aClientId) {
    return gGonkMobileConnectionService.getItemByServiceId(aClientId).voice.emergencyCallsOnly;
  },

  _isRadioOn: function(aClientId) {
    let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
    return connection.radioState === nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED;
  },

  // An array of nsITelephonyListener instances.
  _listeners: null,
  _notifyAllListeners: function(aMethodName, aArgs) {
    let listeners = this._listeners.slice();
    for (let listener of listeners) {
      if (this._listeners.indexOf(listener) == -1) {
        // Listener has been unregistered in previous run.
        continue;
      }

      let handler = listener[aMethodName];
      try {
        handler.apply(listener, aArgs);
      } catch (e) {
        debug("listener for " + aMethodName + " threw an exception: " + e);
      }
    }
  },

  _computeAudioStateForClient: function(aClientId) {
    let indexes = Object.keys(this._currentCalls[aClientId]);
    if (!indexes.length) {
      return nsITelephonyAudioService.PHONE_STATE_NORMAL;
    }

    let firstCall = this._currentCalls[aClientId][indexes[0]];
    if (indexes.length === 1 &&
        firstCall.state === nsITelephonyService.CALL_STATE_INCOMING) {
      return nsITelephonyAudioService.PHONE_STATE_RINGTONE;
    }

    return nsITelephonyAudioService.PHONE_STATE_IN_CALL;
  },

  _updateAudioState: function(aClientId) {
    this._audioStates[aClientId] = this._computeAudioStateForClient(aClientId);

    if (this._audioStates.some(state => state === nsITelephonyAudioService.PHONE_STATE_IN_CALL)) {
      gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_IN_CALL);
    } else if (this._audioStates.some(state => state === nsITelephonyAudioService.PHONE_STATE_RINGTONE)) {
      gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_RINGTONE);
    } else {
      gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_NORMAL);
    }
  },

  _formatInternationalNumber: function(aNumber, aToa) {
    if (aNumber && aToa == RIL.TOA_INTERNATIONAL && aNumber[0] != "+") {
      return "+" + aNumber;
    }

    return aNumber;
  },

  _convertRILCallState: function(aState) {
    switch (aState) {
      case RIL.CALL_STATE_UNKNOWN:
        return nsITelephonyService.CALL_STATE_UNKNOWN;
      case RIL.CALL_STATE_ACTIVE:
        return nsITelephonyService.CALL_STATE_CONNECTED;
      case RIL.CALL_STATE_HOLDING:
        return nsITelephonyService.CALL_STATE_HELD;
      case RIL.CALL_STATE_DIALING:
        return nsITelephonyService.CALL_STATE_DIALING;
      case RIL.CALL_STATE_ALERTING:
        return nsITelephonyService.CALL_STATE_ALERTING;
      case RIL.CALL_STATE_INCOMING:
      case RIL.CALL_STATE_WAITING:
        return nsITelephonyService.CALL_STATE_INCOMING;
      default:
        throw new Error("Unknown rilCallState: " + aState);
    }
  },

  _convertRILSuppSvcNotification: function(aNotification) {
    switch (aNotification) {
      case RIL.GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD:
        return nsITelephonyService.NOTIFICATION_REMOTE_HELD;
      case RIL.GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED:
        return nsITelephonyService.NOTIFICATION_REMOTE_RESUMED;
      default:
        if (DEBUG) {
          debug("Unknown rilSuppSvcNotification: " + aNotification);
        }
        return;
    }
  },

  _updateDebugFlag: function() {
    try {
      DEBUG = RIL.DEBUG_RIL ||
              Services.prefs.getBoolPref(kPrefRilDebuggingEnabled);
    } catch (e) {}
  },

  _getDefaultServiceId: function() {
    let id = Services.prefs.getIntPref(kPrefDefaultServiceId);
    let numRil = Services.prefs.getIntPref(kPrefRilNumRadioInterfaces);

    if (id >= numRil || id < 0) {
      id = 0;
    }

    return id;
  },

  _currentCalls: null,
  _enumerateCallsForClient: function(aClientId) {
    if (DEBUG) debug("Enumeration of calls for client " + aClientId);

    this._sendToRilWorker(aClientId, "getCurrentCalls", null, response => {
      if (response.errorMsg) {
        return;
      }

      // Clear all.
      this._currentCalls[aClientId] = {};

      for (let i in response.calls) {
        let call = this._currentCalls[aClientId][i] = new Call(aClientId, i);
        this._updateCallFromRil(call, response.calls[i]);
      }
    });
  },

  /**
   * Checks whether to temporarily suppress caller id for the call.
   *
   * @param aMmi
   *        MMI full object.
   */
  _isTemporaryCLIR: function(aMmi) {
    return (aMmi && aMmi.serviceCode === MMI_SC_CLIR) && aMmi.dialNumber;
  },

  /**
   * Map MMI procedure to CLIR MODE.
   *
   * @param aProcedure
   *        MMI procedure
   */
  _procedureToCLIRMode: function(aProcedure) {
    // In temporary mode, MMI_PROCEDURE_ACTIVATION means allowing CLI
    // presentation, i.e. CLIR_SUPPRESSION. See TS 22.030, Annex B.
    switch (aProcedure) {
      case MMI_PROCEDURE_ACTIVATION:
        return Ci.nsIMobileConnection.CLIR_SUPPRESSION;
      case MMI_PROCEDURE_DEACTIVATION:
        return Ci.nsIMobileConnection.CLIR_INVOCATION;
      default:
        return Ci.nsIMobileConnection.CLIR_DEFAULT;
    }
  },

  /**
   * nsITelephonyService interface.
   */

  defaultServiceId: 0,

  registerListener: function(aListener) {
    if (this._listeners.indexOf(aListener) >= 0) {
      throw Cr.NS_ERROR_UNEXPECTED;
    }

    this._listeners.push(aListener);
  },

  unregisterListener: function(aListener) {
    let index = this._listeners.indexOf(aListener);
    if (index < 0) {
      throw Cr.NS_ERROR_UNEXPECTED;
    }

    this._listeners.splice(index, 1);
  },

  enumerateCalls: function(aListener) {
    if (DEBUG) debug("Requesting enumeration of calls for callback");

    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]];
        let callInfo = new TelephonyCallInfo(call);
        aListener.enumerateCallState(callInfo);
      }
    }
    aListener.enumerateCallStateComplete();
  },

  _hasCalls: function(aClientId) {
    return Object.keys(this._currentCalls[aClientId]).length !== 0;
  },

  _hasCallsOnOtherClient: function(aClientId) {
    for (let cid = 0; cid < this._numClients; ++cid) {
      if (cid !== aClientId && this._hasCalls(cid)) {
        return true;
      }
    }
    return false;
  },

  // All calls in the conference is regarded as one conference call.
  _numCallsOnLine: function(aClientId) {
    let numCalls = 0;
    let hasConference = false;

    for (let cid in this._currentCalls[aClientId]) {
      let call = this._currentCalls[aClientId][cid];
      if (call.isConference) {
        hasConference = true;
      } else {
        numCalls++;
      }
    }

    return hasConference ? numCalls + 1 : numCalls;
  },

  /**
   * Is there an active call?
   */
  _isActive: function(aClientId) {
    for (let index in this._currentCalls[aClientId]) {
      let call = this._currentCalls[aClientId][index];
      if (call.state === nsITelephonyService.CALL_STATE_CONNECTED) {
        return true;
      }
    }
    return false;
  },

  /**
   * Dial number. Perform call setup or SS procedure accordingly.
   *
   * @see 3GPP TS 22.030 Figure 3.5.3.2
   */
  dial: function(aClientId, aNumber, aIsDialEmergency, aCallback) {
    if (DEBUG) debug("Dialing " + (aIsDialEmergency ? "emergency " : "")
                     + aNumber + ", clientId: " + aClientId);

    // We don't try to be too clever here, as the phone is probably in the
    // locked state. Let's just check if it's a number without normalizing
    if (!aIsDialEmergency) {
      aNumber = PhoneNumberUtils.normalize(aNumber);
    }

    // Validate the number.
    // Note: isPlainPhoneNumber also accepts USSD and SS numbers
    if (!PhoneNumberUtils.isPlainPhoneNumber(aNumber)) {
      if (DEBUG) debug("Error: Number '" + aNumber + "' is not viable. Drop.");
      aCallback.notifyError(DIAL_ERROR_BAD_NUMBER);
      return;
    }

    let isEmergencyNumber = DialNumberUtils.isEmergency(aNumber);

    // DialEmergency accepts only emergency number.
    if (aIsDialEmergency && !isEmergencyNumber) {
      if (!this._isRadioOn(aClientId)) {
        if (DEBUG) debug("Error: Radio is off. Drop.");
        aCallback.notifyError(DIAL_ERROR_RADIO_NOT_AVAILABLE);
        return;
      }

      if (DEBUG) debug("Error: Dial a non-emergency by dialEmergency. Drop.");
      aCallback.notifyError(DIAL_ERROR_BAD_NUMBER);
      return;
    }

    if (isEmergencyNumber) {
      this._dialCall(aClientId, aNumber, undefined, aCallback);
      return;
    }

    // In cdma, we should always handle the request as a call.
    if (this._isCdmaClient(aClientId)) {
      this._dialCall(aClientId, aNumber, undefined, aCallback);
      return;
    }

    let mmi = DialNumberUtils.parseMMI(aNumber);
    if (mmi) {
      if (this._isTemporaryCLIR(mmi)) {
        this._dialCall(aClientId, mmi.dialNumber,
                       this._procedureToCLIRMode(mmi.procedure), aCallback);
      } else {
        this._dialMMI(aClientId, mmi, aCallback);
      }
    } else {
      if (aNumber[aNumber.length - 1] === "#") {  // # string
        this._dialMMI(aClientId, {fullMMI: aNumber}, aCallback);
      } else if (aNumber.length <= 2) {  // short string
        if (this._hasCalls(aClientId)) {
          this._dialInCallMMI(aClientId, aNumber, aCallback);
        } else if (aNumber.length === 2 && aNumber[0] === "1") {
          this._dialCall(aClientId, aNumber, undefined, aCallback);
        } else {
          this._dialMMI(aClientId, {fullMMI: aNumber}, aCallback);
        }
      } else {
        this._dialCall(aClientId, aNumber, undefined, aCallback);
      }
    }
  },

  // Handling of supplementary services within a call as 3GPP TS 22.030 6.5.5
  _dialInCallMMI: function(aClientId, aNumber, aCallback) {
    let mmiCallback = {
      notifyError: () => aCallback.notifyDialMMIError(MMI_ERROR_KS_ERROR),
      notifySuccess: () => aCallback.notifyDialMMISuccess(MMI_SM_KS_CALL_CONTROL)
    };

    if (aNumber === "0") {
      aCallback.notifyDialMMI(MMI_KS_SC_CALL);
      this._hangUpBackground(aClientId, mmiCallback);
    } else if (aNumber === "1") {
      aCallback.notifyDialMMI(MMI_KS_SC_CALL);
      this._hangUpForeground(aClientId, mmiCallback);
    } else if (aNumber[0] === "1" && aNumber.length === 2) {
      aCallback.notifyDialMMI(MMI_KS_SC_CALL);
      this.hangUpCall(aClientId, parseInt(aNumber[1]), mmiCallback);
    } else if (aNumber === "2") {
      aCallback.notifyDialMMI(MMI_KS_SC_CALL);
      this._switchActiveCall(aClientId, mmiCallback);
    } else if (aNumber[0] === "2" && aNumber.length === 2) {
      aCallback.notifyDialMMI(MMI_KS_SC_CALL);
      this._separateCallGsm(aClientId, parseInt(aNumber[1]), mmiCallback);
    } else if (aNumber === "3") {
      aCallback.notifyDialMMI(MMI_KS_SC_CALL);
      this._conferenceCallGsm(aClientId, mmiCallback);
    } else {
      this._dialCall(aClientId, aNumber, undefined, aCallback);
    }
  },

  _dialCall: function(aClientId, aNumber, aClirMode = RIL.CLIR_DEFAULT,
                      aCallback) {
    if (this._isDialing) {
      if (DEBUG) debug("Error: Already has a dialing call.");
      aCallback.notifyError(DIAL_ERROR_INVALID_STATE_ERROR);
      return;
    }

    // We can only have at most two calls on the same line (client).
    if (this._numCallsOnLine(aClientId) >= 2) {
      if (DEBUG) debug("Error: Already has more than 2 calls on line.");
      aCallback.notifyError(DIAL_ERROR_INVALID_STATE_ERROR);
      return;
    }

    // For DSDS, if there is aleady a call on SIM 'aClientId', we cannot place
    // any new call on other SIM.
    if (this._hasCallsOnOtherClient(aClientId)) {
      if (DEBUG) debug("Error: Already has a call on other sim.");
      aCallback.notifyError(DIAL_ERROR_OTHER_CONNECTION_IN_USE);
      return;
    }

    let isEmergency = DialNumberUtils.isEmergency(aNumber);

    if (!isEmergency) {
      if (!this._isRadioOn(aClientId)) {
        if (DEBUG) debug("Error: Dial a normal call when radio off. Drop");
        aCallback.notifyError(DIAL_ERROR_RADIO_NOT_AVAILABLE);
        return;
      }

      if (this._isEmergencyOnly(aClientId)) {
        if (DEBUG) debug("Error: Dial a normal call when emergencyCallsOnly. Drop");
        aCallback.notifyError(DIAL_ERROR_BAD_NUMBER);
        return;
      }
    }

    if (isEmergency) {
      // Automatically select a proper clientId for emergency call.
      aClientId = gRadioInterfaceLayer.getClientIdForEmergencyCall() ;
      if (aClientId === -1) {
        if (DEBUG) debug("Error: No client is avaialble for emergency call.");
        aCallback.notifyError(DIAL_ERROR_INVALID_STATE_ERROR);
        return;
      }

      // Radio is off. Turn it on first.
      if (!this._isRadioOn(aClientId)) {
        let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
        let listener = new MobileConnectionListener();

        listener.notifyRadioStateChanged = () => {
          if (this._isRadioOn(aClientId)) {
            this._dialCall(aClientId, aNumber, undefined, aCallback);
            connection.unregisterListener(listener);
          }
        };
        connection.registerListener(listener);

        // Turn on radio.
        connection.setRadioEnabled(true, {
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifySuccess: () => {},
          notifyError: aErrorMsg => {
            connection.unregisterListener(listener);
            aCallback.notifyError(DIAL_ERROR_RADIO_NOT_AVAILABLE);
          }
        });

        return;
      }
    }

    let options = {
      isEmergency: isEmergency,
      number: aNumber,
      clirMode: aClirMode
    };

    // No active call. Dial it out directly.
    if (!this._isActive(aClientId)) {
      this._sendDialCallRequest(aClientId, options, aCallback);
      return;
    }

    // CDMA 3-way calling.
    if (this._isCdmaClient(aClientId)) {
      this._dialCdmaThreeWayCall(aClientId, aNumber, aCallback);
      return;
    }

    // GSM. Hold the active call before dialing.
    if (DEBUG) debug("There is an active call. Hold it first before dial.");

    if (this._cachedDialRequest) {
      if (DEBUG) debug("Error: There already is a pending dial request.");
      aCallback.notifyError(DIAL_ERROR_INVALID_STATE_ERROR);
      return;
    }

    this._switchActiveCall(aClientId, {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyCallback]),

      notifySuccess: () => {
        this._cachedDialRequest = {
          clientId: aClientId,
          options: options,
          callback: aCallback
        };
      },

      notifyError: (aErrorMsg) => {
        if (DEBUG) debug("Error: Fail to automatically hold the active call.");
        aCallback.notifyError(aErrorMsg);
      }
    });
  },

  _dialCdmaThreeWayCall: function(aClientId, aNumber, aCallback) {
    this._sendToRilWorker(aClientId, "cdmaFlash", { featureStr: aNumber },
                          response => {
      if (response.errorMsg) {
        aCallback.notifyError(response.errorMsg);
        return;
      }

      // RIL doesn't hold the 2nd call. We create one by ourselves.
      aCallback.notifyDialCallSuccess(aClientId, CDMA_SECOND_CALL_INDEX,
                                      aNumber);

      let childCall = this._currentCalls[aClientId][CDMA_SECOND_CALL_INDEX] =
        new Call(aClientId, CDMA_SECOND_CALL_INDEX);

      childCall.parentId = CDMA_FIRST_CALL_INDEX;
      childCall.state = nsITelephonyService.CALL_STATE_DIALING;
      childCall.number = aNumber;
      childCall.isOutgoing = true;
      childCall.isEmergency = DialNumberUtils.isEmergency(aNumber);
      childCall.isConference = false;
      childCall.isSwitchable = false;
      childCall.isMergeable = true;

      // Manual update call state according to the request response.
      this._handleCallStateChanged(aClientId, [childCall]);

      childCall.state = nsITelephonyService.CALL_STATE_CONNECTED;

      let parentCall = this._currentCalls[aClientId][childCall.parentId];
      parentCall.childId = CDMA_SECOND_CALL_INDEX;
      parentCall.state = nsITelephonyService.CALL_STATE_HELD;
      parentCall.isSwitchable = false;
      parentCall.isMergeable = true;
      this._handleCallStateChanged(aClientId, [childCall, parentCall]);
    });
  },

  _sendDialCallRequest: function(aClientId, aOptions, aCallback) {
    this._isDialing = true;

    this._sendToRilWorker(aClientId, "dial", aOptions, response => {
      this._isDialing = false;

      if (response.errorMsg) {
        this._sendToRilWorker(aClientId, "getFailCause", null, response => {
          aCallback.notifyError(response.failCause);
        });
      } else {
        this._ongoingDial = {
          clientId: aClientId,
          callback: aCallback
        };
      }
    });
  },

  /**
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _dialMMI: function(aClientId, aMmi, aCallback) {
    let mmiServiceCode = aMmi ?
      this._serviceCodeToKeyString(aMmi.serviceCode) : MMI_KS_SC_USSD;

    aCallback.notifyDialMMI(mmiServiceCode);

    if (mmiServiceCode !== MMI_KS_SC_IMEI && !this._isRadioOn(aClientId)) {
      aCallback.notifyDialMMIError(DIAL_ERROR_RADIO_NOT_AVAILABLE);
      return;
    }

    // We check if the MMI service code is supported and in that case we
    // trigger the appropriate RIL request if possible.
    switch (mmiServiceCode) {
      // Call Forwarding
      case MMI_KS_SC_CALL_FORWARDING:
        this._callForwardingMMI(aClientId, aMmi, aCallback);
        break;

      // Change the current ICC PIN number.
      case MMI_KS_SC_PIN:
      // Change the current ICC PIN2 number.
      case MMI_KS_SC_PIN2:
        this._iccChangeLockMMI(aClientId, aMmi, aCallback);
        break;

      // Unblock ICC PUK.
      case MMI_KS_SC_PUK:
      // Unblock ICC PUN2.
      case MMI_KS_SC_PUK2:
        this._iccUnlockMMI(aClientId, aMmi, aCallback);
        break;

      // IMEI
      case MMI_KS_SC_IMEI:
        this._getImeiMMI(aClientId, aMmi, aCallback);
        break;

      // CLIP
      case MMI_KS_SC_CLIP:
        this._clipMMI(aClientId, aMmi, aCallback);
        break;

      // CLIR (non-temporary ones)
      case MMI_KS_SC_CLIR:
        this._clirMMI(aClientId, aMmi, aCallback);
        break;

      // Change call barring password
      case MMI_KS_SC_CHANGE_PASSWORD:
        this._callBarringPasswordMMI(aClientId, aMmi, aCallback);
        break;

      // Call barring
      case MMI_KS_SC_CALL_BARRING:
        this._callBarringMMI(aClientId, aMmi, aCallback);
        break;

      // Call waiting
      case MMI_KS_SC_CALL_WAITING:
        this._callWaitingMMI(aClientId, aMmi, aCallback);
        break;

      // Handle unknown MMI code as USSD.
      default:
        if (this._ussdSessions[aClientId] == USSD_SESSION_ONGOING) {
          // Cancel the previous ussd session first.
          this._cancelUSSDInternal(aClientId, aResponse => {
            // Fail to cancel ussd session, report error instead of sending ussd
            // request.
            if (aResponse.errorMsg) {
              aCallback.notifyDialMMIError(aResponse.errorMsg);
              return;
            }
            this._sendUSSDInternal(aClientId, aMmi.fullMMI,
                                   this._defaultMMICallbackHandler.bind(this, aCallback));
          });
          return;
        }
        this._sendUSSDInternal(aClientId, aMmi.fullMMI,
                               this._defaultMMICallbackHandler.bind(this, aCallback));
        break;
    }
  },

  /**
   * Handle call forwarding MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _callForwardingMMI: function(aClientId, aMmi, aCallback) {
    let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
    let action = MMI_PROC_TO_CF_ACTION[aMmi.procedure];
    let reason = MMI_SC_TO_CF_REASON[aMmi.serviceCode];

    if (action === Ci.nsIMobileConnection.CALL_FORWARD_ACTION_QUERY_STATUS) {
      connection.getCallForwarding(reason, {
        QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
        notifyGetCallForwardingSuccess: function(aCount, aResults) {
          aCallback.notifyDialMMISuccessWithCallForwardingOptions(
            MMI_SM_KS_SERVICE_INTERROGATED, aCount, aResults);
        },
        notifyError: function(aErrorMsg) {
          aCallback.notifyDialMMIError(aErrorMsg);
        },
      });
    } else {
      let number = aMmi.sia;
      let serviceClass = this._siToServiceClass(aMmi.sib);
      let timeSeconds = aMmi.sic;
      connection.setCallForwarding(action, reason, number, timeSeconds,
        serviceClass, {
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifySuccess: function() {
            aCallback.notifyDialMMISuccess(CF_ACTION_TO_STATUS_MESSAGE[action]);
          },
          notifyError: function(aErrorMsg) {
            aCallback.notifyDialMMIError(aErrorMsg);
          },
        });
    }
  },

  /**
   * Handle icc change lock MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _iccChangeLockMMI: function(aClientId, aMmi, aCallback) {
    let errorMsg = this._getIccLockMMIError(aMmi);
    if (errorMsg) {
      aCallback.notifyDialMMIError(errorMsg);
      return;
    }

    let icc = gIccService.getIccByServiceId(aClientId);
    let lockType = MMI_SC_TO_LOCK_TYPE[aMmi.serviceCode];

    icc.changeCardLockPassword(lockType, aMmi.sia, aMmi.sib, {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIIccCallback]),
      notifySuccess: function() {
        aCallback.notifyDialMMISuccess(LOCK_TYPE_TO_STATUS_MESSAGE[lockType]);
      },
      notifyCardLockError: function(aErrorMsg, aRetryCount) {
        if (aRetryCount <= 0) {
          if (lockType === Ci.nsIIcc.CARD_LOCK_TYPE_PIN) {
            aErrorMsg = MMI_ERROR_KS_NEEDS_PUK;
          }

          aCallback.notifyDialMMIError(aErrorMsg);
          return;
        }

        aCallback.notifyDialMMIErrorWithInfo(MMI_ERROR_KS_BAD_PIN,
                                             aRetryCount);
      },
    });
  },

  /**
   * Handle icc unlock lock MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _iccUnlockMMI: function(aClientId, aMmi, aCallback) {
    let errorMsg = this._getIccLockMMIError(aMmi);
    if (errorMsg) {
      aCallback.notifyDialMMIError(errorMsg);
      return;
    }

    let icc = gIccService.getIccByServiceId(aClientId);
    let lockType = MMI_SC_TO_LOCK_TYPE[aMmi.serviceCode];

    icc.unlockCardLock(lockType, aMmi.sia, aMmi.sib, {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIIccCallback]),
      notifySuccess: function() {
        aCallback.notifyDialMMISuccess(LOCK_TYPE_TO_STATUS_MESSAGE[lockType]);
      },
      notifyCardLockError: function(aErrorMsg, aRetryCount) {
        if (aRetryCount <= 0) {
          if (lockType === Ci.nsIIcc.CARD_LOCK_TYPE_PUK) {
            aErrorMsg = MMI_ERROR_KS_SIM_BLOCKED;
          }

          aCallback.notifyDialMMIError(aErrorMsg);
          return;
        }

        aCallback.notifyDialMMIErrorWithInfo(MMI_ERROR_KS_BAD_PUK,
                                             aRetryCount);
      },
    });
  },

  /**
   * Handle IMEI MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _getImeiMMI: function(aClientId, aMmi, aCallback) {
    let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
    if (!connection.deviceIdentities || !connection.deviceIdentities.imei) {
      aCallback.notifyDialMMIError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    aCallback.notifyDialMMISuccess(connection.deviceIdentities.imei);
  },

  /**
   * Handle CLIP MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _clipMMI: function(aClientId, aMmi, aCallback) {
    if (aMmi.procedure !== MMI_PROCEDURE_INTERROGATION) {
      aCallback.notifyDialMMIError(MMI_ERROR_KS_NOT_SUPPORTED);
      return;
    }

    this._sendToRilWorker(aClientId, "queryCLIP", {}, aResponse => {
      if (aResponse.errorMsg) {
        aCallback.notifyDialMMIError(aResponse.errorMsg);
        return;
      }

      // aResponse.provisioned informs about the called party receives the
      // calling party's address information:
      // 0 for CLIP not provisioned
      // 1 for CLIP provisioned
      // 2 for unknown
      switch (aResponse.provisioned) {
        case 0:
          aCallback.notifyDialMMISuccess(MMI_SM_KS_SERVICE_DISABLED);
          break;
        case 1:
          aCallback.notifyDialMMISuccess(MMI_SM_KS_SERVICE_ENABLED);
          break;
        default:
          aCallback.notifyDialMMIError(MMI_ERROR_KS_ERROR);
          break;
      }
    });
  },

  /**
   * Handle CLIR MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _clirMMI: function(aClientId, aMmi, aCallback) {
    let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
    switch (aMmi.procedure) {
      case MMI_PROCEDURE_INTERROGATION:
        connection.getCallingLineIdRestriction({
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifyGetClirStatusSuccess: function(aN, aM) {
            let errorMsg;
            let statusMessage;
            // TS 27.007 +CLIR parameter 'm'.
            switch (aM) {
              // CLIR not provisioned.
              case 0:
                statusMessage = MMI_SM_KS_SERVICE_NOT_PROVISIONED;
                break;
              // CLIR provisioned in permanent mode.
              case 1:
                statusMessage = MMI_SM_KS_CLIR_PERMANENT;
                break;
              // Unknown (e.g. no network, etc.).
              case 2:
                errorMsg = MMI_ERROR_KS_ERROR;
                break;
              // CLIR temporary mode presentation restricted.
              case 3:
                // TS 27.007 +CLIR parameter 'n'.
                switch (aN) {
                  // Default.
                  case 0:
                  // CLIR invocation.
                  case 1:
                    statusMessage = MMI_SM_KS_CLIR_DEFAULT_ON_NEXT_CALL_ON;
                    break;
                  // CLIR suppression.
                  case 2:
                    statusMessage = MMI_SM_KS_CLIR_DEFAULT_ON_NEXT_CALL_OFF;
                    break;
                  default:
                    errorMsg = RIL.GECKO_ERROR_GENERIC_FAILURE;
                    break;
                }
                break;
              // CLIR temporary mode presentation allowed.
              case 4:
                // TS 27.007 +CLIR parameter 'n'.
                switch (aN) {
                  // Default.
                  case 0:
                  // CLIR suppression.
                  case 2:
                    statusMessage = MMI_SM_KS_CLIR_DEFAULT_OFF_NEXT_CALL_OFF;
                    break;
                  // CLIR invocation.
                  case 1:
                    statusMessage = MMI_SM_KS_CLIR_DEFAULT_OFF_NEXT_CALL_ON;
                    break;
                  default:
                    errorMsg = RIL.GECKO_ERROR_GENERIC_FAILURE;
                    break;
                }
                break;
              default:
                errorMsg = RIL.GECKO_ERROR_GENERIC_FAILURE;
                break;
            }

            if (errorMsg) {
              aCallback.notifyDialMMIError(errorMsg);
              return;
            }

            aCallback.notifyDialMMISuccess(statusMessage);
          },
          notifyError: function(aErrorMsg) {
            aCallback.notifyDialMMIError(aErrorMsg);
          },
        });
        break;
      case MMI_PROCEDURE_ACTIVATION:
      case MMI_PROCEDURE_DEACTIVATION: {
        let clirMode = MMI_PROC_TO_CLIR_ACTION[aMmi.procedure];
        connection.setCallingLineIdRestriction(clirMode, {
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifySuccess: function() {
            aCallback.notifyDialMMISuccess(CLIR_ACTION_TO_STATUS_MESSAGE[clirMode]);
          },
          notifyError: function(aErrorMsg) {
            aCallback.notifyDialMMIError(aErrorMsg);
          },
        });
        break;
      }
      default:
        aCallback.notifyDialMMIError(MMI_ERROR_KS_NOT_SUPPORTED);
        break;
    }
  },

  /**
   * Handle change call barring password MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _callBarringPasswordMMI: function(aClientId, aMmi, aCallback) {
    if (aMmi.procedure !== MMI_PROCEDURE_REGISTRATION &&
        aMmi.procedure !== MMI_PROCEDURE_ACTIVATION) {
      aCallback.notifyDialMMIError(MMI_ERROR_KS_INVALID_ACTION);
      return;
    }

    if (aMmi.sia !== "" && aMmi.sia !== MMI_ZZ_BARRING_SERVICE) {
      aCallback.notifyDialMMIError(MMI_ERROR_KS_NOT_SUPPORTED);
      return;
    }

    let validPassword = aSi => /^[0-9]{4}$/.test(aSi);
    if (!validPassword(aMmi.sib) || !validPassword(aMmi.sic) ||
        !validPassword(aMmi.pwd)) {
      aCallback.notifyDialMMIError(MMI_ERROR_KS_INVALID_PASSWORD);
      return;
    }

    if (aMmi.sic !== aMmi.pwd) {
      aCallback.notifyDialMMIError(MMI_ERROR_KS_MISMATCH_PASSWORD);
      return;
    }

    let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
    connection.changeCallBarringPassword(aMmi.sib, aMmi.sic, {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
      notifySuccess: function() {
        aCallback.notifyDialMMISuccess(MMI_SM_KS_PASSWORD_CHANGED);
      },
      notifyError: function(aErrorMsg) {
        aCallback.notifyDialMMIError(aErrorMsg);
      },
    });
  },

  /**
   * Handle call barring MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _callBarringMMI: function(aClientId, aMmi, aCallback) {
    let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
    let program = MMI_SC_TO_CB_PROGRAM[aMmi.serviceCode];
    let password = aMmi.sia || "";
    let serviceClass = this._siToServiceClass(aMmi.sib);

    switch (aMmi.procedure) {
      case MMI_PROCEDURE_INTERROGATION:
        connection.getCallBarring(program, password, serviceClass, {
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifyGetCallBarringSuccess: function(aProgram, aEnabled, aServiceClass) {
            if (!aEnabled) {
              aCallback.notifyDialMMISuccess(MMI_SM_KS_SERVICE_DISABLED);
              return;
            }

            let services = this._serviceClassToStringArray(aServiceClass);
            aCallback.notifyDialMMISuccessWithStrings(MMI_SM_KS_SERVICE_ENABLED_FOR,
                                                      services.length, services);
          }.bind(this),
          notifyError: function(aErrorMsg) {
            aCallback.notifyDialMMIError(aErrorMsg);
          },
        });
        break;
      case MMI_PROCEDURE_ACTIVATION:
      case MMI_PROCEDURE_DEACTIVATION: {
        let enabled = (aMmi.procedure === MMI_PROCEDURE_ACTIVATION);
        connection.setCallBarring(program, enabled, password, serviceClass, {
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifySuccess: function() {
            aCallback.notifyDialMMISuccess(
              enabled ? MMI_SM_KS_SERVICE_ENABLED
                      : MMI_SM_KS_SERVICE_DISABLED
            );
          },
          notifyError: function(aErrorMsg) {
            aCallback.notifyDialMMIError(aErrorMsg);
          },
        });
        break;
      }
      default:
        aCallback.notifyDialMMIError(MMI_ERROR_KS_NOT_SUPPORTED);
        break;
    }
  },

  /**
   * Handle call waiting MMI code.
   *
   * @param aClientId
   *        Client id.
   * @param aMmi
   *        Parsed MMI structure.
   * @param aCallback
   *        A nsITelephonyDialCallback object.
   */
  _callWaitingMMI: function(aClientId, aMmi, aCallback) {
    let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);

    switch (aMmi.procedure) {
      case MMI_PROCEDURE_INTERROGATION:
        connection.getCallWaiting({
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifyGetCallWaitingSuccess: function(aServiceClass) {
            if (aServiceClass === Ci.nsIMobileConnection.ICC_SERVICE_CLASS_NONE) {
              aCallback.notifyDialMMISuccess(MMI_SM_KS_SERVICE_DISABLED);
              return;
            }

            let services = this._serviceClassToStringArray(aServiceClass);
            aCallback.notifyDialMMISuccessWithStrings(MMI_SM_KS_SERVICE_ENABLED_FOR,
                                                      services.length, services);
          }.bind(this),
          notifyError: function(aErrorMsg) {
            aCallback.notifyDialMMIError(aErrorMsg);
          },
        });
        break;
      case MMI_PROCEDURE_ACTIVATION:
      case MMI_PROCEDURE_DEACTIVATION: {
        let enabled = (aMmi.procedure === MMI_PROCEDURE_ACTIVATION);
        let serviceClass = this._siToServiceClass(aMmi.sia);
        connection.setCallWaiting(enabled, serviceClass, {
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifySuccess: function() {
            aCallback.notifyDialMMISuccess(
              enabled ? MMI_SM_KS_SERVICE_ENABLED
                      : MMI_SM_KS_SERVICE_DISABLED
            );
          },
          notifyError: function(aErrorMsg) {
            aCallback.notifyDialMMIError(aErrorMsg);
          },
        });
        break;
      }
      default:
        aCallback.notifyDialMMIError(MMI_ERROR_KS_NOT_SUPPORTED);
        break;
    }
  },

  _serviceCodeToKeyString: function(aServiceCode) {
    switch (aServiceCode) {
      case MMI_SC_CFU:
      case MMI_SC_CF_BUSY:
      case MMI_SC_CF_NO_REPLY:
      case MMI_SC_CF_NOT_REACHABLE:
      case MMI_SC_CF_ALL:
      case MMI_SC_CF_ALL_CONDITIONAL:
        return MMI_KS_SC_CALL_FORWARDING;
      case MMI_SC_PIN:
        return MMI_KS_SC_PIN;
      case MMI_SC_PIN2:
        return MMI_KS_SC_PIN2;
      case MMI_SC_PUK:
        return MMI_KS_SC_PUK;
      case MMI_SC_PUK2:
        return MMI_KS_SC_PUK2;
      case MMI_SC_IMEI:
        return MMI_KS_SC_IMEI;
      case MMI_SC_CLIP:
        return MMI_KS_SC_CLIP;
      case MMI_SC_CLIR:
        return MMI_KS_SC_CLIR;
      case MMI_SC_BAOC:
      case MMI_SC_BAOIC:
      case MMI_SC_BAOICxH:
      case MMI_SC_BAIC:
      case MMI_SC_BAICr:
      case MMI_SC_BA_ALL:
      case MMI_SC_BA_MO:
      case MMI_SC_BA_MT:
        return MMI_KS_SC_CALL_BARRING;
      case MMI_SC_CALL_WAITING:
        return MMI_KS_SC_CALL_WAITING;
      case MMI_SC_CHANGE_PASSWORD:
        return MMI_KS_SC_CHANGE_PASSWORD;
      default:
        return MMI_KS_SC_USSD;
    }
  },

  /**
   * Helper for translating basic service group to service class parameter.
   */
  _siToServiceClass: function(aSi) {
    if (!aSi) {
      return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_NONE;
    }

    let serviceCode = parseInt(aSi, 10);
    switch (serviceCode) {
      case 10:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_SMS +
               Ci.nsIMobileConnection.ICC_SERVICE_CLASS_FAX +
               Ci.nsIMobileConnection.ICC_SERVICE_CLASS_VOICE;
      case 11:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_VOICE;
      case 12:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_SMS +
               Ci.nsIMobileConnection.ICC_SERVICE_CLASS_FAX;
      case 13:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_FAX;
      case 16:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_SMS;
      case 19:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_FAX +
               Ci.nsIMobileConnection.ICC_SERVICE_CLASS_VOICE;
      case 21:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_PAD +
               Ci.nsIMobileConnection.ICC_SERVICE_CLASS_DATA_ASYNC;
      case 22:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_PACKET +
               Ci.nsIMobileConnection.ICC_SERVICE_CLASS_DATA_SYNC;
      case 25:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_DATA_ASYNC;
      case 26:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_DATA_SYNC +
               Ci.nsIMobileConnection.ICC_SERVICE_CLASS_VOICE;
      case 99:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_PACKET;
      default:
        return Ci.nsIMobileConnection.ICC_SERVICE_CLASS_NONE;
    }
  },

  _serviceClassToStringArray: function(aServiceClass) {
    let services = [];
    for (let mask = Ci.nsIMobileConnection.ICC_SERVICE_CLASS_VOICE;
         mask <= Ci.nsIMobileConnection.ICC_SERVICE_CLASS_MAX;
         mask <<= 1) {
      if (mask & aServiceClass) {
        services.push(MMI_KS_SERVICE_CLASS_MAPPING[mask]);
      }
    }
    return services;
  },

  _getIccLockMMIError: function(aMmi) {
    // As defined in TS.122.030 6.6.2 to change the ICC PIN we should expect
    // an MMI code of the form **04*OLD_PIN*NEW_PIN*NEW_PIN#, where old PIN
    // should be entered as the SIA parameter and the new PIN as SIB and
    // SIC.
    if (aMmi.procedure !== MMI_PROCEDURE_REGISTRATION) {
      return MMI_ERROR_KS_INVALID_ACTION;
    }

    if (!aMmi.sia || !aMmi.sib || !aMmi.sic) {
      return MMI_ERROR_KS_ERROR;
    }

    if (aMmi.sia.length < 4 || aMmi.sia.length > 8 ||
        aMmi.sib.length < 4 || aMmi.sib.length > 8 ||
        aMmi.sic.length < 4 || aMmi.sic.length > 8) {
      return MMI_ERROR_KS_INVALID_PIN;
    }

    if (aMmi.sib != aMmi.sic) {
      return MMI_ERROR_KS_MISMATCH_PIN;
    }

    return null;
  },

  /**
   * The default callback handler for call operations.
   *
   * @param aCallback
   *        An callback object including notifySuccess() and notifyError(aMsg)
   * @param aResponse
   *        The response from ril_worker.
   */
  _defaultCallbackHandler: function(aCallback, aResponse) {
    if (aResponse.errorMsg) {
      aCallback.notifyError(aResponse.errorMsg);
    } else {
      aCallback.notifySuccess();
    }
  },

  _defaultMMICallbackHandler: function(aCallback, aResponse) {
    if (aResponse.errorMsg) {
      aCallback.notifyDialMMIError(aResponse.errorMsg);
    } else {
      aCallback.notifyDialMMISuccess("");
    }
  },

  _getCallsWithState: function(aClientId, aState) {
    let calls = [];
    for (let i in this._currentCalls[aClientId]) {
      let call = this._currentCalls[aClientId][i];
      if (call.state === aState) {
        calls.push(call);
      }
    }
    return calls;
  },

  /**
   * Update call information from RIL.
   *
   * @return Boolean to indicate whether the data is changed.
   */
  _updateCallFromRil: function(aCall, aRilCall) {
    aRilCall.state = this._convertRILCallState(aRilCall.state);
    aRilCall.number = this._formatInternationalNumber(aRilCall.number,
                                                      aRilCall.toa);

    let change = false;
    const key = ["state", "number", "numberPresentation", "name",
                 "namePresentation"];

    for (let k of key) {
      if (aCall[k] != aRilCall[k]) {
        aCall[k] = aRilCall[k];
        change = true;
      }
    }

    aCall.isOutgoing = !aRilCall.isMT;
    aCall.isEmergency = DialNumberUtils.isEmergency(aCall.number);

    if (!aCall.started &&
        aCall.state == nsITelephonyService.CALL_STATE_CONNECTED) {
      aCall.started = new Date().getTime();
    }

    return change;
  },

  /**
   * Identify the conference group.
   * @return [conference state, array of calls in group]
   *
   * TODO: handle multi-sim case.
   */
  _detectConference: function(aClientId) {
    // There are some difficuties to identify the conference by |.isMpty| from RIL
    // so we don't rely on this flag.
    //  - |.isMpty| becomes false when the conference call is put on hold.
    //  - |.isMpty| may remain true when other participants left the conference.

    // All the calls in the conference should have the same state and it is
    // either CONNECTED or HELD. That means, if we find a group of call with
    // the same state and its size is larger than 2, it must be a conference.
    let connectedCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_CONNECTED);
    let heldCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_HELD);

    if (connectedCalls.length >= 2) {
      return [nsITelephonyService.CALL_STATE_CONNECTED, connectedCalls];
    } else if (heldCalls.length >= 2) {
      return [nsITelephonyService.CALL_STATE_HELD, heldCalls];
    }

    return [nsITelephonyService.CALL_STATE_UNKNOWN, null];
  },

  /**
   * Update the isConference flag of all Calls.
   *
   * @return [conference state, array of calls being updated]
   */
  _updateConference: function(aClientId) {
    let [newConferenceState, conferenceCalls] = this._detectConference(aClientId);
    if (DEBUG) debug("Conference state: " + newConferenceState);

    let changedCalls = [];
    let conference = new Set(conferenceCalls);

    for (let i in this._currentCalls[aClientId]) {
      let call = this._currentCalls[aClientId][i];
      let isConference = conference.has(call);
      if (call.isConference != isConference) {
        call.isConference = isConference;
        changedCalls.push(call);
      }
    }

    return [newConferenceState, changedCalls];
  },

  sendTones: function(aClientId, aDtmfChars, aPauseDuration, aToneDuration,
                      aCallback) {
    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    let tones = aDtmfChars;
    let playTone = (tone) => {
      this._sendToRilWorker(aClientId, "startTone", { dtmfChar: tone }, response => {
        if (response.errorMsg) {
          aCallback.notifyError(response.errorMsg);
          return;
        }

        timer.initWithCallback(() => {
          this.stopTone();
          timer.initWithCallback(() => {
            if (tones.length === 1) {
              aCallback.notifySuccess();
            } else {
              tones = tones.substr(1);
              playTone(tones[0]);
            }
          }, TONES_GAP_DURATION, Ci.nsITimer.TYPE_ONE_SHOT);
        }, aToneDuration, Ci.nsITimer.TYPE_ONE_SHOT);
      });
    };

    timer.initWithCallback(() => {
      playTone(tones[0]);
    }, aPauseDuration, Ci.nsITimer.TYPE_ONE_SHOT);
  },

  startTone: function(aClientId, aDtmfChar) {
    this._sendToRilWorker(aClientId, "startTone", { dtmfChar: aDtmfChar });
  },

  stopTone: function(aClientId) {
    this._sendToRilWorker(aClientId, "stopTone");
  },

  answerCall: function(aClientId, aCallIndex, aCallback) {
    let call = this._currentCalls[aClientId][aCallIndex];
    if (!call || call.state != nsITelephonyService.CALL_STATE_INCOMING) {
      aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    let callNum = Object.keys(this._currentCalls[aClientId]).length;
    if (callNum !== 1) {
      this._switchActiveCall(aClientId, aCallback);
    } else {
      this._sendToRilWorker(aClientId, "answerCall", null,
                            this._defaultCallbackHandler.bind(this, aCallback));
    }
  },

  rejectCall: function(aClientId, aCallIndex, aCallback) {
    if (this._isCdmaClient(aClientId)) {
      this._hangUpBackground(aClientId, aCallback);
      return;
    }

    let call = this._currentCalls[aClientId][aCallIndex];
    if (!call || call.state != nsITelephonyService.CALL_STATE_INCOMING) {
      aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    let callNum = Object.keys(this._currentCalls[aClientId]).length;
    if (callNum !== 1) {
      this._hangUpBackground(aClientId, aCallback);
    } else {
      call.hangUpLocal = true;
      this._sendToRilWorker(aClientId, "udub", null,
                            this._defaultCallbackHandler.bind(this, aCallback));
    }
  },

  _hangUpForeground: function(aClientId, aCallback) {
    let calls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_CONNECTED);
    calls.forEach(call => call.hangUpLocal = true);

    this._sendToRilWorker(aClientId, "hangUpForeground", null,
                          this._defaultCallbackHandler.bind(this, aCallback));
  },

  _hangUpBackground: function(aClientId, aCallback) {
    // When both a held and a waiting call exist, the request shall apply to
    // the waiting call.
    let waitingCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_INCOMING);
    let heldCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_HELD);

    if (waitingCalls.length) {
      waitingCalls.forEach(call => call.hangUpLocal = true);
    } else {
      heldCalls.forEach(call => call.hangUpLocal = true);
    }

    this._sendToRilWorker(aClientId, "hangUpBackground", null,
                          this._defaultCallbackHandler.bind(this, aCallback));
  },

  hangUpCall: function(aClientId, aCallIndex, aCallback) {
    // Should release both, child and parent, together. Since RIL holds only
    // the parent call, we send 'parentId' to RIL.
    aCallIndex = this._currentCalls[aClientId][aCallIndex].parentId || aCallIndex;

    let call = this._currentCalls[aClientId][aCallIndex];
    if (call.state === nsITelephonyService.CALL_STATE_HELD) {
      this._hangUpBackground(aClientId, aCallback);
      return;
    }

    // After hangup a single call, gecko has to resume the held call or conference.
    if (!call.isConference) {
      let heldCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_HELD);

      if (heldCalls.length) {
        if (call.state === nsITelephonyService.CALL_STATE_CONNECTED) {
          // For a foreground call, ril has a request to do two actions together.
          this._hangUpForeground(aClientId, aCallback);
        } else {
          // Otherwise, gecko should send out two consecutive requests by itself.
          this._sendToRilWorker(aClientId, "hangUpCall", { callIndex: aCallIndex }, response => {
            if (response.errorMsg) {
              aCallback.notifyError(response.errorMsg);
            } else {
              aCallback.notifySuccess();

              let emptyCallback = {
                QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyCallback]),
                notifySuccess: () => {},
                notifyError: () => {}
              };

              if (heldCalls.length === 1) {
                this.resumeCall(aClientId, heldCalls[0].callIndex, emptyCallback);
              } else {
                this.resumeConference(aClientId, emptyCallback);
              }
            }
          });
        }

        return;
      }
    }

    call.hangUpLocal = true;
    this._sendToRilWorker(aClientId, "hangUpCall", { callIndex: aCallIndex },
                          this._defaultCallbackHandler.bind(this, aCallback));
  },

  _switchCall: function(aClientId, aCallIndex, aCallback, aRequiredState) {
    let call = this._currentCalls[aClientId][aCallIndex];
    if (!call) {
      aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    if (this._isCdmaClient(aClientId)) {
      this._switchCallCdma(aClientId, aCallIndex, aCallback);
    } else {
      this._switchCallGsm(aClientId, aCallIndex, aCallback, aRequiredState);
    }
  },

  _switchCallGsm: function(aClientId, aCallIndex, aCallback, aRequiredState) {
    let call = this._currentCalls[aClientId][aCallIndex];
    if (call.state != aRequiredState) {
      aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    this._switchActiveCall(aClientId, aCallback);
  },

  _switchActiveCall: function(aClientId, aCallback) {
    this._sendToRilWorker(aClientId, "switchActiveCall", null,
                          this._defaultCallbackHandler.bind(this, aCallback));
  },

  _switchCallCdma: function(aClientId, aCallIndex, aCallback) {
    let call = this._currentCalls[aClientId][aCallIndex];
    if (!call.isSwitchable) {
      aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    this._sendToRilWorker(aClientId, "cdmaFlash", null,
                          this._defaultCallbackHandler.bind(this, aCallback));
  },

  holdCall: function(aClientId, aCallIndex, aCallback) {
    this._switchCall(aClientId, aCallIndex, aCallback,
                     nsITelephonyService.CALL_STATE_CONNECTED);
  },

  resumeCall: function(aClientId, aCallIndex, aCallback) {
    this._switchCall(aClientId, aCallIndex, aCallback,
                     nsITelephonyService.CALL_STATE_HELD);
  },

  _conferenceCallGsm: function(aClientId, aCallback) {
    this._sendToRilWorker(aClientId, "conferenceCall", null, response => {
      if (response.errorMsg) {
        aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
        // TODO: Bug 1124993. Deprecate it. Use callback response is enough.
        this._notifyAllListeners("notifyConferenceError",
                                 ["addError", response.errorMsg]);
        return;
      }

      aCallback.notifySuccess();
    });
  },

  _conferenceCallCdma: function(aClientId, aCallback) {
    for (let index in this._currentCalls[aClientId]) {
      let call = this._currentCalls[aClientId][index];
      if (!call.isMergeable) {
        aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
        return;
      }
    }

    this._sendToRilWorker(aClientId, "cdmaFlash", null, response => {
      if (response.errorMsg) {
        aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
        // TODO: Bug 1124993. Deprecate it. Use callback response is enough.
        this._notifyAllListeners("notifyConferenceError",
                                 ["addError", response.errorMsg]);
        return;
      }

      let calls = [];
      for (let index in this._currentCalls[aClientId]) {
        let call = this._currentCalls[aClientId][index];
        call.state = nsITelephonyService.CALL_STATE_CONNECTED;
        call.isConference = true;
        calls.push(call);
      }
      this._handleCallStateChanged(aClientId, calls);

      aCallback.notifySuccess();
    });
  },

  conferenceCall: function(aClientId, aCallback) {
    if (Object.keys(this._currentCalls[aClientId]).length < 2) {
      aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    if (this._isCdmaClient(aClientId)) {
      this._conferenceCallCdma(aClientId, aCallback);
    } else {
      this._conferenceCallGsm(aClientId, aCallback);
    }
  },

  _separateCallGsm: function(aClientId, aCallIndex, aCallback) {
    this._sendToRilWorker(aClientId, "separateCall", { callIndex: aCallIndex },
                          response => {
      if (response.errorMsg) {
        aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
        // TODO: Bug 1124993. Deprecate it. Use callback response is enough.
        this._notifyAllListeners("notifyConferenceError",
                                 ["removeError", response.errorMsg]);
        return;
      }

      aCallback.notifySuccess();
    });
  },

  _removeCdmaSecondCall: function(aClientId) {
    let childCall = this._currentCalls[aClientId][CDMA_SECOND_CALL_INDEX];
    let parentCall = this._currentCalls[aClientId][CDMA_FIRST_CALL_INDEX];

    this._disconnectCalls(aClientId, [childCall]);

    parentCall.isConference = false;
    parentCall.isSwitchable = true;
    parentCall.isMergeable = true;
    this._handleCallStateChanged(aClientId, [childCall, parentCall]);
  },

  // See 3gpp2, S.R0006-522-A v1.0. Table 4, XID 6S.
  // Release the third party. Optionally apply a warning tone. Connect the
  // controlling subscriber and the second party. Go to the 2-way state.
  _separateCallCdma: function(aClientId, aCallIndex, aCallback) {
    this._sendToRilWorker(aClientId, "cdmaFlash", null, response => {
      if (response.errorMsg) {
        aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
        // TODO: Bug 1124993. Deprecate it. Use callback response is enough.
        this._notifyAllListeners("notifyConferenceError",
                                 ["removeError", response.errorMsg]);
        return;
      }

      this._removeCdmaSecondCall(aClientId);
      aCallback.notifySuccess();
    });
  },

  separateCall: function(aClientId, aCallIndex, aCallback) {
    let call = this._currentCalls[aClientId][aCallIndex];
    if (!call || !call.isConference) {
      aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    if (this._isCdmaClient(aClientId)) {
      this._separateCallCdma(aClientId, aCallIndex, aCallback);
    } else {
      this._separateCallGsm(aClientId, aCallIndex, aCallback);
    }
  },

  hangUpConference: function(aClientId, aCallback) {
    // In cdma, ril only maintains one call index.
    if (this._isCdmaClient(aClientId)) {
      this._sendToRilWorker(aClientId, "hangUpCall",
                            { callIndex: CDMA_FIRST_CALL_INDEX },
                            this._defaultCallbackHandler.bind(this, aCallback));
      return;
    }

    // Find a conference call, and send the corresponding request to RIL worker.
    for (let index in this._currentCalls[aClientId]) {
      let call = this._currentCalls[aClientId][index];
      if (!call.isConference) {
        continue;
      }

      let command = call.state === nsITelephonyService.CALL_STATE_CONNECTED ?
                    "hangUpForeground" : "hangUpBackground";
      this._sendToRilWorker(aClientId, command, null,
                            this._defaultCallbackHandler.bind(this, aCallback));
      return;
    }

    // There is no conference call.
    if (DEBUG) debug("hangUpConference: " +
                     "No conference call in modem[" + aClientId + "].");
    aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
  },

  _switchConference: function(aClientId, aCallback) {
    // Cannot hold/resume a conference in cdma.
    if (this._isCdmaClient(aClientId)) {
      aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
      return;
    }

    this._switchActiveCall(aClientId, aCallback);
  },

  holdConference: function(aClientId, aCallback) {
    this._switchConference(aClientId, aCallback);
  },

  resumeConference: function(aClientId, aCallback) {
    this._switchConference(aClientId, aCallback);
  },

  sendUSSD: function(aClientId, aUssd, aCallback) {
    this._sendUSSDInternal(aClientId, aUssd,
                           this._defaultCallbackHandler.bind(this, aCallback));
  },

  _sendUSSDInternal: function(aClientId, aUssd, aCallback) {
    this._ussdSessions[aClientId] = USSD_SESSION_ONGOING;
    this._sendToRilWorker(aClientId, "sendUSSD", { ussd: aUssd }, aResponse => {
      if (aResponse.errorMsg) {
        this._ussdSessions[aClientId] = USSD_SESSION_DONE;
      }
      aCallback(aResponse);
    });
  },

  cancelUSSD: function(aClientId, aCallback) {
    this._cancelUSSDInternal(aClientId,
                             this._defaultCallbackHandler.bind(this, aCallback));
  },

  _cancelUSSDInternal: function(aClientId, aCallback) {
    this._ussdSessions[aClientId] = USSD_SESSION_CANCELLING;
    this._sendToRilWorker(aClientId, "cancelUSSD", {}, aResponse => {
      if (aResponse.errorMsg) {
        this._ussdSessions[aClientId] = USSD_SESSION_ONGOING;
      }
      aCallback(aResponse);
    });
  },

  get microphoneMuted() {
    return gAudioService.microphoneMuted;
  },

  set microphoneMuted(aMuted) {
    gAudioService.microphoneMuted = aMuted;
  },

  get speakerEnabled() {
    return gAudioService.speakerEnabled;
  },

  set speakerEnabled(aEnabled) {
    gAudioService.speakerEnabled = aEnabled;
  },

  /**
   * nsIGonkTelephonyService interface.
   */

  _notifyCallEnded: function(aCall) {
    let duration = ("started" in aCall && typeof aCall.started == "number") ?
      new Date().getTime() - aCall.started : 0;

    gTelephonyMessenger.notifyCallEnded(aCall.clientId,
                                        aCall.number,
                                        this._cdmaCallWaitingNumber,
                                        aCall.isEmergency,
                                        duration,
                                        aCall.isOutgoing,
                                        aCall.hangUpLocal);

    // Clear cache of this._cdmaCallWaitingNumber after call disconnected.
    this._cdmaCallWaitingNumber = null;
  },

  /**
   * Disconnect calls by updating their states. Sometimes, it may cause other
   * calls being disconnected as well.
   *
   * @return Array a list of calls we need to fire callStateChange
   */
  _disconnectCalls: function(aClientId, aCalls,
                             aFailCause = RIL.GECKO_CALL_ERROR_NORMAL_CALL_CLEARING) {
    if (DEBUG) debug("_disconnectCalls: " + JSON.stringify(aCalls));

    // In addition to the disconnected call itself, its decedent calls should be
    // treated as disconnected calls as well.
    let disconnectedCalls = aCalls.slice();
    for (let call in aCalls) {
      while (call.childId) {
        call = this._currentCalls[aClientId][call.childId];
        disconnectedCalls.push(call);
      }
    }

    // Store unique value in the list.
    disconnectedCalls = [...new Set(disconnectedCalls)];

    let callsForStateChanged = [];

    disconnectedCalls.forEach(call => {
      call.state = nsITelephonyService.CALL_STATE_DISCONNECTED;
      call.disconnectedReason = aFailCause;

      let parentCall = this._currentCalls[aClientId][call.parentId];
      if (parentCall) {
        delete parentCall.childId;
      }

      this._notifyCallEnded(call);

      callsForStateChanged.push(call);

      delete this._currentCalls[aClientId][call.callIndex];
    });

    return callsForStateChanged;
  },

  /**
   * Handle an incoming call.
   *
   * Not much is known about this call at this point, but it's enough
   * to start bringing up the Phone app already.
   */
  notifyCallRing: function() {
    // 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();

    gTelephonyMessenger.notifyNewCall();
  },

  /**
   * Handle current calls reported from RIL.
   *
   * @param aCalls call from RIL, which contains:
   *        state, callIndex, toa, isMT, number, numberPresentation, name,
   *        namePresentation.
   */
  notifyCurrentCalls: function(aClientId, aCalls) {
    // Check whether there is a removed call.
    let hasRemovedCalls = () => {
      let newIndexes = new Set(Object.keys(aCalls));
      for (let i in this._currentCalls[aClientId]) {
        if (!newIndexes.has(i)) {
          return true;
        }
      }
      return false;
    };

    // If there are removedCalls, we should fetch the failCause first.
    if (!hasRemovedCalls()) {
      this._handleCurrentCalls(aClientId, aCalls);
    } else {
      this._sendToRilWorker(aClientId, "getFailCause", null, response => {
        this._handleCurrentCalls(aClientId, aCalls, response.failCause);
      });
    }
  },

  _handleCurrentCalls: function(aClientId, aCalls,
                                aFailCause = RIL.GECKO_CALL_ERROR_NORMAL_CALL_CLEARING) {
    if (DEBUG) debug("handleCurrentCalls: " + JSON.stringify(aCalls) +
                     ", failCause: " + aFailCause);

    let changedCalls = new Set();
    let removedCalls = new Set();

    let allIndexes = new Set([...Object.keys(this._currentCalls[aClientId]),
                              ...Object.keys(aCalls)]);

    for (let i of allIndexes) {
      let call = this._currentCalls[aClientId][i];
      let rilCall = aCalls[i];

      // Determine the change of call.
      if (call && !rilCall) {  // removed.
        removedCalls.add(call);
      } else if (call && rilCall) {  // changed.
        if (this._updateCallFromRil(call, rilCall)) {
          changedCalls.add(call);
        }
      } else {  // !call && rilCall. added.
        this._currentCalls[aClientId][i] = call = new Call(aClientId, i);
        this._updateCallFromRil(call, rilCall);
        changedCalls.add(call);

        // Handle ongoingDial.
        if (this._ongoingDial && this._ongoingDial.clientId === aClientId &&
            call.state !== nsITelephonyService.CALL_STATE_INCOMING) {
          this._ongoingDial.callback.notifyDialCallSuccess(aClientId, i,
                                                           call.number);
          this._ongoingDial = null;
        }
      }
    }

    // For correct conference detection, we should mark removedCalls as
    // DISCONNECTED first.
    let disconnectedCalls = this._disconnectCalls(aClientId, [...removedCalls], aFailCause);
    disconnectedCalls.forEach(call => changedCalls.add(call));

    // Detect conference and update isConference flag.
    let [newConferenceState, conferenceChangedCalls] = this._updateConference(aClientId);
    conferenceChangedCalls.forEach(call => changedCalls.add(call));

    this._handleCallStateChanged(aClientId, [...changedCalls]);

    this._updateAudioState(aClientId);

    // Handle cached dial request.
    if (this._cachedDialRequest && !this._isActive(aClientId)) {
      if (DEBUG) debug("All calls held. Perform the cached dial request.");

      let request = this._cachedDialRequest;
      this._sendDialCallRequest(request.clientId, request.options,
                                request.callback);
      this._cachedDialRequest = null;
    }
  },

  /**
   * Handle call state changes.
   */
  _handleCallStateChanged: function(aClientId, aCalls) {
    if (DEBUG) debug("handleCallStateChanged: " + JSON.stringify(aCalls));

    if (aCalls.length === 0) {
      return;
    }

    if (aCalls.some(call => call.state == nsITelephonyService.CALL_STATE_DIALING)) {
      gTelephonyMessenger.notifyNewCall();
    }

    let allInfo = aCalls.map(call => new TelephonyCallInfo(call));
    this._notifyAllListeners("callStateChanged", [allInfo.length, allInfo]);
  },

  notifyCdmaCallWaiting: function(aClientId, aCall) {
    // 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._removeCdmaSecondCall(aClientId);
    }

    this._cdmaCallWaitingNumber = aCall.number;

    this._notifyAllListeners("notifyCdmaCallWaiting", [aClientId,
                                                       aCall.number,
                                                       aCall.numberPresentation,
                                                       aCall.name,
                                                       aCall.namePresentation]);
  },

  notifySupplementaryService: function(aClientId, aNumber, aNotification) {
    let notification = this._convertRILSuppSvcNotification(aNotification);

    // Get the target call object for this notification.
    let callIndex = -1;

    let indexes = Object.keys(this.currentCalls);
    if (indexes.length === 1) {
      // Only one call exists. This should be the target.
      callIndex = indexes[0];
    } else {
      // Find the call in |currentCalls| by the given number.
      if (aNumber) {
        for (let i in this._currentCalls) {
          let call = this._currentCalls[aClientId][i];
          if (call.number === aNumber) {
            callIndex = i;
            break;
          }
        }
      }
    }

    this._notifyAllListeners("supplementaryServiceNotification",
                             [aClientId, callIndex, notification]);
  },

  notifyUssdReceived: function(aClientId, aMessage, aSessionEnded) {
    if (DEBUG) {
      debug("notifyUssdReceived for " + aClientId + ": " +
            aMessage + " (sessionEnded : " + aSessionEnded + ")");
    }

    let oldSession = this._ussdSessions[aClientId];
    this._ussdSessions[aClientId] =
      aSessionEnded ? USSD_SESSION_DONE : USSD_SESSION_ONGOING;

    // We suppress the empty message only when the session is not changed and
    // is not alive. See Bug 1057455, 1198676.
    // Moreover, we should allow a notification initiated by network
    // in which further response is not required.
    // See |5.2.2 Actions at the UE| in 3GPP TS 22.090:
    // "
    //  The network may explicitly indicate to the UE that a response
    //  from the user is required. ...
    //  If the network does not indicate that a response is required,
    //  then the normal MMI procedures on the UE continue to apply.
    // "
    if (oldSession != USSD_SESSION_ONGOING &&
        this._ussdSessions[aClientId] != USSD_SESSION_ONGOING &&
        !aMessage) {
      return;
    }

    gTelephonyMessenger.notifyUssdReceived(aClientId, aMessage, aSessionEnded);
  },

  /**
   * nsIObserver interface.
   */

  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
        if (aData === kPrefRilDebuggingEnabled) {
          this._updateDebugFlag();
        } 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();

        Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
        break;
    }
  }
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TelephonyService]);
