/* 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/DOMRequestHelper.jsm"); var RIL = {}; Cu.import("resource://gre/modules/ril_consts.js", RIL); // set to true to in ril_consts.js to see debug messages const DEBUG = RIL.DEBUG_CONTENT_HELPER; const RILCONTENTHELPER_CID = Components.ID("{472816e1-1fd6-4405-996c-806f9ea68174}"); const MOBILECONNECTIONINFO_CID = Components.ID("{a35cfd39-2d93-4489-ac7d-396475dacb27}"); const MOBILENETWORKINFO_CID = Components.ID("{a6c8416c-09b4-46d1-bf29-6520d677d085}"); const MOBILECELLINFO_CID = Components.ID("{5e809018-68c0-4c54-af0b-2a9b8f748c45}"); const VOICEMAILSTATUS_CID= Components.ID("{5467f2eb-e214-43ea-9b89-67711241ec8e}"); const RIL_IPC_MSG_NAMES = [ "RIL:CardStateChanged", "RIL:VoiceInfoChanged", "RIL:DataInfoChanged", "RIL:EnumerateCalls", "RIL:GetAvailableNetworks", "RIL:NetworkSelectionModeChanged", "RIL:SelectNetwork", "RIL:SelectNetworkAuto", "RIL:CallStateChanged", "RIL:VoicemailNotification", "RIL:VoicemailNumberChanged", "RIL:CallError", "RIL:CardLockResult", "RIL:UssdReceived", "RIL:SendUssd:Return:OK", "RIL:SendUssd:Return:KO", "RIL:CancelUssd:Return:OK", "RIL:CancelUssd:Return:KO" ]; const kVoiceChangedTopic = "mobile-connection-voice-changed"; const kDataChangedTopic = "mobile-connection-data-changed"; const kCardStateChangedTopic = "mobile-connection-cardstate-changed"; const kUssdReceivedTopic = "mobile-connection-ussd-received"; XPCOMUtils.defineLazyServiceGetter(this, "cpmm", "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender"); XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); function MobileConnectionInfo() {} MobileConnectionInfo.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMMozMobileConnectionInfo]), classID: MOBILECONNECTIONINFO_CID, classInfo: XPCOMUtils.generateCI({ classID: MOBILECONNECTIONINFO_CID, classDescription: "MobileConnectionInfo", flags: Ci.nsIClassInfo.DOM_OBJECT, interfaces: [Ci.nsIDOMMozMobileConnectionInfo] }), // nsIDOMMozMobileConnectionInfo connected: false, state: null, emergencyCallsOnly: false, roaming: false, network: null, cell: null, type: null, signalStrength: null, relSignalStrength: null }; function MobileNetworkInfo() {} MobileNetworkInfo.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMMozMobileNetworkInfo]), classID: MOBILENETWORKINFO_CID, classInfo: XPCOMUtils.generateCI({ classID: MOBILENETWORKINFO_CID, classDescription: "MobileNetworkInfo", flags: Ci.nsIClassInfo.DOM_OBJECT, interfaces: [Ci.nsIDOMMozMobileNetworkInfo] }), // nsIDOMMozMobileNetworkInfo shortName: null, longName: null, mcc: 0, mnc: 0, state: null }; function MobileCellInfo() {} MobileCellInfo.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMMozMobileCellInfo]), classID: MOBILECELLINFO_CID, classInfo: XPCOMUtils.generateCI({ classID: MOBILECELLINFO_CID, classDescription: "MobileCellInfo", flags: Ci.nsIClassInfo.DOM_OBJECT, interfaces: [Ci.nsIDOMMozMobileCellInfo] }), // nsIDOMMozMobileCellInfo gsmLocationAreaCode: null, gsmCellId: null }; function VoicemailStatus() {} VoicemailStatus.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMMozVoicemailStatus]), classID: VOICEMAILSTATUS_CID, classInfo: XPCOMUtils.generateCI({ classID: VOICEMAILSTATUS_CID, classDescription: "VoicemailStatus", flags: Ci.nsIClassInfo.DOM_OBJECT, interfaces: [Ci.nsIDOMMozVoicemailStatus] }), // nsIDOMMozVoicemailStatus hasMessages: false, messageCount: Ci.nsIDOMMozVoicemailStatus.MESSAGE_COUNT_UNKNOWN, returnNumber: null, returnMessage: null }; function RILContentHelper() { this.voiceConnectionInfo = new MobileConnectionInfo(); this.dataConnectionInfo = new MobileConnectionInfo(); this.initRequests(); this.initMessageListener(RIL_IPC_MSG_NAMES); Services.obs.addObserver(this, "xpcom-shutdown", false); // Request initial context. let rilContext = cpmm.sendSyncMessage("RIL:GetRilContext")[0]; if (!rilContext) { debug("Received null rilContext from chrome process."); return; } this.cardState = rilContext.cardState; this.updateConnectionInfo(rilContext.voice, this.voiceConnectionInfo); this.updateConnectionInfo(rilContext.data, this.dataConnectionInfo); } RILContentHelper.prototype = { __proto__: DOMRequestIpcHelper.prototype, QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionProvider, Ci.nsIRILContentHelper, Ci.nsIObserver]), classID: RILCONTENTHELPER_CID, classInfo: XPCOMUtils.generateCI({classID: RILCONTENTHELPER_CID, classDescription: "RILContentHelper", interfaces: [Ci.nsIMobileConnectionProvider, Ci.nsIRILContentHelper]}), updateConnectionInfo: function updateConnectionInfo(srcInfo, destInfo) { for (let key in srcInfo) { if ((key != "network") && (key != "cell")) { destInfo[key] = srcInfo[key]; } } let srcCell = srcInfo.cell; if (!srcCell) { destInfo.cell = null; } else { let cell = destInfo.cell; if (!cell) { cell = destInfo.cell = new MobileCellInfo(); } cell.gsmLocationAreaCode = srcCell.gsmLocationAreaCode; cell.gsmCellId = srcCell.gsmCellId; } let srcNetwork = srcInfo.network; if (!srcNetwork) { destInfo.network= null; return; } let network = destInfo.network; if (!network) { network = destInfo.network = new MobileNetworkInfo(); } network.longName = srcNetwork.longName; network.shortName = srcNetwork.shortName; network.mnc = srcNetwork.mnc; network.mcc = srcNetwork.mcc; }, // nsIRILContentHelper cardState: RIL.GECKO_CARDSTATE_UNAVAILABLE, voiceConnectionInfo: null, dataConnectionInfo: null, networkSelectionMode: RIL.GECKO_NETWORK_SELECTION_UNKNOWN, /** * The network that is currently trying to be selected (or "automatic"). * This helps ensure that only one network is selected at a time. */ _selectingNetwork: null, getNetworks: function getNetworks(window) { if (window == null) { throw Components.Exception("Can't get window object", Cr.NS_ERROR_UNEXPECTED); } let request = Services.DOMRequest.createRequest(window); let requestId = this.getRequestId(request); cpmm.sendAsyncMessage("RIL:GetAvailableNetworks", {requestId: requestId}); return request; }, selectNetwork: function selectNetwork(window, network) { if (window == null) { throw Components.Exception("Can't get window object", Cr.NS_ERROR_UNEXPECTED); } if (this._selectingNetwork) { throw new Error("Already selecting a network: " + this._selectingNetwork); } if (!network) { throw new Error("Invalid network provided: " + network); } let mnc = network.mnc; if (!mnc) { throw new Error("Invalid network MNC: " + mnc); } let mcc = network.mcc; if (!mcc) { throw new Error("Invalid network MCC: " + mcc); } let request = Services.DOMRequest.createRequest(window); let requestId = this.getRequestId(request); if (this.networkSelectionMode == RIL.GECKO_NETWORK_SELECTION_MANUAL && this.voiceConnectionInfo.network === network) { // Already manually selected this network, so schedule // onsuccess to be fired on the next tick this.dispatchFireRequestSuccess(requestId, null); return request; } this._selectingNetwork = network; cpmm.sendAsyncMessage("RIL:SelectNetwork", { requestId: requestId, mnc: mnc, mcc: mcc }); return request; }, selectNetworkAutomatically: function selectNetworkAutomatically(window) { if (window == null) { throw Components.Exception("Can't get window object", Cr.NS_ERROR_UNEXPECTED); } if (this._selectingNetwork) { throw new Error("Already selecting a network: " + this._selectingNetwork); } let request = Services.DOMRequest.createRequest(window); let requestId = this.getRequestId(request); if (this.networkSelectionMode == RIL.GECKO_NETWORK_SELECTION_AUTOMATIC) { // Already using automatic selection mode, so schedule // onsuccess to be be fired on the next tick this.dispatchFireRequestSuccess(requestId, null); return request; } this._selectingNetwork = "automatic"; cpmm.sendAsyncMessage("RIL:SelectNetworkAuto", {requestId: requestId}); return request; }, getCardLock: function getCardLock(window, lockType) { if (window == null) { throw Components.Exception("Can't get window object", Cr.NS_ERROR_UNEXPECTED); } let request = Services.DOMRequest.createRequest(window); let requestId = this.getRequestId(request); cpmm.sendAsyncMessage("RIL:GetCardLock", {lockType: lockType, requestId: requestId}); return request; }, unlockCardLock: function unlockCardLock(window, info) { if (window == null) { throw Components.Exception("Can't get window object", Cr.NS_ERROR_UNEXPECTED); } let request = Services.DOMRequest.createRequest(window); info.requestId = this.getRequestId(request); cpmm.sendAsyncMessage("RIL:UnlockCardLock", info); return request; }, setCardLock: function setCardLock(window, info) { if (window == null) { throw Components.Exception("Can't get window object", Cr.NS_ERROR_UNEXPECTED); } let request = Services.DOMRequest.createRequest(window); info.requestId = this.getRequestId(request); cpmm.sendAsyncMessage("RIL:SetCardLock", info); return request; }, sendUSSD: function sendUSSD(window, ussd) { debug("Sending USSD " + ussd); if (!window) { throw Components.Exception("Can't get window object", Cr.NS_ERROR_EXPECTED); } let request = Services.DOMRequest.createRequest(window); let requestId = this.getRequestId(request); cpmm.sendAsyncMessage("RIL:SendUSSD", {ussd: ussd, requestId: requestId}); return request; }, cancelUSSD: function cancelUSSD(window) { debug("Cancel USSD"); if (!window) { throw Components.Exception("Can't get window object", Cr.NS_ERROR_UNEXPECTED); } let request = Services.DOMRequest.createRequest(window); let requestId = this.getRequestId(request); cpmm.sendAsyncMessage("RIL:CancelUSSD", {requestId: requestId}); return request; }, _telephonyCallbacks: null, _voicemailCallbacks: null, _enumerateTelephonyCallbacks: null, voicemailStatus: null, voicemailNumber: null, voicemailDisplayName: null, registerCallback: function registerCallback(callbackType, callback) { let callbacks = this[callbackType]; if (!callbacks) { callbacks = this[callbackType] = []; } if (callbacks.indexOf(callback) != -1) { throw new Error("Already registered this callback!"); } callbacks.push(callback); if (DEBUG) debug("Registered " + callbackType + " callback: " + callback); }, unregisterCallback: function unregisterCallback(callbackType, callback) { let callbacks = this[callbackType]; if (!callbacks) { return; } let index = callbacks.indexOf(callback); if (index != -1) { callbacks.splice(index, 1); if (DEBUG) debug("Unregistered telephony callback: " + callback); } }, registerTelephonyCallback: function registerTelephonyCallback(callback) { this.registerCallback("_telephonyCallbacks", callback); }, unregisterTelephonyCallback: function unregisteTelephonyCallback(callback) { this.unregisterCallback("_telephonyCallbacks", callback); }, registerVoicemailCallback: function registerVoicemailCallback(callback) { this.registerCallback("_voicemailCallbacks", callback); }, unregisterVoicemailCallback: function unregisteVoicemailCallback(callback) { this.unregisterCallback("_voicemailCallbacks", callback); }, enumerateCalls: function enumerateCalls(callback) { debug("Requesting enumeration of calls for callback: " + callback); // We need 'requestId' to meet the 'RILContentHelper <--> RadioInterfaceLayer' // protocol. let requestId = this._getRandomId(); cpmm.sendAsyncMessage("RIL:EnumerateCalls", {requestId: requestId}); if (!this._enumerationTelephonyCallbacks) { this._enumerationTelephonyCallbacks = []; } this._enumerationTelephonyCallbacks.push(callback); }, startTone: function startTone(dtmfChar) { debug("Sending Tone for " + dtmfChar); cpmm.sendAsyncMessage("RIL:StartTone", dtmfChar); }, stopTone: function stopTone() { debug("Stopping Tone"); cpmm.sendAsyncMessage("RIL:StopTone"); }, dial: function dial(number) { debug("Dialing " + number); cpmm.sendAsyncMessage("RIL:Dial", number); }, dialEmergency: function dialEmergency(number) { debug("Dialing emergency " + number); cpmm.sendAsyncMessage("RIL:DialEmergency", number); }, hangUp: function hangUp(callIndex) { debug("Hanging up call no. " + callIndex); cpmm.sendAsyncMessage("RIL:HangUp", callIndex); }, answerCall: function answerCall(callIndex) { cpmm.sendAsyncMessage("RIL:AnswerCall", callIndex); }, rejectCall: function rejectCall(callIndex) { cpmm.sendAsyncMessage("RIL:RejectCall", callIndex); }, holdCall: function holdCall(callIndex) { cpmm.sendAsyncMessage("RIL:HoldCall", callIndex); }, resumeCall: function resumeCall(callIndex) { cpmm.sendAsyncMessage("RIL:ResumeCall", callIndex); }, get microphoneMuted() { return cpmm.sendSyncMessage("RIL:GetMicrophoneMuted")[0]; }, set microphoneMuted(value) { cpmm.sendAsyncMessage("RIL:SetMicrophoneMuted", value); }, get speakerEnabled() { return cpmm.sendSyncMessage("RIL:GetSpeakerEnabled")[0]; }, set speakerEnabled(value) { cpmm.sendAsyncMessage("RIL:SetSpeakerEnabled", value); }, // nsIObserver observe: function observe(subject, topic, data) { if (topic == "xpcom-shutdown") { this.removeMessageListener(); Services.obs.removeObserver(this, "xpcom-shutdown"); cpmm = null; } }, // nsIMessageListener fireRequestSuccess: function fireRequestSuccess(requestId, result) { let request = this.takeRequest(requestId); if (!request) { if (DEBUG) { debug("not firing success for id: " + requestId + ", result: " + JSON.stringify(result)); } return; } if (DEBUG) { debug("fire request success, id: " + requestId + ", result: " + JSON.stringify(result)); } Services.DOMRequest.fireSuccess(request, result); }, dispatchFireRequestSuccess: function dispatchFireRequestSuccess(requestId, result) { let currentThread = Services.tm.currentThread; currentThread.dispatch(this.fireRequestSuccess.bind(this, requestId, result), Ci.nsIThread.DISPATCH_NORMAL); }, fireRequestError: function fireRequestError(requestId, error) { let request = this.takeRequest(requestId); if (!request) { if (DEBUG) { debug("not firing error for id: " + requestId + ", error: " + JSON.stringify(error)); } return; } if (DEBUG) { debug("fire request error, id: " + requestId + ", result: " + JSON.stringify(error)); } Services.DOMRequest.fireError(request, error); }, receiveMessage: function receiveMessage(msg) { let request; debug("Received message '" + msg.name + "': " + JSON.stringify(msg.json)); switch (msg.name) { case "RIL:CardStateChanged": if (this.cardState != msg.json.cardState) { this.cardState = msg.json.cardState; Services.obs.notifyObservers(null, kCardStateChangedTopic, null); } break; case "RIL:VoiceInfoChanged": this.updateConnectionInfo(msg.json, this.voiceConnectionInfo); Services.obs.notifyObservers(null, kVoiceChangedTopic, null); break; case "RIL:DataInfoChanged": this.updateConnectionInfo(msg.json, this.dataConnectionInfo); Services.obs.notifyObservers(null, kDataChangedTopic, null); break; case "RIL:EnumerateCalls": this.handleEnumerateCalls(msg.json.calls); break; case "RIL:GetAvailableNetworks": this.handleGetAvailableNetworks(msg.json); break; case "RIL:NetworkSelectionModeChanged": this.networkSelectionMode = msg.json.mode; break; case "RIL:SelectNetwork": this.handleSelectNetwork(msg.json, RIL.GECKO_NETWORK_SELECTION_MANUAL); break; case "RIL:SelectNetworkAuto": this.handleSelectNetwork(msg.json, RIL.GECKO_NETWORK_SELECTION_AUTOMATIC); break; case "RIL:CallStateChanged": this._deliverCallback("_telephonyCallbacks", "callStateChanged", [msg.json.callIndex, msg.json.state, msg.json.number, msg.json.isActive]); break; case "RIL:CallError": this._deliverCallback("_telephonyCallbacks", "notifyError", [msg.json.callIndex, msg.json.error]); break; case "RIL:VoicemailNotification": this.handleVoicemailNotification(msg.json); break; case "RIL:VoicemailNumberChanged": this.voicemailNumber = msg.json.number; this.voicemailDisplayName = msg.json.alphaId; break; case "RIL:CardLockResult": if (msg.json.success) { this.fireRequestSuccess(msg.json.requestId, msg.json); } else { this.fireRequestError(msg.json.requestId, msg.json); } break; case "RIL:UssdReceived": Services.obs.notifyObservers(null, kUssdReceivedTopic, msg.json.message); break; case "RIL:SendUssd:Return:OK": case "RIL:CancelUssd:Return:OK": request = this.takeRequest(msg.json.requestId); if (request) { Services.DOMRequest.fireSuccess(request, msg.json); } break; case "RIL:SendUssd:Return:KO": case "RIL:CancelUssd:Return:KO": request = this.takeRequest(msg.json.requestId); if (request) { Services.DOMRequest.fireError(request, msg.json.errorMsg); } break; } }, handleEnumerateCalls: function handleEnumerateCalls(calls) { debug("handleEnumerateCalls: " + JSON.stringify(calls)); let callback = this._enumerationTelephonyCallbacks.shift(); for (let i in calls) { let call = calls[i]; let keepGoing; try { keepGoing = callback.enumerateCallState(call.callIndex, call.state, call.number, call.isActive); } catch (e) { debug("callback handler for 'enumerateCallState' threw an " + " exception: " + e); keepGoing = true; } if (!keepGoing) { break; } } }, handleGetAvailableNetworks: function handleGetAvailableNetworks(message) { debug("handleGetAvailableNetworks: " + JSON.stringify(message)); let requestId = message.requestId; let request = this.takeRequest(requestId); if (!request) { debug("no DOMRequest found with request ID: " + requestId); return; } if (message.error) { debug("Received error from getAvailableNetworks: " + message.error); Services.DOMRequest.fireError(request, message.error); return; } let networks = message.networks; for (let i = 0; i < networks.length; i++) { let network = networks[i]; let info = new MobileNetworkInfo(); for (let key in network) { info[key] = network[key]; } networks[i] = info; } Services.DOMRequest.fireSuccess(request, networks); }, handleSelectNetwork: function handleSelectNetwork(message, mode) { this._selectingNetwork = null; this.networkSelectionMode = mode; if (message.error) { this.fireRequestError(message.requestId, message.error); } else { this.fireRequestSuccess(message.requestId, null); } }, handleVoicemailNotification: function handleVoicemailNotification(message) { let changed = false; if (!this.voicemailStatus) { this.voicemailStatus = new VoicemailStatus(); } if (this.voicemailStatus.hasMessages != message.active) { changed = true; this.voicemailStatus.hasMessages = message.active; } if (this.voicemailStatus.messageCount != message.msgCount) { changed = true; this.voicemailStatus.messageCount = message.msgCount; } if (this.voicemailStatus.returnNumber != message.returnNumber) { changed = true; this.voicemailStatus.returnNumber = message.returnNumber; } if (this.voicemailStatus.returnMessage != message.returnMessage) { changed = true; this.voicemailStatus.returnMessage = message.returnMessage; } if (changed) { this._deliverCallback("_voicemailCallbacks", "voicemailNotification", [this.voicemailStatus]); } }, _getRandomId: function _getRandomId() { return gUUIDGenerator.generateUUID().toString(); }, _deliverCallback: function _deliverCallback(callbackType, name, args) { let thisCallbacks = this[callbackType]; if (!thisCallbacks) { return; } let callbacks = thisCallbacks.slice(); for each (let callback in callbacks) { if (thisCallbacks.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); } } } }; const NSGetFactory = XPCOMUtils.generateNSGetFactory([RILContentHelper]); let debug; if (DEBUG) { debug = function (s) { dump("-*- RILContentHelper: " + s + "\n"); }; } else { debug = function (s) {}; }