/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/Services.jsm");
Cu.import("resource://gre/modules/systemlibs.js");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

var RIL = {};
Cu.import("resource://gre/modules/ril_consts.js", RIL);

const GONK_MOBILECONNECTIONSERVICE_CONTRACTID =
  "@mozilla.org/mobileconnection/gonkmobileconnectionservice;1";

const GONK_MOBILECONNECTIONSERVICE_CID =
  Components.ID("{0c9c1a96-2c72-4c55-9e27-0ca73eb16f63}");
const MOBILECONNECTIONINFO_CID =
  Components.ID("{8162b3c0-664b-45f6-96cd-f07b4e193b0e}");
const MOBILENETWORKINFO_CID =
  Components.ID("{a6c8416c-09b4-46d1-bf29-6520d677d085}");
const MOBILECELLINFO_CID =
  Components.ID("{0635d9ab-997e-4cdf-84e7-c1883752dff3}");
const MOBILECALLFORWARDINGOPTIONS_CID =
  Components.ID("{e0cf4463-ee63-4b05-ab2e-d94bf764836c}");
const NEIGHBORINGCELLINFO_CID =
  Components.ID("{6078cbf1-f34c-44fa-96f8-11a88d4bfdd3}");
const GSMCELLINFO_CID =
  Components.ID("{e3cf3aa0-f992-48fe-967b-ec98a28c8535}");
const WCDMACELLINFO_CID =
  Components.ID("{62e2c83c-b535-4068-9762-8039fac48106}");
const CDMACELLINFO_CID =
  Components.ID("{40f491f0-dd8b-42fd-af32-aef5b002749a}");
const LTECELLINFO_CID =
  Components.ID("{715e2c76-3b08-41e4-8ea5-e60c5ce6393e}");


const NS_XPCOM_SHUTDOWN_OBSERVER_ID      = "xpcom-shutdown";
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID  = "nsPref:changed";
const NS_NETWORK_ACTIVE_CHANGED_TOPIC_ID = "network-active-changed";
const NS_DATA_CALL_ERROR_TOPIC_ID        = "data-call-error";

const kPrefRilDebuggingEnabled = "ril.debugging.enabled";

const UNKNOWN_VALUE = Ci.nsICellInfo.UNKNOWN_VALUE;

XPCOMUtils.defineLazyServiceGetter(this, "gMobileConnectionMessenger",
                                   "@mozilla.org/ril/system-messenger-helper;1",
                                   "nsIMobileConnectionMessenger");

XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager",
                                   "@mozilla.org/network/manager;1",
                                   "nsINetworkManager");

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

XPCOMUtils.defineLazyServiceGetter(this, "gDataCallManager",
                                   "@mozilla.org/datacall/manager;1",
                                   "nsIDataCallManager");

XPCOMUtils.defineLazyModuleGetter(this, "gTelephonyUtils",
                                  "resource://gre/modules/TelephonyUtils.jsm",
                                  "TelephonyUtils");

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

var DEBUG = RIL.DEBUG_RIL;
function debug(s) {
  dump("MobileConnectionService: " + s + "\n");
}

function MobileNetworkInfo() {
  this.shortName = null;
  this.longName = null;
  this.mcc = null;
  this.mnc = null;
  this.stat = null;
}
MobileNetworkInfo.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileNetworkInfo]),
  classID:        MOBILENETWORKINFO_CID,
  classInfo:      XPCOMUtils.generateCI({
    classID:          MOBILENETWORKINFO_CID,
    classDescription: "MobileNetworkInfo",
    interfaces:       [Ci.nsIMobileNetworkInfo]
  })
};

function MobileCellInfo() {
  this.gsmLocationAreaCode = -1;
  this.gsmCellId = -1;
  this.cdmaBaseStationId = -1;
  this.cdmaBaseStationLatitude = -2147483648;
  this.cdmaBaseStationLongitude = -2147483648;
  this.cdmaSystemId = -1;
  this.cdmaNetworkId = -1;
}
MobileCellInfo.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileCellInfo]),
  classID:        MOBILECELLINFO_CID,
  classInfo:      XPCOMUtils.generateCI({
    classID:          MOBILECELLINFO_CID,
    classDescription: "MobileCellInfo",
    interfaces:       [Ci.nsIMobileCellInfo]
  })
};

function MobileConnectionInfo() {}
MobileConnectionInfo.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionInfo]),
  classID: MOBILECONNECTIONINFO_CID,
  classInfo: XPCOMUtils.generateCI({
    classID: MOBILECONNECTIONINFO_CID,
    classDescription: "MobileConnectionInfo",
    interfaces: [Ci.nsIMobileConnectionInfo]
  }),

  state: null,
  connected: false,
  emergencyCallsOnly: false,
  roaming: false,
  network: null,
  cell: null,
  type: null,
  signalStrength: null,
  relSignalStrength: null
};

function MobileCallForwardingOptions(aOptions) {
  for (let key in aOptions) {
    this[key] = aOptions[key];
  }
}
MobileCallForwardingOptions.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileCallForwardingOptions]),
  classID: MOBILECALLFORWARDINGOPTIONS_CID,
  classInfo: XPCOMUtils.generateCI({
    classID:          MOBILECALLFORWARDINGOPTIONS_CID,
    classDescription: "MobileCallForwardingOptions",
    interfaces:       [Ci.nsIMobileCallForwardingOptions]
  }),

  // nsIMobileForwardingOptions

  active: false,
  action: Ci.nsIMobileConnection.CALL_FORWARD_ACTION_UNKNOWN,
  reason: Ci.nsIMobileConnection.CALL_FORWARD_REASON_UNKNOWN,
  number: null,
  timeSeconds: -1,
  serviceClass: Ci.nsIMobileConnection.ICC_SERVICE_CLASS_NONE
}

function NeighboringCellInfo(aOptions) {
  this.networkType = aOptions.networkType;
  this.gsmLocationAreaCode = (aOptions.gsmLocationAreaCode !== undefined &&
                              aOptions.gsmLocationAreaCode >= 0 &&
                              aOptions.gsmLocationAreaCode <= 65535) ?
                             aOptions.gsmLocationAreaCode : UNKNOWN_VALUE;
  this.gsmCellId = (aOptions.gsmCellId !== undefined &&
                    aOptions.gsmCellId >= 0 &&
                    aOptions.gsmCellId <= 65535) ?
                   aOptions.gsmCellId : UNKNOWN_VALUE;
  this.wcdmaPsc = (aOptions.wcdmaPsc !== undefined && aOptions.wcdmaPsc >= 0 &&
                   aOptions.wcdmaPsc <= 511) ?
                  aOptions.wcdmaPsc : UNKNOWN_VALUE;
  this.signalStrength = (aOptions.signalStrength !== undefined &&
                         ((aOptions.signalStrength >= 0 &&
                           aOptions.signalStrength <= 31) ||
                          (aOptions.signalStrength >= -120 &&
                           aOptions.signalStrength <= -25))) ?
                        aOptions.signalStrength : UNKNOWN_VALUE;
}
NeighboringCellInfo.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsINeighboringCellInfo]),
  classID:        NEIGHBORINGCELLINFO_CID,
  classInfo:      XPCOMUtils.generateCI({
    classID:          NEIGHBORINGCELLINFO_CID,
    classDescription: "NeighboringCellInfo",
    interfaces:       [Ci.nsINeighboringCellInfo]
  }),

  isValid: function() {
    return !(this.gsmLocationAreaCode == UNKNOWN_VALUE &&
             this.gsmCellId == UNKNOWN_VALUE &&
             this.wcdmaPsc == UNKNOWN_VALUE &&
             this.signalStrength == UNKNOWN_VALUE);
  },

  // nsINeighboringCellInfo

  networkType: null,
  gsmLocationAreaCode: UNKNOWN_VALUE,
  gsmCellId: UNKNOWN_VALUE,
  wcdmaPsc: UNKNOWN_VALUE,
  signalStrength: UNKNOWN_VALUE
};

function CellInfo(aOptions) {
  this.type = aOptions.type;
  this.registered = aOptions.registered;
  this.timestampType = aOptions.timestampType;
  this.timestamp = aOptions.timestamp;
}
CellInfo.prototype = {

  // nsICellInfo

  type: null,
  registered: false,
  timestampType: Ci.nsICellInfo.TIMESTAMP_TYPE_UNKNOWN,
  timestamp: 0
};

function GsmCellInfo(aOptions) {
  CellInfo.call(this, aOptions);

  // Cell Identity
  this.mcc = (aOptions.mcc !== undefined && aOptions.mcc >= 0 &&
              aOptions.mcc <= 999) ? aOptions.mcc : UNKNOWN_VALUE;
  this.mnc = (aOptions.mnc !== undefined && aOptions.mnc >= 0 &&
              aOptions.mnc <= 999) ? aOptions.mnc : UNKNOWN_VALUE;
  this.lac = (aOptions.lac !== undefined && aOptions.lac >= 0 &&
              aOptions.lac <= 65535) ? aOptions.lac : UNKNOWN_VALUE;
  this.cid = (aOptions.cid !== undefined && aOptions.cid >= 0 &&
              aOptions.cid <= 65535) ? aOptions.cid : UNKNOWN_VALUE;

  // Signal Strength
  this.signalStrength = (aOptions.signalStrength !== undefined &&
                         aOptions.signalStrength >= 0 &&
                         aOptions.signalStrength <= 31) ?
                        aOptions.signalStrength : UNKNOWN_VALUE;
  this.bitErrorRate = (aOptions.bitErrorRate !== undefined &&
                       aOptions.bitErrorRate >= 0 && aOptions.bitErrorRate <= 7)
                      ? aOptions.bitErrorRate : UNKNOWN_VALUE;
}
GsmCellInfo.prototype = {
  __proto__: CellInfo.prototype,
  QueryInterface: XPCOMUtils.generateQI([Ci.nsICellInfo,
                                         Ci.nsIGsmCellInfo]),
  classID: GSMCELLINFO_CID,
  classInfo: XPCOMUtils.generateCI({
    classID:          GSMCELLINFO_CID,
    classDescription: "GsmCellInfo",
    interfaces:       [Ci.nsIGsmCellInfo]
  }),

  isValid: function() {
    return !(this.mcc == UNKNOWN_VALUE && this.mnc == UNKNOWN_VALUE &&
             this.lac == UNKNOWN_VALUE && this.cid == UNKNOWN_VALUE &&
             this.signalStrength == UNKNOWN_VALUE &&
             this.bitErrorRate == UNKNOWN_VALUE);
  },

  // nsIGsmCellInfo

  mcc: UNKNOWN_VALUE,
  mnc: UNKNOWN_VALUE,
  lac: UNKNOWN_VALUE,
  cid: UNKNOWN_VALUE,
  signalStrength: UNKNOWN_VALUE,
  bitErrorRate: UNKNOWN_VALUE
};

function WcdmaCellInfo(aOptions) {
  CellInfo.call(this, aOptions);

  // Cell Identity
  this.mcc = (aOptions.mcc !== undefined && aOptions.mcc >= 0 &&
              aOptions.mcc <= 999) ? aOptions.mcc : UNKNOWN_VALUE;
  this.mnc = (aOptions.mnc !== undefined && aOptions.mnc >= 0 &&
              aOptions.mnc <= 999) ? aOptions.mnc : UNKNOWN_VALUE;
  this.lac = (aOptions.lac !== undefined && aOptions.lac >= 0 &&
              aOptions.lac <= 65535) ? aOptions.lac : UNKNOWN_VALUE;
  this.cid = (aOptions.cid !== undefined && aOptions.cid >= 0 &&
              aOptions.cid <= 268435455) ? aOptions.cid : UNKNOWN_VALUE;
  this.psc = (aOptions.psc !== undefined && aOptions.psc >= 0 &&
              aOptions.psc <= 511) ? aOptions.psc : UNKNOWN_VALUE;

  // Signal Strength
  this.signalStrength = (aOptions.signalStrength !== undefined &&
                         aOptions.signalStrength >= 0 &&
                         aOptions.signalStrength <= 31) ?
                        aOptions.signalStrength : UNKNOWN_VALUE;
  this.bitErrorRate = (aOptions.bitErrorRate !== undefined &&
                       aOptions.bitErrorRate >= 0 && aOptions.bitErrorRate <= 7)
                      ? aOptions.bitErrorRate : UNKNOWN_VALUE;
}
WcdmaCellInfo.prototype = {
  __proto__: CellInfo.prototype,
  QueryInterface: XPCOMUtils.generateQI([Ci.nsICellInfo,
                                         Ci.nsIWcdmaCellInfo]),
  classID: WCDMACELLINFO_CID,
  classInfo: XPCOMUtils.generateCI({
    classID:          WCDMACELLINFO_CID,
    classDescription: "WcdmaCellInfo",
    interfaces:       [Ci.nsIWcdmaCellInfo]
  }),

  isValid: function() {
    return !(this.mcc == UNKNOWN_VALUE && this.mnc == UNKNOWN_VALUE &&
             this.lac == UNKNOWN_VALUE && this.cid == UNKNOWN_VALUE &&
             this.psc == UNKNOWN_VALUE && this.signalStrength == UNKNOWN_VALUE &&
             this.bitErrorRate == UNKNOWN_VALUE);
  },

  // nsIWcdmaCellInfo

  mcc: UNKNOWN_VALUE,
  mnc: UNKNOWN_VALUE,
  lac: UNKNOWN_VALUE,
  cid: UNKNOWN_VALUE,
  psc: UNKNOWN_VALUE,
  signalStrength: UNKNOWN_VALUE,
  bitErrorRate: UNKNOWN_VALUE
};

function LteCellInfo(aOptions) {
  CellInfo.call(this, aOptions);

  // Cell Identity
  this.mcc = (aOptions.mcc !== undefined && aOptions.mcc >= 0 &&
              aOptions.mcc <= 999) ? aOptions.mcc : UNKNOWN_VALUE;
  this.mnc = (aOptions.mnc !== undefined && aOptions.mnc >= 0 &&
              aOptions.mnc <= 999) ? aOptions.mnc : UNKNOWN_VALUE;
  this.cid = (aOptions.cid !== undefined && aOptions.cid >= 0 &&
              aOptions.cid <= 268435455) ? aOptions.cid : UNKNOWN_VALUE;
  this.pcid = (aOptions.pcid !== undefined && aOptions.pcid >= 0 &&
               aOptions.pcid <= 503) ? aOptions.pcid : UNKNOWN_VALUE;
  this.tac = (aOptions.tac !== undefined && aOptions.tac >= 0 &&
              aOptions.tac <= 65535) ? aOptions.tac : UNKNOWN_VALUE;

  // Signal Strength
  this.signalStrength = (aOptions.signalStrength !== undefined &&
                         aOptions.signalStrength >= 0 &&
                         aOptions.signalStrength <= 31) ?
                        aOptions.signalStrength : UNKNOWN_VALUE;
  this.rsrp = (aOptions.rsrp !== undefined && aOptions.rsrp >= 44 &&
               aOptions.rsrp <= 140) ? aOptions.rsrp : UNKNOWN_VALUE;
  this.rsrq = (aOptions.rsrq !== undefined && aOptions.rsrq >= 3 &&
               aOptions.rsrq <= 20) ? aOptions.rsrq : UNKNOWN_VALUE;
  this.rssnr = (aOptions.rssnr !== undefined && aOptions.rssnr >= -200 &&
                aOptions.rssnr <= 300) ? aOptions.rssnr : UNKNOWN_VALUE;
  this.cqi = (aOptions.cqi !== undefined && aOptions.cqi >= 0 &&
                aOptions.cqi <= 15) ? aOptions.cqi : UNKNOWN_VALUE;
  this.timingAdvance = (aOptions.timingAdvance !== undefined &&
                        aOptions.timingAdvance >= 0 &&
                        aOptions.timingAdvance <= 2147483646) ?
                       aOptions.timingAdvance : UNKNOWN_VALUE;
}
LteCellInfo.prototype = {
  __proto__: CellInfo.prototype,
  QueryInterface: XPCOMUtils.generateQI([Ci.nsICellInfo,
                                         Ci.nsILteCellInfo]),
  classID: LTECELLINFO_CID,
  classInfo: XPCOMUtils.generateCI({
    classID:          LTECELLINFO_CID,
    classDescription: "LteCellInfo",
    interfaces:       [Ci.nsILteCellInfo]
  }),

  isValid: function() {
    return !(this.mcc == UNKNOWN_VALUE && this.mnc == UNKNOWN_VALUE &&
             this.cid == UNKNOWN_VALUE && this.pcid == UNKNOWN_VALUE &&
             this.tac == UNKNOWN_VALUE && this.signalStrength == UNKNOWN_VALUE &&
             this.rsrp == UNKNOWN_VALUE && this.rsrq == UNKNOWN_VALUE &&
             this.rssnr == UNKNOWN_VALUE && this.cqi == UNKNOWN_VALUE &&
             this.timingAdvance == UNKNOWN_VALUE);
  },

  // nsILteCellInfo

  mcc: UNKNOWN_VALUE,
  mnc: UNKNOWN_VALUE,
  cid: UNKNOWN_VALUE,
  pcid: UNKNOWN_VALUE,
  tac: UNKNOWN_VALUE,
  signalStrength: UNKNOWN_VALUE,
  rsrp: UNKNOWN_VALUE,
  rsrq: UNKNOWN_VALUE,
  rssnr: UNKNOWN_VALUE,
  cqi: UNKNOWN_VALUE,
  timingAdvance: UNKNOWN_VALUE
};

function CdmaCellInfo(aOptions) {
  CellInfo.call(this, aOptions);

  // Cell Identity
  this.networkId = (aOptions.networkId !== undefined &&
                    aOptions.networkId >= 0 && aOptions.networkId <= 65535) ?
                   aOptions.networkId : UNKNOWN_VALUE;
  this.systemId = (aOptions.systemId !== undefined && aOptions.systemId >= 0 &&
                   aOptions.systemId <= 32767) ?
                  aOptions.systemId : UNKNOWN_VALUE;
  this.baseStationId = (aOptions.baseStationId !== undefined &&
                        aOptions.baseStationId >= 0 &&
                        aOptions.baseStationId <= 65535) ?
                       aOptions.baseStationId : UNKNOWN_VALUE;
  this.longitude = (aOptions.longitude !== undefined &&
                    aOptions.longitude >= -2592000 &&
                    aOptions.longitude <= 2592000) ?
                   aOptions.longitude : UNKNOWN_VALUE;
  this.latitude = (aOptions.latitude !== undefined &&
                   aOptions.latitude >= -1296000 &&
                   aOptions.latitude <= 1296000) ?
                  aOptions.latitude : UNKNOWN_VALUE;

  // Signal Strength
  this.cdmaEcio = (aOptions.cdmaEcio !== undefined &&
                   aOptions.cdmaEcio >= 0) ? aOptions.cdmaEcio : UNKNOWN_VALUE;
  this.evdoDbm = (aOptions.evdoDbm !== undefined &&
                  aOptions.evdoDbm >= 0) ? aOptions.evdoDbm : UNKNOWN_VALUE;
  this.evdoEcio = (aOptions.evdoEcio !== undefined &&
                   aOptions.evdoEcio >= 0) ? aOptions.evdoEcio : UNKNOWN_VALUE;
  this.evdoSnr = (aOptions.evdoSnr !== undefined &&
                  aOptions.evdoSnr >= 0 && aOptions.evdoSnr <= 8) ?
                 aOptions.evdoSnr : UNKNOWN_VALUE;
}
CdmaCellInfo.prototype = {
  __proto__: CellInfo.prototype,
  QueryInterface: XPCOMUtils.generateQI([Ci.nsICellInfo,
                                         Ci.nsICdmaCellInfo]),
  classID: CDMACELLINFO_CID,
  classInfo: XPCOMUtils.generateCI({
    classID:          CDMACELLINFO_CID,
    classDescription: "CdmaCellInfo",
    interfaces:       [Ci.nsICdmaCellInfo]
  }),

  isValid: function() {
    return !(this.networkId == UNKNOWN_VALUE && this.systemId == UNKNOWN_VALUE &&
             this.baseStationId == UNKNOWN_VALUE &&
             this.longitude == UNKNOWN_VALUE &&
             this.latitude == UNKNOWN_VALUE && this.cdmaDbm == UNKNOWN_VALUE &&
             this.cdmaEcio == UNKNOWN_VALUE && this.evdoDbm == UNKNOWN_VALUE &&
             this.evdoEcio == UNKNOWN_VALUE && this.evdoSnr == UNKNOWN_VALUE);
  },

  // nsICdmaCellInfo

  networkId: UNKNOWN_VALUE,
  systemId: UNKNOWN_VALUE,
  baseStationId: UNKNOWN_VALUE,
  longitude: UNKNOWN_VALUE,
  latitude: UNKNOWN_VALUE,
  cdmaDbm: UNKNOWN_VALUE,
  cdmaEcio: UNKNOWN_VALUE,
  evdoDbm: UNKNOWN_VALUE,
  evdoEcio: UNKNOWN_VALUE,
  evdoSnr: UNKNOWN_VALUE
};

function MobileDeviceIdentities(aImei, aImeisv, aEsn, aMeid) {
  this.imei = aImei;
  this.imeisv = aImeisv;
  this.esn = aEsn;
  this.meid = aMeid;
}
MobileDeviceIdentities.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileDeviceIdentities]),

  // nsIMobileDeviceIdentities
  imei: null,
  imeisv: null,
  esn: null,
  meid: null
};

function MobileConnectionProvider(aClientId, aRadioInterface) {
  this._clientId = aClientId;
  this._radioInterface = aRadioInterface;
  this._operatorInfo = new MobileNetworkInfo();
  // An array of nsIMobileConnectionListener instances.
  this._listeners = [];

  this.supportedNetworkTypes = this._getSupportedNetworkTypes();
  this.voice = new MobileConnectionInfo();
  this.data = new MobileConnectionInfo();
}
MobileConnectionProvider.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnection]),

  _clientId: null,
  _radioInterface: null,
  _operatorInfo: null,
  _listeners: null,

  /**
   * The networks that are currently trying to be selected (or "automatic").
   * This helps ensure that only one network per client is selected at a time.
   */
  _selectingNetwork: null,

  /**
   * The two radio states below stand for the user expectation and the hardware
   * status, respectively. |radioState| will be updated based on their values.
   */
  _expectedRadioState: RIL.GECKO_RADIOSTATE_UNKNOWN,
  _hardwareRadioState: RIL.GECKO_RADIOSTATE_UNKNOWN,

  voice: null,
  data: null,
  networkSelectionMode: Ci.nsIMobileConnection.NETWORK_SELECTION_MODE_UNKNOWN,
  radioState: Ci.nsIMobileConnection.MOBILE_RADIO_STATE_UNKNOWN,
  lastKnownNetwork: null,
  lastKnownHomeNetwork: null,
  supportedNetworkTypes: null,
  deviceIdentities: null,

  /**
   * A utility function to dump debug message.
   */
  _debug: function(aMessage) {
    dump("MobileConnectionProvider[" + this._clientId + "]: " + aMessage + "\n");
  },

  /**
   * A utility function to get supportedNetworkTypes from system property.
   */
  _getSupportedNetworkTypes: function() {
    let key = "ro.moz.ril." + this._clientId + ".network_types";
    let supportedNetworkTypes = libcutils.property_get(key, "").split(",");

    // If mozRIL system property is not available, fallback to AOSP system
    // property for support network types.
    if (supportedNetworkTypes.length === 1 && supportedNetworkTypes[0] === "") {
      key = "ro.telephony.default_network";
      let indexString = libcutils.property_get(key, "");
      let index = parseInt(indexString, 10);
      if (DEBUG) this._debug("Fallback to " + key + ": " + index);

      let networkTypes = RIL.RIL_PREFERRED_NETWORK_TYPE_TO_GECKO[index];
      supportedNetworkTypes = networkTypes ?
        networkTypes.replace(/-auto/g, "").split("/") :
        RIL.GECKO_SUPPORTED_NETWORK_TYPES_DEFAULT.split(",");
    }

    let enumNetworkTypes = [];
    for (let type of supportedNetworkTypes) {
      // If the value in system property is not valid, use the default one which
      // is defined in ril_consts.js.
      if (RIL.GECKO_SUPPORTED_NETWORK_TYPES.indexOf(type) < 0) {
        if (DEBUG) {
          this._debug("Unknown network type: " + type);
        }
        RIL.GECKO_SUPPORTED_NETWORK_TYPES_DEFAULT.split(",").forEach(aType => {
          enumNetworkTypes.push(RIL.GECKO_SUPPORTED_NETWORK_TYPES.indexOf(aType));
        });
        break;
      }
      enumNetworkTypes.push(RIL.GECKO_SUPPORTED_NETWORK_TYPES.indexOf(type));
    }
    if (DEBUG) {
      this._debug("Supported Network Types: " + enumNetworkTypes);
    }

    return enumNetworkTypes;
  },

  /**
   * Helper for guarding us against invalid mode for clir.
   */
  _isValidClirMode: function(aMode) {
    switch (aMode) {
      case Ci.nsIMobileConnection.CLIR_DEFAULT:
      case Ci.nsIMobileConnection.CLIR_INVOCATION:
      case Ci.nsIMobileConnection.CLIR_SUPPRESSION:
        return true;
      default:
        return false;
    }
  },

  /**
   * Fix the roaming. RIL can report roaming in some case it is not
   * really the case. See bug 787967
   */
  _checkRoamingBetweenOperators: function(aNetworkInfo) {
    let icc = gIccService.getIccByServiceId(this._clientId);
    let iccInfo = icc ? icc.iccInfo : null;
    let operator = aNetworkInfo.network;
    let state = aNetworkInfo.state;

    if (!iccInfo || !operator ||
        state !== RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED) {
      return false;
    }

    let spn = iccInfo.spn && iccInfo.spn.toLowerCase();
    let longName = operator.longName && operator.longName.toLowerCase();
    let shortName = operator.shortName && operator.shortName.toLowerCase();

    let equalsLongName = longName && (spn == longName);
    let equalsShortName = shortName && (spn == shortName);
    let equalsMcc = iccInfo.mcc == operator.mcc;

    let newRoaming = aNetworkInfo.roaming &&
                     !(equalsMcc && (equalsLongName || equalsShortName));
    if (newRoaming === aNetworkInfo.roaming) {
      return false;
    }

    aNetworkInfo.roaming = newRoaming;
    return true;
  },

  /**
   * The design of this updating function is to update the attribute in
   * |aDestInfo| *only if* new data (e.g. aSrcInfo) contains the same attribute.
   * Thus, for the attribute in |aDestInfo| that isn't showed in |aSrcInfo|, it
   * should just keep the original value unchanged.
   */
  _updateConnectionInfo: function(aDestInfo, aSrcInfo) {
    let isUpdated = false;
    for (let key in aSrcInfo) {
      if (key === "network" || key === "cell") {
        // nsIMobileNetworkInfo and nsIMobileCellInfo are handled explicitly below.
        continue;
      }

      if (aDestInfo[key] !== aSrcInfo[key]) {
        isUpdated = true;
        aDestInfo[key] = aSrcInfo[key];
      }
    }

    // Make sure we also reset the operator and signal strength information
    // if we drop off the network.
    if (aDestInfo.state !== RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED) {
      aDestInfo.cell = null;
      aDestInfo.network = null;
      aDestInfo.signalStrength = null;
      aDestInfo.relSignalStrength = null;
    } else {
      aDestInfo.network = this._operatorInfo;

      // If no new cell data is passed, we should just keep the original cell
      // data unchanged.
      if (aSrcInfo.cell) {
        if (!aDestInfo.cell) {
          aDestInfo.cell = new MobileCellInfo();
        }

        isUpdated = this._updateInfo(aDestInfo.cell, aSrcInfo.cell) || isUpdated;
      }
    }

    // Check roaming state
    isUpdated = this._checkRoamingBetweenOperators(aDestInfo) || isUpdated;
    return isUpdated;
  },

  /**
   * The design of this updating function is to update the attribute in
   * |aDestInfo| *only if* new data (e.g. aSrcInfo) contains the same attribute.
   * Thus, for the attribute in |aDestInfo| that isn't showed in |aSrcInfo|, it
   * should just keep the original value unchanged.
   */
  _updateInfo: function(aDestInfo, aSrcInfo) {
    let isUpdated = false;
    for (let key in aSrcInfo) {
      if (aDestInfo[key] !== aSrcInfo[key]) {
        isUpdated = true;
        aDestInfo[key] = aSrcInfo[key];
      }
    }
    return isUpdated;
  },

  _rulesToCallForwardingOptions: function(aRules) {
    return aRules.map(rule => new MobileCallForwardingOptions(rule));
  },

  _dispatchNotifyError: function(aCallback, aErrorMsg) {
    Services.tm.currentThread.dispatch(() => aCallback.notifyError(aErrorMsg),
                                       Ci.nsIThread.DISPATCH_NORMAL);
  },

  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) {
      this._listeners.splice(index, 1);
    }
  },

  deliverListenerEvent: function(aName, aArgs) {
    let listeners = this._listeners.slice();
    for (let listener of listeners) {
      if (this._listeners.indexOf(listener) === -1) {
        continue;
      }
      let handler = listener[aName];
      if (typeof handler != "function") {
        throw new Error("No handler for " + aName);
      }
      try {
        handler.apply(listener, aArgs);
      } catch (e) {
        if (DEBUG) {
          this._debug("listener for " + aName + " threw an exception: " + e);
        }
      }
    }
  },

  updateVoiceInfo: function(aNewInfo, aBatch = false) {
    let isUpdated = this._updateConnectionInfo(this.voice, aNewInfo);
    if (isUpdated && !aBatch) {
      this.deliverListenerEvent("notifyVoiceChanged");
    }
  },

  updateDataInfo: function(aNewInfo, aBatch = false) {
    // For the data connection, the `connected` flag indicates whether
    // there's an active data call. We get correct `connected` state here.
    let active = gNetworkManager.activeNetworkInfo;
    aNewInfo.connected = false;
    if (active &&
        active.type === Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE &&
        active.serviceId === this._clientId) {
      aNewInfo.connected = true;
    }

    let isUpdated = this._updateConnectionInfo(this.data, aNewInfo);
    if (isUpdated && !aBatch) {
      this.deliverListenerEvent("notifyDataChanged");
    }

    if (isUpdated) {
      this._ensureDataRegistration();
    }
  },

  _dataRegistrationFailed: false,
  _ensureDataRegistration: function() {
    let isDataRegistered =
      this.data &&
      this.data.state == RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED;
    let isVoiceRegistered =
      this.voice &&
      this.voice.state == RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED;

    if (isVoiceRegistered && isDataRegistered) {
      if (DEBUG) {
        this._debug("Voice and data registered.");
      }
      this._dataRegistrationFailed = false;
      return;
    }

    if (isVoiceRegistered && !isDataRegistered &&
        this._clientId == gDataCallManager.dataDefaultServiceId) {

      // We have been here before, no more recovery.
      if (this._dataRegistrationFailed) {
        if (DEBUG) {
          this._debug("Voice and data not consistent: " + this.voice.state +
                      " != " + this.data.state + ".");
        }
        return;
      }

      if (DEBUG) {
        this._debug("Voice and data not consistent: " + this.voice.state +
                    " != " + this.data.state + ", try to recover.");
      }

      this._dataRegistrationFailed = true;
      // If there is any ongoing call, wait for them to disconnect.
      if (gTelephonyUtils.hasAnyCalls(this._clientId)) {
        gTelephonyUtils.waitForNoCalls(this._clientId)
          .then(() => {
            if (this._dataRegistrationFailed) {
              this._recoverDataRegistration();
            }
          });
        return;
      }

      this._recoverDataRegistration();
    }
  },


  /**
   * To recover data registration, get the current preferred network type first,
   * then set it to a temporary preferred network type, and last set back to the
   * previous preferred network type. This is will cause deregistration and
   * registration on both voice and data networks.
   */
  _recoverDataRegistration: function() {
    if (DEBUG) {
      this._debug("Trying to recover data registration...");
    }

    let currentPreferredNetworkType;

    let resetPreferredNetworkType = () => {
      this.setPreferredNetworkType(currentPreferredNetworkType, {
        QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
        notifySuccess: () => {},
        notifyError: aErrorMsg => {}
      });
    };

    let setTemporaryPreferredNetworkType = () => {
      this.setPreferredNetworkType(
        Ci.nsIMobileConnection.PREFERRED_NETWORK_TYPE_WCDMA_GSM_CDMA_EVDO, {
          QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
          notifySuccess: () =>  resetPreferredNetworkType(),
          notifyError: aErrorMsg => resetPreferredNetworkType()
      });
    };

    this.getPreferredNetworkType({
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
      notifyGetPreferredNetworkTypeSuccess: networkType => {
        currentPreferredNetworkType = networkType;
        setTemporaryPreferredNetworkType();
      },
      notifyError: aErrorMsg => {
        currentPreferredNetworkType =
          Ci.nsIMobileConnection.PREFERRED_NETWORK_TYPE_LTE_WCDMA_GSM_CDMA_EVDO;
        setTemporaryPreferredNetworkType();
      }
    });
  },

  updateOperatorInfo: function(aNewInfo, aBatch = false) {
    let isUpdated = this._updateInfo(this._operatorInfo, aNewInfo);

    // Update lastKnownNetwork
    if (this._operatorInfo.mcc && this._operatorInfo.mnc) {
      let network = this._operatorInfo.mcc + "-" + this._operatorInfo.mnc;
      if (this.lastKnownNetwork !== network) {
        if (DEBUG) {
          this._debug("lastKnownNetwork now is " + network);
        }

        this.lastKnownNetwork = network;
        this.deliverListenerEvent("notifyLastKnownNetworkChanged");
      }
    }

    // If the voice is unregistered, no need to send notification.
    if (this.voice.state !== RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED &&
        isUpdated && !aBatch) {
      this.deliverListenerEvent("notifyVoiceChanged");
    }

    // If the data is unregistered, no need to send notification.
    if (this.data.state !== RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED &&
        isUpdated && !aBatch) {
      this.deliverListenerEvent("notifyDataChanged");
    }
  },

  updateSignalInfo: function(aNewInfo, aBatch = false) {
    // If the voice is not registered, no need to update signal information.
    if (this.voice.state === RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED) {
      if (this._updateInfo(this.voice, aNewInfo.voice) && !aBatch) {
        this.deliverListenerEvent("notifyVoiceChanged");
      }
    }

    // If the data is not registered, no need to update signal information.
    if (this.data.state === RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED) {
      if (this._updateInfo(this.data, aNewInfo.data) && !aBatch) {
        this.deliverListenerEvent("notifyDataChanged");
      }
    }
  },

  updateRadioState: function(aMessage, aCallback = null) {
    switch (aMessage.msgType) {
      case "ExpectedRadioState":
        this._expectedRadioState = aMessage.msgData;
        break;
      case "HardwareRadioState":
        this._hardwareRadioState = aMessage.msgData;
        break;
      default:
        if (DEBUG) this._debug("updateRadioState: Invalid message type");
        return;
    }

    if (aMessage.msgType === "ExpectedRadioState" && aCallback &&
        this._hardwareRadioState === this._expectedRadioState) {
      // Early resolved
      aCallback.notifySuccess();
      return;
    }

    let newState;
    switch (this._expectedRadioState) {
      case RIL.GECKO_RADIOSTATE_ENABLED:
        newState = this._hardwareRadioState === this._expectedRadioState ?
          Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED :
          Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLING;
        break;

      case RIL.GECKO_RADIOSTATE_DISABLED:
        newState = this._hardwareRadioState === this._expectedRadioState ?
          Ci.nsIMobileConnection.MOBILE_RADIO_STATE_DISABLED :
          Ci.nsIMobileConnection.MOBILE_RADIO_STATE_DISABLING;
        break;

      default: /* RIL.GECKO_RADIOSTATE_UNKNOWN */
        switch (this._hardwareRadioState) {
          case RIL.GECKO_RADIOSTATE_ENABLED:
            newState = Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED;
            break;
          case RIL.GECKO_RADIOSTATE_DISABLED:
            newState = Ci.nsIMobileConnection.MOBILE_RADIO_STATE_DISABLED;
            break;
          default: /* RIL.GECKO_RADIOSTATE_UNKNOWN */
            newState = Ci.nsIMobileConnection.MOBILE_RADIO_STATE_UNKNOWN;
        }
    }

    // This update is triggered by underlying layers and the state is UNKNOWN
    if (aMessage.msgType === "HardwareRadioState" &&
        aMessage.msgData === RIL.GECKO_RADIOSTATE_UNKNOWN) {
      // TODO: Find a better way than just setting the radio state to UNKNOWN
      newState = Ci.nsIMobileConnection.MOBILE_RADIO_STATE_UNKNOWN;
    }

    if (newState === Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLING ||
        newState === Ci.nsIMobileConnection.MOBILE_RADIO_STATE_DISABLING) {
      let action = this._expectedRadioState === RIL.GECKO_RADIOSTATE_ENABLED;
      this._radioInterface.sendWorkerMessage("setRadioEnabled",
                                             {enabled: action},
                                             function(aResponse) {
        if (!aCallback) {
          return false;
        }
        if (aResponse.errorMsg) {
          aCallback.notifyError(aResponse.errorMsg);
          return false;
        }
        aCallback.notifySuccess();
        return false;
      });
    }

    if (DEBUG) this._debug("Current Radio State is '" + newState + "'");
    if (this.radioState === newState) {
      return;
    }
    this.radioState = newState;
    this.deliverListenerEvent("notifyRadioStateChanged");
  },

  notifyCFStateChanged: function(aAction, aReason, aNumber, aTimeSeconds,
                                 aServiceClass) {
    this.deliverListenerEvent("notifyCFStateChanged",
                              [aAction, aReason, aNumber, aTimeSeconds,
                               aServiceClass]);
  },

  notifyDeviceIdentitiesChanged: function(aImei, aImeisv, aEsn, aMeid) {
    if (this.deviceIdentities) {
      if (DEBUG) this._debug("deviceIdentities shall not be changed once being updated.");
      return;
    }

    this.deviceIdentities =
      new MobileDeviceIdentities(aImei, aImeisv, aEsn, aMeid);
    this.deliverListenerEvent("notifyDeviceIdentitiesChanged");
  },

  getSupportedNetworkTypes: function(aTypes) {
    aTypes.value = this.supportedNetworkTypes.slice();
    return aTypes.value.length;
  },

  getNetworks: function(aCallback) {
    this._radioInterface.sendWorkerMessage("getAvailableNetworks", null,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      let networks = aResponse.networks;
      for (let i = 0; i < networks.length; i++) {
        let info = new MobileNetworkInfo();
        this._updateInfo(info, networks[i]);
        networks[i] = info;
      }

      aCallback.notifyGetNetworksSuccess(networks.length, networks);
      return false;
    }).bind(this));
  },

  selectNetwork: function(aNetwork, aCallback) {
    if (!aNetwork ||
        isNaN(parseInt(aNetwork.mcc, 10)) ||
        isNaN(parseInt(aNetwork.mnc, 10))) {
      this._dispatchNotifyError(aCallback, RIL.GECKO_ERROR_INVALID_PARAMETER);
      return;
    }

    if (this._selectingNetwork) {
      this._dispatchNotifyError(aCallback, "AlreadySelectingANetwork");
      return;
    }

    let options = {mcc: aNetwork.mcc, mnc: aNetwork.mnc};
    this._selectingNetwork = options;
    this._radioInterface.sendWorkerMessage("selectNetwork", options,
                                           (function(aResponse) {
      this._selectingNetwork = null;
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  selectNetworkAutomatically: function(aCallback) {
    if (this._selectingNetwork) {
      this._dispatchNotifyError(aCallback, "AlreadySelectingANetwork");
      return;
    }

    this._selectingNetwork = "automatic";
    this._radioInterface.sendWorkerMessage("selectNetworkAuto", null,
                                           (function(aResponse) {
      this._selectingNetwork = null;
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  setPreferredNetworkType: function(aType, aCallback) {
    if (this.radioState !== Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED) {
      this._dispatchNotifyError(aCallback, RIL.GECKO_ERROR_RADIO_NOT_AVAILABLE);
      return;
    }

    this._radioInterface.sendWorkerMessage("setPreferredNetworkType",
                                           {type: aType},
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  getPreferredNetworkType: function(aCallback) {
    if (this.radioState !== Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED) {
      this._dispatchNotifyError(aCallback, RIL.GECKO_ERROR_RADIO_NOT_AVAILABLE);
      return;
    }

    this._radioInterface.sendWorkerMessage("getPreferredNetworkType", null,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifyGetPreferredNetworkTypeSuccess(aResponse.type);
      return false;
    }).bind(this));
  },

  setRoamingPreference: function(aMode, aCallback) {
    this._radioInterface.sendWorkerMessage("setRoamingPreference",
                                           {mode: aMode},
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  getRoamingPreference: function(aCallback) {
    this._radioInterface.sendWorkerMessage("queryRoamingPreference", null,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifyGetRoamingPreferenceSuccess(aResponse.mode);
      return false;
    }).bind(this));
  },

  setVoicePrivacyMode: function(aEnabled, aCallback) {
    this._radioInterface.sendWorkerMessage("setVoicePrivacyMode",
                                           {enabled: aEnabled},
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  getVoicePrivacyMode: function(aCallback) {
    this._radioInterface.sendWorkerMessage("queryVoicePrivacyMode", null,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccessWithBoolean(aResponse.enabled);
      return false;
    }).bind(this));
  },

  setCallForwarding: function(aAction, aReason, aNumber, aTimeSeconds,
                              aServiceClass, aCallback) {
    let options = {
      action: aAction,
      reason: aReason,
      number: aNumber,
      timeSeconds: aTimeSeconds,
      serviceClass: RIL.ICC_SERVICE_CLASS_VOICE
    };

    this._radioInterface.sendWorkerMessage("setCallForward", options,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      this.notifyCFStateChanged(aResponse.action, aResponse.reason,
                                aResponse.number, aResponse.timeSeconds,
                                aResponse.serviceClass);
      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  getCallForwarding: function(aReason, aCallback) {
    this._radioInterface.sendWorkerMessage("queryCallForwardStatus",
                                           {reason: aReason},
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      let infos = this._rulesToCallForwardingOptions(aResponse.rules);
      aCallback.notifyGetCallForwardingSuccess(infos.length, infos);
      return false;
    }).bind(this));
  },

  setCallBarring: function(aProgram, aEnabled, aPassword, aServiceClass,
                           aCallback) {
    let options = {
      program: aProgram,
      enabled: aEnabled,
      password: aPassword,
      serviceClass: aServiceClass
    };

    this._radioInterface.sendWorkerMessage("setCallBarring", options,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  getCallBarring: function(aProgram, aPassword, aServiceClass, aCallback) {
    let options = {
      program: aProgram,
      password: aPassword,
      serviceClass: aServiceClass
    };

    this._radioInterface.sendWorkerMessage("queryCallBarringStatus", options,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifyGetCallBarringSuccess(aResponse.program,
                                            aResponse.enabled,
                                            aResponse.serviceClass);
      return false;
    }).bind(this));
  },

  changeCallBarringPassword: function(aPin, aNewPin, aCallback) {
    let options = {
      pin: aPin,
      newPin: aNewPin
    };

    this._radioInterface.sendWorkerMessage("changeCallBarringPassword", options,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  setCallWaiting: function(aEnabled, aServiceClass, aCallback) {
    let options = {
      enabled: aEnabled,
      serviceClass: aServiceClass
    };

    this._radioInterface.sendWorkerMessage("setCallWaiting", options,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  getCallWaiting: function(aCallback) {
    this._radioInterface.sendWorkerMessage("queryCallWaiting", null,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifyGetCallWaitingSuccess(aResponse.serviceClass);
      return false;
    }).bind(this));
  },

  setCallingLineIdRestriction: function(aMode, aCallback) {
    if (!this._isValidClirMode(aMode)) {
      this._dispatchNotifyError(aCallback, RIL.GECKO_ERROR_INVALID_PARAMETER);
      return;
    }

    if (this.radioState !== Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED) {
      this._dispatchNotifyError(aCallback, RIL.GECKO_ERROR_RADIO_NOT_AVAILABLE);
      return;
    }

    this._radioInterface.sendWorkerMessage("setCLIR", {clirMode: aMode},
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      this.deliverListenerEvent("notifyClirModeChanged", [aResponse.mode]);
      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  getCallingLineIdRestriction: function(aCallback) {
    if (this.radioState !== Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED) {
      this._dispatchNotifyError(aCallback, RIL.GECKO_ERROR_RADIO_NOT_AVAILABLE);
      return;
    }

    this._radioInterface.sendWorkerMessage("getCLIR", null,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifyGetClirStatusSuccess(aResponse.n, aResponse.m);
      return false;
    }).bind(this));
  },

  exitEmergencyCbMode: function(aCallback) {
    this._radioInterface.sendWorkerMessage("exitEmergencyCbMode", null,
                                           (function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyError(aResponse.errorMsg);
        return false;
      }

      aCallback.notifySuccess();
      return false;
    }).bind(this));
  },

  setRadioEnabled: function(aEnabled, aCallback) {
    if (DEBUG) {
      this._debug("setRadioEnabled: " + aEnabled);
    }

    // Before sending a equest to |ril_worker.js|, we should check radioState.
    switch (this.radioState) {
      case Ci.nsIMobileConnection.MOBILE_RADIO_STATE_UNKNOWN:
      case Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED:
      case Ci.nsIMobileConnection.MOBILE_RADIO_STATE_DISABLED:
        break;
      default:
        aCallback.notifyError("InvalidStateError");
        return;
    }

    let message = {
      msgType: "ExpectedRadioState",
      msgData: (aEnabled ? RIL.GECKO_RADIOSTATE_ENABLED :
                           RIL.GECKO_RADIOSTATE_DISABLED)
    };
    this.updateRadioState(message, aCallback);
  },

  getCellInfoList: function(aCallback) {
    this._radioInterface.sendWorkerMessage("getCellInfoList",
                                           null,
                                           function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyGetCellInfoListFailed(aResponse.errorMsg);
        return;
      }

      let cellInfoList = [];
      let count = aResponse.result.length;
      for (let i = 0; i < count; i++) {
        let srcCellInfo = aResponse.result[i];
        let cellInfo;
        switch (srcCellInfo.type) {
          case RIL.CELL_INFO_TYPE_GSM:
            cellInfo = new GsmCellInfo(srcCellInfo);
            break;
          case RIL.CELL_INFO_TYPE_WCDMA:
            cellInfo = new WcdmaCellInfo(srcCellInfo);
            break;
          case RIL.CELL_INFO_TYPE_LTE:
            cellInfo = new LteCellInfo(srcCellInfo);
            break;
          case RIL.CELL_INFO_TYPE_CDMA:
            cellInfo = new CdmaCellInfo(srcCellInfo);
            break;
        }

        if (!cellInfo || !cellInfo.isValid()) {
          continue;
        }

        cellInfoList.push(cellInfo);
      }
      aCallback.notifyGetCellInfoList(cellInfoList.length, cellInfoList);
    }.bind(this));
  },

  getNeighboringCellIds: function(aCallback) {
    this._radioInterface.sendWorkerMessage("getNeighboringCellIds",
                                           null,
                                           function(aResponse) {
      if (aResponse.errorMsg) {
        aCallback.notifyGetNeighboringCellIdsFailed(aResponse.errorMsg);
        return;
      }

      let neighboringCellIds = [];
      let count = aResponse.result.length;
      for (let i = 0; i < count; i++) {
        let srcCellInfo = aResponse.result[i];
        let cellInfo = new NeighboringCellInfo(srcCellInfo);
        if (cellInfo && cellInfo.isValid()) {
          neighboringCellIds.push(cellInfo);
        }
      }
      aCallback.notifyGetNeighboringCellIds(neighboringCellIds.length,
                                            neighboringCellIds);

    }.bind(this));
  },
};

function MobileConnectionService() {
  this._providers = [];

  let numClients = gRadioInterfaceLayer.numRadioInterfaces;
  for (let i = 0; i < numClients; i++) {
    let radioInterface = gRadioInterfaceLayer.getRadioInterface(i);
    let provider = new MobileConnectionProvider(i, radioInterface);
    this._providers.push(provider);
  }

  Services.prefs.addObserver(kPrefRilDebuggingEnabled, this, false);
  Services.obs.addObserver(this, NS_NETWORK_ACTIVE_CHANGED_TOPIC_ID, false);
  Services.obs.addObserver(this, NS_DATA_CALL_ERROR_TOPIC_ID, false);
  Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);

  debug("init complete");
}
MobileConnectionService.prototype = {
  classID: GONK_MOBILECONNECTIONSERVICE_CID,
  classInfo: XPCOMUtils.generateCI({classID: GONK_MOBILECONNECTIONSERVICE_CID,
                                    contractID: GONK_MOBILECONNECTIONSERVICE_CONTRACTID,
                                    classDescription: "MobileConnectionService",
                                    interfaces: [Ci.nsIGonkMobileConnectionService,
                                                 Ci.nsIMobileConnectionService],
                                    flags: Ci.nsIClassInfo.SINGLETON}),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIGonkMobileConnectionService,
                                         Ci.nsIMobileConnectionService,
                                         Ci.nsIObserver]),

  // An array of MobileConnectionProvider instances.
  _providers: null,

  _shutdown: function() {
    Services.prefs.removeObserver(kPrefRilDebuggingEnabled, this);
    Services.obs.removeObserver(this, NS_NETWORK_ACTIVE_CHANGED_TOPIC_ID);
    Services.obs.removeObserver(this, NS_DATA_CALL_ERROR_TOPIC_ID);
    Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
  },

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

  /**
   * nsIMobileConnectionService interface.
   */
  get numItems() {
    return this._providers.length;
  },

  getItemByServiceId: function(aServiceId) {
    let provider = this._providers[aServiceId];
    if (!provider) {
      throw Cr.NS_ERROR_UNEXPECTED;
    }

    return provider;
  },

  /**
   * nsIGonkMobileConnectionService interface.
   */
  notifyVoiceInfoChanged: function(aClientId, aVoiceInfo) {
    if (DEBUG) {
      debug("notifyVoiceInfoChanged for " + aClientId + ": " +
            JSON.stringify(aVoiceInfo));
    }

    this.getItemByServiceId(aClientId).updateVoiceInfo(aVoiceInfo);
  },

  notifyDataInfoChanged: function(aClientId, aDataInfo) {
    if (DEBUG) {
      debug("notifyDataInfoChanged for " + aClientId + ": " +
            JSON.stringify(aDataInfo));
    }

    this.getItemByServiceId(aClientId).updateDataInfo(aDataInfo);
  },

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

    this.getItemByServiceId(aClientId)
        .deliverListenerEvent("notifyDataError", [aMessage]);
  },

  notifyEmergencyCallbackModeChanged: function(aClientId, aActive, aTimeoutMs) {
    if (DEBUG) {
      debug("notifyEmergencyCbModeChanged for " + aClientId + ": " +
            JSON.stringify({active: aActive, timeoutMs: aTimeoutMs}));
    }

    this.getItemByServiceId(aClientId)
        .deliverListenerEvent("notifyEmergencyCbModeChanged",
                              [aActive, aTimeoutMs]);
  },

  notifyOtaStatusChanged: function(aClientId, aStatus) {
    if (DEBUG) {
      debug("notifyOtaStatusChanged for " + aClientId + ": " + aStatus);
    }

    this.getItemByServiceId(aClientId)
        .deliverListenerEvent("notifyOtaStatusChanged", [aStatus]);
  },

  notifyRadioStateChanged: function(aClientId, aRadioState) {
    if (DEBUG) {
      debug("notifyRadioStateChanged for " + aClientId + ": " + aRadioState);
    }

    let message = {
      msgType: "HardwareRadioState",
      msgData: aRadioState
    };
    this.getItemByServiceId(aClientId).updateRadioState(message);
  },

  notifyNetworkInfoChanged: function(aClientId, aNetworkInfo) {
    if (DEBUG) {
      debug("notifyNetworkInfoChanged for " + aClientId + ": " +
            JSON.stringify(aNetworkInfo));
    }

    let provider = this.getItemByServiceId(aClientId);

    let isVoiceUpdated = false;
    let isDataUpdated = false;
    let operatorMessage = aNetworkInfo[RIL.NETWORK_INFO_OPERATOR];
    let voiceMessage = aNetworkInfo[RIL.NETWORK_INFO_VOICE_REGISTRATION_STATE];
    let dataMessage = aNetworkInfo[RIL.NETWORK_INFO_DATA_REGISTRATION_STATE];
    let signalMessage = aNetworkInfo[RIL.NETWORK_INFO_SIGNAL];
    let selectionMessage = aNetworkInfo[RIL.NETWORK_INFO_NETWORK_SELECTION_MODE];

    // Batch the *InfoChanged messages together
    if (operatorMessage) {
      provider.updateOperatorInfo(operatorMessage, true);
    }

    if (voiceMessage) {
      provider.updateVoiceInfo(voiceMessage, true);
    }

    if (dataMessage) {
      provider.updateDataInfo(dataMessage, true);
    }

    if (signalMessage) {
      provider.updateSignalInfo(signalMessage, true);
    }

    if (selectionMessage) {
      this.notifyNetworkSelectModeChanged(aClientId, selectionMessage.mode);
    }

    if (voiceMessage || operatorMessage || signalMessage) {
      provider.deliverListenerEvent("notifyVoiceChanged");
    }

    if (dataMessage || operatorMessage || signalMessage) {
      provider.deliverListenerEvent("notifyDataChanged");
    }
  },

  notifySignalStrengthChanged: function(aClientId, aSignal) {
    if (DEBUG) {
      debug("notifySignalStrengthChanged for " + aClientId + ": " +
            JSON.stringify(aSignal));
    }

    this.getItemByServiceId(aClientId).updateSignalInfo(aSignal);
  },

  notifyOperatorChanged: function(aClientId, aOperator) {
    if (DEBUG) {
      debug("notifyOperatorChanged for " + aClientId + ": " +
            JSON.stringify(aOperator));
    }

    this.getItemByServiceId(aClientId).updateOperatorInfo(aOperator);
  },

  notifyNetworkSelectModeChanged: function(aClientId, aMode) {
    if (DEBUG) {
      debug("notifyNetworkSelectModeChanged for " + aClientId + ": " + aMode);
    }

    let provider = this.getItemByServiceId(aClientId);
    if (provider.networkSelectionMode === aMode) {
      return;
    }

    provider.networkSelectionMode = aMode;
    provider.deliverListenerEvent("notifyNetworkSelectionModeChanged");
  },

  notifySpnAvailable: function(aClientId) {
    if (DEBUG) {
      debug("notifySpnAvailable for " + aClientId);
    }

    let provider = this.getItemByServiceId(aClientId);

    // Update voice roaming state
    provider.updateVoiceInfo({});

    // Update data roaming state
    provider.updateDataInfo({});
  },

  notifyLastHomeNetworkChanged: function(aClientId, aNetwork) {
    if (DEBUG) {
      debug("notifyLastHomeNetworkChanged for " + aClientId + ": " + aNetwork);
    }

    let provider = this.getItemByServiceId(aClientId);
    if (provider.lastKnownHomeNetwork === aNetwork) {
      return;
    }

    provider.lastKnownHomeNetwork = aNetwork;
    provider.deliverListenerEvent("notifyLastKnownHomeNetworkChanged");
  },

  notifyCFStateChanged: function(aClientId, aAction, aReason, aNumber,
                                 aTimeSeconds, aServiceClass) {
    if (DEBUG) {
      debug("notifyCFStateChanged for " + aClientId);
    }

    let provider = this.getItemByServiceId(aClientId);
    provider.notifyCFStateChanged(aAction, aReason, aNumber, aTimeSeconds,
                                  aServiceClass);
  },

  notifyCdmaInfoRecDisplay: function(aClientId, aDisplay) {
    gMobileConnectionMessenger.notifyCdmaInfoRecDisplay(aClientId, aDisplay);
  },

  notifyCdmaInfoRecCalledPartyNumber: function(aClientId, aType, aPlan, aNumber,
                                               aPi, aSi) {
    gMobileConnectionMessenger
      .notifyCdmaInfoRecCalledPartyNumber(aClientId, aType, aPlan, aNumber,
                                          aPi, aSi);
  },

  notifyCdmaInfoRecCallingPartyNumber: function(aClientId, aType, aPlan, aNumber,
                                                aPi, aSi) {
    gMobileConnectionMessenger
      .notifyCdmaInfoRecCallingPartyNumber(aClientId, aType, aPlan, aNumber,
                                           aPi, aSi);
  },

  notifyCdmaInfoRecConnectedPartyNumber: function(aClientId, aType, aPlan, aNumber,
                                                  aPi, aSi) {
    gMobileConnectionMessenger
      .notifyCdmaInfoRecConnectedPartyNumber(aClientId, aType, aPlan, aNumber,
                                             aPi, aSi);
  },

  notifyCdmaInfoRecSignal: function(aClientId, aType, aAlertPitch, aSignal){
    gMobileConnectionMessenger
      .notifyCdmaInfoRecSignal(aClientId, aType, aAlertPitch, aSignal);
  },

  notifyCdmaInfoRecRedirectingNumber: function(aClientId, aType, aPlan, aNumber,
                                               aPi, aSi, aReason) {
    gMobileConnectionMessenger
      .notifyCdmaInfoRecRedirectingNumber(aClientId, aType, aPlan, aNumber,
                                          aPi, aSi, aReason);
  },

  notifyCdmaInfoRecLineControl: function(aClientId, aPolarityIncluded, aToggle,
                                         aReverse, aPowerDenial) {
    gMobileConnectionMessenger
      .notifyCdmaInfoRecLineControl(aClientId, aPolarityIncluded, aToggle,
                                    aReverse, aPowerDenial);
  },

  notifyCdmaInfoRecClir: function(aClientId, aCause) {
    gMobileConnectionMessenger.notifyCdmaInfoRecClir(aClientId, aCause);
  },

  notifyCdmaInfoRecAudioControl: function(aClientId, aUpLink, aDownLink) {
    gMobileConnectionMessenger
      .notifyCdmaInfoRecAudioControl(aClientId, aUpLink, aDownLink);
  },

  notifyDeviceIdentitiesChanged: function(aClientId, aImei, aImeisv,
                                          aEsn, aMeid) {
    this.getItemByServiceId(aClientId)
      .notifyDeviceIdentitiesChanged(aImei, aImeisv, aEsn, aMeid);
  },

  /**
   * nsIObserver interface.
   */
  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case NS_NETWORK_ACTIVE_CHANGED_TOPIC_ID:
        for (let i = 0; i < this.numItems; i++) {
          let provider = this._providers[i];
          // Update connected flag only.
          provider.updateDataInfo({});
        }
        break;
      case NS_DATA_CALL_ERROR_TOPIC_ID:
        try {
          if (aSubject instanceof Ci.nsIRilNetworkInfo) {
            let rilInfo = aSubject.QueryInterface(Ci.nsIRilNetworkInfo);
            this.notifyDataError(rilInfo.serviceId, aData);
          }
        } catch (e) {}
        break;
      case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
        if (aData === kPrefRilDebuggingEnabled) {
          this._updateDebugFlag();
        }
        break;
      case NS_XPCOM_SHUTDOWN_OBSERVER_ID:
        this._shutdown();
        break;
    }
  }
};

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