diff -r 000000000000 -r 6474c204b198 dom/system/gonk/RadioInterfaceLayer.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/system/gonk/RadioInterfaceLayer.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,4774 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"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/Sntp.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); + +var RIL = {}; +Cu.import("resource://gre/modules/ril_consts.js", RIL); + +// set to true in ril_consts.js to see debug messages +var DEBUG = RIL.DEBUG_RIL; + +// Read debug setting from pref +let debugPref = false; +try { + debugPref = Services.prefs.getBoolPref("ril.debugging.enabled"); +} catch(e) { + debugPref = false; +} +DEBUG = RIL.DEBUG_RIL || debugPref; + +function debug(s) { + dump("-*- RadioInterfaceLayer: " + s + "\n"); +} + +// Ril quirk to attach data registration on demand. +let RILQUIRKS_DATA_REGISTRATION_ON_DEMAND = + libcutils.property_get("ro.moz.ril.data_reg_on_demand", "false") == "true"; + +// Ril quirk to always turn the radio off for the client without SIM card +// except hw default client. +let RILQUIRKS_RADIO_OFF_WO_CARD = + libcutils.property_get("ro.moz.ril.radio_off_wo_card", "false") == "true"; + +// Ril quirk to enable IPv6 protocol/roaming protocol in APN settings. +let RILQUIRKS_HAVE_IPV6 = + libcutils.property_get("ro.moz.ril.ipv6", "false") == "true"; + +const RADIOINTERFACELAYER_CID = + Components.ID("{2d831c8d-6017-435b-a80c-e5d422810cea}"); +const RADIOINTERFACE_CID = + Components.ID("{6a7c91f0-a2b3-4193-8562-8969296c0b54}"); +const RILNETWORKINTERFACE_CID = + Components.ID("{3bdd52a9-3965-4130-b569-0ac5afed045e}"); +const GSMICCINFO_CID = + Components.ID("{d90c4261-a99d-47bc-8b05-b057bb7e8f8a}"); +const CDMAICCINFO_CID = + Components.ID("{39ba3c08-aacc-46d0-8c04-9b619c387061}"); + +const NS_XPCOM_SHUTDOWN_OBSERVER_ID = "xpcom-shutdown"; +const kNetworkInterfaceStateChangedTopic = "network-interface-state-changed"; +const kNetworkConnStateChangedTopic = "network-connection-state-changed"; +const kNetworkActiveChangedTopic = "network-active-changed"; +const kSmsReceivedObserverTopic = "sms-received"; +const kSilentSmsReceivedObserverTopic = "silent-sms-received"; +const kSmsSendingObserverTopic = "sms-sending"; +const kSmsSentObserverTopic = "sms-sent"; +const kSmsFailedObserverTopic = "sms-failed"; +const kSmsDeliverySuccessObserverTopic = "sms-delivery-success"; +const kSmsDeliveryErrorObserverTopic = "sms-delivery-error"; +const kMozSettingsChangedObserverTopic = "mozsettings-changed"; +const kSysMsgListenerReadyObserverTopic = "system-message-listener-ready"; +const kSysClockChangeObserverTopic = "system-clock-change"; +const kScreenStateChangedTopic = "screen-state-changed"; + +const kSettingsCellBroadcastSearchList = "ril.cellbroadcast.searchlist"; +const kSettingsClockAutoUpdateEnabled = "time.clock.automatic-update.enabled"; +const kSettingsClockAutoUpdateAvailable = "time.clock.automatic-update.available"; +const kSettingsTimezoneAutoUpdateEnabled = "time.timezone.automatic-update.enabled"; +const kSettingsTimezoneAutoUpdateAvailable = "time.timezone.automatic-update.available"; + +const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; + +const kPrefCellBroadcastDisabled = "ril.cellbroadcast.disabled"; +const kPrefClirModePreference = "ril.clirMode"; +const kPrefRilNumRadioInterfaces = "ril.numRadioInterfaces"; + +const DOM_MOBILE_MESSAGE_DELIVERY_RECEIVED = "received"; +const DOM_MOBILE_MESSAGE_DELIVERY_SENDING = "sending"; +const DOM_MOBILE_MESSAGE_DELIVERY_SENT = "sent"; +const DOM_MOBILE_MESSAGE_DELIVERY_ERROR = "error"; + +const RADIO_POWER_OFF_TIMEOUT = 30000; +const SMS_HANDLED_WAKELOCK_TIMEOUT = 5000; +const HW_DEFAULT_CLIENT_ID = 0; + +const RIL_IPC_MOBILECONNECTION_MSG_NAMES = [ + "RIL:GetRilContext", + "RIL:GetAvailableNetworks", + "RIL:SelectNetwork", + "RIL:SelectNetworkAuto", + "RIL:SetPreferredNetworkType", + "RIL:GetPreferredNetworkType", + "RIL:SendMMI", + "RIL:CancelMMI", + "RIL:RegisterMobileConnectionMsg", + "RIL:SetCallForwardingOptions", + "RIL:GetCallForwardingOptions", + "RIL:SetCallBarringOptions", + "RIL:GetCallBarringOptions", + "RIL:ChangeCallBarringPassword", + "RIL:SetCallWaitingOptions", + "RIL:GetCallWaitingOptions", + "RIL:SetCallingLineIdRestriction", + "RIL:GetCallingLineIdRestriction", + "RIL:SetRoamingPreference", + "RIL:GetRoamingPreference", + "RIL:ExitEmergencyCbMode", + "RIL:SetRadioEnabled", + "RIL:SetVoicePrivacyMode", + "RIL:GetVoicePrivacyMode", + "RIL:GetSupportedNetworkTypes" +]; + +const RIL_IPC_MOBILENETWORK_MSG_NAMES = [ + "RIL:GetLastKnownNetwork", + "RIL:GetLastKnownHomeNetwork" +]; + +const RIL_IPC_ICCMANAGER_MSG_NAMES = [ + "RIL:SendStkResponse", + "RIL:SendStkMenuSelection", + "RIL:SendStkTimerExpiration", + "RIL:SendStkEventDownload", + "RIL:GetCardLockState", + "RIL:UnlockCardLock", + "RIL:SetCardLock", + "RIL:GetCardLockRetryCount", + "RIL:IccOpenChannel", + "RIL:IccExchangeAPDU", + "RIL:IccCloseChannel", + "RIL:ReadIccContacts", + "RIL:UpdateIccContact", + "RIL:RegisterIccMsg", + "RIL:MatchMvno" +]; + +const RIL_IPC_VOICEMAIL_MSG_NAMES = [ + "RIL:RegisterVoicemailMsg", + "RIL:GetVoicemailInfo" +]; + +const RIL_IPC_CELLBROADCAST_MSG_NAMES = [ + "RIL:RegisterCellBroadcastMsg" +]; + +XPCOMUtils.defineLazyServiceGetter(this, "gPowerManagerService", + "@mozilla.org/power/powermanagerservice;1", + "nsIPowerManagerService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageService", + "@mozilla.org/mobilemessage/mobilemessageservice;1", + "nsIMobileMessageService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSmsService", + "@mozilla.org/sms/smsservice;1", + "nsISmsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageDatabaseService", + "@mozilla.org/mobilemessage/rilmobilemessagedatabaseservice;1", + "nsIRilMobileMessageDatabaseService"); + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageBroadcaster"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger", + "@mozilla.org/system-message-internal;1", + "nsISystemMessagesInternal"); + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager", + "@mozilla.org/network/manager;1", + "nsINetworkManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gTimeService", + "@mozilla.org/time/timeservice;1", + "nsITimeService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSystemWorkerManager", + "@mozilla.org/telephony/system-worker-manager;1", + "nsISystemWorkerManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gTelephonyProvider", + "@mozilla.org/telephony/telephonyprovider;1", + "nsIGonkTelephonyProvider"); + +XPCOMUtils.defineLazyGetter(this, "WAP", function() { + let wap = {}; + Cu.import("resource://gre/modules/WapPushManager.js", wap); + return wap; +}); + +XPCOMUtils.defineLazyGetter(this, "PhoneNumberUtils", function() { + let ns = {}; + Cu.import("resource://gre/modules/PhoneNumberUtils.jsm", ns); + return ns.PhoneNumberUtils; +}); + +XPCOMUtils.defineLazyGetter(this, "gMessageManager", function() { + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener, + Ci.nsIObserver]), + + ril: null, + + // Manage message targets in terms of topic. Only the authorized and + // registered contents can receive related messages. + targetsByTopic: {}, + topics: [], + + targetMessageQueue: [], + ready: false, + + init: function(ril) { + this.ril = ril; + + Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + Services.obs.addObserver(this, kSysMsgListenerReadyObserverTopic, false); + this._registerMessageListeners(); + }, + + _shutdown: function() { + this.ril = null; + + Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + this._unregisterMessageListeners(); + }, + + _registerMessageListeners: function() { + ppmm.addMessageListener("child-process-shutdown", this); + for (let msgname of RIL_IPC_MOBILECONNECTION_MSG_NAMES) { + ppmm.addMessageListener(msgname, this); + } + for (let msgname of RIL_IPC_MOBILENETWORK_MSG_NAMES) { + ppmm.addMessageListener(msgname, this); + } + for (let msgName of RIL_IPC_ICCMANAGER_MSG_NAMES) { + ppmm.addMessageListener(msgName, this); + } + for (let msgname of RIL_IPC_VOICEMAIL_MSG_NAMES) { + ppmm.addMessageListener(msgname, this); + } + for (let msgname of RIL_IPC_CELLBROADCAST_MSG_NAMES) { + ppmm.addMessageListener(msgname, this); + } + }, + + _unregisterMessageListeners: function() { + ppmm.removeMessageListener("child-process-shutdown", this); + for (let msgname of RIL_IPC_MOBILECONNECTION_MSG_NAMES) { + ppmm.removeMessageListener(msgname, this); + } + for (let msgname of RIL_IPC_MOBILENETWORK_MSG_NAMES) { + ppmm.removeMessageListener(msgname, this); + } + for (let msgName of RIL_IPC_ICCMANAGER_MSG_NAMES) { + ppmm.removeMessageListener(msgName, this); + } + for (let msgname of RIL_IPC_VOICEMAIL_MSG_NAMES) { + ppmm.removeMessageListener(msgname, this); + } + for (let msgname of RIL_IPC_CELLBROADCAST_MSG_NAMES) { + ppmm.removeMessageListener(msgname, this); + } + ppmm = null; + }, + + _registerMessageTarget: function(topic, target) { + let targets = this.targetsByTopic[topic]; + if (!targets) { + targets = this.targetsByTopic[topic] = []; + let list = this.topics; + if (list.indexOf(topic) == -1) { + list.push(topic); + } + } + + if (targets.indexOf(target) != -1) { + if (DEBUG) debug("Already registered this target!"); + return; + } + + targets.push(target); + if (DEBUG) debug("Registered " + topic + " target: " + target); + }, + + _unregisterMessageTarget: function(topic, target) { + if (topic == null) { + // Unregister the target for every topic when no topic is specified. + for (let type of this.topics) { + this._unregisterMessageTarget(type, target); + } + return; + } + + // Unregister the target for a specified topic. + let targets = this.targetsByTopic[topic]; + if (!targets) { + return; + } + + let index = targets.indexOf(target); + if (index != -1) { + targets.splice(index, 1); + if (DEBUG) debug("Unregistered " + topic + " target: " + target); + } + }, + + _enqueueTargetMessage: function(topic, message, options) { + let msg = { topic : topic, + message : message, + options : options }; + // Remove previous queued message with the same message type and client Id + // , only one message per (message type + client Id) is allowed in queue. + let messageQueue = this.targetMessageQueue; + for(let i = 0; i < messageQueue.length; i++) { + if (messageQueue[i].message === message && + messageQueue[i].options.clientId === options.clientId) { + messageQueue.splice(i, 1); + break; + } + } + + messageQueue.push(msg); + }, + + _sendTargetMessage: function(topic, message, options) { + if (!this.ready) { + this._enqueueTargetMessage(topic, message, options); + return; + } + + let targets = this.targetsByTopic[topic]; + if (!targets) { + return; + } + + for (let target of targets) { + target.sendAsyncMessage(message, options); + } + }, + + _resendQueuedTargetMessage: function() { + this.ready = true; + + // Here uses this._sendTargetMessage() to resend message, which will + // enqueue message if listener is not ready. + // So only resend after listener is ready, or it will cause infinate loop and + // hang the system. + + // Dequeue and resend messages. + for each (let msg in this.targetMessageQueue) { + this._sendTargetMessage(msg.topic, msg.message, msg.options); + } + this.targetMessageQueue = null; + }, + + /** + * nsIMessageListener interface methods. + */ + + receiveMessage: function(msg) { + if (DEBUG) debug("Received '" + msg.name + "' message from content process"); + if (msg.name == "child-process-shutdown") { + // By the time we receive child-process-shutdown, the child process has + // already forgotten its permissions so we need to unregister the target + // for every permission. + this._unregisterMessageTarget(null, msg.target); + return null; + } + + if (RIL_IPC_MOBILECONNECTION_MSG_NAMES.indexOf(msg.name) != -1) { + if (!msg.target.assertPermission("mobileconnection")) { + if (DEBUG) { + debug("MobileConnection message " + msg.name + + " from a content process with no 'mobileconnection' privileges."); + } + return null; + } + } else if (RIL_IPC_MOBILENETWORK_MSG_NAMES.indexOf(msg.name) != -1) { + if (!msg.target.assertPermission("mobilenetwork")) { + if (DEBUG) { + debug("MobileNetwork message " + msg.name + + " from a content process with no 'mobilenetwork' privileges."); + } + return null; + } + } else if (RIL_IPC_ICCMANAGER_MSG_NAMES.indexOf(msg.name) != -1) { + if (!msg.target.assertPermission("mobileconnection")) { + if (DEBUG) { + debug("IccManager message " + msg.name + + " from a content process with no 'mobileconnection' privileges."); + } + return null; + } + } else if (RIL_IPC_VOICEMAIL_MSG_NAMES.indexOf(msg.name) != -1) { + if (!msg.target.assertPermission("voicemail")) { + if (DEBUG) { + debug("Voicemail message " + msg.name + + " from a content process with no 'voicemail' privileges."); + } + return null; + } + } else if (RIL_IPC_CELLBROADCAST_MSG_NAMES.indexOf(msg.name) != -1) { + if (!msg.target.assertPermission("cellbroadcast")) { + if (DEBUG) { + debug("Cell Broadcast message " + msg.name + + " from a content process with no 'cellbroadcast' privileges."); + } + return null; + } + } else { + if (DEBUG) debug("Ignoring unknown message type: " + msg.name); + return null; + } + + switch (msg.name) { + case "RIL:RegisterMobileConnectionMsg": + this._registerMessageTarget("mobileconnection", msg.target); + return null; + case "RIL:RegisterIccMsg": + this._registerMessageTarget("icc", msg.target); + return null; + case "RIL:RegisterVoicemailMsg": + this._registerMessageTarget("voicemail", msg.target); + return null; + case "RIL:RegisterCellBroadcastMsg": + this._registerMessageTarget("cellbroadcast", msg.target); + return null; + } + + let clientId = msg.json.clientId || 0; + let radioInterface = this.ril.getRadioInterface(clientId); + if (!radioInterface) { + if (DEBUG) debug("No such radio interface: " + clientId); + return null; + } + + if (msg.name === "RIL:SetRadioEnabled") { + // Special handler for SetRadioEnabled. + return gRadioEnabledController.receiveMessage(msg); + } + + return radioInterface.receiveMessage(msg); + }, + + /** + * nsIObserver interface methods. + */ + + observe: function(subject, topic, data) { + switch (topic) { + case kSysMsgListenerReadyObserverTopic: + Services.obs.removeObserver(this, kSysMsgListenerReadyObserverTopic); + this._resendQueuedTargetMessage(); + break; + case NS_XPCOM_SHUTDOWN_OBSERVER_ID: + this._shutdown(); + break; + } + }, + + sendMobileConnectionMessage: function(message, clientId, data) { + this._sendTargetMessage("mobileconnection", message, { + clientId: clientId, + data: data + }); + }, + + sendVoicemailMessage: function(message, clientId, data) { + this._sendTargetMessage("voicemail", message, { + clientId: clientId, + data: data + }); + }, + + sendCellBroadcastMessage: function(message, clientId, data) { + this._sendTargetMessage("cellbroadcast", message, { + clientId: clientId, + data: data + }); + }, + + sendIccMessage: function(message, clientId, data) { + this._sendTargetMessage("icc", message, { + clientId: clientId, + data: data + }); + } + }; +}); + +XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() { + let _ril = null; + let _pendingMessages = []; // For queueing "RIL =SetRadioEnabled" messages. + let _isProcessingPending = false; + let _timer = null; + let _request = null; + let _deactivatingDeferred = {}; + let _initializedCardState = {}; + let _allCardStateInitialized = !RILQUIRKS_RADIO_OFF_WO_CARD; + + return { + init: function(ril) { + _ril = ril; + }, + + receiveCardState: function(clientId) { + if (_allCardStateInitialized) { + return; + } + + if (DEBUG) debug("RadioControl: receive cardState from " + clientId); + _initializedCardState[clientId] = true; + if (Object.keys(_initializedCardState).length == _ril.numRadioInterfaces) { + _allCardStateInitialized = true; + this._startProcessingPending(); + } + }, + + receiveMessage: function(msg) { + if (DEBUG) debug("RadioControl: receiveMessage: " + JSON.stringify(msg)); + _pendingMessages.push(msg); + this._startProcessingPending(); + }, + + isDeactivatingDataCalls: function() { + return _request !== null; + }, + + finishDeactivatingDataCalls: function(clientId) { + if (DEBUG) debug("RadioControl: finishDeactivatingDataCalls: " + clientId); + let deferred = _deactivatingDeferred[clientId]; + if (deferred) { + deferred.resolve(); + } + }, + + _startProcessingPending: function() { + if (!_isProcessingPending) { + if (DEBUG) debug("RadioControl: start dequeue"); + _isProcessingPending = true; + this._processNextMessage(); + } + }, + + _processNextMessage: function() { + if (_pendingMessages.length === 0 || !_allCardStateInitialized) { + if (DEBUG) debug("RadioControl: stop dequeue"); + _isProcessingPending = false; + return; + } + + let msg = _pendingMessages.shift(); + this._handleMessage(msg); + }, + + _getNumCards: function() { + let numCards = 0; + for (let i = 0, N = _ril.numRadioInterfaces; i < N; ++i) { + if (this._isCardPresentAtClient(i)) { + numCards++; + } + } + return numCards; + }, + + _isCardPresentAtClient: function(clientId) { + let cardState = _ril.getRadioInterface(clientId).rilContext.cardState; + return cardState !== RIL.GECKO_CARDSTATE_UNDETECTED && + cardState !== RIL.GECKO_CARDSTATE_UNKNOWN; + }, + + _isRadioAbleToEnableAtClient: function(clientId, numCards) { + if (!RILQUIRKS_RADIO_OFF_WO_CARD) { + return true; + } + + // We could only turn on the radio for clientId if + // 1. a SIM card is presented or + // 2. it is the default clientId and there is no any SIM card at any client. + + if (this._isCardPresentAtClient(clientId)) { + return true; + } + + numCards = numCards == null ? this._getNumCards() : numCards; + if (clientId === HW_DEFAULT_CLIENT_ID && numCards === 0) { + return true; + } + + return false; + }, + + _handleMessage: function(msg) { + if (DEBUG) debug("RadioControl: handleMessage: " + JSON.stringify(msg)); + let clientId = msg.json.clientId || 0; + let radioInterface = _ril.getRadioInterface(clientId); + + if (!radioInterface.isValidStateForSetRadioEnabled()) { + radioInterface.setRadioEnabledResponse(msg.target, msg.json.data, + "InvalidStateError"); + this._processNextMessage(); + return; + } + + if (radioInterface.isDummyForSetRadioEnabled(msg.json.data)) { + radioInterface.setRadioEnabledResponse(msg.target, msg.json.data); + this._processNextMessage(); + return; + } + + if (msg.json.data.enabled) { + if (this._isRadioAbleToEnableAtClient(clientId)) { + radioInterface.receiveMessage(msg); + } else { + // Not really do it but respond success. + radioInterface.setRadioEnabledResponse(msg.target, msg.json.data); + } + + this._processNextMessage(); + } else { + _request = function() { + radioInterface.receiveMessage(msg); + }; + + // In 2G network, modem takes 35+ seconds to process deactivate data + // call request if device has active voice call (please see bug 964974 + // for more details). Therefore we should hangup all active voice calls + // first. And considering some DSDS architecture, toggling one radio may + // toggle both, so we send hangUpAll to all clients. + for (let i = 0, N = _ril.numRadioInterfaces; i < N; ++i) { + let iface = _ril.getRadioInterface(i); + iface.workerMessenger.send("hangUpAll"); + } + + // In some DSDS architecture with only one modem, toggling one radio may + // toggle both. Therefore, for safely turning off, we should first + // explicitly deactivate all data calls from all clients. + this._deactivateDataCalls().then(() => { + if (DEBUG) debug("RadioControl: deactivation done"); + this._executeRequest(); + }); + + this._createTimer(); + } + }, + + _deactivateDataCalls: function() { + if (DEBUG) debug("RadioControl: deactivating data calls..."); + _deactivatingDeferred = {}; + + let promise = Promise.resolve(); + for (let i = 0, N = _ril.numRadioInterfaces; i < N; ++i) { + promise = promise.then(this._deactivateDataCallsForClient(i)); + } + + return promise; + }, + + _deactivateDataCallsForClient: function(clientId) { + return function() { + let deferred = _deactivatingDeferred[clientId] = Promise.defer(); + let dataConnectionHandler = gDataConnectionManager.getConnectionHandler(clientId); + dataConnectionHandler.deactivateDataCalls(); + return deferred.promise; + }; + }, + + _createTimer: function() { + if (!_timer) { + _timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + _timer.initWithCallback(this._executeRequest.bind(this), + RADIO_POWER_OFF_TIMEOUT, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + _cancelTimer: function() { + if (_timer) { + _timer.cancel(); + } + }, + + _executeRequest: function() { + if (typeof _request === "function") { + if (DEBUG) debug("RadioControl: executeRequest"); + this._cancelTimer(); + _request(); + _request = null; + } + this._processNextMessage(); + }, + }; +}); + +XPCOMUtils.defineLazyGetter(this, "gDataConnectionManager", function () { + return { + QueryInterface: XPCOMUtils.generateQI([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(s) { + dump("-*- DataConnectionManager: " + s + "\n"); + }, + + init: function(ril) { + if (!ril) { + return; + } + + this._connectionHandlers = []; + for (let clientId = 0; clientId < ril.numRadioInterfaces; clientId++) { + let radioInterface = ril.getRadioInterface(clientId); + this._connectionHandlers.push( + new DataConnectionHandler(clientId, radioInterface)); + } + + 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, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false); + Services.obs.addObserver(this, kNetworkInterfaceStateChangedTopic, false); + }, + + getConnectionHandler: function(clientId) { + return this._connectionHandlers[clientId]; + }, + + _handleDataClientIdChange: function(newDefault) { + if (this._dataDefaultClientId === newDefault) { + return; + } + this._dataDefaultClientId = newDefault; + + if (this._currentDataClientId == -1) { + // This is to handle boot up stage. + this._currentDataClientId = this._dataDefaultClientId; + let connHandler = this._connectionHandlers[this._currentDataClientId]; + let radioInterface = connHandler.radioInterface; + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND) { + radioInterface.setDataRegistration(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.radioInterface; + let oldSettings = oldConnHandler.dataCallSettings; + let newConnHandler = this._connectionHandlers[this._dataDefaultClientId]; + let newIface = newConnHandler.radioInterface; + let newSettings = newConnHandler.dataCallSettings; + + if (!this._dataEnabled) { + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND) { + oldIface.setDataRegistration(false); + newIface.setDataRegistration(true); + } + this._currentDataClientId = this._dataDefaultClientId; + return; + } + + oldSettings.oldEnabled = oldSettings.enabled; + oldSettings.enabled = false; + + if (oldConnHandler.anyDataConnected()) { + this._pendingDataCallRequest = function () { + if (DEBUG) { + this.debug("Executing pending data call request."); + } + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND) { + newIface.setDataRegistration(true); + } + newSettings.oldEnabled = newSettings.enabled; + newSettings.enabled = this._dataEnabled; + + this._currentDataClientId = this._dataDefaultClientId; + newConnHandler.updateRILNetworkInterface(); + }; + + if (DEBUG) { + this.debug("_handleDataClientIdChange: existing data call(s) active" + + ", wait for them to get disconnected."); + } + oldConnHandler.deactivateDataCalls(); + return; + } + + newSettings.oldEnabled = newSettings.enabled; + newSettings.enabled = true; + + this._currentDataClientId = this._dataDefaultClientId; + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND) { + oldIface.setDataRegistration(false); + newIface.setDataRegistration(true); + } + newConnHandler.updateRILNetworkInterface(); + }, + + _shutdown: function() { + for (let handler of this._connectionHandlers) { + handler.shutdown(); + } + this._connectionHandlers = null; + Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic); + Services.obs.removeObserver(this, kNetworkInterfaceStateChangedTopic); + }, + + /** + * nsISettingsServiceCallback + */ + handle: function(name, result) { + switch(name) { + case "ril.data.apnSettings": + if (DEBUG) { + this.debug("'ril.data.apnSettings' is now " + + JSON.stringify(result)); + } + if (!result) { + break; + } + for (let clientId in this._connectionHandlers) { + let handler = this._connectionHandlers[clientId]; + let apnSetting = result[clientId]; + if (handler && apnSetting) { + handler.updateApnSettings(apnSetting); + handler.updateRILNetworkInterface(); + } + } + break; + case "ril.data.enabled": + if (DEBUG) { + this.debug("'ril.data.enabled' is now " + result); + } + if (this._dataEnabled === result) { + break; + } + this._dataEnabled = result; + + 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 = result; + connHandler.updateRILNetworkInterface(); + break; + case "ril.data.roaming_enabled": + if (DEBUG) { + this.debug("'ril.data.roaming_enabled' is now " + result); + 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(result) ? result[clientId] : result; + } + 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": + result = result || 0; + if (DEBUG) { + this.debug("'ril.data.defaultServiceId' is now " + result); + } + this._handleDataClientIdChange(result); + break; + } + }, + + handleError: function(errorMessage) { + if (DEBUG) { + this.debug("There was an error while reading RIL settings."); + } + }, + + /** + * nsIObserver interface methods. + */ + observe: function(subject, topic, data) { + switch (topic) { + case kMozSettingsChangedObserverTopic: + let setting = JSON.parse(data); + this.handle(setting.key, setting.value); + break; + case kNetworkInterfaceStateChangedTopic: + let network = subject.QueryInterface(Ci.nsINetworkInterface); + // DSDS: setup pending data connection when switching the default id + // for data call. We can not use network.type to tell if it's + // NETWORK_TYPE_MOBILE, since the type is removed from + // RILNetworkInterface.connectedTypes on disconnect(). + if (network.state == Ci.nsINetworkInterface.NETWORK_STATE_UNKNOWN) { + let connHandler = this._connectionHandlers[this._currentDataClientId]; + let radioInterface = connHandler.radioInterface; + if (connHandler.allDataDisconnected() && + typeof this._pendingDataCallRequest === "function") { + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND) { + radioInterface.setDataRegistration(false); + } + if (DEBUG) { + this.debug("All data calls disconnected, setup pending data call."); + } + this._pendingDataCallRequest(); + this._pendingDataCallRequest = null; + } + } + break; + case NS_XPCOM_SHUTDOWN_OBSERVER_ID: + this._shutdown(); + break; + } + }, + }; +}); + +// Initialize shared preference "ril.numRadioInterfaces" according to system +// property. +try { + Services.prefs.setIntPref(kPrefRilNumRadioInterfaces, (function() { + // When Gonk property "ro.moz.ril.numclients" is not set, return 1; if + // explicitly set to any number larger-equal than 0, return num; else, return + // 1 for compatibility. + try { + let numString = libcutils.property_get("ro.moz.ril.numclients", "1"); + let num = parseInt(numString, 10); + if (num >= 0) { + return num; + } + } catch (e) {} + + return 1; + })()); +} catch (e) {} + +function IccInfo() {} +IccInfo.prototype = { + iccType: null, + iccid: null, + mcc: null, + mnc: null, + spn: null, + isDisplayNetworkNameRequired: null, + isDisplaySpnRequired: null +}; + +function GsmIccInfo() {} +GsmIccInfo.prototype = { + __proto__: IccInfo.prototype, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMMozGsmIccInfo]), + classID: GSMICCINFO_CID, + classInfo: XPCOMUtils.generateCI({ + classID: GSMICCINFO_CID, + classDescription: "MozGsmIccInfo", + flags: Ci.nsIClassInfo.DOM_OBJECT, + interfaces: [Ci.nsIDOMMozGsmIccInfo] + }), + + // nsIDOMMozGsmIccInfo + + msisdn: null +}; + +function CdmaIccInfo() {} +CdmaIccInfo.prototype = { + __proto__: IccInfo.prototype, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMMozCdmaIccInfo]), + classID: CDMAICCINFO_CID, + classInfo: XPCOMUtils.generateCI({ + classID: CDMAICCINFO_CID, + classDescription: "MozCdmaIccInfo", + flags: Ci.nsIClassInfo.DOM_OBJECT, + interfaces: [Ci.nsIDOMMozCdmaIccInfo] + }), + + // nsIDOMMozCdmaIccInfo + + mdn: null, + prlVersion: 0 +}; + +function DataConnectionHandler(clientId, radioInterface) { + // Initial owning attributes. + this.clientId = clientId; + this.radioInterface = radioInterface; + this.dataCallSettings = { + oldEnabled: false, + enabled: false, + roamingEnabled: false + }; + this._dataCallbacks = []; + + // This matrix is used to keep all the APN settings. + // - |byApn| object makes it easier to get the corresponding APN setting + // via a given set of APN, user name and password. + // - |byType| object makes it easier to get the corresponding APN setting + // via a given APN type. + this.apnSettings = { + byType: {}, + byApn: {} + }; +} +DataConnectionHandler.prototype = { + clientId: 0, + radioInterface: null, + // Data calls setting. + dataCallSettings: null, + apnSettings: null, + + // Apn settings to be setup after data call are cleared. + _pendingApnSettings: null, + + debug: function(s) { + dump("-*- DataConnectionHandler[" + this.clientId + "]: " + s + "\n"); + }, + + shutdown: function() { + // Shutdown all RIL network interfaces + for (let [, apnSetting] of Iterator(this.apnSettings.byApn)) { + if (apnSetting.iface) { + apnSetting.iface.shutdown(); + } + } + this.clientId = null; + this.radioInterface = null; + }, + + /** + * Check if we get all necessary APN data. + */ + _validateApnSetting: function(apnSetting) { + return (apnSetting && + apnSetting.apn && + apnSetting.types && + apnSetting.types.length); + }, + + _deliverDataCallCallback: function(name, args) { + // We need to worry about callback registration state mutations during the + // callback firing. The behaviour we want is to *not* call any callbacks + // that are added during the firing and to *not* call any callbacks that are + // removed during the firing. To address this, we make a copy of the + // callback list before dispatching and then double-check that each callback + // is still registered before calling it. + let callbacks = this._dataCallbacks.slice(); + for (let callback of callbacks) { + if (this._dataCallbacks.indexOf(callback) == -1) { + continue; + } + try { + let handler = callback[name]; + if (typeof handler !== "function") { + throw new Error("No handler for " + name); + } + handler.apply(callback, args); + } catch (e) { + if (DEBUG) { + this.debug("callback handler for " + name + " threw an exception: " + e); + } + } + } + }, + + /** + * 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(newApnSettings) { + if (!newApnSettings) { + return; + } + if (DEBUG) this.debug("setupApnSettings: " + JSON.stringify(newApnSettings)); + + // Unregister anything from iface and delete it. + for (let [, apnSetting] in Iterator(this.apnSettings.byApn)) { + if (apnSetting.iface.name in gNetworkManager.networkInterfaces) { + gNetworkManager.unregisterNetworkInterface(apnSetting.iface); + } + this.unregisterDataCallCallback(apnSetting.iface); + delete apnSetting.iface; + } + this.apnSettings.byApn = {}; + this.apnSettings.byType = {}; + + // Cache the APN settings by APNs and by types in the RIL. + for (let inputApnSetting of newApnSettings) { + if (!this._validateApnSetting(inputApnSetting)) { + continue; + } + + // Combine APN, user name, and password as the key of |byApn| object to + // refer to the corresponding APN setting. + let apnKey = inputApnSetting.apn + + (inputApnSetting.user || "") + + (inputApnSetting.password || ""); + + if (!this.apnSettings.byApn[apnKey]) { + this.apnSettings.byApn[apnKey] = inputApnSetting; + } else { + this.apnSettings.byApn[apnKey].types = + this.apnSettings.byApn[apnKey].types.concat(inputApnSetting.types); + } + + // Use APN type as the index of |byType| object to refer to the + // corresponding APN setting. + for (let type of inputApnSetting.types) { + this.apnSettings.byType[type] = this.apnSettings.byApn[apnKey]; + } + } + + // Create RilNetworkInterface for each APN setting that just cached. + for (let [, apnSetting] in Iterator(this.apnSettings.byApn)) { + apnSetting.iface = new RILNetworkInterface(this, apnSetting); + } + }, + + /** + * Check if all data is disconnected. + */ + allDataDisconnected: function() { + for (let [, apnSetting] of Iterator(this.apnSettings.byApn)) { + let iface = apnSetting.iface; + if (iface && iface.state != RIL.GECKO_NETWORK_STATE_UNKNOWN && + iface.state != RIL.GECKO_NETWORK_STATE_DISCONNECTED) { + return false; + } + } + return true; + }, + + /** + * Check if there is any activated data connection. + */ + anyDataConnected: function() { + for (let [, apnSetting] of Iterator(this.apnSettings.byApn)) { + let iface = apnSetting.iface; + if (iface && iface.state == RIL.GECKO_NETWORK_STATE_CONNECTED) { + return true; + } + } + return false; + }, + + updateApnSettings: function(newApnSettings) { + if (!newApnSettings) { + return; + } + if (this._pendingApnSettings) { + // Change of apn settings in process, just update to the newest. + this._pengingApnSettings = newApnSettings; + return; + } + + let isDeactivatingDataCalls = false; + // Clear the cached APN settings in the RIL. + for (let [, apnSetting] of Iterator(this.apnSettings.byApn)) { + // Clear all existing connections based on APN types. + for (let type of apnSetting.types) { + if (this.getDataCallStateByType(type) == + RIL.GECKO_NETWORK_STATE_CONNECTED) { + this.deactivateDataCallByType(type); + isDeactivatingDataCalls = true; + } + } + } + if (isDeactivatingDataCalls) { + // Defer apn settings setup until all data calls are cleared. + this._pendingApnSettings = newApnSettings; + return; + } + this._setupApnSettings(newApnSettings); + }, + + updateRILNetworkInterface: function() { + let apnSetting = this.apnSettings.byType.default; + if (!this._validateApnSetting(apnSetting)) { + if (DEBUG) { + this.debug("We haven't gotten completely the APN data."); + } + return; + } + + // This check avoids data call connection if the radio is not ready + // yet after toggling off airplane mode. + let rilContext = this.radioInterface.rilContext; + if (rilContext.radioState != RIL.GECKO_RADIOSTATE_READY) { + 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 defaultDataCallState = this.getDataCallStateByType("default"); + if (defaultDataCallState == RIL.GECKO_NETWORK_STATE_CONNECTING || + defaultDataCallState == RIL.GECKO_NETWORK_STATE_DISCONNECTING) { + if (DEBUG) { + this.debug("Nothing to do during connecting/disconnecting in progress."); + } + return; + } + + let dataInfo = rilContext.data; + let isRegistered = + dataInfo.state == RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED; + let haveDataConnection = + 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.active && + gNetworkManager.active.type == Ci.nsINetworkInterface.NETWORK_TYPE_WIFI) { + wifi_active = true; + } + + let defaultDataCallConnected = defaultDataCallState == + RIL.GECKO_NETWORK_STATE_CONNECTED; + if (defaultDataCallConnected && + (!this.dataCallSettings.enabled || + (dataInfo.roaming && !this.dataCallSettings.roamingEnabled))) { + if (DEBUG) { + this.debug("Data call settings: disconnect data call."); + } + this.deactivateDataCallByType("default"); + return; + } + + if (defaultDataCallConnected && wifi_active) { + if (DEBUG) { + this.debug("Disconnect data call when Wifi is connected."); + } + this.deactivateDataCallByType("default"); + 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; + } + + let detailedRadioState = rilContext.detailedRadioState; + if (gRadioEnabledController.isDeactivatingDataCalls() || + detailedRadioState == RIL.GECKO_DETAILED_RADIOSTATE_ENABLING || + detailedRadioState == RIL.GECKO_DETAILED_RADIOSTATE_DISABLING) { + // We're changing the radio power currently, ignore any changes. + return; + } + + if (DEBUG) { + this.debug("Data call settings: connect data call."); + } + this.setupDataCallByType("default"); + }, + + getDataCallStateByType: function(apnType) { + let apnSetting = this.apnSettings.byType[apnType]; + if (!apnSetting) { + return RIL.GECKO_NETWORK_STATE_UNKNOWN; + } + if (!apnSetting.iface.inConnectedTypes(apnType)) { + return RIL.GECKO_NETWORK_STATE_DISCONNECTED; + } + return apnSetting.iface.state; + }, + + setupDataCallByType: function(apnType) { + if (DEBUG) { + this.debug("setupDataCallByType: " + apnType); + } + let apnSetting = this.apnSettings.byType[apnType]; + if (!apnSetting) { + if (DEBUG) { + this.debug("No apn setting for type: " + apnType); + } + return; + } + + let dataInfo = this.radioInterface.rilContext.data; + if (dataInfo.state != RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED || + dataInfo.type == RIL.GECKO_MOBILE_CONNECTION_STATE_UNKNOWN) { + return; + } + + apnSetting.iface.connect(apnType); + // We just call connect() function, so this interface should be in + // connecting state. If this interface is already in connected state, we + // are sure that this interface have successfully established connection + // for other data call types before we call connect() function for current + // data call type. In this circumstance, we have to directly update the + // necessary data call and interface information to RILContentHelper + // and network manager for current data call type. + if (apnSetting.iface.connected) { + // Update the interface status via-registration if the interface has + // already been registered in the network manager. + if (apnSetting.iface.name in gNetworkManager.networkInterfaces) { + gNetworkManager.unregisterNetworkInterface(apnSetting.iface); + } + gNetworkManager.registerNetworkInterface(apnSetting.iface); + + Services.obs.notifyObservers(apnSetting.iface, + kNetworkInterfaceStateChangedTopic, + null); + } + }, + + deactivateDataCallByType: function(apnType) { + if (DEBUG) { + this.debug("deactivateDataCallByType: " + apnType); + } + let apnSetting = this.apnSettings.byType[apnType]; + if (!apnSetting) { + if (DEBUG) { + this.debug("No apn setting for type: " + apnType); + } + return; + } + + apnSetting.iface.disconnect(apnType); + // We just call disconnect() function, so this interface should be in + // disconnecting state. If this interface is still in connected state, we + // are sure that other data call types still need this connection of this + // interface. In this circumstance, we have to directly update the + // necessary data call and interface information to RILContentHelper + // and network manager for current data call type. + if (apnSetting.iface.connectedTypes.length && apnSetting.iface.connected) { + // Update the interface status via-registration if the interface has + // already been registered in the network manager. + if (apnSetting.iface.name in gNetworkManager.networkInterfaces) { + gNetworkManager.unregisterNetworkInterface(apnSetting.iface); + } + gNetworkManager.registerNetworkInterface(apnSetting.iface); + + Services.obs.notifyObservers(apnSetting.iface, + kNetworkInterfaceStateChangedTopic, + null); + } + }, + + deactivateDataCalls: function() { + let dataDisconnecting = false; + for (let [, apnSetting] of Iterator(this.apnSettings.byApn)) { + for (let type of apnSetting.types) { + if (this.getDataCallStateByType(type) == + RIL.GECKO_NETWORK_STATE_CONNECTED) { + this.deactivateDataCallByType(type); + dataDisconnecting = true; + } + } + } + + // No data calls exist. It's safe to proceed the pending radio power off + // request. + if (gRadioEnabledController.isDeactivatingDataCalls() && !dataDisconnecting) { + gRadioEnabledController.finishDeactivatingDataCalls(this.clientId); + } + }, + + registerDataCallCallback: function(callback) { + if (this._dataCallbacks.indexOf(callback) != -1) { + throw new Error("Already registered this callback: " + callback); + } + this._dataCallbacks.push(callback); + if (DEBUG) { + this.debug("Registering callback: " + callback); + } + }, + + unregisterDataCallCallback: function(callback) { + let index = this._dataCallbacks.indexOf(callback); + if (index != -1) { + this._dataCallbacks.splice(index, 1); + if (DEBUG) { + this.debug("Unregistering callback: " + callback); + } + } + }, + + /** + * Handle data errors. + */ + handleDataCallError: function(message) { + // Notify data call error only for data APN + let apnSetting = this.apnSettings && this.apnSettings.byType.default; + if (apnSetting) { + if (message.apn == apnSetting.apn && + apnSetting.iface.inConnectedTypes("default")) { + gMessageManager.sendMobileConnectionMessage("RIL:DataError", + this.clientId, message); + } + } + + this._deliverDataCallCallback("dataCallError", [message]); + }, + + /** + * Handle data call state changes. + */ + handleDataCallState: function(datacall) { + this._deliverDataCallCallback("dataCallStateChanged", [datacall]); + + // Process pending radio power off request after all data calls + // are disconnected. + if (datacall.state == RIL.GECKO_NETWORK_STATE_UNKNOWN && + this.allDataDisconnected()) { + if (gRadioEnabledController.isDeactivatingDataCalls()) { + if (DEBUG) { + this.debug("All data connections are disconnected."); + } + gRadioEnabledController.finishDeactivatingDataCalls(this.clientId); + } + + if (this._pendingApnSettings) { + if (DEBUG) { + this.debug("Setup pending apn settings."); + } + this._setupApnSettings(this._pendingApnSettings); + this._pendingApnSettings = null; + this.updateRILNetworkInterface(); + } + } + }, +}; + +function RadioInterfaceLayer() { + let workerMessenger = new WorkerMessenger(); + workerMessenger.init(); + + let numIfaces = this.numRadioInterfaces; + if (DEBUG) debug(numIfaces + " interfaces"); + this.radioInterfaces = []; + for (let clientId = 0; clientId < numIfaces; clientId++) { + this.radioInterfaces.push(new RadioInterface(clientId, workerMessenger)); + } + + Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + + gMessageManager.init(this); + gRadioEnabledController.init(this); + gDataConnectionManager.init(this); +} +RadioInterfaceLayer.prototype = { + + classID: RADIOINTERFACELAYER_CID, + classInfo: XPCOMUtils.generateCI({classID: RADIOINTERFACELAYER_CID, + classDescription: "RadioInterfaceLayer", + interfaces: [Ci.nsIRadioInterfaceLayer]}), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRadioInterfaceLayer, + Ci.nsIObserver]), + + /** + * nsIObserver interface methods. + */ + + observe: function(subject, topic, data) { + switch (topic) { + case NS_XPCOM_SHUTDOWN_OBSERVER_ID: + for (let radioInterface of this.radioInterfaces) { + radioInterface.shutdown(); + } + this.radioInterfaces = null; + Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + break; + } + }, + + /** + * nsIRadioInterfaceLayer interface methods. + */ + + getRadioInterface: function(clientId) { + return this.radioInterfaces[clientId]; + }, + + setMicrophoneMuted: function(muted) { + for (let clientId = 0; clientId < this.numRadioInterfaces; clientId++) { + let radioInterface = this.radioInterfaces[clientId]; + radioInterface.workerMessenger.send("setMute", { muted: muted }); + } + } +}; + +XPCOMUtils.defineLazyGetter(RadioInterfaceLayer.prototype, + "numRadioInterfaces", function() { + try { + return Services.prefs.getIntPref(kPrefRilNumRadioInterfaces); + } catch(e) {} + + return 1; +}); + +function WorkerMessenger() { + // Initial owning attributes. + this.radioInterfaces = []; + this.tokenCallbackMap = {}; + + this.worker = new ChromeWorker("resource://gre/modules/ril_worker.js"); + this.worker.onerror = this.onerror.bind(this); + this.worker.onmessage = this.onmessage.bind(this); +} +WorkerMessenger.prototype = { + radioInterfaces: null, + worker: null, + + // This gets incremented each time we send out a message. + token: 1, + + // Maps tokens we send out with messages to the message callback. + tokenCallbackMap: null, + + init: function() { + let options = { + debug: DEBUG, + cellBroadcastDisabled: false, + clirMode: RIL.CLIR_DEFAULT, + quirks: { + callstateExtraUint32: + libcutils.property_get("ro.moz.ril.callstate_extra_int", "false") === "true", + v5Legacy: + libcutils.property_get("ro.moz.ril.v5_legacy", "true") === "true", + requestUseDialEmergencyCall: + libcutils.property_get("ro.moz.ril.dial_emergency_call", "false") === "true", + simAppStateExtraFields: + libcutils.property_get("ro.moz.ril.simstate_extra_field", "false") === "true", + extraUint2ndCall: + libcutils.property_get("ro.moz.ril.extra_int_2nd_call", "false") == "true", + haveQueryIccLockRetryCount: + libcutils.property_get("ro.moz.ril.query_icc_count", "false") == "true", + sendStkProfileDownload: + libcutils.property_get("ro.moz.ril.send_stk_profile_dl", "false") == "true", + dataRegistrationOnDemand: + libcutils.property_get("ro.moz.ril.data_reg_on_demand", "false") == "true" + }, + rilEmergencyNumbers: libcutils.property_get("ril.ecclist") || + libcutils.property_get("ro.ril.ecclist") + }; + + try { + options.cellBroadcastDisabled = + Services.prefs.getBoolPref(kPrefCellBroadcastDisabled); + } catch(e) {} + + try { + options.clirMode = Services.prefs.getIntPref(kPrefClirModePreference); + } catch(e) {} + + this.send(null, "setInitialOptions", options); + }, + + debug: function(aClientId, aMessage) { + // We use the same debug subject with RadioInterface's here. + dump("-*- RadioInterface[" + aClientId + "]: " + aMessage + "\n"); + }, + + onerror: function(event) { + if (DEBUG) { + this.debug("X", "Got an error: " + event.filename + ":" + + event.lineno + ": " + event.message + "\n"); + } + event.preventDefault(); + }, + + /** + * Process the incoming message from the RIL worker. + */ + onmessage: function(event) { + let message = event.data; + let clientId = message.rilMessageClientId; + if (clientId === null) { + return; + } + + if (DEBUG) { + this.debug(clientId, "Received message from worker: " + JSON.stringify(message)); + } + + let token = message.rilMessageToken; + if (token == null) { + // That's an unsolicited message. Pass to RadioInterface directly. + let radioInterface = this.radioInterfaces[clientId]; + radioInterface.handleUnsolicitedWorkerMessage(message); + return; + } + + let callback = this.tokenCallbackMap[message.rilMessageToken]; + if (!callback) { + if (DEBUG) this.debug(clientId, "Ignore orphan token: " + message.rilMessageToken); + return; + } + + let keep = false; + try { + keep = callback(message); + } catch(e) { + if (DEBUG) this.debug(clientId, "callback throws an exception: " + e); + } + + if (!keep) { + delete this.tokenCallbackMap[message.rilMessageToken]; + } + }, + + registerClient: function(aClientId, aRadioInterface) { + if (DEBUG) this.debug(aClientId, "Starting RIL Worker"); + + // Keep a reference so that we can dispatch unsolicited messages to it. + this.radioInterfaces[aClientId] = aRadioInterface; + + this.send(null, "registerClient", { clientId: aClientId }); + gSystemWorkerManager.registerRilWorker(aClientId, this.worker); + }, + + /** + * Send arbitrary message to worker. + * + * @param rilMessageType + * A text message type. + * @param message [optional] + * An optional message object to send. + * @param callback [optional] + * An optional callback function which is called when worker replies + * with an message containing a "rilMessageToken" attribute of the + * same value we passed. This callback function accepts only one + * parameter -- the reply from worker. It also returns a boolean + * value true to keep current token-callback mapping and wait for + * another worker reply, or false to remove the mapping. + */ + send: function(clientId, rilMessageType, message, callback) { + message = message || {}; + + message.rilMessageClientId = clientId; + message.rilMessageToken = this.token; + this.token++; + + if (callback) { + // Only create the map if callback is provided. For sending a request + // and intentionally leaving the callback undefined, that reply will + // be dropped in |this.onmessage| because of that orphan token. + // + // For sending a request that never replied at all, we're fine with this + // because no callback shall be passed and we leave nothing to be cleaned + // up later. + this.tokenCallbackMap[message.rilMessageToken] = callback; + } + + message.rilMessageType = rilMessageType; + this.worker.postMessage(message); + }, + + /** + * Send message to worker and return worker reply to RILContentHelper. + * + * @param msg + * A message object from ppmm. + * @param rilMessageType + * A text string for worker message type. + * @param ipcType [optinal] + * A text string for ipc message type. "msg.name" if omitted. + * + * @TODO: Bug 815526 - deprecate RILContentHelper. + */ + sendWithIPCMessage: function(clientId, msg, rilMessageType, ipcType) { + this.send(clientId, rilMessageType, msg.json.data, (function(reply) { + ipcType = ipcType || msg.name; + msg.target.sendAsyncMessage(ipcType, { + clientId: clientId, + data: reply + }); + return false; + }).bind(this)); + } +}; + +function RadioInterface(aClientId, aWorkerMessenger) { + this.clientId = aClientId; + this.workerMessenger = { + send: aWorkerMessenger.send.bind(aWorkerMessenger, aClientId), + sendWithIPCMessage: + aWorkerMessenger.sendWithIPCMessage.bind(aWorkerMessenger, aClientId), + }; + aWorkerMessenger.registerClient(aClientId, this); + + this.supportedNetworkTypes = this.getSupportedNetworkTypes(); + + this.rilContext = { + radioState: RIL.GECKO_RADIOSTATE_UNAVAILABLE, + detailedRadioState: null, + cardState: RIL.GECKO_CARDSTATE_UNKNOWN, + networkSelectionMode: RIL.GECKO_NETWORK_SELECTION_UNKNOWN, + iccInfo: null, + imsi: null, + + // These objects implement the nsIDOMMozMobileConnectionInfo interface, + // although the actual implementation lives in the content process. So are + // the child attributes `network` and `cell`, which implement + // nsIDOMMozMobileNetworkInfo and nsIDOMMozMobileCellInfo respectively. + voice: {connected: false, + emergencyCallsOnly: false, + roaming: false, + network: null, + cell: null, + type: null, + signalStrength: null, + relSignalStrength: null}, + data: {connected: false, + emergencyCallsOnly: false, + roaming: false, + network: null, + cell: null, + type: null, + signalStrength: null, + relSignalStrength: null}, + }; + + this.voicemailInfo = { + number: null, + displayName: null + }; + + this.operatorInfo = {}; + + let lock = gSettingsService.createLock(); + + // Read the "time.clock.automatic-update.enabled" setting to see if + // we need to adjust the system clock time by NITZ or SNTP. + lock.get(kSettingsClockAutoUpdateEnabled, this); + + // Read the "time.timezone.automatic-update.enabled" setting to see if + // we need to adjust the system timezone by NITZ. + lock.get(kSettingsTimezoneAutoUpdateEnabled, this); + + // Set "time.clock.automatic-update.available" to false when starting up. + this.setClockAutoUpdateAvailable(false); + + // Set "time.timezone.automatic-update.available" to false when starting up. + this.setTimezoneAutoUpdateAvailable(false); + + // Read the Cell Broadcast Search List setting, string of integers or integer + // ranges separated by comma, to set listening channels. + lock.get(kSettingsCellBroadcastSearchList, this); + + Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false); + Services.obs.addObserver(this, kSysClockChangeObserverTopic, false); + Services.obs.addObserver(this, kScreenStateChangedTopic, false); + + Services.obs.addObserver(this, kNetworkConnStateChangedTopic, false); + Services.obs.addObserver(this, kNetworkActiveChangedTopic, false); + Services.prefs.addObserver(kPrefCellBroadcastDisabled, this, false); + + this.portAddressedSmsApps = {}; + this.portAddressedSmsApps[WAP.WDP_PORT_PUSH] = this.handleSmsWdpPortPush.bind(this); + + this._receivedSmsSegmentsMap = {}; + + this._sntp = new Sntp(this.setClockBySntp.bind(this), + Services.prefs.getIntPref("network.sntp.maxRetryCount"), + Services.prefs.getIntPref("network.sntp.refreshPeriod"), + Services.prefs.getIntPref("network.sntp.timeout"), + Services.prefs.getCharPref("network.sntp.pools").split(";"), + Services.prefs.getIntPref("network.sntp.port")); +} + +RadioInterface.prototype = { + + classID: RADIOINTERFACE_CID, + classInfo: XPCOMUtils.generateCI({classID: RADIOINTERFACE_CID, + classDescription: "RadioInterface", + interfaces: [Ci.nsIRadioInterface]}), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRadioInterface, + Ci.nsIObserver, + Ci.nsISettingsServiceCallback]), + + // A private wrapped WorkerMessenger instance. + workerMessenger: null, + + debug: function(s) { + dump("-*- RadioInterface[" + this.clientId + "]: " + s + "\n"); + }, + + shutdown: function() { + // Release the CPU wake lock for handling the received SMS. + this._releaseSmsHandledWakeLock(); + + Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic); + Services.obs.removeObserver(this, kSysClockChangeObserverTopic); + Services.obs.removeObserver(this, kScreenStateChangedTopic); + Services.obs.removeObserver(this, kNetworkConnStateChangedTopic); + Services.obs.removeObserver(this, kNetworkActiveChangedTopic); + }, + + /** + * A utility function to copy objects. The srcInfo may contain + * "rilMessageType", should ignore it. + */ + updateInfo: function(srcInfo, destInfo) { + for (let key in srcInfo) { + if (key === "rilMessageType") { + continue; + } + destInfo[key] = srcInfo[key]; + } + }, + + /** + * A utility function to compare objects. The srcInfo may contain + * "rilMessageType", should ignore it. + */ + isInfoChanged: function(srcInfo, destInfo) { + if (!destInfo) { + return true; + } + + for (let key in srcInfo) { + if (key === "rilMessageType") { + continue; + } + if (srcInfo[key] !== destInfo[key]) { + return true; + } + } + + return false; + }, + + /** + * 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(","); + 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); + supportedNetworkTypes = + RIL.GECKO_SUPPORTED_NETWORK_TYPES_DEFAULT.split(","); + break; + } + } + if (DEBUG) this.debug("Supported Network Types: " + supportedNetworkTypes); + return supportedNetworkTypes; + }, + + /** + * Process a message from the content process. + */ + receiveMessage: function(msg) { + switch (msg.name) { + case "RIL:GetRilContext": + // This message is sync. + return this.rilContext; + case "RIL:GetLastKnownNetwork": + // This message is sync. + return this._lastKnownNetwork; + case "RIL:GetLastKnownHomeNetwork": + // This message is sync. + return this._lastKnownHomeNetwork; + case "RIL:GetAvailableNetworks": + this.workerMessenger.sendWithIPCMessage(msg, "getAvailableNetworks"); + break; + case "RIL:SelectNetwork": + this.workerMessenger.sendWithIPCMessage(msg, "selectNetwork"); + break; + case "RIL:SelectNetworkAuto": + this.workerMessenger.sendWithIPCMessage(msg, "selectNetworkAuto"); + break; + case "RIL:SetPreferredNetworkType": + this.setPreferredNetworkType(msg.target, msg.json.data); + break; + case "RIL:GetPreferredNetworkType": + this.getPreferredNetworkType(msg.target, msg.json.data); + break; + case "RIL:GetCardLockState": + this.workerMessenger.sendWithIPCMessage(msg, "iccGetCardLockState", + "RIL:CardLockResult"); + break; + case "RIL:UnlockCardLock": + this.workerMessenger.sendWithIPCMessage(msg, "iccUnlockCardLock", + "RIL:CardLockResult"); + break; + case "RIL:SetCardLock": + this.workerMessenger.sendWithIPCMessage(msg, "iccSetCardLock", + "RIL:CardLockResult"); + break; + case "RIL:GetCardLockRetryCount": + this.workerMessenger.sendWithIPCMessage(msg, "iccGetCardLockRetryCount", + "RIL:CardLockRetryCount"); + break; + case "RIL:SendMMI": + this.sendMMI(msg.target, msg.json.data); + break; + case "RIL:CancelMMI": + this.workerMessenger.sendWithIPCMessage(msg, "cancelUSSD"); + break; + case "RIL:SendStkResponse": + this.workerMessenger.send("sendStkTerminalResponse", msg.json.data); + break; + case "RIL:SendStkMenuSelection": + this.workerMessenger.send("sendStkMenuSelection", msg.json.data); + break; + case "RIL:SendStkTimerExpiration": + this.workerMessenger.send("sendStkTimerExpiration", msg.json.data); + break; + case "RIL:SendStkEventDownload": + this.workerMessenger.send("sendStkEventDownload", msg.json.data); + break; + case "RIL:IccOpenChannel": + this.workerMessenger.sendWithIPCMessage(msg, "iccOpenChannel"); + break; + case "RIL:IccCloseChannel": + this.workerMessenger.sendWithIPCMessage(msg, "iccCloseChannel"); + break; + case "RIL:IccExchangeAPDU": + this.workerMessenger.sendWithIPCMessage(msg, "iccExchangeAPDU"); + break; + case "RIL:ReadIccContacts": + this.workerMessenger.sendWithIPCMessage(msg, "readICCContacts"); + break; + case "RIL:UpdateIccContact": + this.workerMessenger.sendWithIPCMessage(msg, "updateICCContact"); + break; + case "RIL:MatchMvno": + this.matchMvno(msg.target, msg.json.data); + break; + case "RIL:SetCallForwardingOptions": + this.setCallForwardingOptions(msg.target, msg.json.data); + break; + case "RIL:GetCallForwardingOptions": + this.workerMessenger.sendWithIPCMessage(msg, "queryCallForwardStatus"); + break; + case "RIL:SetCallBarringOptions": + this.workerMessenger.sendWithIPCMessage(msg, "setCallBarring"); + break; + case "RIL:GetCallBarringOptions": + this.workerMessenger.sendWithIPCMessage(msg, "queryCallBarringStatus"); + break; + case "RIL:ChangeCallBarringPassword": + this.workerMessenger.sendWithIPCMessage(msg, "changeCallBarringPassword"); + break; + case "RIL:SetCallWaitingOptions": + this.workerMessenger.sendWithIPCMessage(msg, "setCallWaiting"); + break; + case "RIL:GetCallWaitingOptions": + this.workerMessenger.sendWithIPCMessage(msg, "queryCallWaiting"); + break; + case "RIL:SetCallingLineIdRestriction": + this.setCallingLineIdRestriction(msg.target, msg.json.data); + break; + case "RIL:GetCallingLineIdRestriction": + this.workerMessenger.sendWithIPCMessage(msg, "getCLIR"); + break; + case "RIL:ExitEmergencyCbMode": + this.workerMessenger.sendWithIPCMessage(msg, "exitEmergencyCbMode"); + break; + case "RIL:SetRadioEnabled": + this.setRadioEnabled(msg.target, msg.json.data); + break; + case "RIL:GetVoicemailInfo": + // This message is sync. + return this.voicemailInfo; + case "RIL:SetRoamingPreference": + this.workerMessenger.sendWithIPCMessage(msg, "setRoamingPreference"); + break; + case "RIL:GetRoamingPreference": + this.workerMessenger.sendWithIPCMessage(msg, "queryRoamingPreference"); + break; + case "RIL:SetVoicePrivacyMode": + this.workerMessenger.sendWithIPCMessage(msg, "setVoicePrivacyMode"); + break; + case "RIL:GetVoicePrivacyMode": + this.workerMessenger.sendWithIPCMessage(msg, "queryVoicePrivacyMode"); + break; + case "RIL:GetSupportedNetworkTypes": + // This message is sync. + return this.supportedNetworkTypes; + } + return null; + }, + + handleUnsolicitedWorkerMessage: function(message) { + let connHandler = gDataConnectionManager.getConnectionHandler(this.clientId); + switch (message.rilMessageType) { + case "callRing": + gTelephonyProvider.notifyCallRing(); + break; + case "callStateChange": + gTelephonyProvider.notifyCallStateChanged(this.clientId, message.call); + break; + case "callDisconnected": + gTelephonyProvider.notifyCallDisconnected(this.clientId, message.call); + break; + case "conferenceCallStateChanged": + gTelephonyProvider.notifyConferenceCallStateChanged(message.state); + break; + case "cdmaCallWaiting": + gTelephonyProvider.notifyCdmaCallWaiting(this.clientId, message.number); + break; + case "suppSvcNotification": + gTelephonyProvider.notifySupplementaryService(this.clientId, + message.callIndex, + message.notification); + break; + case "datacallerror": + connHandler.handleDataCallError(message); + break; + case "datacallstatechange": + let addresses = []; + for (let i = 0; i < message.addresses.length; i++) { + let [address, prefixLength] = message.addresses[i].split("/"); + // From AOSP hardware/ril/include/telephony/ril.h, that address prefix + // is said to be OPTIONAL, but we never met such case before. + addresses.push({ + address: address, + prefixLength: prefixLength ? parseInt(prefixLength, 10) : 0 + }); + } + message.addresses = addresses; + connHandler.handleDataCallState(message); + break; + case "emergencyCbModeChange": + this.handleEmergencyCbModeChange(message); + break; + case "networkinfochanged": + this.updateNetworkInfo(message); + break; + case "networkselectionmodechange": + this.updateNetworkSelectionMode(message); + break; + case "voiceregistrationstatechange": + this.updateVoiceConnection(message); + break; + case "dataregistrationstatechange": + this.updateDataConnection(message); + break; + case "signalstrengthchange": + this.handleSignalStrengthChange(message); + break; + case "operatorchange": + this.handleOperatorChange(message); + break; + case "otastatuschange": + this.handleOtaStatus(message); + break; + case "radiostatechange": + this.handleRadioStateChange(message); + break; + case "cardstatechange": + this.rilContext.cardState = message.cardState; + gRadioEnabledController.receiveCardState(this.clientId); + gMessageManager.sendIccMessage("RIL:CardStateChanged", + this.clientId, message); + break; + case "sms-received": + this.handleSmsMultipart(message); + break; + case "cellbroadcast-received": + message.timestamp = Date.now(); + gMessageManager.sendCellBroadcastMessage("RIL:CellBroadcastReceived", + this.clientId, message); + break; + case "nitzTime": + this.handleNitzTime(message); + break; + case "iccinfochange": + this.handleIccInfoChange(message); + break; + case "iccimsi": + this.rilContext.imsi = message.imsi; + break; + case "iccmbdn": + this.handleIccMbdn(message); + break; + case "iccmwis": + gMessageManager.sendVoicemailMessage("RIL:VoicemailNotification", + this.clientId, message.mwi); + break; + case "USSDReceived": + if (DEBUG) this.debug("USSDReceived " + JSON.stringify(message)); + this.handleUSSDReceived(message); + break; + case "stkcommand": + this.handleStkProactiveCommand(message); + break; + case "stksessionend": + gMessageManager.sendIccMessage("RIL:StkSessionEnd", this.clientId, null); + break; + case "exitEmergencyCbMode": + this.handleExitEmergencyCbMode(message); + break; + case "cdma-info-rec-received": + if (DEBUG) this.debug("cdma-info-rec-received: " + JSON.stringify(message)); + gSystemMessenger.broadcastMessage("cdma-info-rec-received", message); + break; + default: + throw new Error("Don't know about this message type: " + + message.rilMessageType); + } + }, + + /** + * Get phone number from iccInfo. + * + * If the icc card is gsm card, the phone number is in msisdn. + * @see nsIDOMMozGsmIccInfo + * + * Otherwise, the phone number is in mdn. + * @see nsIDOMMozCdmaIccInfo + */ + getPhoneNumber: function() { + let iccInfo = this.rilContext.iccInfo; + + if (!iccInfo) { + return null; + } + + // After moving SMS code out of RadioInterfaceLayer, we could use + // |iccInfo instanceof Ci.nsIDOMMozGsmIccInfo| here. + // TODO: Bug 873351 - B2G SMS: move SMS code out of RadioInterfaceLayer to + // SmsService + let number = (iccInfo instanceof GsmIccInfo) ? iccInfo.msisdn : iccInfo.mdn; + + // Workaround an xpconnect issue with undefined string objects. + // See bug 808220 + if (number === undefined || number === "undefined") { + return null; + } + + return number; + }, + + /** + * A utility function to get the ICC ID of the SIM card (if installed). + */ + getIccId: function() { + let iccInfo = this.rilContext.iccInfo; + + if (!iccInfo) { + return null; + } + + let iccId = iccInfo.iccid; + + // Workaround an xpconnect issue with undefined string objects. + // See bug 808220 + if (iccId === undefined || iccId === "undefined") { + return null; + } + + return iccId; + }, + + // Matches the mvnoData pattern with imsi. Characters 'x' and 'X' are skipped + // and not compared. E.g., if the mvnoData passed is '310260x10xxxxxx', + // then the function returns true only if imsi has the same first 6 digits, + // 8th and 9th digit. + isImsiMatches: function(mvnoData) { + let imsi = this.rilContext.imsi; + + // This should not be an error, but a mismatch. + if (mvnoData.length > imsi.length) { + return false; + } + + for (let i = 0; i < mvnoData.length; i++) { + let c = mvnoData[i]; + if ((c !== 'x') && (c !== 'X') && (c !== imsi[i])) { + return false; + } + } + return true; + }, + + matchMvno: function(target, message) { + if (DEBUG) this.debug("matchMvno: " + JSON.stringify(message)); + + if (!message || !message.mvnoType || !message.mvnoData) { + message.errorMsg = RIL.GECKO_ERROR_INVALID_PARAMETER; + } + // Currently we only support imsi matching. + if (message.mvnoType != "imsi") { + message.errorMsg = RIL.GECKO_ERROR_MODE_NOT_SUPPORTED; + } + // Fire error if mvnoType is imsi but imsi is not available. + if (!this.rilContext.imsi) { + message.errorMsg = RIL.GECKO_ERROR_GENERIC_FAILURE; + } + + if (!message.errorMsg) { + message.result = this.isImsiMatches(message.mvnoData); + } + + target.sendAsyncMessage("RIL:MatchMvno", { + clientId: this.clientId, + data: message + }); + }, + + updateNetworkInfo: function(message) { + let voiceMessage = message[RIL.NETWORK_INFO_VOICE_REGISTRATION_STATE]; + let dataMessage = message[RIL.NETWORK_INFO_DATA_REGISTRATION_STATE]; + let operatorMessage = message[RIL.NETWORK_INFO_OPERATOR]; + let selectionMessage = message[RIL.NETWORK_INFO_NETWORK_SELECTION_MODE]; + let signalMessage = message[RIL.NETWORK_INFO_SIGNAL]; + + // Batch the *InfoChanged messages together + if (voiceMessage) { + this.updateVoiceConnection(voiceMessage, true); + } + + if (dataMessage) { + this.updateDataConnection(dataMessage, true); + } + + if (operatorMessage) { + this.handleOperatorChange(operatorMessage, true); + } + + if (signalMessage) { + this.handleSignalStrengthChange(signalMessage, true); + } + + let voice = this.rilContext.voice; + let data = this.rilContext.data; + + this.checkRoamingBetweenOperators(voice); + this.checkRoamingBetweenOperators(data); + + if (voiceMessage || operatorMessage || signalMessage) { + gMessageManager.sendMobileConnectionMessage("RIL:VoiceInfoChanged", + this.clientId, voice); + } + if (dataMessage || operatorMessage || signalMessage) { + gMessageManager.sendMobileConnectionMessage("RIL:DataInfoChanged", + this.clientId, data); + } + + if (selectionMessage) { + this.updateNetworkSelectionMode(selectionMessage); + } + }, + + /** + * Fix the roaming. RIL can report roaming in some case it is not + * really the case. See bug 787967 + * + * @param registration The voiceMessage or dataMessage from which the + * roaming state will be changed (maybe, if needed). + */ + checkRoamingBetweenOperators: function(registration) { + let iccInfo = this.rilContext.iccInfo; + let operator = registration.network; + let state = registration.state; + + if (!iccInfo || !operator || + state != RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED) { + return; + } + + 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; + + registration.roaming = registration.roaming && + !(equalsMcc && (equalsLongName || equalsShortName)); + }, + + /** + * Handle data connection changes. + * + * @param newInfo The new voice connection information. + * @param batch When batch is true, the RIL:VoiceInfoChanged message will + * not be sent. + */ + updateVoiceConnection: function(newInfo, batch) { + let voiceInfo = this.rilContext.voice; + voiceInfo.state = newInfo.state; + voiceInfo.connected = newInfo.connected; + voiceInfo.roaming = newInfo.roaming; + voiceInfo.emergencyCallsOnly = newInfo.emergencyCallsOnly; + voiceInfo.type = newInfo.type; + + // Make sure we also reset the operator and signal strength information + // if we drop off the network. + if (newInfo.state !== RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED) { + voiceInfo.cell = null; + voiceInfo.network = null; + voiceInfo.signalStrength = null; + voiceInfo.relSignalStrength = null; + } else { + voiceInfo.cell = newInfo.cell; + voiceInfo.network = this.operatorInfo; + } + + if (!batch) { + gMessageManager.sendMobileConnectionMessage("RIL:VoiceInfoChanged", + this.clientId, voiceInfo); + } + }, + + /** + * Handle the data connection's state has changed. + * + * @param newInfo The new data connection information. + * @param batch When batch is true, the RIL:DataInfoChanged message will + * not be sent. + */ + updateDataConnection: function(newInfo, batch) { + let dataInfo = this.rilContext.data; + dataInfo.state = newInfo.state; + dataInfo.roaming = newInfo.roaming; + dataInfo.emergencyCallsOnly = newInfo.emergencyCallsOnly; + dataInfo.type = newInfo.type; + // For the data connection, the `connected` flag indicates whether + // there's an active data call. + dataInfo.connected = false; + if (gNetworkManager.active && + gNetworkManager.active.type === + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE && + gNetworkManager.active.serviceId === this.clientId) { + dataInfo.connected = true; + } + + // Make sure we also reset the operator and signal strength information + // if we drop off the network. + if (newInfo.state !== RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED) { + dataInfo.cell = null; + dataInfo.network = null; + dataInfo.signalStrength = null; + dataInfo.relSignalStrength = null; + } else { + dataInfo.cell = newInfo.cell; + dataInfo.network = this.operatorInfo; + } + + if (!batch) { + gMessageManager.sendMobileConnectionMessage("RIL:DataInfoChanged", + this.clientId, dataInfo); + } + + let connHandler = gDataConnectionManager.getConnectionHandler(this.clientId); + connHandler.updateRILNetworkInterface(); + }, + + getPreferredNetworkType: function(target, message) { + this.workerMessenger.send("getPreferredNetworkType", message, (function(response) { + if (response.success) { + response.type = RIL.RIL_PREFERRED_NETWORK_TYPE_TO_GECKO[response.networkType]; + } + + target.sendAsyncMessage("RIL:GetPreferredNetworkType", { + clientId: this.clientId, + data: response + }); + return false; + }).bind(this)); + }, + + setPreferredNetworkType: function(target, message) { + let networkType = RIL.RIL_PREFERRED_NETWORK_TYPE_TO_GECKO.indexOf(message.type); + if (networkType < 0) { + message.errorMsg = RIL.GECKO_ERROR_INVALID_PARAMETER; + target.sendAsyncMessage("RIL:SetPreferredNetworkType", { + clientId: this.clientId, + data: message + }); + return false; + } + message.networkType = networkType; + + this.workerMessenger.send("setPreferredNetworkType", message, (function(response) { + target.sendAsyncMessage("RIL:SetPreferredNetworkType", { + clientId: this.clientId, + data: response + }); + return false; + }).bind(this)); + }, + + setCellBroadcastSearchList: function(newSearchList) { + if ((newSearchList == this._cellBroadcastSearchList) || + (newSearchList && this._cellBroadcastSearchList && + newSearchList.gsm == this._cellBroadcastSearchList.gsm && + newSearchList.cdma == this._cellBroadcastSearchList.cdma)) { + return; + } + + this.workerMessenger.send("setCellBroadcastSearchList", + { searchList: newSearchList }, + (function callback(response) { + if (!response.success) { + let lock = gSettingsService.createLock(); + lock.set(kSettingsCellBroadcastSearchList, + this._cellBroadcastSearchList, null); + } else { + this._cellBroadcastSearchList = response.searchList; + } + + return false; + }).bind(this)); + }, + + /** + * Handle signal strength changes. + * + * @param message The new signal strength. + * @param batch When batch is true, the RIL:VoiceInfoChanged and + * RIL:DataInfoChanged message will not be sent. + */ + handleSignalStrengthChange: function(message, batch) { + let voiceInfo = this.rilContext.voice; + // If the voice is not registered, need not to update signal information. + if (voiceInfo.state === RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED && + this.isInfoChanged(message.voice, voiceInfo)) { + this.updateInfo(message.voice, voiceInfo); + if (!batch) { + gMessageManager.sendMobileConnectionMessage("RIL:VoiceInfoChanged", + this.clientId, voiceInfo); + } + } + + let dataInfo = this.rilContext.data; + // If the data is not registered, need not to update signal information. + if (dataInfo.state === RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED && + this.isInfoChanged(message.data, dataInfo)) { + this.updateInfo(message.data, dataInfo); + if (!batch) { + gMessageManager.sendMobileConnectionMessage("RIL:DataInfoChanged", + this.clientId, dataInfo); + } + } + }, + + /** + * Handle operator information changes. + * + * @param message The new operator information. + * @param batch When batch is true, the RIL:VoiceInfoChanged and + * RIL:DataInfoChanged message will not be sent. + */ + handleOperatorChange: function(message, batch) { + let operatorInfo = this.operatorInfo; + let voice = this.rilContext.voice; + let data = this.rilContext.data; + + if (this.isInfoChanged(message, operatorInfo)) { + this.updateInfo(message, operatorInfo); + + // Update lastKnownNetwork + if (message.mcc && message.mnc) { + this._lastKnownNetwork = message.mcc + "-" + message.mnc; + } + + // If the voice is unregistered, no need to send RIL:VoiceInfoChanged. + if (voice.network && !batch) { + gMessageManager.sendMobileConnectionMessage("RIL:VoiceInfoChanged", + this.clientId, voice); + } + + // If the data is unregistered, no need to send RIL:DataInfoChanged. + if (data.network && !batch) { + gMessageManager.sendMobileConnectionMessage("RIL:DataInfoChanged", + this.clientId, data); + } + } + }, + + handleOtaStatus: function(message) { + if (message.status < 0 || + RIL.CDMA_OTA_PROVISION_STATUS_TO_GECKO.length <= message.status) { + return; + } + + let status = RIL.CDMA_OTA_PROVISION_STATUS_TO_GECKO[message.status]; + + gMessageManager.sendMobileConnectionMessage("RIL:OtaStatusChanged", + this.clientId, status); + }, + + _convertRadioState: function(state) { + switch (state) { + case RIL.GECKO_RADIOSTATE_OFF: + return RIL.GECKO_DETAILED_RADIOSTATE_DISABLED; + case RIL.GECKO_RADIOSTATE_READY: + return RIL.GECKO_DETAILED_RADIOSTATE_ENABLED; + default: + return RIL.GECKO_DETAILED_RADIOSTATE_UNKNOWN; + } + }, + + handleRadioStateChange: function(message) { + let newState = message.radioState; + if (this.rilContext.radioState == newState) { + return; + } + this.rilContext.radioState = newState; + this.handleDetailedRadioStateChanged(this._convertRadioState(newState)); + + //TODO Should we notify this change as a card state change? + }, + + handleDetailedRadioStateChanged: function(state) { + if (this.rilContext.detailedRadioState == state) { + return; + } + this.rilContext.detailedRadioState = state; + gMessageManager.sendMobileConnectionMessage("RIL:RadioStateChanged", + this.clientId, state); + }, + + setDataRegistration: function(attach) { + this.workerMessenger.send("setDataRegistration", {attach: attach}); + }, + + /** + * TODO: Bug 911713 - B2G NetworkManager: Move policy control logic to + * NetworkManager + */ + updateRILNetworkInterface: function() { + let connHandler = gDataConnectionManager.getConnectionHandler(this.clientId); + connHandler.updateRILNetworkInterface(); + }, + + /** + * Update network selection mode + */ + updateNetworkSelectionMode: function(message) { + if (DEBUG) this.debug("updateNetworkSelectionMode: " + JSON.stringify(message)); + this.rilContext.networkSelectionMode = message.mode; + gMessageManager.sendMobileConnectionMessage("RIL:NetworkSelectionModeChanged", + this.clientId, message); + }, + + /** + * Handle emergency callback mode change. + */ + handleEmergencyCbModeChange: function(message) { + if (DEBUG) this.debug("handleEmergencyCbModeChange: " + JSON.stringify(message)); + gMessageManager.sendMobileConnectionMessage("RIL:EmergencyCbModeChanged", + this.clientId, message); + }, + + /** + * Handle WDP port push PDU. Constructor WDP bearer information and deliver + * to WapPushManager. + * + * @param message + * A SMS message. + */ + handleSmsWdpPortPush: function(message) { + if (message.encoding != RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + if (DEBUG) { + this.debug("Got port addressed SMS but not encoded in 8-bit alphabet." + + " Drop!"); + } + return; + } + + let options = { + bearer: WAP.WDP_BEARER_GSM_SMS_GSM_MSISDN, + sourceAddress: message.sender, + sourcePort: message.originatorPort, + destinationAddress: this.rilContext.iccInfo.msisdn, + destinationPort: message.destinationPort, + serviceId: this.clientId + }; + WAP.WapPushManager.receiveWdpPDU(message.fullData, message.fullData.length, + 0, options); + }, + + /** + * A helper to broadcast the system message to launch registered apps + * like Costcontrol, Notification and Message app... etc. + * + * @param aName + * The system message name. + * @param aDomMessage + * The nsIDOMMozSmsMessage object. + */ + broadcastSmsSystemMessage: function(aName, aDomMessage) { + if (DEBUG) this.debug("Broadcasting the SMS system message: " + aName); + + // Sadly we cannot directly broadcast the aDomMessage object + // because the system message mechamism will rewrap the object + // based on the content window, which needs to know the properties. + gSystemMessenger.broadcastMessage(aName, { + iccId: aDomMessage.iccId, + type: aDomMessage.type, + id: aDomMessage.id, + threadId: aDomMessage.threadId, + delivery: aDomMessage.delivery, + deliveryStatus: aDomMessage.deliveryStatus, + sender: aDomMessage.sender, + receiver: aDomMessage.receiver, + body: aDomMessage.body, + messageClass: aDomMessage.messageClass, + timestamp: aDomMessage.timestamp, + sentTimestamp: aDomMessage.sentTimestamp, + deliveryTimestamp: aDomMessage.deliveryTimestamp, + read: aDomMessage.read + }); + }, + + // The following attributes/functions are used for acquiring/releasing the + // CPU wake lock when the RIL handles the received SMS. Note that we need + // a timer to bound the lock's life cycle to avoid exhausting the battery. + _smsHandledWakeLock: null, + _smsHandledWakeLockTimer: null, + + _acquireSmsHandledWakeLock: function() { + if (!this._smsHandledWakeLock) { + if (DEBUG) this.debug("Acquiring a CPU wake lock for handling SMS."); + this._smsHandledWakeLock = gPowerManagerService.newWakeLock("cpu"); + } + if (!this._smsHandledWakeLockTimer) { + if (DEBUG) this.debug("Creating a timer for releasing the CPU wake lock."); + this._smsHandledWakeLockTimer = + Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + if (DEBUG) this.debug("Setting the timer for releasing the CPU wake lock."); + this._smsHandledWakeLockTimer + .initWithCallback(this._releaseSmsHandledWakeLock.bind(this), + SMS_HANDLED_WAKELOCK_TIMEOUT, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + _releaseSmsHandledWakeLock: function() { + if (DEBUG) this.debug("Releasing the CPU wake lock for handling SMS."); + if (this._smsHandledWakeLockTimer) { + this._smsHandledWakeLockTimer.cancel(); + } + if (this._smsHandledWakeLock) { + this._smsHandledWakeLock.unlock(); + this._smsHandledWakeLock = null; + } + }, + + /** + * Hash map for received multipart sms fragments. Messages are hashed with + * its sender address and concatenation reference number. Three additional + * attributes `segmentMaxSeq`, `receivedSegments`, `segments` are inserted. + */ + _receivedSmsSegmentsMap: null, + + /** + * Helper for processing received multipart SMS. + * + * @return null for handled segments, and an object containing full message + * body/data once all segments are received. + */ + _processReceivedSmsSegment: function(aSegment) { + + // Directly replace full message body for single SMS. + if (!(aSegment.segmentMaxSeq && (aSegment.segmentMaxSeq > 1))) { + if (aSegment.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + aSegment.fullData = aSegment.data; + } else { + aSegment.fullBody = aSegment.body; + } + return aSegment; + } + + // Handle Concatenation for Class 0 SMS + let hash = aSegment.sender + ":" + + aSegment.segmentRef + ":" + + aSegment.segmentMaxSeq; + let seq = aSegment.segmentSeq; + + let options = this._receivedSmsSegmentsMap[hash]; + if (!options) { + options = aSegment; + this._receivedSmsSegmentsMap[hash] = options; + + options.receivedSegments = 0; + options.segments = []; + } else if (options.segments[seq]) { + // Duplicated segment? + if (DEBUG) { + this.debug("Got duplicated segment no." + seq + + " of a multipart SMS: " + JSON.stringify(aSegment)); + } + return null; + } + + if (options.receivedSegments > 0) { + // Update received timestamp. + options.timestamp = aSegment.timestamp; + } + + if (options.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + options.segments[seq] = aSegment.data; + } else { + options.segments[seq] = aSegment.body; + } + options.receivedSegments++; + + // The port information is only available in 1st segment for CDMA WAP Push. + // If the segments of a WAP Push are not received in sequence + // (e.g., SMS with seq == 1 is not the 1st segment received by the device), + // we have to retrieve the port information from 1st segment and + // save it into the cached options. + if (aSegment.teleservice === RIL.PDU_CDMA_MSG_TELESERIVCIE_ID_WAP + && seq === 1) { + if (!options.originatorPort && aSegment.originatorPort) { + options.originatorPort = aSegment.originatorPort; + } + + if (!options.destinationPort && aSegment.destinationPort) { + options.destinationPort = aSegment.destinationPort; + } + } + + if (options.receivedSegments < options.segmentMaxSeq) { + if (DEBUG) { + this.debug("Got segment no." + seq + " of a multipart SMS: " + + JSON.stringify(options)); + } + return null; + } + + // Remove from map + delete this._receivedSmsSegmentsMap[hash]; + + // Rebuild full body + if (options.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + // Uint8Array doesn't have `concat`, so we have to merge all segements + // by hand. + let fullDataLen = 0; + for (let i = 1; i <= options.segmentMaxSeq; i++) { + fullDataLen += options.segments[i].length; + } + + options.fullData = new Uint8Array(fullDataLen); + for (let d= 0, i = 1; i <= options.segmentMaxSeq; i++) { + let data = options.segments[i]; + for (let j = 0; j < data.length; j++) { + options.fullData[d++] = data[j]; + } + } + } else { + options.fullBody = options.segments.join(""); + } + + // Remove handy fields after completing the concatenation. + delete options.receivedSegments; + delete options.segments; + + if (DEBUG) { + this.debug("Got full multipart SMS: " + JSON.stringify(options)); + } + + return options; + }, + + /** + * Helper to create Savable SmsSegment. + */ + _createSavableSmsSegment: function(aMessage) { + // We precisely define what data fields to be stored into + // DB here for better data migration. + let segment = {}; + segment.messageType = aMessage.messageType; + segment.teleservice = aMessage.teleservice; + segment.SMSC = aMessage.SMSC; + segment.sentTimestamp = aMessage.sentTimestamp; + segment.timestamp = Date.now(); + segment.sender = aMessage.sender; + segment.pid = aMessage.pid; + segment.encoding = aMessage.encoding; + segment.messageClass = aMessage.messageClass; + segment.iccId = this.getIccId(); + if (aMessage.header) { + segment.segmentRef = aMessage.header.segmentRef; + segment.segmentSeq = aMessage.header.segmentSeq; + segment.segmentMaxSeq = aMessage.header.segmentMaxSeq; + segment.originatorPort = aMessage.header.originatorPort; + segment.destinationPort = aMessage.header.destinationPort; + } + segment.mwiPresent = (aMessage.mwi)? true: false; + segment.mwiDiscard = (segment.mwiPresent)? aMessage.mwi.discard: false; + segment.mwiMsgCount = (segment.mwiPresent)? aMessage.mwi.msgCount: 0; + segment.mwiActive = (segment.mwiPresent)? aMessage.mwi.active: false; + segment.serviceCategory = aMessage.serviceCategory; + segment.language = aMessage.language; + segment.data = aMessage.data; + segment.body = aMessage.body; + + return segment; + }, + + /** + * Helper to purge complete message. + * + * We remove unnessary fields defined in _createSavableSmsSegment() after + * completing the concatenation. + */ + _purgeCompleteSmsMessage: function(aMessage) { + // Purge concatenation info + delete aMessage.segmentRef; + delete aMessage.segmentSeq; + delete aMessage.segmentMaxSeq; + + // Purge partial message body + delete aMessage.data; + delete aMessage.body; + }, + + /** + * handle concatenation of received SMS. + */ + handleSmsMultipart: function(aMessage) { + if (DEBUG) this.debug("handleSmsMultipart: " + JSON.stringify(aMessage)); + + this._acquireSmsHandledWakeLock(); + + let segment = this._createSavableSmsSegment(aMessage); + + let isMultipart = (segment.segmentMaxSeq && (segment.segmentMaxSeq > 1)); + let messageClass = segment.messageClass; + + let handleReceivedAndAck = function(aRvOfIncompleteMsg, aCompleteMessage) { + if (aCompleteMessage) { + this._purgeCompleteSmsMessage(aCompleteMessage); + if (this.handleSmsReceived(aCompleteMessage)) { + this.sendAckSms(Cr.NS_OK, aCompleteMessage); + } + // else Ack will be sent after further process in handleSmsReceived. + } else { + this.sendAckSms(aRvOfIncompleteMsg, segment); + } + }.bind(this); + + // No need to access SmsSegmentStore for Class 0 SMS and Single SMS. + if (!isMultipart || + (messageClass == RIL.GECKO_SMS_MESSAGE_CLASSES[RIL.PDU_DCS_MSG_CLASS_0])) { + // `When a mobile terminated message is class 0 and the MS has the + // capability of displaying short messages, the MS shall display the + // message immediately and send an acknowledgement to the SC when the + // message has successfully reached the MS irrespective of whether + // there is memory available in the (U)SIM or ME. The message shall + // not be automatically stored in the (U)SIM or ME.` + // ~ 3GPP 23.038 clause 4 + + handleReceivedAndAck(Cr.NS_OK, // ACK OK For Incomplete Class 0 + this._processReceivedSmsSegment(segment)); + } else { + gMobileMessageDatabaseService + .saveSmsSegment(segment, function notifyResult(aRv, aCompleteMessage) { + handleReceivedAndAck(aRv, // Ack according to the result after saving + aCompleteMessage); + }); + } + }, + + portAddressedSmsApps: null, + handleSmsReceived: function(message) { + if (DEBUG) this.debug("handleSmsReceived: " + JSON.stringify(message)); + + if (message.messageType == RIL.PDU_CDMA_MSG_TYPE_BROADCAST) { + gMessageManager.sendCellBroadcastMessage("RIL:CellBroadcastReceived", + this.clientId, message); + return true; + } + + // Dispatch to registered handler if application port addressing is + // available. Note that the destination port can possibly be zero when + // representing a UDP/TCP port. + if (message.destinationPort != null) { + let handler = this.portAddressedSmsApps[message.destinationPort]; + if (handler) { + handler(message); + } + return true; + } + + if (message.encoding == RIL.PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + // Don't know how to handle binary data yet. + return true; + } + + message.type = "sms"; + message.sender = message.sender || null; + message.receiver = this.getPhoneNumber(); + message.body = message.fullBody = message.fullBody || null; + + if (gSmsService.isSilentNumber(message.sender)) { + message.id = -1; + message.threadId = 0; + message.delivery = DOM_MOBILE_MESSAGE_DELIVERY_RECEIVED; + message.deliveryStatus = RIL.GECKO_SMS_DELIVERY_STATUS_SUCCESS; + message.read = false; + + let domMessage = + gMobileMessageService.createSmsMessage(message.id, + message.threadId, + message.iccId, + message.delivery, + message.deliveryStatus, + message.sender, + message.receiver, + message.body, + message.messageClass, + message.timestamp, + message.sentTimestamp, + 0, + message.read); + + Services.obs.notifyObservers(domMessage, + kSilentSmsReceivedObserverTopic, + null); + return true; + } + + if (message.mwiPresent) { + let mwi = { + discard: message.mwiDiscard, + msgCount: message.mwiMsgCount, + active: message.mwiActive + }; + this.workerMessenger.send("updateMwis", { mwi: mwi }); + + mwi.returnNumber = message.sender; + mwi.returnMessage = message.fullBody; + gMessageManager.sendVoicemailMessage("RIL:VoicemailNotification", + this.clientId, mwi); + + // Dicarded MWI comes without text body. + // Hence, we discard it here after notifying the MWI status. + if (message.mwiDiscard) { + return true; + } + } + + let notifyReceived = function notifyReceived(rv, domMessage) { + let success = Components.isSuccessCode(rv); + + this.sendAckSms(rv, message); + + if (!success) { + // At this point we could send a message to content to notify the user + // that storing an incoming SMS failed, most likely due to a full disk. + if (DEBUG) { + this.debug("Could not store SMS, error code " + rv); + } + return; + } + + this.broadcastSmsSystemMessage(kSmsReceivedObserverTopic, domMessage); + Services.obs.notifyObservers(domMessage, kSmsReceivedObserverTopic, null); + }.bind(this); + + if (message.messageClass != RIL.GECKO_SMS_MESSAGE_CLASSES[RIL.PDU_DCS_MSG_CLASS_0]) { + gMobileMessageDatabaseService.saveReceivedMessage(message, + notifyReceived); + } else { + message.id = -1; + message.threadId = 0; + message.delivery = DOM_MOBILE_MESSAGE_DELIVERY_RECEIVED; + message.deliveryStatus = RIL.GECKO_SMS_DELIVERY_STATUS_SUCCESS; + message.read = false; + + let domMessage = + gMobileMessageService.createSmsMessage(message.id, + message.threadId, + message.iccId, + message.delivery, + message.deliveryStatus, + message.sender, + message.receiver, + message.body, + message.messageClass, + message.timestamp, + message.sentTimestamp, + 0, + message.read); + + notifyReceived(Cr.NS_OK, domMessage); + } + + // SMS ACK will be sent in notifyReceived. Return false here. + return false; + }, + + /** + * Handle ACK response of received SMS. + */ + sendAckSms: function(aRv, aMessage) { + if (aMessage.messageClass === RIL.GECKO_SMS_MESSAGE_CLASSES[RIL.PDU_DCS_MSG_CLASS_2]) { + return; + } + + let result = RIL.PDU_FCS_OK; + if (!Components.isSuccessCode(aRv)) { + if (DEBUG) this.debug("Failed to handle received sms: " + aRv); + result = (aRv === Cr.NS_ERROR_FILE_NO_DEVICE_SPACE) + ? RIL.PDU_FCS_MEMORY_CAPACITY_EXCEEDED + : RIL.PDU_FCS_UNSPECIFIED; + } + + this.workerMessenger.send("ackSMS", { result: result }); + + }, + + /** + * Set the setting value of "time.clock.automatic-update.available". + */ + setClockAutoUpdateAvailable: function(value) { + gSettingsService.createLock().set(kSettingsClockAutoUpdateAvailable, value, null, + "fromInternalSetting"); + }, + + /** + * Set the setting value of "time.timezone.automatic-update.available". + */ + setTimezoneAutoUpdateAvailable: function(value) { + gSettingsService.createLock().set(kSettingsTimezoneAutoUpdateAvailable, value, null, + "fromInternalSetting"); + }, + + /** + * Set the system clock by NITZ. + */ + setClockByNitz: function(message) { + // To set the system clock time. Note that there could be a time diff + // between when the NITZ was received and when the time is actually set. + gTimeService.set( + message.networkTimeInMS + (Date.now() - message.receiveTimeInMS)); + }, + + /** + * Set the system time zone by NITZ. + */ + setTimezoneByNitz: function(message) { + // To set the sytem timezone. Note that we need to convert the time zone + // value to a UTC repesentation string in the format of "UTC(+/-)hh:mm". + // Ex, time zone -480 is "UTC+08:00"; time zone 630 is "UTC-10:30". + // + // We can unapply the DST correction if we want the raw time zone offset: + // message.networkTimeZoneInMinutes -= message.networkDSTInMinutes; + if (message.networkTimeZoneInMinutes != (new Date()).getTimezoneOffset()) { + let absTimeZoneInMinutes = Math.abs(message.networkTimeZoneInMinutes); + let timeZoneStr = "UTC"; + timeZoneStr += (message.networkTimeZoneInMinutes > 0 ? "-" : "+"); + timeZoneStr += ("0" + Math.floor(absTimeZoneInMinutes / 60)).slice(-2); + timeZoneStr += ":"; + timeZoneStr += ("0" + absTimeZoneInMinutes % 60).slice(-2); + gSettingsService.createLock().set("time.timezone", timeZoneStr, null); + } + }, + + /** + * Handle the NITZ message. + */ + handleNitzTime: function(message) { + // Got the NITZ info received from the ril_worker. + this.setClockAutoUpdateAvailable(true); + this.setTimezoneAutoUpdateAvailable(true); + + // Cache the latest NITZ message whenever receiving it. + this._lastNitzMessage = message; + + // Set the received NITZ clock if the setting is enabled. + if (this._clockAutoUpdateEnabled) { + this.setClockByNitz(message); + } + // Set the received NITZ timezone if the setting is enabled. + if (this._timezoneAutoUpdateEnabled) { + this.setTimezoneByNitz(message); + } + }, + + /** + * Set the system clock by SNTP. + */ + setClockBySntp: function(offset) { + // Got the SNTP info. + this.setClockAutoUpdateAvailable(true); + if (!this._clockAutoUpdateEnabled) { + return; + } + if (this._lastNitzMessage) { + if (DEBUG) debug("SNTP: NITZ available, discard SNTP"); + return; + } + gTimeService.set(Date.now() + offset); + }, + + handleIccMbdn: function(message) { + let voicemailInfo = this.voicemailInfo; + + voicemailInfo.number = message.number; + voicemailInfo.displayName = message.alphaId; + + gMessageManager.sendVoicemailMessage("RIL:VoicemailInfoChanged", + this.clientId, voicemailInfo); + }, + + handleIccInfoChange: function(message) { + let oldSpn = this.rilContext.iccInfo ? this.rilContext.iccInfo.spn : null; + + if (!message || !message.iccType) { + // Card is not detected, clear iccInfo to null. + this.rilContext.iccInfo = null; + } else { + if (!this.rilContext.iccInfo) { + if (message.iccType === "ruim" || message.iccType === "csim") { + this.rilContext.iccInfo = new CdmaIccInfo(); + } else { + this.rilContext.iccInfo = new GsmIccInfo(); + } + } + + if (!this.isInfoChanged(message, this.rilContext.iccInfo)) { + return; + } + + this.updateInfo(message, this.rilContext.iccInfo); + } + + // RIL:IccInfoChanged corresponds to a DOM event that gets fired only + // when iccInfo has changed. + gMessageManager.sendIccMessage("RIL:IccInfoChanged", + this.clientId, + message.iccType ? message : null); + + // Update lastKnownSimMcc. + if (message.mcc) { + try { + Services.prefs.setCharPref("ril.lastKnownSimMcc", + message.mcc.toString()); + } catch (e) {} + } + + // Update lastKnownHomeNetwork. + if (message.mcc && message.mnc) { + this._lastKnownHomeNetwork = message.mcc + "-" + message.mnc; + } + + // If spn becomes available, we should check roaming again. + if (!oldSpn && message.spn) { + let voice = this.rilContext.voice; + let data = this.rilContext.data; + let voiceRoaming = voice.roaming; + let dataRoaming = data.roaming; + this.checkRoamingBetweenOperators(voice); + this.checkRoamingBetweenOperators(data); + if (voiceRoaming != voice.roaming) { + gMessageManager.sendMobileConnectionMessage("RIL:VoiceInfoChanged", + this.clientId, voice); + } + if (dataRoaming != data.roaming) { + gMessageManager.sendMobileConnectionMessage("RIL:DataInfoChanged", + this.clientId, data); + } + } + }, + + handleUSSDReceived: function(ussd) { + if (DEBUG) this.debug("handleUSSDReceived " + JSON.stringify(ussd)); + gSystemMessenger.broadcastMessage("ussd-received", ussd); + gMessageManager.sendMobileConnectionMessage("RIL:USSDReceived", + this.clientId, ussd); + }, + + handleStkProactiveCommand: function(message) { + if (DEBUG) this.debug("handleStkProactiveCommand " + JSON.stringify(message)); + let iccId = this.rilContext.iccInfo && this.rilContext.iccInfo.iccid; + if (iccId) { + gSystemMessenger.broadcastMessage("icc-stkcommand", + {iccId: iccId, + command: message}); + } + gMessageManager.sendIccMessage("RIL:StkCommand", this.clientId, message); + }, + + handleExitEmergencyCbMode: function(message) { + if (DEBUG) this.debug("handleExitEmergencyCbMode: " + JSON.stringify(message)); + gMessageManager.sendRequestResults("RIL:ExitEmergencyCbMode", message); + }, + + // nsIObserver + + observe: function(subject, topic, data) { + switch (topic) { + case kMozSettingsChangedObserverTopic: + let setting = JSON.parse(data); + this.handleSettingsChange(setting.key, setting.value, setting.message); + break; + case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID: + if (data === kPrefCellBroadcastDisabled) { + let value = false; + try { + value = Services.prefs.getBoolPref(kPrefCellBroadcastDisabled); + } catch(e) {} + this.workerMessenger.send("setCellBroadcastDisabled", + { disabled: value }); + } + break; + case kSysClockChangeObserverTopic: + let offset = parseInt(data, 10); + if (this._lastNitzMessage) { + this._lastNitzMessage.receiveTimeInMS += offset; + } + this._sntp.updateOffset(offset); + break; + case kNetworkConnStateChangedTopic: + let network = subject.QueryInterface(Ci.nsINetworkInterface); + if (network.state != Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED) { + return; + } + + // SNTP can only update when we have mobile or Wifi connections. + if (network.type != Ci.nsINetworkInterface.NETWORK_TYPE_WIFI && + network.type != Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE) { + return; + } + + // If the network comes from RIL, make sure the RIL service is matched. + if (subject instanceof Ci.nsIRilNetworkInterface) { + network = subject.QueryInterface(Ci.nsIRilNetworkInterface); + if (network.serviceId != this.clientId) { + return; + } + } + + // SNTP won't update unless the SNTP is already expired. + if (this._sntp.isExpired()) { + this._sntp.request(); + } + break; + case kNetworkActiveChangedTopic: + let dataInfo = this.rilContext.data; + let connected = false; + if (gNetworkManager.active && + gNetworkManager.active.type === + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE && + gNetworkManager.active.serviceId === this.clientId) { + connected = true; + } + if (dataInfo.connected !== connected) { + dataInfo.connected = connected; + gMessageManager.sendMobileConnectionMessage("RIL:DataInfoChanged", + this.clientId, dataInfo); + } + break; + case kScreenStateChangedTopic: + this.workerMessenger.send("setScreenState", { on: (data === "on") }); + break; + } + }, + + supportedNetworkTypes: null, + + // Flag to determine whether to update system clock automatically. It + // corresponds to the "time.clock.automatic-update.enabled" setting. + _clockAutoUpdateEnabled: null, + + // Flag to determine whether to update system timezone automatically. It + // corresponds to the "time.clock.automatic-update.enabled" setting. + _timezoneAutoUpdateEnabled: null, + + // Remember the last NITZ message so that we can set the time based on + // the network immediately when users enable network-based time. + _lastNitzMessage: null, + + // Object that handles SNTP. + _sntp: null, + + // Cell Broadcast settings values. + _cellBroadcastSearchList: null, + + // Operator's mcc-mnc. + _lastKnownNetwork: null, + + // ICC's mcc-mnc. + _lastKnownHomeNetwork: null, + + handleSettingsChange: function(aName, aResult, aMessage) { + // Don't allow any content processes to modify the setting + // "time.clock.automatic-update.available" except for the chrome process. + if (aName === kSettingsClockAutoUpdateAvailable && + aMessage !== "fromInternalSetting") { + let isClockAutoUpdateAvailable = this._lastNitzMessage !== null || + this._sntp.isAvailable(); + if (aResult !== isClockAutoUpdateAvailable) { + if (DEBUG) { + debug("Content processes cannot modify 'time.clock.automatic-update.available'. Restore!"); + } + // Restore the setting to the current value. + this.setClockAutoUpdateAvailable(isClockAutoUpdateAvailable); + } + } + + // Don't allow any content processes to modify the setting + // "time.timezone.automatic-update.available" except for the chrome + // process. + if (aName === kSettingsTimezoneAutoUpdateAvailable && + aMessage !== "fromInternalSetting") { + let isTimezoneAutoUpdateAvailable = this._lastNitzMessage !== null; + if (aResult !== isTimezoneAutoUpdateAvailable) { + if (DEBUG) { + this.debug("Content processes cannot modify 'time.timezone.automatic-update.available'. Restore!"); + } + // Restore the setting to the current value. + this.setTimezoneAutoUpdateAvailable(isTimezoneAutoUpdateAvailable); + } + } + + this.handle(aName, aResult); + }, + + // nsISettingsServiceCallback + handle: function(aName, aResult) { + switch(aName) { + case kSettingsClockAutoUpdateEnabled: + this._clockAutoUpdateEnabled = aResult; + if (!this._clockAutoUpdateEnabled) { + break; + } + + // Set the latest cached NITZ time if it's available. + if (this._lastNitzMessage) { + this.setClockByNitz(this._lastNitzMessage); + } else if (gNetworkManager.active && gNetworkManager.active.state == + Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED) { + // Set the latest cached SNTP time if it's available. + if (!this._sntp.isExpired()) { + this.setClockBySntp(this._sntp.getOffset()); + } else { + // Or refresh the SNTP. + this._sntp.request(); + } + } else { + // Set a sane minimum time. + let buildTime = libcutils.property_get("ro.build.date.utc", "0") * 1000; + let file = FileUtils.File("/system/b2g/b2g"); + if (file.lastModifiedTime > buildTime) { + buildTime = file.lastModifiedTime; + } + if (buildTime > Date.now()) { + gTimeService.set(buildTime); + } + } + break; + case kSettingsTimezoneAutoUpdateEnabled: + this._timezoneAutoUpdateEnabled = aResult; + + if (this._timezoneAutoUpdateEnabled) { + // Apply the latest cached NITZ for timezone if it's available. + if (this._timezoneAutoUpdateEnabled && this._lastNitzMessage) { + this.setTimezoneByNitz(this._lastNitzMessage); + } + } + break; + case kSettingsCellBroadcastSearchList: + if (DEBUG) { + this.debug("'" + kSettingsCellBroadcastSearchList + + "' is now " + JSON.stringify(aResult)); + } + // TODO: Set searchlist for Multi-SIM. See Bug 921326. + let result = Array.isArray(aResult) ? aResult[0] : aResult; + this.setCellBroadcastSearchList(result); + break; + } + }, + + handleError: function(aErrorMessage) { + if (DEBUG) { + this.debug("There was an error while reading RIL settings."); + } + }, + + // nsIRadioInterface + + rilContext: null, + + // Handle phone functions of nsIRILContentHelper + + _sendCfStateChanged: function(message) { + gMessageManager.sendMobileConnectionMessage("RIL:CfStateChanged", + this.clientId, message); + }, + + _updateCallingLineIdRestrictionPref: function(mode) { + try { + Services.prefs.setIntPref(kPrefClirModePreference, mode); + Services.prefs.savePrefFile(null); + if (DEBUG) { + this.debug(kPrefClirModePreference + " pref is now " + mode); + } + } catch (e) {} + }, + + sendMMI: function(target, message) { + if (DEBUG) this.debug("SendMMI " + JSON.stringify(message)); + this.workerMessenger.send("sendMMI", message, (function(response) { + if (response.isSetCallForward) { + this._sendCfStateChanged(response); + } else if (response.isSetCLIR && response.success) { + this._updateCallingLineIdRestrictionPref(response.clirMode); + } + + target.sendAsyncMessage("RIL:SendMMI", { + clientId: this.clientId, + data: response + }); + return false; + }).bind(this)); + }, + + setCallForwardingOptions: function(target, message) { + if (DEBUG) this.debug("setCallForwardingOptions: " + JSON.stringify(message)); + message.serviceClass = RIL.ICC_SERVICE_CLASS_VOICE; + this.workerMessenger.send("setCallForward", message, (function(response) { + this._sendCfStateChanged(response); + target.sendAsyncMessage("RIL:SetCallForwardingOptions", { + clientId: this.clientId, + data: response + }); + return false; + }).bind(this)); + }, + + setCallingLineIdRestriction: function(target, message) { + if (DEBUG) { + this.debug("setCallingLineIdRestriction: " + JSON.stringify(message)); + } + this.workerMessenger.send("setCLIR", message, (function(response) { + if (response.success) { + this._updateCallingLineIdRestrictionPref(response.clirMode); + } + target.sendAsyncMessage("RIL:SetCallingLineIdRestriction", { + clientId: this.clientId, + data: response + }); + return false; + }).bind(this)); + }, + + isValidStateForSetRadioEnabled: function() { + let state = this.rilContext.detailedRadioState; + return state == RIL.GECKO_DETAILED_RADIOSTATE_ENABLED || + state == RIL.GECKO_DETAILED_RADIOSTATE_DISABLED; + }, + + isDummyForSetRadioEnabled: function(message) { + let state = this.rilContext.detailedRadioState; + return (state == RIL.GECKO_DETAILED_RADIOSTATE_ENABLED && message.enabled) || + (state == RIL.GECKO_DETAILED_RADIOSTATE_DISABLED && !message.enabled); + }, + + setRadioEnabledResponse: function(target, message, errorMsg) { + if (errorMsg) { + message.errorMsg = errorMsg; + } + + target.sendAsyncMessage("RIL:SetRadioEnabled", { + clientId: this.clientId, + data: message + }); + }, + + setRadioEnabled: function(target, message) { + if (DEBUG) { + this.debug("setRadioEnabled: " + JSON.stringify(message)); + } + + if (!this.isValidStateForSetRadioEnabled()) { + this.setRadioEnabledResponse(target, message, "InvalidStateError"); + return; + } + + if (this.isDummyForSetRadioEnabled(message)) { + this.setRadioEnabledResponse(target, message); + return; + } + + let callback = (function(response) { + if (response.errorMsg) { + // Request fails. Rollback to the original radiostate. + let state = message.enabled ? RIL.GECKO_DETAILED_RADIOSTATE_DISABLED + : RIL.GECKO_DETAILED_RADIOSTATE_ENABLED; + this.handleDetailedRadioStateChanged(state); + } + this.setRadioEnabledResponse(target, response); + return false; + }).bind(this); + + this.setRadioEnabledInternal(message, callback); + }, + + setRadioEnabledInternal: function(message, callback) { + let state = message.enabled ? RIL.GECKO_DETAILED_RADIOSTATE_ENABLING + : RIL.GECKO_DETAILED_RADIOSTATE_DISABLING; + this.handleDetailedRadioStateChanged(state); + this.workerMessenger.send("setRadioEnabled", message, callback); + }, + + /** + * List of tuples of national language identifier pairs. + * + * TODO: Support static/runtime settings, see bug 733331. + */ + enabledGsmTableTuples: [ + [RIL.PDU_NL_IDENTIFIER_DEFAULT, RIL.PDU_NL_IDENTIFIER_DEFAULT], + ], + + /** + * Use 16-bit reference number for concatenated outgoint messages. + * + * TODO: Support static/runtime settings, see bug 733331. + */ + segmentRef16Bit: false, + + /** + * Get valid SMS concatenation reference number. + */ + _segmentRef: 0, + get nextSegmentRef() { + let ref = this._segmentRef++; + + this._segmentRef %= (this.segmentRef16Bit ? 65535 : 255); + + // 0 is not a valid SMS concatenation reference number. + return ref + 1; + }, + + /** + * Calculate encoded length using specified locking/single shift table + * + * @param message + * message string to be encoded. + * @param langTable + * locking shift table string. + * @param langShiftTable + * single shift table string. + * @param strict7BitEncoding + * Optional. Enable Latin characters replacement with corresponding + * ones in GSM SMS 7-bit default alphabet. + * + * @return encoded length in septets. + * + * @note that the algorithm used in this function must match exactly with + * GsmPDUHelper#writeStringAsSeptets. + */ + _countGsm7BitSeptets: function(message, langTable, langShiftTable, strict7BitEncoding) { + let length = 0; + for (let msgIndex = 0; msgIndex < message.length; msgIndex++) { + let c = message.charAt(msgIndex); + if (strict7BitEncoding) { + c = RIL.GSM_SMS_STRICT_7BIT_CHARMAP[c] || c; + } + + let septet = langTable.indexOf(c); + + // According to 3GPP TS 23.038, section 6.1.1 General notes, "The + // characters marked '1)' are not used but are displayed as a space." + if (septet == RIL.PDU_NL_EXTENDED_ESCAPE) { + continue; + } + + if (septet >= 0) { + length++; + continue; + } + + septet = langShiftTable.indexOf(c); + if (septet < 0) { + if (!strict7BitEncoding) { + return -1; + } + + // Bug 816082, when strict7BitEncoding is enabled, we should replace + // characters that can't be encoded with GSM 7-Bit alphabets with '*'. + c = "*"; + if (langTable.indexOf(c) >= 0) { + length++; + } else if (langShiftTable.indexOf(c) >= 0) { + length += 2; + } else { + // We can't even encode a '*' character with current configuration. + return -1; + } + + continue; + } + + // According to 3GPP TS 23.038 B.2, "This code represents a control + // character and therefore must not be used for language specific + // characters." + if (septet == RIL.PDU_NL_RESERVED_CONTROL) { + continue; + } + + // The character is not found in locking shfit table, but could be + // encoded as with single shift table. Note that it's + // still possible for septet to has the value of PDU_NL_EXTENDED_ESCAPE, + // but we can display it as a space in this case as said in previous + // comment. + length += 2; + } + + return length; + }, + + /** + * Calculate user data length of specified message string encoded in GSM 7Bit + * alphabets. + * + * @param message + * a message string to be encoded. + * @param strict7BitEncoding + * Optional. Enable Latin characters replacement with corresponding + * ones in GSM SMS 7-bit default alphabet. + * + * @return null or an options object with attributes `dcs`, + * `userDataHeaderLength`, `encodedFullBodyLength`, `langIndex`, + * `langShiftIndex`, `segmentMaxSeq` set. + * + * @see #_calculateUserDataLength(). + */ + _calculateUserDataLength7Bit: function(message, strict7BitEncoding) { + let options = null; + let minUserDataSeptets = Number.MAX_VALUE; + for (let i = 0; i < this.enabledGsmTableTuples.length; i++) { + let [langIndex, langShiftIndex] = this.enabledGsmTableTuples[i]; + + const langTable = RIL.PDU_NL_LOCKING_SHIFT_TABLES[langIndex]; + const langShiftTable = RIL.PDU_NL_SINGLE_SHIFT_TABLES[langShiftIndex]; + + let bodySeptets = this._countGsm7BitSeptets(message, + langTable, + langShiftTable, + strict7BitEncoding); + if (bodySeptets < 0) { + continue; + } + + let headerLen = 0; + if (langIndex != RIL.PDU_NL_IDENTIFIER_DEFAULT) { + headerLen += 3; // IEI + len + langIndex + } + if (langShiftIndex != RIL.PDU_NL_IDENTIFIER_DEFAULT) { + headerLen += 3; // IEI + len + langShiftIndex + } + + // Calculate full user data length, note the extra byte is for header len + let headerSeptets = Math.ceil((headerLen ? headerLen + 1 : 0) * 8 / 7); + let segmentSeptets = RIL.PDU_MAX_USER_DATA_7BIT; + if ((bodySeptets + headerSeptets) > segmentSeptets) { + headerLen += this.segmentRef16Bit ? 6 : 5; + headerSeptets = Math.ceil((headerLen + 1) * 8 / 7); + segmentSeptets -= headerSeptets; + } + + let segments = Math.ceil(bodySeptets / segmentSeptets); + let userDataSeptets = bodySeptets + headerSeptets * segments; + if (userDataSeptets >= minUserDataSeptets) { + continue; + } + + minUserDataSeptets = userDataSeptets; + + options = { + dcs: RIL.PDU_DCS_MSG_CODING_7BITS_ALPHABET, + encodedFullBodyLength: bodySeptets, + userDataHeaderLength: headerLen, + langIndex: langIndex, + langShiftIndex: langShiftIndex, + segmentMaxSeq: segments, + segmentChars: segmentSeptets, + }; + } + + return options; + }, + + /** + * Calculate user data length of specified message string encoded in UCS2. + * + * @param message + * a message string to be encoded. + * + * @return an options object with attributes `dcs`, `userDataHeaderLength`, + * `encodedFullBodyLength`, `segmentMaxSeq` set. + * + * @see #_calculateUserDataLength(). + */ + _calculateUserDataLengthUCS2: function(message) { + let bodyChars = message.length; + let headerLen = 0; + let headerChars = Math.ceil((headerLen ? headerLen + 1 : 0) / 2); + let segmentChars = RIL.PDU_MAX_USER_DATA_UCS2; + if ((bodyChars + headerChars) > segmentChars) { + headerLen += this.segmentRef16Bit ? 6 : 5; + headerChars = Math.ceil((headerLen + 1) / 2); + segmentChars -= headerChars; + } + + let segments = Math.ceil(bodyChars / segmentChars); + + return { + dcs: RIL.PDU_DCS_MSG_CODING_16BITS_ALPHABET, + encodedFullBodyLength: bodyChars * 2, + userDataHeaderLength: headerLen, + segmentMaxSeq: segments, + segmentChars: segmentChars, + }; + }, + + /** + * Calculate user data length and its encoding. + * + * @param message + * a message string to be encoded. + * @param strict7BitEncoding + * Optional. Enable Latin characters replacement with corresponding + * ones in GSM SMS 7-bit default alphabet. + * + * @return an options object with some or all of following attributes set: + * + * @param dcs + * Data coding scheme. One of the PDU_DCS_MSG_CODING_*BITS_ALPHABET + * constants. + * @param userDataHeaderLength + * Length of embedded user data header, in bytes. The whole header + * size will be userDataHeaderLength + 1; 0 for no header. + * @param encodedFullBodyLength + * Length of the message body when encoded with the given DCS. For + * UCS2, in bytes; for 7-bit, in septets. + * @param langIndex + * Table index used for normal 7-bit encoded character lookup. + * @param langShiftIndex + * Table index used for escaped 7-bit encoded character lookup. + * @param segmentMaxSeq + * Max sequence number of a multi-part messages, or 1 for single one. + * This number might not be accurate for a multi-part message until + * it's processed by #_fragmentText() again. + */ + _calculateUserDataLength: function(message, strict7BitEncoding) { + let options = this._calculateUserDataLength7Bit(message, strict7BitEncoding); + if (!options) { + options = this._calculateUserDataLengthUCS2(message); + } + + if (DEBUG) this.debug("_calculateUserDataLength: " + JSON.stringify(options)); + return options; + }, + + /** + * Fragment GSM 7-Bit encodable string for transmission. + * + * @param text + * text string to be fragmented. + * @param langTable + * locking shift table string. + * @param langShiftTable + * single shift table string. + * @param segmentSeptets + * Number of available spetets per segment. + * @param strict7BitEncoding + * Optional. Enable Latin characters replacement with corresponding + * ones in GSM SMS 7-bit default alphabet. + * + * @return an array of objects. See #_fragmentText() for detailed definition. + */ + _fragmentText7Bit: function(text, langTable, langShiftTable, segmentSeptets, strict7BitEncoding) { + let ret = []; + let body = "", len = 0; + // If the message is empty, we only push the empty message to ret. + if (text.length === 0) { + ret.push({ + body: text, + encodedBodyLength: text.length, + }); + return ret; + } + + for (let i = 0, inc = 0; i < text.length; i++) { + let c = text.charAt(i); + if (strict7BitEncoding) { + c = RIL.GSM_SMS_STRICT_7BIT_CHARMAP[c] || c; + } + + let septet = langTable.indexOf(c); + if (septet == RIL.PDU_NL_EXTENDED_ESCAPE) { + continue; + } + + if (septet >= 0) { + inc = 1; + } else { + septet = langShiftTable.indexOf(c); + if (septet == RIL.PDU_NL_RESERVED_CONTROL) { + continue; + } + + inc = 2; + if (septet < 0) { + if (!strict7BitEncoding) { + throw new Error("Given text cannot be encoded with GSM 7-bit Alphabet!"); + } + + // Bug 816082, when strict7BitEncoding is enabled, we should replace + // characters that can't be encoded with GSM 7-Bit alphabets with '*'. + c = "*"; + if (langTable.indexOf(c) >= 0) { + inc = 1; + } + } + } + + if ((len + inc) > segmentSeptets) { + ret.push({ + body: body, + encodedBodyLength: len, + }); + body = c; + len = inc; + } else { + body += c; + len += inc; + } + } + + if (len) { + ret.push({ + body: body, + encodedBodyLength: len, + }); + } + + return ret; + }, + + /** + * Fragment UCS2 encodable string for transmission. + * + * @param text + * text string to be fragmented. + * @param segmentChars + * Number of available characters per segment. + * + * @return an array of objects. See #_fragmentText() for detailed definition. + */ + _fragmentTextUCS2: function(text, segmentChars) { + let ret = []; + // If the message is empty, we only push the empty message to ret. + if (text.length === 0) { + ret.push({ + body: text, + encodedBodyLength: text.length, + }); + return ret; + } + + for (let offset = 0; offset < text.length; offset += segmentChars) { + let str = text.substr(offset, segmentChars); + ret.push({ + body: str, + encodedBodyLength: str.length * 2, + }); + } + + return ret; + }, + + /** + * Fragment string for transmission. + * + * Fragment input text string into an array of objects that contains + * attributes `body`, substring for this segment, `encodedBodyLength`, + * length of the encoded segment body in septets. + * + * @param text + * Text string to be fragmented. + * @param options + * Optional pre-calculated option object. The output array will be + * stored at options.segments if there are multiple segments. + * @param strict7BitEncoding + * Optional. Enable Latin characters replacement with corresponding + * ones in GSM SMS 7-bit default alphabet. + * + * @return Populated options object. + */ + _fragmentText: function(text, options, strict7BitEncoding) { + if (!options) { + options = this._calculateUserDataLength(text, strict7BitEncoding); + } + + if (options.dcs == RIL.PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + const langTable = RIL.PDU_NL_LOCKING_SHIFT_TABLES[options.langIndex]; + const langShiftTable = RIL.PDU_NL_SINGLE_SHIFT_TABLES[options.langShiftIndex]; + options.segments = this._fragmentText7Bit(text, + langTable, langShiftTable, + options.segmentChars, + strict7BitEncoding); + } else { + options.segments = this._fragmentTextUCS2(text, + options.segmentChars); + } + + // Re-sync options.segmentMaxSeq with actual length of returning array. + options.segmentMaxSeq = options.segments.length; + + return options; + }, + + getSegmentInfoForText: function(text, request) { + let strict7BitEncoding; + try { + strict7BitEncoding = Services.prefs.getBoolPref("dom.sms.strict7BitEncoding"); + } catch (e) { + strict7BitEncoding = false; + } + + let options = this._fragmentText(text, null, strict7BitEncoding); + let charsInLastSegment; + if (options.segmentMaxSeq) { + let lastSegment = options.segments[options.segmentMaxSeq - 1]; + charsInLastSegment = lastSegment.encodedBodyLength; + if (options.dcs == RIL.PDU_DCS_MSG_CODING_16BITS_ALPHABET) { + // In UCS2 encoding, encodedBodyLength is in octets. + charsInLastSegment /= 2; + } + } else { + charsInLastSegment = 0; + } + + let result = gMobileMessageService + .createSmsSegmentInfo(options.segmentMaxSeq, + options.segmentChars, + options.segmentChars - charsInLastSegment); + request.notifySegmentInfoForTextGot(result); + }, + + getSmscAddress: function(request) { + this.workerMessenger.send("getSmscAddress", + null, + (function(response) { + if (!response.errorMsg) { + request.notifyGetSmscAddress(response.smscAddress); + } else { + request.notifyGetSmscAddressFailed(response.errorMsg); + } + }).bind(this)); + }, + + sendSMS: function(number, message, silent, request) { + let strict7BitEncoding; + try { + strict7BitEncoding = Services.prefs.getBoolPref("dom.sms.strict7BitEncoding"); + } catch (e) { + strict7BitEncoding = false; + } + + let options = this._fragmentText(message, null, strict7BitEncoding); + options.number = PhoneNumberUtils.normalize(number); + let requestStatusReport; + try { + requestStatusReport = + Services.prefs.getBoolPref("dom.sms.requestStatusReport"); + } catch (e) { + requestStatusReport = true; + } + options.requestStatusReport = requestStatusReport && !silent; + if (options.segmentMaxSeq > 1) { + options.segmentRef16Bit = this.segmentRef16Bit; + options.segmentRef = this.nextSegmentRef; + } + + let notifyResult = (function notifyResult(rv, domMessage) { + if (!Components.isSuccessCode(rv)) { + if (DEBUG) this.debug("Error! Fail to save sending message! rv = " + rv); + request.notifySendMessageFailed( + gMobileMessageDatabaseService.translateCrErrorToMessageCallbackError(rv)); + Services.obs.notifyObservers(domMessage, kSmsFailedObserverTopic, null); + return; + } + + if (!silent) { + Services.obs.notifyObservers(domMessage, kSmsSendingObserverTopic, null); + } + + // If the radio is disabled or the SIM card is not ready, just directly + // return with the corresponding error code. + let errorCode; + if (!PhoneNumberUtils.isPlainPhoneNumber(options.number)) { + if (DEBUG) this.debug("Error! Address is invalid when sending SMS: " + + options.number); + errorCode = Ci.nsIMobileMessageCallback.INVALID_ADDRESS_ERROR; + } else if (this.rilContext.detailedRadioState == + RIL.GECKO_DETAILED_RADIOSTATE_DISABLED) { + if (DEBUG) this.debug("Error! Radio is disabled when sending SMS."); + errorCode = Ci.nsIMobileMessageCallback.RADIO_DISABLED_ERROR; + } else if (this.rilContext.cardState != "ready") { + if (DEBUG) this.debug("Error! SIM card is not ready when sending SMS."); + errorCode = Ci.nsIMobileMessageCallback.NO_SIM_CARD_ERROR; + } + if (errorCode) { + if (silent) { + request.notifySendMessageFailed(errorCode); + return; + } + + gMobileMessageDatabaseService + .setMessageDeliveryByMessageId(domMessage.id, + null, + DOM_MOBILE_MESSAGE_DELIVERY_ERROR, + RIL.GECKO_SMS_DELIVERY_STATUS_ERROR, + null, + function notifyResult(rv, domMessage) { + // TODO bug 832140 handle !Components.isSuccessCode(rv) + request.notifySendMessageFailed(errorCode); + Services.obs.notifyObservers(domMessage, kSmsFailedObserverTopic, null); + }); + return; + } + + // Keep current SMS message info for sent/delivered notifications + let context = { + request: request, + sms: domMessage, + requestStatusReport: options.requestStatusReport, + silent: silent + }; + + // This is the entry point starting to send SMS. + this.workerMessenger.send("sendSMS", options, + (function(context, response) { + if (response.errorMsg) { + // Failed to send SMS out. + let error = Ci.nsIMobileMessageCallback.UNKNOWN_ERROR; + switch (response.errorMsg) { + case RIL.ERROR_RADIO_NOT_AVAILABLE: + error = Ci.nsIMobileMessageCallback.NO_SIGNAL_ERROR; + break; + case RIL.ERROR_FDN_CHECK_FAILURE: + error = Ci.nsIMobileMessageCallback.FDN_CHECK_ERROR; + break; + } + + if (context.silent) { + context.request.notifySendMessageFailed(error); + return false; + } + + gMobileMessageDatabaseService + .setMessageDeliveryByMessageId(context.sms.id, + null, + DOM_MOBILE_MESSAGE_DELIVERY_ERROR, + RIL.GECKO_SMS_DELIVERY_STATUS_ERROR, + null, + function notifyResult(rv, domMessage) { + // TODO bug 832140 handle !Components.isSuccessCode(rv) + context.request.notifySendMessageFailed(error); + Services.obs.notifyObservers(domMessage, kSmsFailedObserverTopic, null); + }); + return false; + } // End of send failure. + + if (response.deliveryStatus) { + // Message delivery. + gMobileMessageDatabaseService + .setMessageDeliveryByMessageId(context.sms.id, + null, + context.sms.delivery, + response.deliveryStatus, + null, + (function notifyResult(rv, domMessage) { + // TODO bug 832140 handle !Components.isSuccessCode(rv) + + let topic = (response.deliveryStatus == + RIL.GECKO_SMS_DELIVERY_STATUS_SUCCESS) + ? kSmsDeliverySuccessObserverTopic + : kSmsDeliveryErrorObserverTopic; + + // Broadcasting a "sms-delivery-success" system message to open apps. + if (topic == kSmsDeliverySuccessObserverTopic) { + this.broadcastSmsSystemMessage(topic, domMessage); + } + + // Notifying observers the delivery status is updated. + Services.obs.notifyObservers(domMessage, topic, null); + }).bind(this)); + + // Send transaction has ended completely. + return false; + } // End of message delivery. + + // Message sent. + if (context.silent) { + // There is no way to modify nsIDOMMozSmsMessage attributes as they + // are read only so we just create a new sms instance to send along + // with the notification. + let sms = context.sms; + context.request.notifyMessageSent( + gMobileMessageService.createSmsMessage(sms.id, + sms.threadId, + sms.iccId, + DOM_MOBILE_MESSAGE_DELIVERY_SENT, + sms.deliveryStatus, + sms.sender, + sms.receiver, + sms.body, + sms.messageClass, + sms.timestamp, + Date.now(), + 0, + sms.read)); + // We don't wait for SMS-DELIVER-REPORT for silent one. + return false; + } + + gMobileMessageDatabaseService + .setMessageDeliveryByMessageId(context.sms.id, + null, + DOM_MOBILE_MESSAGE_DELIVERY_SENT, + context.sms.deliveryStatus, + null, + (function notifyResult(rv, domMessage) { + // TODO bug 832140 handle !Components.isSuccessCode(rv) + + if (context.requestStatusReport) { + context.sms = domMessage; + } + + this.broadcastSmsSystemMessage(kSmsSentObserverTopic, domMessage); + context.request.notifyMessageSent(domMessage); + Services.obs.notifyObservers(domMessage, kSmsSentObserverTopic, null); + }).bind(this)); + + // Only keep current context if we have requested for delivery report. + return context.requestStatusReport; + }).bind(this, context)); // End of |workerMessenger.send| callback. + }).bind(this); // End of DB saveSendingMessage callback. + + let sendingMessage = { + type: "sms", + sender: this.getPhoneNumber(), + receiver: number, + body: message, + deliveryStatusRequested: options.requestStatusReport, + timestamp: Date.now(), + iccId: this.getIccId() + }; + + if (silent) { + let delivery = DOM_MOBILE_MESSAGE_DELIVERY_SENDING; + let deliveryStatus = RIL.GECKO_SMS_DELIVERY_STATUS_PENDING; + let domMessage = + gMobileMessageService.createSmsMessage(-1, // id + 0, // threadId + sendingMessage.iccId, + delivery, + deliveryStatus, + sendingMessage.sender, + sendingMessage.receiver, + sendingMessage.body, + "normal", // message class + sendingMessage.timestamp, + 0, + 0, + false); + notifyResult(Cr.NS_OK, domMessage); + return; + } + + let id = gMobileMessageDatabaseService.saveSendingMessage( + sendingMessage, notifyResult); + }, + + // TODO: Bug 928861 - B2G NetworkManager: Provide a more generic function + // for connecting + setupDataCallByType: function(apntype) { + let connHandler = gDataConnectionManager.getConnectionHandler(this.clientId); + connHandler.setupDataCallByType(apntype); + }, + + // TODO: Bug 928861 - B2G NetworkManager: Provide a more generic function + // for connecting + deactivateDataCallByType: function(apntype) { + let connHandler = gDataConnectionManager.getConnectionHandler(this.clientId); + connHandler.deactivateDataCallByType(apntype); + }, + + // TODO: Bug 904514 - [meta] NetworkManager enhancement + getDataCallStateByType: function(apntype) { + let connHandler = gDataConnectionManager.getConnectionHandler(this.clientId); + return connHandler.getDataCallStateByType(apntype); + }, + + setupDataCall: function(radioTech, apn, user, passwd, chappap, pdptype) { + this.workerMessenger.send("setupDataCall", { radioTech: radioTech, + apn: apn, + user: user, + passwd: passwd, + chappap: chappap, + pdptype: pdptype }); + }, + + deactivateDataCall: function(cid, reason) { + this.workerMessenger.send("deactivateDataCall", { cid: cid, + reason: reason }); + }, + + sendWorkerMessage: function(rilMessageType, message, callback) { + if (callback) { + this.workerMessenger.send(rilMessageType, message, function(response) { + return callback.handleResponse(response); + }); + } else { + this.workerMessenger.send(rilMessageType, message); + } + } +}; + +function RILNetworkInterface(dataConnectionHandler, apnSetting) { + this.dataConnectionHandler = dataConnectionHandler; + this.apnSetting = apnSetting; + this.connectedTypes = []; + + this.ips = []; + this.prefixLengths = []; + this.dnses = []; + this.gateways = []; +} + +RILNetworkInterface.prototype = { + classID: RILNETWORKINTERFACE_CID, + classInfo: XPCOMUtils.generateCI({classID: RILNETWORKINTERFACE_CID, + classDescription: "RILNetworkInterface", + interfaces: [Ci.nsINetworkInterface, + Ci.nsIRilNetworkInterface]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterface, + Ci.nsIRilNetworkInterface]), + + // nsINetworkInterface + + NETWORK_STATE_UNKNOWN: Ci.nsINetworkInterface.NETWORK_STATE_UNKNOWN, + NETWORK_STATE_CONNECTING: Ci.nsINetworkInterface.CONNECTING, + NETWORK_STATE_CONNECTED: Ci.nsINetworkInterface.CONNECTED, + NETWORK_STATE_DISCONNECTING: Ci.nsINetworkInterface.DISCONNECTING, + NETWORK_STATE_DISCONNECTED: Ci.nsINetworkInterface.DISCONNECTED, + + NETWORK_TYPE_WIFI: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + NETWORK_TYPE_MOBILE: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, + NETWORK_TYPE_MOBILE_MMS: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_MMS, + NETWORK_TYPE_MOBILE_SUPL: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_SUPL, + NETWORK_TYPE_MOBILE_IMS: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_IMS, + NETWORK_TYPE_MOBILE_DUN: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN, + // The network manager should only need to add the host route for "other" + // types, which is the same handling method as the supl type. So let the + // definition of other types to be the same as the one of supl type. + NETWORK_TYPE_MOBILE_OTHERS: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_SUPL, + + /** + * 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, + + // Event timer for connection retries + timer: null, + + /** + * nsINetworkInterface Implementation + */ + + state: Ci.nsINetworkInterface.NETWORK_STATE_UNKNOWN, + + get type() { + if (this.connectedTypes.indexOf("default") != -1) { + return this.NETWORK_TYPE_MOBILE; + } + if (this.connectedTypes.indexOf("mms") != -1) { + return this.NETWORK_TYPE_MOBILE_MMS; + } + if (this.connectedTypes.indexOf("supl") != -1) { + return this.NETWORK_TYPE_MOBILE_SUPL; + } + if (this.connectedTypes.indexOf("ims") != -1) { + return this.NETWORK_TYPE_MOBILE_IMS; + } + if (this.connectedTypes.indexOf("dun") != -1) { + return this.NETWORK_TYPE_MOBILE_DUN; + } + + return this.NETWORK_TYPE_MOBILE_OTHERS; + }, + + name: null, + + ips: null, + + prefixLengths: null, + + gateways: null, + + dnses: null, + + get httpProxyHost() { + return this.apnSetting.proxy || ""; + }, + + get httpProxyPort() { + return this.apnSetting.port || ""; + }, + + /** + * nsIRilNetworkInterface Implementation + */ + + get serviceId() { + return this.dataConnectionHandler.clientId; + }, + + get iccId() { + let iccInfo = this.dataConnectionHandler.radioInterface.rilContext.iccInfo; + return iccInfo && iccInfo.iccid; + }, + + get mmsc() { + if (!this.inConnectedTypes("mms")) { + if (DEBUG) this.debug("Error! Only MMS network can get MMSC."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + let mmsc = this.apnSetting.mmsc; + if (!mmsc) { + try { + mmsc = Services.prefs.getCharPref("ril.mms.mmsc"); + } catch (e) { + mmsc = ""; + } + } + + return mmsc; + }, + + get mmsProxy() { + if (!this.inConnectedTypes("mms")) { + if (DEBUG) this.debug("Error! Only MMS network can get MMS proxy."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + let proxy = this.apnSetting.mmsproxy; + if (!proxy) { + try { + proxy = Services.prefs.getCharPref("ril.mms.mmsproxy"); + } catch (e) { + proxy = ""; + } + } + + return proxy; + }, + + get mmsPort() { + if (!this.inConnectedTypes("mms")) { + if (DEBUG) this.debug("Error! Only MMS network can get MMS port."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + let port = this.apnSetting.mmsport; + if (!port) { + try { + port = Services.prefs.getIntPref("ril.mms.mmsport"); + } catch (e) { + port = -1; + } + } + + return port; + }, + + getAddresses: function (ips, prefixLengths) { + ips.value = this.ips.slice(); + prefixLengths.value = this.prefixLengths.slice(); + + return this.ips.length; + }, + + getGateways: function (count) { + if (count) { + count.value = this.gateways.length; + } + return this.gateways.slice(); + }, + + getDnses: function (count) { + if (count) { + count.value = this.dnses.length; + } + return this.dnses.slice(); + }, + + debug: function(s) { + dump("-*- RILNetworkInterface[" + this.dataConnectionHandler.clientId + ":" + + this.type + "]: " + s + "\n"); + }, + + dataCallError: function(message) { + if (message.apn != this.apnSetting.apn) { + return; + } + if (DEBUG) this.debug("Data call error on APN: " + message.apn); + this.reset(); + }, + + dataCallStateChanged: function(datacall) { + if (this.cid && this.cid != datacall.cid) { + // If data call for this connection existed but cid mismatched, + // it means this datacall state change is not for us. + return; + } + // If data call for this connection does not exist, it could be state + // change for new data call. We only update data call state change + // if APN name matched. + if (!this.cid && datacall.apn != this.apnSetting.apn) { + return; + } + if (DEBUG) { + this.debug("Data call ID: " + datacall.cid + ", interface name: " + + datacall.ifname + ", APN name: " + datacall.apn); + } + if (this.connecting && + (datacall.state == RIL.GECKO_NETWORK_STATE_CONNECTING || + datacall.state == RIL.GECKO_NETWORK_STATE_CONNECTED)) { + this.connecting = false; + this.cid = datacall.cid; + this.name = datacall.ifname; + for (let entry of datacall.addresses) { + this.ips.push(entry.address); + this.prefixLengths.push(entry.prefixLength); + } + this.gateways = datacall.gateways.slice(); + this.dnses = datacall.dnses.slice(); + if (!this.registeredAsNetworkInterface) { + gNetworkManager.registerNetworkInterface(this); + this.registeredAsNetworkInterface = true; + } + } + // In current design, we don't update status of secondary APN if it shares + // same APN name with the default APN. In this condition, this.cid will + // not be set and we don't want to update its status. + if (this.cid == null) { + return; + } + + if (this.state == datacall.state) { + if (datacall.state != RIL.GECKO_NETWORK_STATE_CONNECTED) { + return; + } + // State remains connected, check for minor changes. + let changed = false; + if (this.ips.length != datacall.addresses.length) { + changed = true; + this.ips = []; + this.prefixLengths = []; + for (let entry of datacall.addresses) { + this.ips.push(entry.address); + this.prefixLengths.push(entry.prefixLength); + } + } + + let reduceFunc = function(aRhs, aChanged, aElement, aIndex) { + return aChanged || (aElement != aRhs[aIndex]); + }; + for (let field of ["gateways", "dnses"]) { + let lhs = this[field], rhs = datacall[field]; + if (lhs.length != rhs.length || + lhs.reduce(reduceFunc.bind(null, rhs), false)) { + changed = true; + this[field] = rhs.slice(); + } + } + + if (changed) { + if (DEBUG) this.debug("Notify for data call minor changes."); + Services.obs.notifyObservers(this, + kNetworkInterfaceStateChangedTopic, + null); + } + return; + } + + this.state = datacall.state; + + Services.obs.notifyObservers(this, + kNetworkInterfaceStateChangedTopic, + null); + + if ((this.state == RIL.GECKO_NETWORK_STATE_UNKNOWN || + this.state == RIL.GECKO_NETWORK_STATE_DISCONNECTED) && + this.registeredAsNetworkInterface) { + gNetworkManager.unregisterNetworkInterface(this); + this.registeredAsNetworkInterface = false; + this.cid = null; + this.connectedTypes = []; + + this.ips = []; + this.prefixLengths = []; + this.dnses = []; + this.gateways = []; + } + + // In case the data setting changed while the datacall was being started or + // ended, let's re-check the setting and potentially adjust the datacall + // state again. + let apnSettings = this.dataConnectionHandler.apnSettings; + if (apnSettings.byType.default && + (apnSettings.byType.default.apn == this.apnSetting.apn)) { + this.dataConnectionHandler.updateRILNetworkInterface(); + } + }, + + // Helpers + + cid: null, + registeredAsDataCallCallback: false, + registeredAsNetworkInterface: false, + connecting: false, + apnSetting: null, + + // APN failed connections. Retry counter + apnRetryCounter: 0, + + connectedTypes: null, + + inConnectedTypes: function(type) { + return this.connectedTypes.indexOf(type) != -1; + }, + + get connected() { + return this.state == RIL.GECKO_NETWORK_STATE_CONNECTED; + }, + + connect: function(apntype) { + if (apntype && !this.inConnectedTypes(apntype)) { + this.connectedTypes.push(apntype); + } + + if (this.connecting || this.connected) { + return; + } + + // When the retry mechanism is running in background and someone calls + // disconnect(), this.connectedTypes.length has chances to become 0. + if (!this.connectedTypes.length) { + return; + } + + if (!this.registeredAsDataCallCallback) { + this.dataConnectionHandler.registerDataCallCallback(this); + this.registeredAsDataCallCallback = true; + } + + if (!this.apnSetting.apn) { + if (DEBUG) this.debug("APN name is empty, nothing to do."); + return; + } + + if (DEBUG) { + this.debug("Going to set up data connection with APN " + + this.apnSetting.apn); + } + let radioInterface = this.dataConnectionHandler.radioInterface; + let radioTechType = radioInterface.rilContext.data.type; + let radioTechnology = RIL.GECKO_RADIO_TECH.indexOf(radioTechType); + let authType = RIL.RIL_DATACALL_AUTH_TO_GECKO.indexOf(this.apnSetting.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.apnSetting.authtype); + } + authType = RIL.RIL_DATACALL_AUTH_TO_GECKO.indexOf(RIL.GECKO_DATACALL_AUTH_DEFAULT); + } + let pdpType = RIL.GECKO_DATACALL_PDP_TYPE_IP; + if (RILQUIRKS_HAVE_IPV6) { + pdpType = !radioInterface.rilContext.data.roaming + ? this.apnSetting.protocol + : this.apnSetting.roaming_protocol; + if (RIL.RIL_DATACALL_PDP_TYPES.indexOf(pdpType) < 0) { + if (DEBUG) { + this.debug("Invalid pdpType '" + pdpType + "', using '" + + RIL.GECKO_DATACALL_PDP_TYPE_DEFAULT + "'"); + } + pdpType = RIL.GECKO_DATACALL_PDP_TYPE_DEFAULT; + } + } + radioInterface.setupDataCall(radioTechnology, + this.apnSetting.apn, + this.apnSetting.user, + this.apnSetting.password, + authType, + pdpType); + this.connecting = true; + }, + + reset: function() { + let apnRetryTimer; + this.connecting = false; + // 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; + this.connectedTypes = []; + if (DEBUG) this.debug("Too many APN Connection retries - STOP retrying"); + return; + } + + 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(apntype) { + let index = this.connectedTypes.indexOf(apntype); + if (index != -1) { + this.connectedTypes.splice(index, 1); + } + + if (this.connectedTypes.length) { + return; + } + + if (this.state == RIL.GECKO_NETWORK_STATE_DISCONNECTING || + this.state == RIL.GECKO_NETWORK_STATE_DISCONNECTED || + this.state == RIL.GECKO_NETWORK_STATE_UNKNOWN) { + return; + } + let reason = RIL.DATACALL_DEACTIVATE_NO_REASON; + if (DEBUG) this.debug("Going to disconnet data connection " + this.cid); + this.dataConnectionHandler.radioInterface.deactivateDataCall(this.cid, + reason); + }, + + // Entry method for timer events. Used to reconnect to a failed APN + notify: function(timer) { + this.connect(); + }, + + shutdown: function() { + this.timer = null; + } + +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RadioInterfaceLayer]);