diff options
Diffstat (limited to 'dom/system/gonk/DataCallManager.js')
-rw-r--r-- | dom/system/gonk/DataCallManager.js | 1726 |
1 files changed, 1726 insertions, 0 deletions
diff --git a/dom/system/gonk/DataCallManager.js b/dom/system/gonk/DataCallManager.js new file mode 100644 index 000000000..5411987cd --- /dev/null +++ b/dom/system/gonk/DataCallManager.js @@ -0,0 +1,1726 @@ +/* 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/systemlibs.js"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager", + "@mozilla.org/network/manager;1", + "nsINetworkManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gMobileConnectionService", + "@mozilla.org/mobileconnection/mobileconnectionservice;1", + "nsIMobileConnectionService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gIccService", + "@mozilla.org/icc/iccservice;1", + "nsIIccService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gDataCallInterfaceService", + "@mozilla.org/datacall/interfaceservice;1", + "nsIDataCallInterfaceService"); + +XPCOMUtils.defineLazyGetter(this, "RIL", function() { + let obj = {}; + Cu.import("resource://gre/modules/ril_consts.js", obj); + return obj; +}); + +// Ril quirk to attach data registration on demand. +var RILQUIRKS_DATA_REGISTRATION_ON_DEMAND = + libcutils.property_get("ro.moz.ril.data_reg_on_demand", "false") == "true"; + +// Ril quirk to control the uicc/data subscription. +var RILQUIRKS_SUBSCRIPTION_CONTROL = + libcutils.property_get("ro.moz.ril.subscription_control", "false") == "true"; + +// Ril quirk to enable IPv6 protocol/roaming protocol in APN settings. +var RILQUIRKS_HAVE_IPV6 = + libcutils.property_get("ro.moz.ril.ipv6", "false") == "true"; + +const DATACALLMANAGER_CID = + Components.ID("{35b9efa2-e42c-45ce-8210-0a13e6f4aadc}"); +const DATACALLHANDLER_CID = + Components.ID("{132b650f-c4d8-4731-96c5-83785cb31dee}"); +const RILNETWORKINTERFACE_CID = + Components.ID("{9574ee84-5d0d-4814-b9e6-8b279e03dcf4}"); +const RILNETWORKINFO_CID = + Components.ID("{dd6cf2f0-f0e3-449f-a69e-7c34fdcb8d4b}"); + +const TOPIC_XPCOM_SHUTDOWN = "xpcom-shutdown"; +const TOPIC_MOZSETTINGS_CHANGED = "mozsettings-changed"; +const TOPIC_PREF_CHANGED = "nsPref:changed"; +const TOPIC_DATA_CALL_ERROR = "data-call-error"; +const PREF_RIL_DEBUG_ENABLED = "ril.debugging.enabled"; + +const NETWORK_TYPE_UNKNOWN = Ci.nsINetworkInfo.NETWORK_TYPE_UNKNOWN; +const NETWORK_TYPE_WIFI = Ci.nsINetworkInfo.NETWORK_TYPE_WIFI; +const NETWORK_TYPE_MOBILE = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE; +const NETWORK_TYPE_MOBILE_MMS = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS; +const NETWORK_TYPE_MOBILE_SUPL = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL; +const NETWORK_TYPE_MOBILE_IMS = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_IMS; +const NETWORK_TYPE_MOBILE_DUN = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN; +const NETWORK_TYPE_MOBILE_FOTA = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_FOTA; + +const NETWORK_STATE_UNKNOWN = Ci.nsINetworkInfo.NETWORK_STATE_UNKNOWN; +const NETWORK_STATE_CONNECTING = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTING; +const NETWORK_STATE_CONNECTED = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED; +const NETWORK_STATE_DISCONNECTING = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTING; +const NETWORK_STATE_DISCONNECTED = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED; + +const INT32_MAX = 2147483647; + +// set to true in ril_consts.js to see debug messages +var DEBUG = RIL.DEBUG_RIL; + +function updateDebugFlag() { + // Read debug setting from pref + let debugPref; + try { + debugPref = Services.prefs.getBoolPref(PREF_RIL_DEBUG_ENABLED); + } catch (e) { + debugPref = false; + } + DEBUG = debugPref || RIL.DEBUG_RIL; +} +updateDebugFlag(); + +function DataCallManager() { + this._connectionHandlers = []; + + let numRadioInterfaces = gMobileConnectionService.numItems; + for (let clientId = 0; clientId < numRadioInterfaces; clientId++) { + this._connectionHandlers.push(new DataCallHandler(clientId)); + } + + let lock = gSettingsService.createLock(); + // Read the APN data from the settings DB. + lock.get("ril.data.apnSettings", this); + // Read the data enabled setting from DB. + lock.get("ril.data.enabled", this); + lock.get("ril.data.roaming_enabled", this); + // Read the default client id for data call. + lock.get("ril.data.defaultServiceId", this); + + Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false); + Services.obs.addObserver(this, TOPIC_MOZSETTINGS_CHANGED, false); + Services.prefs.addObserver(PREF_RIL_DEBUG_ENABLED, this, false); +} +DataCallManager.prototype = { + classID: DATACALLMANAGER_CID, + classInfo: XPCOMUtils.generateCI({classID: DATACALLMANAGER_CID, + classDescription: "Data Call Manager", + interfaces: [Ci.nsIDataCallManager]}), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallManager, + Ci.nsIObserver, + Ci.nsISettingsServiceCallback]), + + _connectionHandlers: null, + + // Flag to determine the data state to start with when we boot up. It + // corresponds to the 'ril.data.enabled' setting from the UI. + _dataEnabled: false, + + // Flag to record the default client id for data call. It corresponds to + // the 'ril.data.defaultServiceId' setting from the UI. + _dataDefaultClientId: -1, + + // Flag to record the current default client id for data call. + // It differs from _dataDefaultClientId in that it is set only when + // the switch of client id process is done. + _currentDataClientId: -1, + + // Pending function to execute when we are notified that another data call has + // been disconnected. + _pendingDataCallRequest: null, + + debug: function(aMsg) { + dump("-*- DataCallManager: " + aMsg + "\n"); + }, + + get dataDefaultServiceId() { + return this._dataDefaultClientId; + }, + + getDataCallHandler: function(aClientId) { + let handler = this._connectionHandlers[aClientId] + if (!handler) { + throw Cr.NS_ERROR_UNEXPECTED; + } + + return handler; + }, + + _setDataRegistration: function(aDataCallInterface, aAttach) { + return new Promise(function(aResolve, aReject) { + let callback = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallCallback]), + notifySuccess: function() { + aResolve(); + }, + notifyError: function(aErrorMsg) { + aReject(aErrorMsg); + } + }; + + aDataCallInterface.setDataRegistration(aAttach, callback); + }); + }, + + _handleDataClientIdChange: function(aNewClientId) { + if (this._dataDefaultClientId === aNewClientId) { + return; + } + this._dataDefaultClientId = aNewClientId; + + // This is to handle boot up stage. + if (this._currentDataClientId == -1) { + this._currentDataClientId = this._dataDefaultClientId; + let connHandler = this._connectionHandlers[this._currentDataClientId]; + let dcInterface = connHandler.dataCallInterface; + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND || + RILQUIRKS_SUBSCRIPTION_CONTROL) { + this._setDataRegistration(dcInterface, true); + } + if (this._dataEnabled) { + let settings = connHandler.dataCallSettings; + settings.oldEnabled = settings.enabled; + settings.enabled = true; + connHandler.updateRILNetworkInterface(); + } + return; + } + + let oldConnHandler = this._connectionHandlers[this._currentDataClientId]; + let oldIface = oldConnHandler.dataCallInterface; + let oldSettings = oldConnHandler.dataCallSettings; + let newConnHandler = this._connectionHandlers[this._dataDefaultClientId]; + let newIface = newConnHandler.dataCallInterface; + let newSettings = newConnHandler.dataCallSettings; + + let applyPendingDataSettings = () => { + if (DEBUG) { + this.debug("Apply pending data registration and settings."); + } + + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND || + RILQUIRKS_SUBSCRIPTION_CONTROL) { + this._setDataRegistration(oldIface, false).then(() => { + if (this._dataEnabled) { + newSettings.oldEnabled = newSettings.enabled; + newSettings.enabled = true; + } + this._currentDataClientId = this._dataDefaultClientId; + + this._setDataRegistration(newIface, true).then(() => { + newConnHandler.updateRILNetworkInterface(); + }); + }); + return; + } + + if (this._dataEnabled) { + newSettings.oldEnabled = newSettings.enabled; + newSettings.enabled = true; + } + + this._currentDataClientId = this._dataDefaultClientId; + newConnHandler.updateRILNetworkInterface(); + }; + + if (this._dataEnabled) { + oldSettings.oldEnabled = oldSettings.enabled; + oldSettings.enabled = false; + } + + oldConnHandler.deactivateDataCallsAndWait().then(() => { + applyPendingDataSettings(); + }); + }, + + _shutdown: function() { + for (let handler of this._connectionHandlers) { + handler.shutdown(); + } + this._connectionHandlers = null; + Services.prefs.removeObserver(PREF_RIL_DEBUG_ENABLED, this); + Services.obs.removeObserver(this, TOPIC_XPCOM_SHUTDOWN); + Services.obs.removeObserver(this, TOPIC_MOZSETTINGS_CHANGED); + }, + + /** + * nsISettingsServiceCallback + */ + handle: function(aName, aResult) { + switch (aName) { + case "ril.data.apnSettings": + if (DEBUG) { + this.debug("'ril.data.apnSettings' is now " + + JSON.stringify(aResult)); + } + if (!aResult) { + break; + } + for (let clientId in this._connectionHandlers) { + let handler = this._connectionHandlers[clientId]; + let apnSetting = aResult[clientId]; + if (handler && apnSetting) { + handler.updateApnSettings(apnSetting); + } + } + break; + case "ril.data.enabled": + if (DEBUG) { + this.debug("'ril.data.enabled' is now " + aResult); + } + if (this._dataEnabled === aResult) { + break; + } + this._dataEnabled = aResult; + + if (DEBUG) { + this.debug("Default id for data call: " + this._dataDefaultClientId); + } + if (this._dataDefaultClientId === -1) { + // We haven't got the default id for data from db. + break; + } + + let connHandler = this._connectionHandlers[this._dataDefaultClientId]; + let settings = connHandler.dataCallSettings; + settings.oldEnabled = settings.enabled; + settings.enabled = aResult; + connHandler.updateRILNetworkInterface(); + break; + case "ril.data.roaming_enabled": + if (DEBUG) { + this.debug("'ril.data.roaming_enabled' is now " + aResult); + this.debug("Default id for data call: " + this._dataDefaultClientId); + } + for (let clientId = 0; clientId < this._connectionHandlers.length; clientId++) { + let connHandler = this._connectionHandlers[clientId]; + let settings = connHandler.dataCallSettings; + settings.roamingEnabled = Array.isArray(aResult) ? aResult[clientId] + : aResult; + } + if (this._dataDefaultClientId === -1) { + // We haven't got the default id for data from db. + break; + } + this._connectionHandlers[this._dataDefaultClientId].updateRILNetworkInterface(); + break; + case "ril.data.defaultServiceId": + aResult = aResult || 0; + if (DEBUG) { + this.debug("'ril.data.defaultServiceId' is now " + aResult); + } + this._handleDataClientIdChange(aResult); + break; + } + }, + + handleError: function(aErrorMessage) { + if (DEBUG) { + this.debug("There was an error while reading RIL settings."); + } + }, + + /** + * nsIObserver interface methods. + */ + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case TOPIC_MOZSETTINGS_CHANGED: + if ("wrappedJSObject" in aSubject) { + aSubject = aSubject.wrappedJSObject; + } + this.handle(aSubject.key, aSubject.value); + break; + case TOPIC_PREF_CHANGED: + if (aData === PREF_RIL_DEBUG_ENABLED) { + updateDebugFlag(); + } + break; + case TOPIC_XPCOM_SHUTDOWN: + this._shutdown(); + break; + } + }, +}; + +function DataCallHandler(aClientId) { + // Initial owning attributes. + this.clientId = aClientId; + this.dataCallSettings = { + oldEnabled: false, + enabled: false, + roamingEnabled: false + }; + this._dataCalls = []; + this._listeners = []; + + // This map is used to collect all the apn types and its corresponding + // RILNetworkInterface. + this.dataNetworkInterfaces = new Map(); + + this.dataCallInterface = gDataCallInterfaceService.getDataCallInterface(aClientId); + this.dataCallInterface.registerListener(this); + + let mobileConnection = gMobileConnectionService.getItemByServiceId(aClientId); + mobileConnection.registerListener(this); + + this._dataInfo = { + state: mobileConnection.data.state, + type: mobileConnection.data.type, + roaming: mobileConnection.data.roaming + } +} +DataCallHandler.prototype = { + classID: DATACALLHANDLER_CID, + classInfo: XPCOMUtils.generateCI({classID: DATACALLHANDLER_CID, + classDescription: "Data Call Handler", + interfaces: [Ci.nsIDataCallHandler]}), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallHandler, + Ci.nsIDataCallInterfaceListener, + Ci.nsIMobileConnectionListener]), + + clientId: 0, + dataCallInterface: null, + dataCallSettings: null, + dataNetworkInterfaces: null, + _dataCalls: null, + _dataInfo: null, + + // Apn settings to be setup after data call are cleared. + _pendingApnSettings: null, + + debug: function(aMsg) { + dump("-*- DataCallHandler[" + this.clientId + "]: " + aMsg + "\n"); + }, + + shutdown: function() { + // Shutdown all RIL network interfaces + this.dataNetworkInterfaces.forEach(function(networkInterface) { + gNetworkManager.unregisterNetworkInterface(networkInterface); + networkInterface.shutdown(); + networkInterface = null; + }); + this.dataNetworkInterfaces.clear(); + this._dataCalls = []; + this.clientId = null; + + this.dataCallInterface.unregisterListener(this); + this.dataCallInterface = null; + + let mobileConnection = + gMobileConnectionService.getItemByServiceId(this.clientId); + mobileConnection.unregisterListener(this); + }, + + /** + * Check if we get all necessary APN data. + */ + _validateApnSetting: function(aApnSetting) { + return (aApnSetting && + aApnSetting.apn && + aApnSetting.types && + aApnSetting.types.length); + }, + + _convertApnType: function(aApnType) { + switch (aApnType) { + case "default": + return NETWORK_TYPE_MOBILE; + case "mms": + return NETWORK_TYPE_MOBILE_MMS; + case "supl": + return NETWORK_TYPE_MOBILE_SUPL; + case "ims": + return NETWORK_TYPE_MOBILE_IMS; + case "dun": + return NETWORK_TYPE_MOBILE_DUN; + case "fota": + return NETWORK_TYPE_MOBILE_FOTA; + default: + return NETWORK_TYPE_UNKNOWN; + } + }, + + _compareDataCallOptions: function(aDataCall, aNewDataCall) { + return aDataCall.apnProfile.apn == aNewDataCall.apnProfile.apn && + aDataCall.apnProfile.user == aNewDataCall.apnProfile.user && + aDataCall.apnProfile.password == aNewDataCall.apnProfile.passwd && + aDataCall.apnProfile.authType == aNewDataCall.apnProfile.authType && + aDataCall.apnProfile.protocol == aNewDataCall.apnProfile.protocol && + aDataCall.apnProfile.roaming_protocol == aNewDataCall.apnProfile.roaming_protocol; + }, + + /** + * This function will do the following steps: + * 1. Clear the cached APN settings in the RIL. + * 2. Combine APN, user name, and password as the key of |byApn| object to + * refer to the corresponding APN setting. + * 3. Use APN type as the index of |byType| object to refer to the + * corresponding APN setting. + * 4. Create RilNetworkInterface for each APN setting created at step 2. + */ + _setupApnSettings: function(aNewApnSettings) { + if (!aNewApnSettings) { + return; + } + if (DEBUG) this.debug("setupApnSettings: " + JSON.stringify(aNewApnSettings)); + + // Shutdown all network interfaces and clear data calls. + this.dataNetworkInterfaces.forEach(function(networkInterface) { + gNetworkManager.unregisterNetworkInterface(networkInterface); + networkInterface.shutdown(); + networkInterface = null; + }); + this.dataNetworkInterfaces.clear(); + this._dataCalls = []; + + // Cache the APN settings by APNs and by types in the RIL. + for (let inputApnSetting of aNewApnSettings) { + if (!this._validateApnSetting(inputApnSetting)) { + continue; + } + + // Use APN type as the key of dataNetworkInterfaces to refer to the + // corresponding RILNetworkInterface. + for (let i = 0; i < inputApnSetting.types.length; i++) { + let apnType = inputApnSetting.types[i]; + let networkType = this._convertApnType(apnType); + if (networkType === NETWORK_TYPE_UNKNOWN) { + if (DEBUG) this.debug("Invalid apn type: " + apnType); + continue; + } + + if (DEBUG) this.debug("Preparing RILNetworkInterface for type: " + apnType); + // Create DataCall for RILNetworkInterface or reuse one that is shareable. + let dataCall; + for (let i = 0; i < this._dataCalls.length; i++) { + if (this._dataCalls[i].canHandleApn(inputApnSetting)) { + if (DEBUG) this.debug("Found shareable DataCall, reusing it."); + dataCall = this._dataCalls[i]; + break; + } + } + + if (!dataCall) { + if (DEBUG) this.debug("No shareable DataCall found, creating one."); + dataCall = new DataCall(this.clientId, inputApnSetting, this); + this._dataCalls.push(dataCall); + } + + try { + let networkInterface = new RILNetworkInterface(this, networkType, + inputApnSetting, + dataCall); + gNetworkManager.registerNetworkInterface(networkInterface); + this.dataNetworkInterfaces.set(networkType, networkInterface); + } catch (e) { + if (DEBUG) { + this.debug("Error setting up RILNetworkInterface for type " + + apnType + ": " + e); + } + } + } + } + }, + + /** + * Check if all data is disconnected. + */ + allDataDisconnected: function() { + for (let i = 0; i < this._dataCalls.length; i++) { + let dataCall = this._dataCalls[i]; + if (dataCall.state != NETWORK_STATE_UNKNOWN && + dataCall.state != NETWORK_STATE_DISCONNECTED) { + return false; + } + } + return true; + }, + + deactivateDataCallsAndWait: function() { + return new Promise((aResolve, aReject) => { + this.deactivateDataCalls({ + notifyDataCallsDisconnected: function() { + aResolve(); + } + }); + }); + }, + + updateApnSettings: function(aNewApnSettings) { + if (!aNewApnSettings) { + return; + } + if (this._pendingApnSettings) { + // Change of apn settings in process, just update to the newest. + this._pengingApnSettings = aNewApnSettings; + return; + } + + this._pendingApnSettings = aNewApnSettings; + this.deactivateDataCallsAndWait().then(() => { + this._setupApnSettings(this._pendingApnSettings); + this._pendingApnSettings = null; + this.updateRILNetworkInterface(); + }); + }, + + updateRILNetworkInterface: function() { + let networkInterface = this.dataNetworkInterfaces.get(NETWORK_TYPE_MOBILE); + if (!networkInterface) { + if (DEBUG) { + this.debug("No network interface for default data."); + } + return; + } + + let connection = + gMobileConnectionService.getItemByServiceId(this.clientId); + + // This check avoids data call connection if the radio is not ready + // yet after toggling off airplane mode. + let radioState = connection && connection.radioState; + if (radioState != Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED) { + if (DEBUG) { + this.debug("RIL is not ready for data connection: radio's not ready"); + } + return; + } + + // We only watch at "ril.data.enabled" flag changes for connecting or + // disconnecting the data call. If the value of "ril.data.enabled" is + // true and any of the remaining flags change the setting application + // should turn this flag to false and then to true in order to reload + // the new values and reconnect the data call. + if (this.dataCallSettings.oldEnabled === this.dataCallSettings.enabled) { + if (DEBUG) { + this.debug("No changes for ril.data.enabled flag. Nothing to do."); + } + return; + } + + let dataInfo = connection && connection.data; + let isRegistered = + dataInfo && + dataInfo.state == RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED; + let haveDataConnection = + dataInfo && + dataInfo.type != RIL.GECKO_MOBILE_CONNECTION_STATE_UNKNOWN; + if (!isRegistered || !haveDataConnection) { + if (DEBUG) { + this.debug("RIL is not ready for data connection: Phone's not " + + "registered or doesn't have data connection."); + } + return; + } + let wifi_active = false; + if (gNetworkManager.activeNetworkInfo && + gNetworkManager.activeNetworkInfo.type == NETWORK_TYPE_WIFI) { + wifi_active = true; + } + + let defaultDataCallConnected = networkInterface.connected; + + // We have moved part of the decision making into DataCall, the rest will be + // moved after Bug 904514 - [meta] NetworkManager enhancement. + if (networkInterface.enabled && + (!this.dataCallSettings.enabled || + (dataInfo.roaming && !this.dataCallSettings.roamingEnabled))) { + if (DEBUG) { + this.debug("Data call settings: disconnect data call."); + } + networkInterface.disconnect(); + return; + } + + if (networkInterface.enabled && wifi_active) { + if (DEBUG) { + this.debug("Disconnect data call when Wifi is connected."); + } + networkInterface.disconnect(); + return; + } + + if (!this.dataCallSettings.enabled || defaultDataCallConnected) { + if (DEBUG) { + this.debug("Data call settings: nothing to do."); + } + return; + } + if (dataInfo.roaming && !this.dataCallSettings.roamingEnabled) { + if (DEBUG) { + this.debug("We're roaming, but data roaming is disabled."); + } + return; + } + if (wifi_active) { + if (DEBUG) { + this.debug("Don't connect data call when Wifi is connected."); + } + return; + } + if (this._pendingApnSettings) { + if (DEBUG) this.debug("We're changing apn settings, ignore any changes."); + return; + } + + if (this._deactivatingDataCalls) { + if (DEBUG) this.debug("We're deactivating all data calls, ignore any changes."); + return; + } + + if (DEBUG) { + this.debug("Data call settings: connect data call."); + } + networkInterface.connect(); + }, + + _isMobileNetworkType: function(aNetworkType) { + if (aNetworkType === NETWORK_TYPE_MOBILE || + aNetworkType === NETWORK_TYPE_MOBILE_MMS || + aNetworkType === NETWORK_TYPE_MOBILE_SUPL || + aNetworkType === NETWORK_TYPE_MOBILE_IMS || + aNetworkType === NETWORK_TYPE_MOBILE_DUN || + aNetworkType === NETWORK_TYPE_MOBILE_FOTA) { + return true; + } + + return false; + }, + + getDataCallStateByType: function(aNetworkType) { + if (!this._isMobileNetworkType(aNetworkType)) { + if (DEBUG) this.debug(aNetworkType + " is not a mobile network type!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + + let networkInterface = this.dataNetworkInterfaces.get(aNetworkType); + if (!networkInterface) { + return NETWORK_STATE_UNKNOWN; + } + return networkInterface.info.state; + }, + + setupDataCallByType: function(aNetworkType) { + if (DEBUG) { + this.debug("setupDataCallByType: " + aNetworkType); + } + + if (!this._isMobileNetworkType(aNetworkType)) { + if (DEBUG) this.debug(aNetworkType + " is not a mobile network type!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + + let networkInterface = this.dataNetworkInterfaces.get(aNetworkType); + if (!networkInterface) { + if (DEBUG) { + this.debug("No network interface for type: " + aNetworkType); + } + return; + } + + networkInterface.connect(); + }, + + deactivateDataCallByType: function(aNetworkType) { + if (DEBUG) { + this.debug("deactivateDataCallByType: " + aNetworkType); + } + + if (!this._isMobileNetworkType(aNetworkType)) { + if (DEBUG) this.debug(aNetworkType + " is not a mobile network type!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + + let networkInterface = this.dataNetworkInterfaces.get(aNetworkType); + if (!networkInterface) { + if (DEBUG) { + this.debug("No network interface for type: " + aNetworkType); + } + return; + } + + networkInterface.disconnect(); + }, + + _deactivatingDataCalls: false, + + deactivateDataCalls: function(aCallback) { + let dataDisconnecting = false; + this.dataNetworkInterfaces.forEach(function(networkInterface) { + if (networkInterface.enabled) { + if (networkInterface.info.state != NETWORK_STATE_UNKNOWN && + networkInterface.info.state != NETWORK_STATE_DISCONNECTED) { + dataDisconnecting = true; + } + networkInterface.disconnect(); + } + }); + + this._deactivatingDataCalls = dataDisconnecting; + if (!dataDisconnecting) { + aCallback.notifyDataCallsDisconnected(); + return; + } + + let callback = { + notifyAllDataDisconnected: () => { + this._unregisterListener(callback); + aCallback.notifyDataCallsDisconnected(); + } + }; + this._registerListener(callback); + }, + + _listeners: null, + + _notifyListeners: 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) { + this.debug("listener for " + aMethodName + " threw an exception: " + e); + } + } + }, + + _registerListener: function(aListener) { + if (this._listeners.indexOf(aListener) >= 0) { + return; + } + + this._listeners.push(aListener); + }, + + _unregisterListener: function(aListener) { + let index = this._listeners.indexOf(aListener); + if (index >= 0) { + this._listeners.splice(index, 1); + } + }, + + _findDataCallByCid: function(aCid) { + if (aCid === undefined || aCid < 0) { + return -1; + } + + for (let i = 0; i < this._dataCalls.length; i++) { + let datacall = this._dataCalls[i]; + if (datacall.linkInfo.cid != null && + datacall.linkInfo.cid == aCid) { + return i; + } + } + + return -1; + }, + + /** + * Notify about data call setup error, called from DataCall. + */ + notifyDataCallError: function(aDataCall, aErrorMsg) { + // Notify data call error only for data APN + let networkInterface = this.dataNetworkInterfaces.get(NETWORK_TYPE_MOBILE); + if (networkInterface && networkInterface.enabled) { + let dataCall = networkInterface.dataCall; + if (this._compareDataCallOptions(dataCall, aDataCall)) { + Services.obs.notifyObservers(networkInterface.info, + TOPIC_DATA_CALL_ERROR, aErrorMsg); + } + } + }, + + /** + * Notify about data call changed, called from DataCall. + */ + notifyDataCallChanged: function(aUpdatedDataCall) { + // Process pending radio power off request after all data calls + // are disconnected. + if (aUpdatedDataCall.state == NETWORK_STATE_DISCONNECTED || + aUpdatedDataCall.state == NETWORK_STATE_UNKNOWN && + this.allDataDisconnected() && this._deactivatingDataCalls) { + this._deactivatingDataCalls = false; + this._notifyListeners("notifyAllDataDisconnected", { + clientId: this.clientId + }); + } + }, + + // nsIDataCallInterfaceListener + + notifyDataCallListChanged: function(aCount, aDataCallList) { + let currentDataCalls = this._dataCalls.slice(); + for (let i = 0; i < aDataCallList.length; i++) { + let dataCall = aDataCallList[i]; + let index = this._findDataCallByCid(dataCall.cid); + if (index == -1) { + if (DEBUG) { + this.debug("Unexpected new data call: " + JSON.stringify(dataCall)); + } + continue; + } + currentDataCalls[index].onDataCallChanged(dataCall); + currentDataCalls[index] = null; + } + + // If there is any CONNECTED DataCall left in currentDataCalls, means that + // it is missing in dataCallList, we should send a DISCONNECTED event to + // notify about this. + for (let i = 0; i < currentDataCalls.length; i++) { + let currentDataCall = currentDataCalls[i]; + if (currentDataCall && currentDataCall.linkInfo.cid != null && + currentDataCall.state == NETWORK_STATE_CONNECTED) { + if (DEBUG) { + this.debug("Expected data call missing: " + JSON.stringify( + currentDataCall.apnProfile) + ", must have been DISCONNECTED."); + } + currentDataCall.onDataCallChanged({ + state: NETWORK_STATE_DISCONNECTED + }); + } + } + }, + + // nsIMobileConnectionListener + + notifyVoiceChanged: function() {}, + + notifyDataChanged: function () { + let connection = gMobileConnectionService.getItemByServiceId(this.clientId); + let newDataInfo = connection.data; + + if (this._dataInfo.state == newDataInfo.state && + this._dataInfo.type == newDataInfo.type && + this._dataInfo.roaming == newDataInfo.roaming) { + return; + } + + this._dataInfo.state = newDataInfo.state; + this._dataInfo.type = newDataInfo.type; + this._dataInfo.roaming = newDataInfo.roaming; + this.updateRILNetworkInterface(); + }, + + notifyDataError: function (aMessage) {}, + + notifyCFStateChanged: function(aAction, aReason, aNumber, aTimeSeconds, aServiceClass) {}, + + notifyEmergencyCbModeChanged: function(aActive, aTimeoutMs) {}, + + notifyOtaStatusChanged: function(aStatus) {}, + + notifyRadioStateChanged: function() {}, + + notifyClirModeChanged: function(aMode) {}, + + notifyLastKnownNetworkChanged: function() {}, + + notifyLastKnownHomeNetworkChanged: function() {}, + + notifyNetworkSelectionModeChanged: function() {}, + + notifyDeviceIdentitiesChanged: function() {} +}; + +function DataCall(aClientId, aApnSetting, aDataCallHandler) { + this.clientId = aClientId; + this.dataCallHandler = aDataCallHandler; + this.apnProfile = { + apn: aApnSetting.apn, + user: aApnSetting.user, + password: aApnSetting.password, + authType: aApnSetting.authtype, + protocol: aApnSetting.protocol, + roaming_protocol: aApnSetting.roaming_protocol + }; + this.linkInfo = { + cid: null, + ifname: null, + addresses: [], + dnses: [], + gateways: [], + pcscf: [], + mtu: null + }; + this.state = NETWORK_STATE_UNKNOWN; + this.requestedNetworkIfaces = []; +} +DataCall.prototype = { + /** + * Standard values for the APN connection retry process + * Retry funcion: time(secs) = A * numer_of_retries^2 + B + */ + NETWORK_APNRETRY_FACTOR: 8, + NETWORK_APNRETRY_ORIGIN: 3, + NETWORK_APNRETRY_MAXRETRIES: 10, + + dataCallHandler: null, + + // Event timer for connection retries + timer: null, + + // APN failed connections. Retry counter + apnRetryCounter: 0, + + // Array to hold RILNetworkInterfaces that requested this DataCall. + requestedNetworkIfaces: null, + + /** + * @return "deactivate" if <ifname> changes or one of the aCurrentDataCall + * addresses is missing in updatedDataCall, or "identical" if no + * changes found, or "changed" otherwise. + */ + _compareDataCallLink: function(aUpdatedDataCall, aCurrentDataCall) { + // If network interface is changed, report as "deactivate". + if (aUpdatedDataCall.ifname != aCurrentDataCall.ifname) { + return "deactivate"; + } + + // If any existing address is missing, report as "deactivate". + for (let i = 0; i < aCurrentDataCall.addresses.length; i++) { + let address = aCurrentDataCall.addresses[i]; + if (aUpdatedDataCall.addresses.indexOf(address) < 0) { + return "deactivate"; + } + } + + if (aCurrentDataCall.addresses.length != aUpdatedDataCall.addresses.length) { + // Since now all |aCurrentDataCall.addresses| are found in + // |aUpdatedDataCall.addresses|, this means one or more new addresses are + // reported. + return "changed"; + } + + let fields = ["gateways", "dnses"]; + for (let i = 0; i < fields.length; i++) { + // Compare <datacall>.<field>. + let field = fields[i]; + let lhs = aUpdatedDataCall[field], rhs = aCurrentDataCall[field]; + if (lhs.length != rhs.length) { + return "changed"; + } + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] != rhs[i]) { + return "changed"; + } + } + } + + if (aCurrentDataCall.mtu != aUpdatedDataCall.mtu) { + return "changed"; + } + + return "identical"; + }, + + _getGeckoDataCallState:function (aDataCall) { + if (aDataCall.active == Ci.nsIDataCallInterface.DATACALL_STATE_ACTIVE_UP || + aDataCall.active == Ci.nsIDataCallInterface.DATACALL_STATE_ACTIVE_DOWN) { + return NETWORK_STATE_CONNECTED; + } + + return NETWORK_STATE_DISCONNECTED; + }, + + onSetupDataCallResult: function(aDataCall) { + this.debug("onSetupDataCallResult: " + JSON.stringify(aDataCall)); + let errorMsg = aDataCall.errorMsg; + if (aDataCall.failCause && + aDataCall.failCause != Ci.nsIDataCallInterface.DATACALL_FAIL_NONE) { + errorMsg = + RIL.RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[aDataCall.failCause]; + } + + if (errorMsg) { + if (DEBUG) { + this.debug("SetupDataCall error for apn " + this.apnProfile.apn + ": " + + errorMsg + " (" + aDataCall.failCause + "), retry time: " + + aDataCall.suggestedRetryTime); + } + + this.state = NETWORK_STATE_DISCONNECTED; + + if (this.requestedNetworkIfaces.length === 0) { + if (DEBUG) this.debug("This DataCall is not requested anymore."); + return; + } + + // Let DataCallHandler notify MobileConnectionService + this.dataCallHandler.notifyDataCallError(this, errorMsg); + + // For suggestedRetryTime, the value of INT32_MAX(0x7fffffff) means no retry. + if (aDataCall.suggestedRetryTime === INT32_MAX || + this.isPermanentFail(aDataCall.failCause, errorMsg)) { + if (DEBUG) this.debug("Data call error: no retry needed."); + return; + } + + this.retry(aDataCall.suggestedRetryTime); + return; + } + + this.apnRetryCounter = 0; + this.linkInfo.cid = aDataCall.cid; + + if (this.requestedNetworkIfaces.length === 0) { + if (DEBUG) { + this.debug("State is connected, but no network interface requested" + + " this DataCall"); + } + this.deactivate(); + return; + } + + this.linkInfo.ifname = aDataCall.ifname; + this.linkInfo.addresses = aDataCall.addresses ? aDataCall.addresses.split(" ") : []; + this.linkInfo.gateways = aDataCall.gateways ? aDataCall.gateways.split(" ") : []; + this.linkInfo.dnses = aDataCall.dnses ? aDataCall.dnses.split(" ") : []; + this.linkInfo.pcscf = aDataCall.pcscf ? aDataCall.pcscf.split(" ") : []; + this.linkInfo.mtu = aDataCall.mtu > 0 ? aDataCall.mtu : 0; + this.state = this._getGeckoDataCallState(aDataCall); + + // Notify DataCallHandler about data call connected. + this.dataCallHandler.notifyDataCallChanged(this); + + for (let i = 0; i < this.requestedNetworkIfaces.length; i++) { + this.requestedNetworkIfaces[i].notifyRILNetworkInterface(); + } + }, + + onDeactivateDataCallResult: function() { + if (DEBUG) this.debug("onDeactivateDataCallResult"); + + this.reset(); + + if (this.requestedNetworkIfaces.length > 0) { + if (DEBUG) { + this.debug("State is disconnected/unknown, but this DataCall is" + + " requested."); + } + this.setup(); + return; + } + + // Notify DataCallHandler about data call disconnected. + this.dataCallHandler.notifyDataCallChanged(this); + }, + + onDataCallChanged: function(aUpdatedDataCall) { + if (DEBUG) { + this.debug("onDataCallChanged: " + JSON.stringify(aUpdatedDataCall)); + } + + if (this.state == NETWORK_STATE_CONNECTING || + this.state == NETWORK_STATE_DISCONNECTING) { + if (DEBUG) { + this.debug("We are in connecting/disconnecting state, ignore any " + + "unsolicited event for now."); + } + return; + } + + let dataCallState = this._getGeckoDataCallState(aUpdatedDataCall); + if (this.state == dataCallState && + dataCallState != NETWORK_STATE_CONNECTED) { + return; + } + + let newLinkInfo = { + ifname: aUpdatedDataCall.ifname, + addresses: aUpdatedDataCall.addresses ? aUpdatedDataCall.addresses.split(" ") : [], + dnses: aUpdatedDataCall.dnses ? aUpdatedDataCall.dnses.split(" ") : [], + gateways: aUpdatedDataCall.gateways ? aUpdatedDataCall.gateways.split(" ") : [], + pcscf: aUpdatedDataCall.pcscf ? aUpdatedDataCall.pcscf.split(" ") : [], + mtu: aUpdatedDataCall.mtu > 0 ? aUpdatedDataCall.mtu : 0 + }; + + switch (dataCallState) { + case NETWORK_STATE_CONNECTED: + if (this.state == NETWORK_STATE_CONNECTED) { + let result = + this._compareDataCallLink(newLinkInfo, this.linkInfo); + + if (result == "identical") { + if (DEBUG) this.debug("No changes in data call."); + return; + } + if (result == "deactivate") { + if (DEBUG) this.debug("Data link changed, cleanup."); + this.deactivate(); + return; + } + // Minor change, just update and notify. + if (DEBUG) { + this.debug("Data link minor change, just update and notify."); + } + + this.linkInfo.addresses = newLinkInfo.addresses.slice(); + this.linkInfo.gateways = newLinkInfo.gateways.slice(); + this.linkInfo.dnses = newLinkInfo.dnses.slice(); + this.linkInfo.pcscf = newLinkInfo.pcscf.slice(); + this.linkInfo.mtu = newLinkInfo.mtu; + } + break; + case NETWORK_STATE_DISCONNECTED: + case NETWORK_STATE_UNKNOWN: + if (this.state == NETWORK_STATE_CONNECTED) { + // Notify first on unexpected data call disconnection. + this.state = dataCallState; + for (let i = 0; i < this.requestedNetworkIfaces.length; i++) { + this.requestedNetworkIfaces[i].notifyRILNetworkInterface(); + } + } + this.reset(); + + if (this.requestedNetworkIfaces.length > 0) { + if (DEBUG) { + this.debug("State is disconnected/unknown, but this DataCall is" + + " requested."); + } + this.setup(); + return; + } + break; + } + + this.state = dataCallState; + + // Notify DataCallHandler about data call changed. + this.dataCallHandler.notifyDataCallChanged(this); + + for (let i = 0; i < this.requestedNetworkIfaces.length; i++) { + this.requestedNetworkIfaces[i].notifyRILNetworkInterface(); + } + }, + + // Helpers + + debug: function(aMsg) { + dump("-*- DataCall[" + this.clientId + ":" + this.apnProfile.apn + "]: " + + aMsg + "\n"); + }, + + get connected() { + return this.state == NETWORK_STATE_CONNECTED; + }, + + isPermanentFail: function(aDataFailCause, aErrorMsg) { + // Check ril.h for 'no retry' data call fail causes. + if (aErrorMsg === RIL.GECKO_ERROR_RADIO_NOT_AVAILABLE || + aErrorMsg === RIL.GECKO_ERROR_INVALID_PARAMETER || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_OPERATOR_BARRED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_MISSING_UKNOWN_APN || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_UNKNOWN_PDP_ADDRESS_TYPE || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_USER_AUTHENTICATION || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_ACTIVATION_REJECT_GGSN || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_SERVICE_OPTION_NOT_SUPPORTED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_SERVICE_OPTION_NOT_SUBSCRIBED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_NSAPI_IN_USE || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_ONLY_IPV4_ALLOWED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_ONLY_IPV6_ALLOWED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_PROTOCOL_ERRORS || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_RADIO_POWER_OFF || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_TETHERED_CALL_ACTIVE) { + return true; + } + + return false; + }, + + inRequestedTypes: function(aType) { + for (let i = 0; i < this.requestedNetworkIfaces.length; i++) { + if (this.requestedNetworkIfaces[i].info.type == aType) { + return true; + } + } + return false; + }, + + canHandleApn: function(aApnSetting) { + let isIdentical = this.apnProfile.apn == aApnSetting.apn && + (this.apnProfile.user || '') == (aApnSetting.user || '') && + (this.apnProfile.password || '') == (aApnSetting.password || '') && + (this.apnProfile.authType || '') == (aApnSetting.authtype || ''); + + if (RILQUIRKS_HAVE_IPV6) { + isIdentical = isIdentical && + (this.apnProfile.protocol || '') == (aApnSetting.protocol || '') && + (this.apnProfile.roaming_protocol || '') == (aApnSetting.roaming_protocol || ''); + } + + return isIdentical; + }, + + resetLinkInfo: function() { + this.linkInfo.cid = null; + this.linkInfo.ifname = null; + this.linkInfo.addresses = []; + this.linkInfo.dnses = []; + this.linkInfo.gateways = []; + this.linkInfo.pcscf = []; + this.linkInfo.mtu = null; + }, + + reset: function() { + this.resetLinkInfo(); + + this.state = NETWORK_STATE_UNKNOWN; + }, + + connect: function(aNetworkInterface) { + if (DEBUG) this.debug("connect: " + aNetworkInterface.info.type); + + if (this.requestedNetworkIfaces.indexOf(aNetworkInterface) == -1) { + this.requestedNetworkIfaces.push(aNetworkInterface); + } + + if (this.state == NETWORK_STATE_CONNECTING || + this.state == NETWORK_STATE_DISCONNECTING) { + return; + } + if (this.state == NETWORK_STATE_CONNECTED) { + // This needs to run asynchronously, to behave the same way as the case of + // non-shared apn, see bug 1059110. + Services.tm.currentThread.dispatch(() => { + // Do not notify if state changed while this event was being dispatched, + // the state probably was notified already or need not to be notified. + if (aNetworkInterface.info.state == RIL.GECKO_NETWORK_STATE_CONNECTED) { + aNetworkInterface.notifyRILNetworkInterface(); + } + }, Ci.nsIEventTarget.DISPATCH_NORMAL); + return; + } + + // If retry mechanism is running on background, stop it since we are going + // to setup data call now. + if (this.timer) { + this.timer.cancel(); + } + + this.setup(); + }, + + setup: function() { + if (DEBUG) { + this.debug("Going to set up data connection with APN " + + this.apnProfile.apn); + } + + let connection = + gMobileConnectionService.getItemByServiceId(this.clientId); + let dataInfo = connection && connection.data; + if (dataInfo == null || + dataInfo.state != RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED || + dataInfo.type == RIL.GECKO_MOBILE_CONNECTION_STATE_UNKNOWN) { + return; + } + + let radioTechType = dataInfo.type; + let radioTechnology = RIL.GECKO_RADIO_TECH.indexOf(radioTechType); + let authType = RIL.RIL_DATACALL_AUTH_TO_GECKO.indexOf(this.apnProfile.authType); + // Use the default authType if the value in database is invalid. + // For the case that user might not select the authentication type. + if (authType == -1) { + if (DEBUG) { + this.debug("Invalid authType '" + this.apnProfile.authtype + + "', using '" + RIL.GECKO_DATACALL_AUTH_DEFAULT + "'"); + } + authType = RIL.RIL_DATACALL_AUTH_TO_GECKO.indexOf(RIL.GECKO_DATACALL_AUTH_DEFAULT); + } + + let pdpType = Ci.nsIDataCallInterface.DATACALL_PDP_TYPE_IPV4; + if (RILQUIRKS_HAVE_IPV6) { + pdpType = !dataInfo.roaming + ? RIL.RIL_DATACALL_PDP_TYPES.indexOf(this.apnProfile.protocol) + : RIL.RIL_DATACALL_PDP_TYPES.indexOf(this.apnProfile.roaming_protocol); + if (pdpType == -1) { + if (DEBUG) { + this.debug("Invalid pdpType '" + (!dataInfo.roaming + ? this.apnProfile.protocol + : this.apnProfile.roaming_protocol) + + "', using '" + RIL.GECKO_DATACALL_PDP_TYPE_DEFAULT + "'"); + } + pdpType = RIL.RIL_DATACALL_PDP_TYPES.indexOf(RIL.GECKO_DATACALL_PDP_TYPE_DEFAULT); + } + } + + let dcInterface = this.dataCallHandler.dataCallInterface; + dcInterface.setupDataCall( + this.apnProfile.apn, this.apnProfile.user, this.apnProfile.password, + authType, pdpType, { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallCallback]), + notifySetupDataCallSuccess: (aDataCall) => { + this.onSetupDataCallResult(aDataCall); + }, + notifyError: (aErrorMsg) => { + this.onSetupDataCallResult({errorMsg: aErrorMsg}); + } + }); + this.state = NETWORK_STATE_CONNECTING; + }, + + retry: function(aSuggestedRetryTime) { + let apnRetryTimer; + + // We will retry the connection in increasing times + // based on the function: time = A * numer_of_retries^2 + B + if (this.apnRetryCounter >= this.NETWORK_APNRETRY_MAXRETRIES) { + this.apnRetryCounter = 0; + this.timer = null; + if (DEBUG) this.debug("Too many APN Connection retries - STOP retrying"); + return; + } + + // If there is a valid aSuggestedRetryTime, override the retry timer. + if (aSuggestedRetryTime !== undefined && aSuggestedRetryTime >= 0) { + apnRetryTimer = aSuggestedRetryTime / 1000; + } else { + apnRetryTimer = this.NETWORK_APNRETRY_FACTOR * + (this.apnRetryCounter * this.apnRetryCounter) + + this.NETWORK_APNRETRY_ORIGIN; + } + this.apnRetryCounter++; + if (DEBUG) { + this.debug("Data call - APN Connection Retry Timer (secs-counter): " + + apnRetryTimer + "-" + this.apnRetryCounter); + } + + if (this.timer == null) { + // Event timer for connection retries + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + this.timer.initWithCallback(this, apnRetryTimer * 1000, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + disconnect: function(aNetworkInterface) { + if (DEBUG) this.debug("disconnect: " + aNetworkInterface.info.type); + + let index = this.requestedNetworkIfaces.indexOf(aNetworkInterface); + if (index != -1) { + this.requestedNetworkIfaces.splice(index, 1); + + if (this.state == NETWORK_STATE_DISCONNECTED || + this.state == NETWORK_STATE_UNKNOWN) { + if (this.timer) { + this.timer.cancel(); + } + this.reset(); + return; + } + + // Notify the DISCONNECTED event immediately after network interface is + // removed from requestedNetworkIfaces, to make the DataCall, shared or + // not, to have the same behavior. + Services.tm.currentThread.dispatch(() => { + // Do not notify if state changed while this event was being dispatched, + // the state probably was notified already or need not to be notified. + if (aNetworkInterface.info.state == RIL.GECKO_NETWORK_STATE_DISCONNECTED) { + aNetworkInterface.notifyRILNetworkInterface(); + + // Clear link info after notifying NetworkManager. + if (this.requestedNetworkIfaces.length === 0) { + this.resetLinkInfo(); + } + } + }, Ci.nsIEventTarget.DISPATCH_NORMAL); + } + + // Only deactivate data call if no more network interface needs this + // DataCall and if state is CONNECTED, for other states, we simply remove + // the network interface from requestedNetworkIfaces. + if (this.requestedNetworkIfaces.length > 0 || + this.state != NETWORK_STATE_CONNECTED) { + return; + } + + this.deactivate(); + }, + + deactivate: function() { + let reason = Ci.nsIDataCallInterface.DATACALL_DEACTIVATE_NO_REASON; + if (DEBUG) { + this.debug("Going to disconnect data connection cid " + this.linkInfo.cid); + } + + let dcInterface = this.dataCallHandler.dataCallInterface; + dcInterface.deactivateDataCall(this.linkInfo.cid, reason, { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallCallback]), + notifySuccess: () => { + this.onDeactivateDataCallResult(); + }, + notifyError: (aErrorMsg) => { + this.onDeactivateDataCallResult(); + } + }); + + this.state = NETWORK_STATE_DISCONNECTING; + }, + + // Entry method for timer events. Used to reconnect to a failed APN + notify: function(aTimer) { + this.setup(); + }, + + shutdown: function() { + if (this.timer) { + this.timer.cancel(); + this.timer = null; + } + } +}; + +function RILNetworkInfo(aClientId, aType, aNetworkInterface) +{ + this.serviceId = aClientId; + this.type = aType; + + this.networkInterface = aNetworkInterface; +} +RILNetworkInfo.prototype = { + classID: RILNETWORKINFO_CID, + classInfo: XPCOMUtils.generateCI({classID: RILNETWORKINFO_CID, + classDescription: "RILNetworkInfo", + interfaces: [Ci.nsINetworkInfo, + Ci.nsIRilNetworkInfo]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInfo, + Ci.nsIRilNetworkInfo]), + + networkInterface: null, + + getDataCall: function() { + return this.networkInterface.dataCall; + }, + + getApnSetting: function() { + return this.networkInterface.apnSetting; + }, + + debug: function(aMsg) { + dump("-*- RILNetworkInfo[" + this.serviceId + ":" + this.type + "]: " + + aMsg + "\n"); + }, + + /** + * nsINetworkInfo Implementation + */ + get state() { + let dataCall = this.getDataCall(); + if (!dataCall.inRequestedTypes(this.type)) { + return NETWORK_STATE_DISCONNECTED; + } + return dataCall.state; + }, + + type: null, + + get name() { + return this.getDataCall().linkInfo.ifname; + }, + + getAddresses: function(aIps, aPrefixLengths) { + let addresses = this.getDataCall().linkInfo.addresses; + + let ips = []; + let prefixLengths = []; + for (let i = 0; i < addresses.length; i++) { + let [ip, prefixLength] = addresses[i].split("/"); + ips.push(ip); + prefixLengths.push(prefixLength); + } + + aIps.value = ips.slice(); + aPrefixLengths.value = prefixLengths.slice(); + + return ips.length; + }, + + getGateways: function(aCount) { + let linkInfo = this.getDataCall().linkInfo; + + if (aCount) { + aCount.value = linkInfo.gateways.length; + } + + return linkInfo.gateways.slice(); + }, + + getDnses: function(aCount) { + let linkInfo = this.getDataCall().linkInfo; + + if (aCount) { + aCount.value = linkInfo.dnses.length; + } + + return linkInfo.dnses.slice(); + }, + + /** + * nsIRilNetworkInfo Implementation + */ + + serviceId: 0, + + get iccId() { + let icc = gIccService.getIccByServiceId(this.serviceId); + let iccInfo = icc && icc.iccInfo; + + return iccInfo && iccInfo.iccid; + }, + + get mmsc() { + if (this.type != NETWORK_TYPE_MOBILE_MMS) { + if (DEBUG) this.debug("Error! Only MMS network can get MMSC."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + return this.getApnSetting().mmsc || ""; + }, + + get mmsProxy() { + if (this.type != NETWORK_TYPE_MOBILE_MMS) { + if (DEBUG) this.debug("Error! Only MMS network can get MMS proxy."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + return this.getApnSetting().mmsproxy || ""; + }, + + get mmsPort() { + if (this.type != NETWORK_TYPE_MOBILE_MMS) { + if (DEBUG) this.debug("Error! Only MMS network can get MMS port."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + // Note: Port 0 is reserved, so we treat it as invalid as well. + // See http://www.iana.org/assignments/port-numbers + return this.getApnSetting().mmsport || -1; + }, + + getPcscf: function(aCount) { + if (this.type != NETWORK_TYPE_MOBILE_IMS) { + if (DEBUG) this.debug("Error! Only IMS network can get pcscf."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + let linkInfo = this.getDataCall().linkInfo; + + if (aCount) { + aCount.value = linkInfo.pcscf.length; + } + return linkInfo.pcscf.slice(); + }, +}; + +function RILNetworkInterface(aDataCallHandler, aType, aApnSetting, aDataCall) { + if (!aDataCall) { + throw new Error("No dataCall for RILNetworkInterface: " + type); + } + + this.dataCallHandler = aDataCallHandler; + this.enabled = false; + this.dataCall = aDataCall; + this.apnSetting = aApnSetting; + + this.info = new RILNetworkInfo(aDataCallHandler.clientId, aType, this); +} + +RILNetworkInterface.prototype = { + classID: RILNETWORKINTERFACE_CID, + classInfo: XPCOMUtils.generateCI({classID: RILNETWORKINTERFACE_CID, + classDescription: "RILNetworkInterface", + interfaces: [Ci.nsINetworkInterface]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterface]), + + // If this RILNetworkInterface type is enabled or not. + enabled: null, + + apnSetting: null, + + dataCall: null, + + /** + * nsINetworkInterface Implementation + */ + + info: null, + + get httpProxyHost() { + return this.apnSetting.proxy || ""; + }, + + get httpProxyPort() { + return this.apnSetting.port || ""; + }, + + get mtu() { + // Value provided by network has higher priority than apn settings. + return this.dataCall.linkInfo.mtu || this.apnSetting.mtu || -1; + }, + + // Helpers + + debug: function(aMsg) { + dump("-*- RILNetworkInterface[" + this.dataCallHandler.clientId + ":" + + this.info.type + "]: " + aMsg + "\n"); + }, + + get connected() { + return this.info.state == NETWORK_STATE_CONNECTED; + }, + + notifyRILNetworkInterface: function() { + if (DEBUG) { + this.debug("notifyRILNetworkInterface type: " + this.info.type + + ", state: " + this.info.state); + } + + gNetworkManager.updateNetworkInterface(this); + }, + + connect: function() { + this.enabled = true; + + this.dataCall.connect(this); + }, + + disconnect: function() { + if (!this.enabled) { + return; + } + this.enabled = false; + + this.dataCall.disconnect(this); + }, + + shutdown: function() { + this.dataCall.shutdown(); + this.dataCall = null; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataCallManager]);
\ No newline at end of file |