From dcd33e80974180b9fe110fbb707c0d40f46d8052 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Tue, 28 Apr 2026 11:26:13 +0300 Subject: [PATCH] Added Telecom Framework audio setting --- .../kotlin/com/tutpro/baresip/AudioScreen.kt | 42 +++++ .../com/tutpro/baresip/BaresipService.kt | 143 ++++++++++++++---- .../main/kotlin/com/tutpro/baresip/Config.kt | 7 + .../kotlin/com/tutpro/baresip/MainScreen.kt | 84 ++++++---- app/src/main/res/values-fi/strings.xml | 2 + app/src/main/res/values/strings.xml | 4 +- 6 files changed, 227 insertions(+), 55 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt index 394f1f05..9b827cc0 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt @@ -164,6 +164,8 @@ private var newOpusPacketLoss = oldOpusPacketLoss private var newAudioDelay = BaresipService.audioDelay.toString() private var newToneCountry = BaresipService.toneCountry private var newRingtoneUri = "" +private var oldTelecom = BaresipService.telecom +private var newTelecom = oldTelecom private var save = false @@ -176,6 +178,8 @@ private fun AudioContent(contentPadding: PaddingValues) { oldSpeakerPhone = Config.variable("speaker_phone") == "yes" newSpeakerPhone = oldSpeakerPhone + oldTelecom = Config.variable("telecom") == "yes" + newTelecom = oldTelecom oldAudioModules = Config.variables("module") oldOpusBitrate = Config.variable("opus_bitrate") oldOpusPacketLoss = Config.variable("opus_packet_loss") @@ -202,6 +206,7 @@ private fun AudioContent(contentPadding: PaddingValues) { .verticalScroll(state = scrollState), verticalArrangement = Arrangement.spacedBy(12.dp), ) { + Telecom() ToneCountry() Ringtone() SpeakerPhone() @@ -214,6 +219,37 @@ private fun AudioContent(contentPadding: PaddingValues) { } } +@Composable +private fun Telecom() { + Row( + Modifier + .fillMaxWidth() + .padding(end = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + val telecomTitle = stringResource(R.string.telecom) + val telecomHelp = stringResource(R.string.telecom_help) + Text(text = telecomTitle, + modifier = Modifier + .weight(1f) + .clickable { + alertTitle.value = telecomTitle + alertMessage.value = telecomHelp + showAlert.value = true + }, + fontSize = 18.sp) + var telecom by remember { mutableStateOf(oldTelecom) } + Switch( + checked = telecom, + onCheckedChange = { + telecom = it + newTelecom = telecom + } + ) + } +} + @Composable private fun ToneCountry() { Row( @@ -709,6 +745,12 @@ private fun checkOnClick(ctx: Context): Result { save = true } + if (newTelecom != oldTelecom) { + Config.replaceVariable("telecom", if (newTelecom) "yes" else "no") + BaresipService.telecom = newTelecom + save = true + } + if (save) Config.save() return if (restart) Result.RESTART else Result.OK diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 8a0e4253..c302ed02 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -473,7 +473,12 @@ class BaresipService: Service() { stopMediaPlayer() setCallVolume() proximitySensing(proximitySensing) - Api.ua_answer(uap, callp, Api.VIDMODE_OFF) + if (telecom) + Api.ua_answer(uap, callp, Api.VIDMODE_OFF) + else { + Api.ua_answer(uap, callp, Api.VIDMODE_OFF) + ensureCommunicationMode() + } } "Call Reject" -> { @@ -583,6 +588,18 @@ class BaresipService: Service() { updateStatusNotification() } + "Start Call" -> { + val uap = intent!!.getLongExtra("uap", 0L) + val uri = intent.getStringExtra("uri")!! + val conferenceCall = intent.getBooleanExtra("conferenceCall", false) + val onHoldCallp = intent.getLongExtra("onHoldCallp", 0L) + if (!requestAudioFocus(applicationContext)) { + toast(getString(R.string.audio_focus_denied)) + return START_STICKY + } + runCall(uap, uri, conferenceCall, onHoldCallp) + } + "Stop" -> { cleanService() if (isServiceRunning) { @@ -782,12 +799,13 @@ class BaresipService: Service() { "call outgoing" -> { if (call!!.status.value == "transferring") break - if (speakerPhoneAuto) { - Log.d(TAG, "Auto-on speakerphone for outgoing call") - speakerPhone = true - } + speakerPhone = speakerPhoneAuto + // stopRinging() stopMediaPlayer() - setCallVolume() + val hasTelecom = ConnectionService.connections.containsKey(callp) || + ConnectionService.pendingOutgoingConnection != null + if (!hasTelecom) + setCallVolume() ensureCommunicationMode() proximitySensing(proximitySensing) } @@ -808,10 +826,7 @@ class BaresipService: Service() { return } "incoming call" -> { - if (speakerPhoneAuto) { - Log.d(TAG, "Auto-on speakerphone for incoming call") - speakerPhone = true - } + speakerPhone = speakerPhoneAuto val peerUri = ev[1] val toastMsg = if (Call.isAnyCallActive(applicationContext)) String.format(getString(R.string.call_auto_rejected), @@ -866,14 +881,23 @@ class BaresipService: Service() { Log.d(TAG, "Incoming call $uap/$callp/$peerUri") if (Call.ofCallp(callp) == null) Call(callp, ua, peerUri, "in", "incoming").add() - val extras = android.os.Bundle() - extras.putLong("uap", uap) - extras.putLong("callp", callp) - extras.putString("peerUri", peerUri) - try { - tm.addNewIncomingCall(getPhoneAccountHandle(this), extras) - } catch (e: Exception) { - Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}") + if (telecom) { + val extras = android.os.Bundle() + extras.putLong("uap", uap) + extras.putLong("callp", callp) + extras.putString("peerUri", peerUri) + try { + tm.addNewIncomingCall(getPhoneAccountHandle(this), extras) + } catch (e: Exception) { + Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}") + } + } else { + if (!requestAudioFocus(applicationContext)) { + Log.w(TAG, "Audio focus denied for incoming call") + Api.ua_hangup(uap, callp, 486, "Busy Here") + return + } + handleIncomingCall(Call.ofCallp(callp)!!) } return } @@ -1069,8 +1093,13 @@ class BaresipService: Service() { val tone = ev[2] if (tone == "busy") playBusy() - else + else { + if (!telecom && !Call.inCall()) + abandonAudioFocus(applicationContext) ensureCommunicationMode() + } + if (!telecom && !Call.inCall()) + abandonAudioFocus(applicationContext) if (call.dir == "out") call.rejected = call.startTime == null && !reason.startsWith("408") && @@ -1398,16 +1427,22 @@ class BaresipService: Service() { }, audioDelay) } - if (VERSION.SDK_INT < 31) { + val isTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } || + ConnectionService.pendingOutgoingConnection != null + if (isTelecom) + executeCall() + else if (VERSION.SDK_INT < 31) { Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION") am.mode = MODE_IN_COMMUNICATION executeCall() - } else { + } + else { val isAnyCallMode = am.mode == MODE_IN_COMMUNICATION || am.mode == AudioManager.MODE_IN_CALL if (isAnyCallMode) { Log.d(TAG, "Audio mode already in a call mode (${am.mode})") executeCall() - } else { + } + else { audioModeChangedListener = AudioManager.OnModeChangedListener { mode -> if (mode == MODE_IN_COMMUNICATION || mode == AudioManager.MODE_IN_CALL) { Log.d(TAG, "Audio mode changed to $mode") @@ -1946,8 +1981,11 @@ class BaresipService: Service() { cleanupRunnable = null } if (isSpeakerphoneOn == speakerPhone) { - Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.") - return + val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } + if (hasTelecom || currentMode == MODE_IN_COMMUNICATION) { + Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.") + return + } } } else if (!Call.inCall() && currentMode == MODE_NORMAL) { if (speakerPhone) { @@ -1971,12 +2009,13 @@ class BaresipService: Service() { val runnable = Runnable { cleanupRunnable = null if (Call.inCall()) { - if (am.mode != MODE_IN_COMMUNICATION && am.mode != AudioManager.MODE_IN_CALL) { + val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } + if (!hasTelecom && am.mode != MODE_IN_COMMUNICATION && am.mode != AudioManager.MODE_IN_CALL) { am.mode = MODE_IN_COMMUNICATION Log.d(TAG, "Manual Mode Guard: Setting MODE_IN_COMMUNICATON from ${am.mode}") } Log.d(TAG, "Applying speakerphone state: $speakerPhone") - if (!Call.calls().any { ConnectionService.connections.containsKey(it.callp) }) { + if (!hasTelecom) { Log.d(TAG, "No Telecom connection, using AudioManager for speaker") Utils.setSpeakerPhone(mainExecutor, am, speakerPhone) } else { @@ -2011,7 +2050,8 @@ class BaresipService: Service() { ) ) } - resetCallVolume() + if (!Call.calls().any { ConnectionService.connections.containsKey(it.callp) }) + resetCallVolume() proximitySensing(false) } } @@ -2264,6 +2304,7 @@ class BaresipService: Service() { var isRecOn = false var toneCountry = "us" var proximitySensing = true + var telecom = true val uas = mutableStateOf(emptyList()) val uasStatus = mutableStateOf(emptyMap()) @@ -2297,6 +2338,7 @@ class BaresipService: Service() { private var agc: AutomaticGainControl? = null private var ns: NoiseSuppressor? = null private var recorderSessionId = 0 + private var audioFocusRequest: androidx.media.AudioFocusRequestCompat? = null var rt: Ringtone? = null @@ -2325,5 +2367,52 @@ class BaresipService: Service() { Log.d(TAG, "Added service event ${event.event}") } } + + fun requestAudioFocus(ctx: Context): Boolean { + Log.d(TAG, "Requesting audio focus") + if (audioFocusRequest != null) { + Log.d(TAG, "Already focused") + return true + } + val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager + val attributes = androidx.media.AudioAttributesCompat.Builder() + .setUsage(androidx.media.AudioAttributesCompat.USAGE_VOICE_COMMUNICATION) + .setContentType(androidx.media.AudioAttributesCompat.CONTENT_TYPE_SPEECH) + .build() + audioFocusRequest = + androidx.media.AudioFocusRequestCompat.Builder(androidx.media.AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) + .setAudioAttributes(attributes) + .setOnAudioFocusChangeListener { } + .build() + if (androidx.media.AudioManagerCompat.requestAudioFocus( + am, + audioFocusRequest!! + ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED + ) { + Log.d(TAG, "requestAudioFocus granted") + } else { + Log.w(TAG, "requestAudioFocus denied") + audioFocusRequest = null + } + return audioFocusRequest != null + } + + fun abandonAudioFocus(ctx: Context) { + val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager + if (audioFocusRequest != null) { + Log.d(TAG, "Abandoning audio focus") + if (androidx.media.AudioManagerCompat.abandonAudioFocusRequest( + am, + audioFocusRequest!! + ) == + AudioManager.AUDIOFOCUS_REQUEST_GRANTED + ) { + audioFocusRequest = null + } else { + Log.e(TAG, "Failed to abandon audio focus") + } + } + am.mode = MODE_NORMAL + } } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/Config.kt b/app/src/main/kotlin/com/tutpro/baresip/Config.kt index 189aefb5..602f13b8 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Config.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Config.kt @@ -224,6 +224,13 @@ object Config { BaresipService.toneCountry = toneCountry config = "${config}tone_country ${BaresipService.toneCountry}\n" + val telecom = previousVariable("telecom") + if (telecom != "") + BaresipService.telecom = telecom == "yes" + else + BaresipService.telecom = true + config = "${config}telecom ${if (BaresipService.telecom) "yes" else "no"}\n" + save() BaresipService.isConfigInitialized = true diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 08bfddc6..26c05d14 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -1505,10 +1505,15 @@ private fun CallRow( onClick = { if (call.terminated.value) return@IconButton call.terminated.value = true - val connection = ConnectionService.connections[call.callp] - if (connection != null) - connection.onDisconnect() - else { + if (BaresipService.telecom) { + val connection = ConnectionService.connections[call.callp] + if (connection != null) + connection.onDisconnect() + else { + Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}") + Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") + } + } else { Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}") Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") } @@ -1532,10 +1537,15 @@ private fun CallRow( onClick = { if (call.terminated.value) return@IconButton call.terminated.value = true - val connection = ConnectionService.connections[call.callp] - if (connection != null) - connection.onDisconnect() - else { + if (BaresipService.telecom) { + val connection = ConnectionService.connections[call.callp] + if (connection != null) + connection.onDisconnect() + else { + Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}") + Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") + } + } else { Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}") Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") } @@ -2074,28 +2084,40 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer !Call.calls().any { it.ua.account.aor == ua.account.aor }) Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show() else { - val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager - val extras = android.os.Bundle() - extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, - BaresipService.getPhoneAccountHandle(ctx)) - val callExtras = android.os.Bundle() - callExtras.putBoolean("conferenceCall", conferenceCall) - callExtras.putLong("uap", ua.uap) - if (onHoldCallp != 0L) - callExtras.putLong("onHoldCallp", onHoldCallp) - extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) - try { - Log.d(TAG, "Placing Telecom call to $uri with uap=${ua.uap}") - tm.placeCall(uri.toUri(), extras) - } catch (e: SecurityException) { - Log.e(TAG, "placeCall failed: ${e.message}") + if (BaresipService.telecom) { + val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager + val extras = android.os.Bundle() + extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + BaresipService.getPhoneAccountHandle(ctx)) + val callExtras = android.os.Bundle() + callExtras.putBoolean("conferenceCall", conferenceCall) + callExtras.putLong("uap", ua.uap) + if (onHoldCallp != 0L) + callExtras.putLong("onHoldCallp", onHoldCallp) + extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) + try { + Log.d(TAG, "Placing Telecom call to $uri with uap=${ua.uap}") + tm.placeCall(uri.toUri(), extras) + } catch (e: SecurityException) { + Log.e(TAG, "placeCall failed: ${e.message}") + } + } + else { + val intent = Intent(ctx, BaresipService::class.java) + intent.action = "Start Call" + intent.putExtra("uap", ua.uap) + intent.putExtra("uri", uri) + intent.putExtra("conferenceCall", conferenceCall) + intent.putExtra("onHoldCallp", onHoldCallp) + ctx.startService(intent) } } } private fun answer(ctx: Context, call: Call) { Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}") - ConnectionService.connections[call.callp]?.setActive() + if (BaresipService.telecom) + ConnectionService.connections[call.callp]?.setActive() val intent = Intent(ctx, BaresipService::class.java) intent.action = "Call Answer" intent.putExtra("uap", call.ua.uap) @@ -2105,9 +2127,15 @@ private fun answer(ctx: Context, call: Call) { private fun reject(call: Call) { Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}") - val connection = ConnectionService.connections[call.callp] - if (connection != null) - connection.onReject() + if (BaresipService.telecom) { + val connection = ConnectionService.connections[call.callp] + if (connection != null) + connection.onReject() + else { + call.rejected = true + Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here") + } + } else { call.rejected = true Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here") @@ -2323,6 +2351,8 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params when (ev[0]) { "call rejected" -> { + if (!BaresipService.telecom) + BaresipService.abandonAudioFocus(ctx) if (aor == viewModel.selectedAor.value) viewModel.triggerAccountUpdate() } diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 01d7ea78..a988ff32 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -435,6 +435,8 @@ Läheisyyden tunnistus Jos merkitty, läheisyyden tunnistus on aktiivinen puhelun aikana. + Telecom-kehys + Käytä Android Telecom -kehystä puheluihin. Poista valinta, jos vastapuoli ei kuule ääntäsi tai jos sinulla on ongelmia Bluetooth-kuulokkeiden kanssa. Videon kehyskoko Lähetettävän videon kehyskoko (leveys x korkeus) Videokehysten lähetystaajuus diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aaf08402..738e3c6e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -406,8 +406,10 @@ Use dynamic colors if enabled in Android Settings Colorblind Use colorblind friendly registration status icons - Proximity Sensing"> + Proximity Sensing If checked, proximity sensing is active during calls. + Telecom Framework + Use Android Telecom framework for calls. Uncheck this if peer does not hear your audio or if you have issues with Bluetooth headsets. Video Frame Size Size of transmitted video frames (width x height) Video Frames Per Second