/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); var RIL = {}; Cu.import("resource://gre/modules/ril_consts.js", RIL); const GONK_TELEPHONYPROVIDER_CONTRACTID = "@mozilla.org/telephony/gonktelephonyprovider;1"; const GONK_TELEPHONYPROVIDER_CID = Components.ID("{67d26434-d063-4d28-9f48-5b3189788155}"); const NS_XPCOM_SHUTDOWN_OBSERVER_ID = "xpcom-shutdown"; const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; const kPrefRilNumRadioInterfaces = "ril.numRadioInterfaces"; const kPrefRilDebuggingEnabled = "ril.debugging.enabled"; const kPrefDefaultServiceId = "dom.telephony.defaultServiceId"; const nsIAudioManager = Ci.nsIAudioManager; const nsITelephonyProvider = Ci.nsITelephonyProvider; const CALL_WAKELOCK_TIMEOUT = 5000; let DEBUG; function debug(s) { dump("TelephonyProvider: " + s + "\n"); } XPCOMUtils.defineLazyGetter(this, "gAudioManager", function getAudioManager() { try { return Cc["@mozilla.org/telephony/audiomanager;1"] .getService(nsIAudioManager); } catch (ex) { //TODO on the phone this should not fall back as silently. // Fake nsIAudioManager implementation so that we can run the telephony // code in a non-Gonk build. if (DEBUG) debug("Using fake audio manager."); return { microphoneMuted: false, masterVolume: 1.0, masterMuted: false, phoneState: nsIAudioManager.PHONE_STATE_CURRENT, _forceForUse: {}, setForceForUse: function setForceForUse(usage, force) { this._forceForUse[usage] = force; }, getForceForUse: function setForceForUse(usage) { return this._forceForUse[usage] || nsIAudioManager.FORCE_NONE; } }; } }); XPCOMUtils.defineLazyServiceGetter(this, "gPowerManagerService", "@mozilla.org/power/powermanagerservice;1", "nsIPowerManagerService"); XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger", "@mozilla.org/system-message-internal;1", "nsISystemMessagesInternal"); XPCOMUtils.defineLazyGetter(this, "gRadioInterface", function () { let ril = Cc["@mozilla.org/ril;1"].getService(Ci["nsIRadioInterfaceLayer"]); // TODO: Bug 854326 - B2G Multi-SIM: support multiple SIM cards for SMS/MMS return ril.getRadioInterface(0); }); XPCOMUtils.defineLazyGetter(this, "gPhoneNumberUtils", function () { let ns = {}; Cu.import("resource://gre/modules/PhoneNumberUtils.jsm", ns); return ns.PhoneNumberUtils; }); function TelephonyProvider() { this._listeners = []; this._updateDebugFlag(); this.defaultServiceId = this._getDefaultServiceId(); Services.prefs.addObserver(kPrefRilDebuggingEnabled, this, false); Services.prefs.addObserver(kPrefDefaultServiceId, this, false); Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); } TelephonyProvider.prototype = { classID: GONK_TELEPHONYPROVIDER_CID, classInfo: XPCOMUtils.generateCI({classID: GONK_TELEPHONYPROVIDER_CID, contractID: GONK_TELEPHONYPROVIDER_CONTRACTID, classDescription: "TelephonyProvider", interfaces: [Ci.nsITelephonyProvider, Ci.nsIGonkTelephonyProvider], flags: Ci.nsIClassInfo.SINGLETON}), QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyProvider, Ci.nsIGonkTelephonyProvider, Ci.nsIObserver]), _callRingWakeLock: null, _callRingWakeLockTimer: null, _cancelCallRingWakeLockTimer: function _cancelCallRingWakeLockTimer() { if (this._callRingWakeLockTimer) { this._callRingWakeLockTimer.cancel(); } if (this._callRingWakeLock) { this._callRingWakeLock.unlock(); this._callRingWakeLock = null; } }, // An array of nsITelephonyListener instances. _listeners: null, _notifyAllListeners: function _notifyAllListeners(aMethodName, aArgs) { let listeners = this._listeners.slice(); for (let listener of listeners) { if (this._listeners.indexOf(listener) == -1) { // Listener has been unregistered in previous run. continue; } let handler = listener[aMethodName]; try { handler.apply(listener, aArgs); } catch (e) { debug("listener for " + aMethodName + " threw an exception: " + e); } } }, /** * Track the active call and update the audio system as its state changes. */ _activeCall: null, _updateCallAudioState: function _updateCallAudioState(aCall, aConferenceState) { if (aConferenceState === nsITelephonyProvider.CALL_STATE_CONNECTED) { gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_IN_CALL; if (this.speakerEnabled) { gAudioManager.setForceForUse(nsIAudioManager.USE_COMMUNICATION, nsIAudioManager.FORCE_SPEAKER); } return; } if (aConferenceState === nsITelephonyProvider.CALL_STATE_UNKNOWN || aConferenceState === nsITelephonyProvider.CALL_STATE_HELD) { if (!this._activeCall) { gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_NORMAL; } return; } if (!aCall) { return; } if (aCall.isConference) { if (this._activeCall && this._activeCall.callIndex == aCall.callIndex) { this._activeCall = null; } return; } switch (aCall.state) { case nsITelephonyProvider.CALL_STATE_DIALING: // Fall through... case nsITelephonyProvider.CALL_STATE_ALERTING: case nsITelephonyProvider.CALL_STATE_CONNECTED: aCall.isActive = true; this._activeCall = aCall; gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_IN_CALL; if (this.speakerEnabled) { gAudioManager.setForceForUse(nsIAudioManager.USE_COMMUNICATION, nsIAudioManager.FORCE_SPEAKER); } if (DEBUG) { debug("Active call, put audio system into PHONE_STATE_IN_CALL: " + gAudioManager.phoneState); } break; case nsITelephonyProvider.CALL_STATE_INCOMING: aCall.isActive = false; if (!this._activeCall) { // We can change the phone state into RINGTONE only when there's // no active call. gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_RINGTONE; if (DEBUG) { debug("Incoming call, put audio system into PHONE_STATE_RINGTONE: " + gAudioManager.phoneState); } } break; case nsITelephonyProvider.CALL_STATE_HELD: // Fall through... case nsITelephonyProvider.CALL_STATE_DISCONNECTED: aCall.isActive = false; if (this._activeCall && this._activeCall.callIndex == aCall.callIndex) { // Previously active call is not active now. this._activeCall = null; } if (!this._activeCall) { // No active call. Disable the audio. gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_NORMAL; if (DEBUG) { debug("No active call, put audio system into PHONE_STATE_NORMAL: " + gAudioManager.phoneState); } } break; } }, _convertRILCallState: function _convertRILCallState(aState) { switch (aState) { case RIL.CALL_STATE_UNKNOWN: return nsITelephonyProvider.CALL_STATE_UNKNOWN; case RIL.CALL_STATE_ACTIVE: return nsITelephonyProvider.CALL_STATE_CONNECTED; case RIL.CALL_STATE_HOLDING: return nsITelephonyProvider.CALL_STATE_HELD; case RIL.CALL_STATE_DIALING: return nsITelephonyProvider.CALL_STATE_DIALING; case RIL.CALL_STATE_ALERTING: return nsITelephonyProvider.CALL_STATE_ALERTING; case RIL.CALL_STATE_INCOMING: case RIL.CALL_STATE_WAITING: return nsITelephonyProvider.CALL_STATE_INCOMING; default: throw new Error("Unknown rilCallState: " + aState); } }, _convertRILSuppSvcNotification: function _convertRILSuppSvcNotification(aNotification) { switch (aNotification) { case RIL.GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD: return nsITelephonyProvider.NOTIFICATION_REMOTE_HELD; case RIL.GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED: return nsITelephonyProvider.NOTIFICATION_REMOTE_RESUMED; default: throw new Error("Unknown rilSuppSvcNotification: " + aNotification); } }, _validateNumber: function _validateNumber(aNumber) { // note: isPlainPhoneNumber also accepts USSD and SS numbers if (gPhoneNumberUtils.isPlainPhoneNumber(aNumber)) { return true; } let errorMsg = RIL.RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[RIL.CALL_FAIL_UNOBTAINABLE_NUMBER]; let currentThread = Services.tm.currentThread; currentThread.dispatch(this.notifyCallError.bind(this, -1, errorMsg), Ci.nsIThread.DISPATCH_NORMAL); if (DEBUG) { debug("Number '" + aNumber + "' doesn't seem to be a viable number. Drop."); } return false; }, _updateDebugFlag: function _updateDebugFlag() { try { DEBUG = RIL.DEBUG_RIL || Services.prefs.getBoolPref(kPrefRilDebuggingEnabled); } catch (e) {} }, _getDefaultServiceId: function _getDefaultServiceId() { let id = Services.prefs.getIntPref(kPrefDefaultServiceId); let numRil = Services.prefs.getIntPref(kPrefRilNumRadioInterfaces); if (id >= numRil || id < 0) { id = 0; } return id; }, /** * nsITelephonyProvider interface. */ defaultServiceId: 0, registerListener: function(aListener) { if (this._listeners.indexOf(aListener) >= 0) { throw Cr.NS_ERROR_UNEXPECTED; } this._listeners.push(aListener); }, unregisterListener: function(aListener) { let index = this._listeners.indexOf(aListener); if (index < 0) { throw Cr.NS_ERROR_UNEXPECTED; } this._listeners.splice(index, 1); }, enumerateCalls: function(aListener) { if (DEBUG) debug("Requesting enumeration of calls for callback"); gRadioInterface.sendWorkerMessage("enumerateCalls", null, (function(response) { for (let call of response.calls) { call.state = this._convertRILCallState(call.state); call.isActive = this._activeCall ? (call.callIndex == this._activeCall.callIndex) : false; aListener.enumerateCallState(call.callIndex, call.state, call.number, call.isActive, call.isOutgoing, call.isEmergency, call.isConference); } aListener.enumerateCallStateComplete(); return false; }).bind(this)); }, dial: function(aNumber, aIsEmergency) { if (DEBUG) debug("Dialing " + (aIsEmergency ? "emergency " : "") + aNumber); // we don't try to be too clever here, as the phone is probably in the // locked state. Let's just check if it's a number without normalizing if (!aIsEmergency) { aNumber = gPhoneNumberUtils.normalize(aNumber); } if (this._validateNumber(aNumber)) { gRadioInterface.sendWorkerMessage("dial", { number: aNumber, isDialEmergency: aIsEmergency }); } }, hangUp: function(aCallIndex) { gRadioInterface.sendWorkerMessage("hangUp", { callIndex: aCallIndex }); }, startTone: function(aDtmfChar) { gRadioInterface.sendWorkerMessage("startTone", { dtmfChar: aDtmfChar }); }, stopTone: function() { gRadioInterface.sendWorkerMessage("stopTone"); }, answerCall: function(aCallIndex) { gRadioInterface.sendWorkerMessage("answerCall", { callIndex: aCallIndex }); }, rejectCall: function(aCallIndex) { gRadioInterface.sendWorkerMessage("rejectCall", { callIndex: aCallIndex }); }, holdCall: function(aCallIndex) { gRadioInterface.sendWorkerMessage("holdCall", { callIndex: aCallIndex }); }, resumeCall: function(aCallIndex) { gRadioInterface.sendWorkerMessage("resumeCall", { callIndex: aCallIndex }); }, conferenceCall: function conferenceCall() { gRadioInterface.sendWorkerMessage("conferenceCall"); }, separateCall: function separateCall(aCallIndex) { gRadioInterface.sendWorkerMessage("separateCall", { callIndex: aCallIndex }); }, holdConference: function holdConference() { gRadioInterface.sendWorkerMessage("holdConference"); }, resumeConference: function resumeConference() { gRadioInterface.sendWorkerMessage("resumeConference"); }, get microphoneMuted() { return gAudioManager.microphoneMuted; }, set microphoneMuted(aMuted) { if (aMuted == this.microphoneMuted) { return; } gAudioManager.microphoneMuted = aMuted; if (!this._activeCall) { gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_NORMAL; } }, get speakerEnabled() { let force = gAudioManager.getForceForUse(nsIAudioManager.USE_COMMUNICATION); return (force == nsIAudioManager.FORCE_SPEAKER); }, set speakerEnabled(aEnabled) { if (aEnabled == this.speakerEnabled) { return; } let force = aEnabled ? nsIAudioManager.FORCE_SPEAKER : nsIAudioManager.FORCE_NONE; gAudioManager.setForceForUse(nsIAudioManager.USE_COMMUNICATION, force); if (!this._activeCall) { gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_NORMAL; } }, /** * nsIGonkTelephonyProvider interface. */ /** * Handle call disconnects by updating our current state and the audio system. */ notifyCallDisconnected: function notifyCallDisconnected(aCall) { if (DEBUG) debug("handleCallDisconnected: " + JSON.stringify(aCall)); aCall.state = nsITelephonyProvider.CALL_STATE_DISCONNECTED; let duration = ("started" in aCall && typeof aCall.started == "number") ? new Date().getTime() - aCall.started : 0; let data = { number: aCall.number, duration: duration, direction: aCall.isOutgoing ? "outgoing" : "incoming" }; gSystemMessenger.broadcastMessage("telephony-call-ended", data); this._updateCallAudioState(aCall, null); this._notifyAllListeners("callStateChanged", [aCall.callIndex, aCall.state, aCall.number, aCall.isActive, aCall.isOutgoing, aCall.isEmergency, aCall.isConference]); }, /** * Handle call error. */ notifyCallError: function notifyCallError(aCallIndex, aErrorMsg) { this._notifyAllListeners("notifyError", [aCallIndex, aErrorMsg]); }, /** * Handle an incoming call. * * Not much is known about this call at this point, but it's enough * to start bringing up the Phone app already. */ notifyCallRing: function notifyCallRing() { if (!this._callRingWakeLock) { this._callRingWakeLock = gPowerManagerService.newWakeLock("cpu"); } if (!this._callRingWakeLockTimer) { this._callRingWakeLockTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); } this._callRingWakeLockTimer .initWithCallback(this._cancelCallRingWakeLockTimer.bind(this), CALL_WAKELOCK_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT); gSystemMessenger.broadcastMessage("telephony-new-call", {}); }, /** * Handle call state changes by updating our current state and the audio * system. */ notifyCallStateChanged: function notifyCallStateChanged(aCall) { if (DEBUG) debug("handleCallStateChange: " + JSON.stringify(aCall)); aCall.state = this._convertRILCallState(aCall.state); if (aCall.state == nsITelephonyProvider.CALL_STATE_DIALING) { gSystemMessenger.broadcastMessage("telephony-new-call", {}); } this._updateCallAudioState(aCall, null); this._notifyAllListeners("callStateChanged", [aCall.callIndex, aCall.state, aCall.number, aCall.isActive, aCall.isOutgoing, aCall.isEmergency, aCall.isConference]); }, notifyCdmaCallWaiting: function notifyCdmaCallWaiting(aNumber) { this._notifyAllListeners("notifyCdmaCallWaiting", [aNumber]); }, notifySupplementaryService: function notifySupplementaryService(aCallIndex, aNotification) { let notification = this._convertRILSuppSvcNotification(aNotification); this._notifyAllListeners("supplementaryServiceNotification", [aCallIndex, notification]); }, notifyConferenceCallStateChanged: function notifyConferenceCallStateChanged(aState) { if (DEBUG) debug("handleConferenceCallStateChanged: " + aState); aState = this._convertRILCallState(aState); this._updateCallAudioState(null, aState); this._notifyAllListeners("conferenceCallStateChanged", [aState]); }, /** * nsIObserver interface. */ observe: function observe(aSubject, aTopic, aData) { switch (aTopic) { case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID: if (aData === kPrefRilDebuggingEnabled) { this._updateDebugFlag(); } else if (aData === kPrefDefaultServiceId) { this.defaultServiceId = this._getDefaultServiceId(); } break; case NS_XPCOM_SHUTDOWN_OBSERVER_ID: // Cancel the timer for the call-ring wake lock. this._cancelCallRingWakeLockTimer(); Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); break; } } }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TelephonyProvider]);