diff --git a/dom/telephony/nsITelephone.idl b/dom/telephony/nsITelephone.idl index 0d6c94b0f1b..7b42522a83a 100644 --- a/dom/telephony/nsITelephone.idl +++ b/dom/telephony/nsITelephone.idl @@ -53,7 +53,31 @@ interface nsITelephoneCallback : nsISupports in boolean isActive); }; -[scriptable, uuid(5be6e41d-3aee-4f5c-8284-95cf529dd6fe)] +[scriptable, uuid(8399fddd-471c-41ac-8f35-99f7dbb738ec)] +interface nsIDataCallInfo : nsISupports +{ + readonly attribute unsigned long callState; + readonly attribute AString cid; + readonly attribute AString apn; +}; + +[scriptable, uuid(36cc4b89-0338-4ff7-a3c2-d78e60f2ea98)] +interface nsIPhoneDataCallCallback : nsISupports +{ + /** + * This method is called when the state of a data call is changed. + * + * @param dataState use DATACALL_STATE_* values from nsITelephone. + */ + void dataCallStateChanged(in AString cid, + in AString interfaceName, + in unsigned short callState); + + void receiveDataCallList([array,size_is(aLength)] in nsIDataCallInfo aDataCalls, + in unsigned long aLength); +}; + +[scriptable, uuid(78ed0beb-d6ad-42f8-929a-8d003285784f)] interface nsITelephone : nsISupports { const unsigned short CALL_STATE_UNKNOWN = 0; @@ -69,6 +93,13 @@ interface nsITelephone : nsISupports const unsigned short CALL_STATE_DISCONNECTED = 10; const unsigned short CALL_STATE_INCOMING = 11; + // Keep consistent with GECKO_DATACALL_STATE_* values in ril_consts.js + const unsigned short DATACALL_STATE_UNKNOWN = 0; + const unsigned short DATACALL_STATE_CONNECTING = 1; + const unsigned short DATACALL_STATE_CONNECTED = 2; + const unsigned short DATACALL_STATE_DISCONNECTING = 3; + const unsigned short DATACALL_STATE_DISCONNECTED = 4; + readonly attribute jsval currentState; void registerCallback(in nsITelephoneCallback callback); @@ -95,6 +126,20 @@ interface nsITelephone : nsISupports attribute bool microphoneMuted; attribute bool speakerEnabled; + // PDP APIs + void setupDataCall(in long radioTech, + in DOMString apn, + in DOMString user, + in DOMString passwd, + in long chappap, + in DOMString pdptype); + void deactivateDataCall(in DOMString cid, + in DOMString reason); + void getDataCallList(); + + void registerDataCallCallback(in nsIPhoneDataCallCallback callback); + void unregisterDataCallCallback(in nsIPhoneDataCallCallback callback); + /** * SMS-related functionality. */ diff --git a/dom/telephony/nsTelephonyWorker.js b/dom/telephony/nsTelephonyWorker.js index cbf885715cc..882d4ff78bb 100644 --- a/dom/telephony/nsTelephonyWorker.js +++ b/dom/telephony/nsTelephonyWorker.js @@ -50,6 +50,8 @@ const DEBUG = true; // set to false to suppress debug messages const TELEPHONYWORKER_CID = Components.ID("{2d831c8d-6017-435b-a80c-e5d422810cea}"); +const DATACALLINFO_CID = + Components.ID("{ef474cd9-94f7-4c05-a31b-29b9de8a10d2}"); const nsIAudioManager = Ci.nsIAudioManager; const nsITelephone = Ci.nsITelephone; @@ -110,6 +112,20 @@ XPCOMUtils.defineLazyGetter(this, "gAudioManager", function getAudioManager() { }); +function DataCallInfo(state, cid, apn) { + this.callState = state; + this.cid = cid; + this.apn = apn; +} +DataCallInfo.protoptype = { + classID: DATACALLINFO_CID, + classInfo: XPCOMUtils.generateCI({classID: DATACALLINFO_CID, + classDescription: "DataCallInfo", + interfaces: [Ci.nsIDataCallInfo]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallInfo]), +}; + + function nsTelephonyWorker() { this.worker = new ChromeWorker("resource://gre/modules/ril_worker.js"); this.worker.onerror = this.onerror.bind(this); @@ -178,6 +194,12 @@ nsTelephonyWorker.prototype = { case "sms-received": this.handleSmsReceived(message); return; + case "datacallstatechange": + this.handleDataCallState(message); + break; + case "datacalllist": + this.handleDataCallList(message); + break; default: throw new Error("Don't know about this message type: " + message.type); } @@ -278,6 +300,29 @@ nsTelephonyWorker.prototype = { Services.obs.notifyObservers(sms, kSmsReceivedObserverTopic, null); }, + /** + * Handle data call state changes. + */ + handleDataCallState: function handleDataCallState(message) { + let ifname = message.ifname ? message.ifname : ""; + this._deliverDataCallCallback("dataCallStateChanged", + [message.cid, ifname, message.state]); + }, + + /** + * Handle data call list. + */ + handleDataCallList: function handleDataCallList(message) { + let datacalls = []; + for each (let datacall in message.datacalls) { + datacalls.push(new DataCallInfo(datacall.state, + datacall.cid, + datacall.apn)); + } + this._deliverDataCallCallback("receiveDataCallList", + [datacalls, datacalls.length]); + }, + // nsIRadioWorker worker: null, @@ -295,7 +340,7 @@ nsTelephonyWorker.prototype = { debug("Hanging up call no. " + callIndex); this.worker.postMessage({type: "hangUp", callIndex: callIndex}); }, - + startTone: function startTone(dtmfChar) { debug("Sending Tone for " + dtmfChar); this.worker.postMessage({type: "startTone", dtmfChar: dtmfChar}); @@ -415,6 +460,84 @@ nsTelephonyWorker.prototype = { } } }, + + registerDataCallCallback: function registerDataCallCallback(callback) { + if (this._datacall_callbacks) { + if (this._datacall_callbacks.indexOf(callback) != -1) { + throw new Error("Already registered this callback!"); + } + } else { + this._datacall_callbacks = []; + } + this._datacall_callbacks.push(callback); + debug("Registering callback: " + callback); + }, + + unregisterDataCallCallback: function unregisterDataCallCallback(callback) { + if (!this._datacall_callbacks) { + return; + } + let index = this._datacall_callbacks.indexOf(callback); + if (index != -1) { + this._datacall_callbacks.splice(index, 1); + debug("Unregistering callback: " + callback); + } + }, + + _deliverDataCallCallback: function _deliverDataCallCallback(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. + if (!this._datacall_callbacks) { + return; + } + let callbacks = this._datacall_callbacks.slice(); + for each (let callback in callbacks) { + if (this._datacall_callbacks.indexOf(callback) == -1) { + continue; + } + let handler = callback[name]; + if (typeof handler != "function") { + throw new Error("No handler for " + name); + } + try { + handler.apply(callback, args); + } catch (e) { + debug("callback handler for " + name + " threw an exception: " + e); + } + } + }, + + setupDataCall: function(radioTech, apn, user, passwd, chappap, pdptype) { + this.worker.postMessage({type: "setupDataCall", + radioTech: radioTech, + apn: apn, + user: user, + passwd: passwd, + chappap: chappap, + pdptype: pdptype}); + this._deliverDataCallCallback("dataCallStateChanged", + [message.cid, "", + RIL.GECKO_DATACALL_STATE_CONNECTING]); + }, + + deactivateDataCall: function(cid, reason) { + this.worker.postMessage({type: "deactivateDataCall", + cid: cid, + reason: reason}); + this._deliverDataCallCallback("dataCallStateChanged", + [message.cid, + "", + RIL.GECKO_DATACALL_STATE_DISCONNECTING]); + }, + + getDataCallList: function getDataCallList() { + this.worker.postMessage({type: "getDataCallList"}); + }, + }; const NSGetFactory = XPCOMUtils.generateNSGetFactory([nsTelephonyWorker]); diff --git a/dom/telephony/ril_consts.js b/dom/telephony/ril_consts.js index ddada301405..0b8a649627d 100644 --- a/dom/telephony/ril_consts.js +++ b/dom/telephony/ril_consts.js @@ -407,6 +407,32 @@ const PDU_ALPHABET_7BIT_DEFAULT = [ "\xe0" // LATIN SMALL LETTER A WITH GRAVE ]; +const DATACALL_RADIOTECHONLOGY_CDMA = 0; +const DATACALL_RADIOTECHONLOGY_GSM = 1; + +const DATACALL_AUTH_NONE = 0; +const DATACALL_AUTH_PAP = 1; +const DATACALL_AUTH_CHAP = 2; +const DATACALL_AUTH_PAP_OR_CHAP = 3; + +const DATACALL_PROFILE_DEFAULT = 0; +const DATACALL_PROFILE_TETHERED = 1; +const DATACALL_PROFILE_OEM_BASE = 1000; + +const DATACALL_DEACTIVATE_NO_REASON = 0; +const DATACALL_DEACTIVATE_RADIO_SHUTDOWN = 1; + +const DATACALL_INACTIVE = 0; +const DATACALL_ACTIVE_DOWN = 1; +const DATACALL_ACTIVE_UP = 2; + +// Keep consistent with nsITelephone.DATACALL_STATE_*. +const GECKO_DATACALL_STATE_UNKNOWN = 0; +const GECKO_DATACALL_STATE_CONNECTING = 1; +const GECKO_DATACALL_STATE_CONNECTED = 2; +const GECKO_DATACALL_STATE_DISCONNECTING = 3; +const GECKO_DATACALL_STATE_DISCONNECTED = 4; + // Allow this file to be imported via Components.utils.import(). const EXPORTED_SYMBOLS = Object.keys(this); diff --git a/dom/telephony/ril_worker.js b/dom/telephony/ril_worker.js index 188846b2d96..551369721ca 100644 --- a/dom/telephony/ril_worker.js +++ b/dom/telephony/ril_worker.js @@ -116,6 +116,9 @@ let Buf = { // Maps tokens we send out with requests to the request type, so that // when we get a response parcel back, we know what request it was for. this.tokenRequestMap = {}; + + // This is the token of last solicited response. + this.lastSolicitedToken = 0; }, /** @@ -449,6 +452,7 @@ let Buf = { debug("Solicited response for request type " + request_type + ", token " + token); delete this.tokenRequestMap[token]; + this.lastSolicitedToken = token; } else if (response_type == RESPONSE_TYPE_UNSOLICITED) { request_type = this.readUint32(); length -= UINT32_SIZE; @@ -750,7 +754,6 @@ let RIL = { * @param dtmfChar * DTMF signal to send, 0-9, *, + */ - startTone: function startTone(dtmfChar) { Buf.newParcel(REQUEST_DTMF_START); Buf.writeString(dtmfChar); @@ -780,11 +783,78 @@ let RIL = { * @param smsc * Short Message Service Center address in PDU format. */ - setSMSCAddress: function setSMSCAddress(smsc) { - Buf.newParcel(REQUEST_SET_SMSC_ADDRESS); - Buf.writeString(smsc); - Buf.sendParcel(); - }, + setSMSCAddress: function setSMSCAddress(smsc) { + Buf.newParcel(REQUEST_SET_SMSC_ADDRESS); + Buf.writeString(smsc); + Buf.sendParcel(); + }, + + /** + * Setup a data call. + * + * @param radioTech + * Integer to indicate radio technology. + * DATACALL_RADIOTECHONLOGY_CDMA => CDMA. + * DATACALL_RADIOTECHONLOGY_GSM => GSM. + * @param apn + * String containing the name of the APN to connect to. + * @param user + * String containing the username for the APN. + * @param passwd + * String containing the password for the APN. + * @param chappap + * Integer containing CHAP/PAP auth type. + * DATACALL_AUTH_NONE => PAP and CHAP is never performed. + * DATACALL_AUTH_PAP => PAP may be performed. + * DATACALL_AUTH_CHAP => CHAP may be performed. + * DATACALL_AUTH_PAP_OR_CHAP => PAP / CHAP may be performed. + * @param pdptype + * String containing PDP type to request. ("IP", "IPV6", ...) + */ + setupDataCall: function (radioTech, apn, user, passwd, chappap, pdptype) { + let token = Buf.newParcel(REQUEST_SETUP_DATA_CALL); + Buf.writeUint32(7); + Buf.writeString(radioTech.toString()); + Buf.writeString(DATACALL_PROFILE_DEFAULT.toString()); + Buf.writeString(apn); + Buf.writeString(user); + Buf.writeString(passwd); + Buf.writeString(chappap.toString()); + Buf.writeString(pdptype); + Buf.sendParcel(); + return token; + }, + + /** + * Deactivate a data call. + * + * @param cid + * String containing CID. + * @param reason + * One of DATACALL_DEACTIVATE_* constants. + */ + deactivateDataCall: function (cid, reason) { + let token = Buf.newParcel(REQUEST_DEACTIVATE_DATA_CALL); + Buf.writeUint32(2); + Buf.writeString(cid); + Buf.writeString(reason); + Buf.sendParcel(); + return token; + }, + + /** + * Get a list of data calls. + */ + getDataCallList: function getDataCallList() { + Buf.simpleRequest(REQUEST_DATA_CALL_LIST); + }, + + /** + * Get failure casue code for the most recently failed PDP context. + */ + getFailCauseCode: function getFailCauseCode() { + Buf.simpleRequest(REQUEST_LAST_CALL_FAIL_CAUSE); + }, /** * Handle incoming requests from the RIL. We find the method that @@ -949,7 +1019,10 @@ RIL[REQUEST_SEND_SMS] = function REQUEST_SEND_SMS() { Phone.onSendSMS(messageRef, ackPDU, errorCode); }; RIL[REQUEST_SEND_SMS_EXPECT_MORE] = null; -RIL[REQUEST_SETUP_DATA_CALL] = null; +RIL[REQUEST_SETUP_DATA_CALL] = function REQUEST_SETUP_DATA_CALL() { + let [cid, ifname, ipaddr, dns, gw] = Buf.readStringList(); + Phone.onSetupDataCall(Buf.lastSolicitedToken, cid, ifname, ipaddr, dns, gw); +}; RIL[REQUEST_SIM_IO] = null; RIL[REQUEST_SEND_USSD] = null; RIL[REQUEST_CANCEL_USSD] = null; @@ -973,7 +1046,9 @@ RIL[REQUEST_GET_IMEISV] = function REQUEST_GET_IMEISV() { RIL[REQUEST_ANSWER] = function REQUEST_ANSWER(length) { Phone.onAnswerCall(); }; -RIL[REQUEST_DEACTIVATE_DATA_CALL] = null; +RIL[REQUEST_DEACTIVATE_DATA_CALL] = function REQUEST_DEACTIVATE_DATA_CALL() { + Phone.onDeactivateDataCall(Buf.lastSolicitedToken); +}; RIL[REQUEST_QUERY_FACILITY_LOCK] = null; RIL[REQUEST_SET_FACILITY_LOCK] = null; RIL[REQUEST_CHANGE_BARRING_PASSWORD] = null; @@ -993,7 +1068,7 @@ RIL[REQUEST_DTMF_STOP] = function REQUEST_DTMF_STOP() { RIL[REQUEST_BASEBAND_VERSION] = function REQUEST_BASEBAND_VERSION() { let version = Buf.readString(); Phone.onBasebandVersion(version); -}, +}; RIL[REQUEST_SEPARATE_CONNECTION] = null; RIL[REQUEST_SET_MUTE] = function REQUEST_SET_MUTE(length) { Phone.onSetMute(); @@ -1001,7 +1076,26 @@ RIL[REQUEST_SET_MUTE] = function REQUEST_SET_MUTE(length) { RIL[REQUEST_GET_MUTE] = null; RIL[REQUEST_QUERY_CLIP] = null; RIL[REQUEST_LAST_DATA_CALL_FAIL_CAUSE] = null; -RIL[REQUEST_DATA_CALL_LIST] = null; +RIL[REQUEST_DATA_CALL_LIST] = function REQUEST_DATA_CALL_LIST(length) { + let datacalls = []; + + if (!length) { + return; + } + + let num = Buf.readUint32(); + for (let i = 0; i < num; i++) { + datacalls.push({ + cid: Buf.readUint32().toString(), + active: Buf.readUint32(), + type: Buf.readString(), + apn: Buf.readString(), + address: Buf.readString() + }); + } + + Phone.onDataCallList(datacalls); +}; RIL[REQUEST_RESET_RADIO] = null; RIL[REQUEST_OEM_HOOK_RAW] = null; RIL[REQUEST_OEM_HOOK_STRINGS] = null; @@ -1176,6 +1270,16 @@ let Phone = { */ _muted: true, + /** + * Existing data calls. + */ + currentDataCalls: {}, + + /** + * Tracks active requests to the RIL concerning 3G data calls. + */ + activeDataRequests: {}, + get muted() { return this._muted; }, @@ -1583,6 +1687,88 @@ let Phone = { onAcknowledgeSMS: function onAcknowledgeSMS() { }, + onSetupDataCall: function onSetupDataCall(token, cid, ifname, ipaddr, + dns, gw) { + let options = this.activeDataRequests[token]; + delete this.activeDataRequests[token]; + + this.currentDataCalls[cid] = { + state: GECKO_DATACALL_STATE_CONNECTED, + cid: cid, + apn: options.apn, + ifname: ifname, + ipaddr: ipaddr, + dns: dns, + gw: gw, + }; + this.sendDOMMessage({type: "datacallstatechange", + state: GECKO_DATACALL_STATE_CONNECTED, + cid: cid, + apn: options.apn, + ifname: ifname, + ipaddr: ipaddr, + dns: dns, + gateway: gw}); + }, + + onDeactivateDataCall: function onDeactivateDataCall(token) { + let options = this.activeDataRequests[token]; + delete this.activeDataRequests[token]; + + let cid = options.cid; + if (!(cid in this.currentDataCalls)) { + return; + } + + let apn = this.currentDataCalls[cid].apn; + delete this.currentDataCalls[cid]; + this.sendDOMMessage({type: "datacallstatechange", + state: GECKO_DATACALL_STATE_DISCONNECTED, + cid: cid, + apn: apn}); + }, + + onDataCallList: function onDataCallList(datacalls) { + let currentDataCalls = this.currentDataCalls; + + // Sync content of currentDataCalls and data call list. + for each (let datacall in datacalls) { + let {cid, apn} = datacall; + + if (datacall.active != DATACALL_INACTIVE) { + // XXX: This should be followed up. + // datacall.active == DATACALL_ACTIVE_DOWN(1) for my device + if (!(cid in currentDataCalls)) { + let datacall = {state: GECKO_DATACALL_STATE_CONNECTED, + cid: cid, + apn: apn, + ipaddr: datacall.address}; + currentDataCalls[cid] = datacall; + + this.sendDOMMessage({type: "datacallstatechange", + state: GECKO_DATACALL_STATE_CONNECTED, + cid: cid, + apn: apn}); + } + } else { // datacall.active == DATACALL_INACTIVE + if (cid in currentDataCalls) { + delete currentDataCalls[cid]; + this.sendDOMMessage({type: "datacallstatechange", + state: GECKO_DATACALL_STATE_DISCONNECTED, + cid: cid, + apn: apn}); + } + } + } + + let datacall_list = []; + for each (let datacall in this.currentDataCalls) { + datacall_list.push(datacall); + } + this.sendDOMMessage({type: "datacalllist", + datacalls: datacall_list}); + }, + /** * Outgoing requests to the RIL. These can be triggered from the * main thread via messages that look like this: @@ -1729,6 +1915,54 @@ let Phone = { Math.ceil(options.body.length * 7 / 8)); //TODO: ditto }, + /** + * Setup a data call (PDP). + */ + setupDataCall: function setupDataCall(options) { + if (DEBUG) debug("setupDataCall: " + JSON.stringify(options)); + + let token = RIL.setupDataCall(options.radioTech, options.apn, + options.user, options.passwd, + options.chappap, options.reason); + this.activeDataRequests[token] = options; + this.sendDOMMessage({type: "datacallstatechange", + state: GECKO_DATACALL_STATE_CONNECTING, + apn: options.apn}); + }, + + /** + * Deactivate a data call (PDP). + */ + deactivateDataCall: function deactivateDataCall(options) { + if (!(options.cid in this.currentDataCalls)) { + return; + } + + let datacall = this.currentDataCalls[options.cid]; + datacall.state = GECKO_DATACALL_STATE_DISCONNECTING; + + let token = RIL.deactivateDataCall(options.cid, options.reason); + this.activeDataRequests[token] = options; + this.sendDOMMessage({type: "datacallstatechange", + state: GECKO_DATACALL_STATE_DISCONNECTING, + cid: options.cid, + apn: datacall.apn}); + }, + + /** + * Get the list of data calls. + */ + getDataCallList: function getDataCallList(options) { + RIL.getDataCallList(); + }, + + /** + * Get failure cause code for the last failed PDP context. + */ + getFailCauseCode: function getFailCauseCode(options) { + RIL.getFailCauseCode(); + }, + /** * Handle incoming messages from the main UI thread. *