From 0a04a3e1326ac85bbda55f343d59b5a84f94b874 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Fri, 8 May 2026 17:25:19 +0300 Subject: [PATCH 01/25] Started experimenting with baresip as PSTN dialer --- .../com/tutpro/baresip/BaresipService.kt | 71 +++++++++++ .../main/kotlin/com/tutpro/baresip/Call.kt | 71 ++++++++--- .../com/tutpro/baresip/ConnectionService.kt | 19 ++- .../com/tutpro/baresip/InCallService.kt | 43 ++++--- .../kotlin/com/tutpro/baresip/MainScreen.kt | 117 ++++++++++++++---- .../main/kotlin/com/tutpro/baresip/Utils.kt | 21 ++++ .../kotlin/com/tutpro/baresip/ViewModel.kt | 1 + app/src/main/res/drawable/call_tel.xml | 32 +++++ 8 files changed, 309 insertions(+), 66 deletions(-) create mode 100644 app/src/main/res/drawable/call_tel.xml diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 10610182..2e74485a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -1748,6 +1748,77 @@ class BaresipService: Service() { } } + fun handleExternalCall(telecomCall: android.telecom.Call) { + val uri = telecomCall.details.handle?.schemeSpecificPart ?: "Unknown" + Log.d(TAG, "Handling external call from $uri") + + if (uas.value.isEmpty()) { + Log.e(TAG, "No User Agents available to handle external call") + return + } + val ua = UserAgent.statusMap().keys.firstOrNull()?.let { UserAgent.ofAor(it) } ?: uas.value[0] + + val telecomState = if (VERSION.SDK_INT >= 31) + telecomCall.details.state + else + @Suppress("DEPRECATION") telecomCall.state + val initialStatus = when (telecomState) { + android.telecom.Call.STATE_RINGING -> "incoming" + android.telecom.Call.STATE_DIALING, android.telecom.Call.STATE_CONNECTING -> "outgoing" + else -> "connected" + } + + val call = Call.ExternalCall( + telecomCall, + ua, + uri, + if (telecomState == android.telecom.Call.STATE_RINGING) "in" else "out", + initialStatus + ) + + telecomCall.registerCallback(object : android.telecom.Call.Callback() { + override fun onStateChanged(call: android.telecom.Call, state: Int) { + super.onStateChanged(call, state) + val newStatus = when (state) { + android.telecom.Call.STATE_RINGING -> "incoming" + android.telecom.Call.STATE_DIALING, android.telecom.Call.STATE_CONNECTING -> "outgoing" + android.telecom.Call.STATE_ACTIVE -> "connected" + android.telecom.Call.STATE_DISCONNECTED -> "closed" + android.telecom.Call.STATE_HOLDING -> { + calls.find { it.callp == call.hashCode().toLong() }?.onhold = true + "connected" + } + else -> "connected" + } + calls.find { it.callp == call.hashCode().toLong() }?.let { + if (it.status.value != newStatus) { + it.status.value = newStatus + postServiceEvent(ServiceEvent( + "call update", + arrayListOf(it.ua.uap, it.callp), + System.nanoTime()) + ) + if (newStatus == "closed") + handleExternalCallRemoved(call) + } + } + } + }) + + calls.add(call) + postServiceEvent(ServiceEvent( + "call incoming", + arrayListOf(ua.uap, call.callp), + System.nanoTime()) + ) + } + + fun handleExternalCallRemoved(telecomCall: android.telecom.Call) { + val callp = telecomCall.hashCode().toLong() + calls.removeAll { it.callp == callp } + messageUpdate.postValue(System.currentTimeMillis()) + } + private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { Handler(Looper.getMainLooper()).post { Toast.makeText(this@BaresipService.applicationContext, message, length).show() diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index 3dbeb1a4..643c0f27 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.core.net.toUri import java.util.* -class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, initialStatus: String) { +open class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, initialStatus: String) { var status: MutableState = mutableStateOf(initialStatus) @@ -55,11 +55,11 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str BaresipService.calls.remove(this) } - fun connect(uri: String): Boolean { + open fun connect(uri: String): Boolean { return Api.call_connect(callp, uri) == 0 } - fun hold(): Boolean { + open fun hold(): Boolean { if (onhold) return true if (Api.call_hold(callp, true)) { onhold = true @@ -71,7 +71,7 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str return false } - fun resume(): Boolean { + open fun resume(): Boolean { if (!onhold && !held) return true // 1. Hold other calls first for (c in BaresipService.calls) { @@ -94,13 +94,13 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str return false } - fun transfer(uri: String): Boolean { + open fun transfer(uri: String): Boolean { if (!onhold) hold() Log.d(TAG, "Transferring call $callp to $uri") return Api.call_transfer(callp, uri) == 0 } - fun executeTransfer(): Boolean { + open fun executeTransfer(): Boolean { return if (onHoldCall != null) { if (Api.call_hold(callp, true)) Api.call_replace_transfer(onHoldCall!!.callp, callp) @@ -110,31 +110,31 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str false } - fun sendDigit(digit: Char): Int { + open fun sendDigit(digit: Char): Int { return Api.call_send_digit(callp, digit) } - fun notifySipfrag(code: Int, reason: String) { + open fun notifySipfrag(code: Int, reason: String) { Api.call_notify_sipfrag(callp, code, reason) } - fun duration(): Int { + open fun duration(): Int { return Api.call_duration(callp) } - fun stats(stream: String): String { + open fun stats(stream: String): String { return Api.call_stats(callp, stream) } - fun state(): Int { + open fun state(): Int { return Api.call_state(callp) } - fun audioCodecs(): String { + open fun audioCodecs(): String { return Api.call_audio_codecs(callp) } - fun replaces(): Boolean { + open fun replaces(): Boolean { return Api.call_replaces(callp) } @@ -146,10 +146,53 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str if (ua.account.mediaEnc != "") security = R.color.colorTrafficRed } - fun destroy() { + open fun destroy() { Api.call_destroy(callp) } + class ExternalCall( + val telecomCall: android.telecom.Call, + ua: UserAgent, + peerUri: String, + dir: String, + initialStatus: String + ) : Call(telecomCall.hashCode().toLong(), ua, peerUri, dir, initialStatus) { + + override fun connect(uri: String): Boolean { + telecomCall.answer(android.telecom.VideoProfile.STATE_AUDIO_ONLY) + return true + } + + override fun hold(): Boolean { + telecomCall.hold() + onhold = true + callOnHold.value = true + return true + } + + override fun resume(): Boolean { + telecomCall.unhold() + onhold = false + callOnHold.value = false + return true + } + + override fun destroy() { + telecomCall.disconnect() + } + + override fun sendDigit(digit: Char): Int { + telecomCall.playDtmfTone(digit) + telecomCall.stopDtmfTone() + return 0 + } + + override fun duration(): Int = 0 + override fun stats(stream: String): String = "" + override fun state(): Int = 0 + override fun audioCodecs(): String = "PSTN" + } + companion object { fun calls(): ArrayList { diff --git a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt index ee4fa1b7..c80fe272 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt @@ -1,6 +1,7 @@ package com.tutpro.baresip import android.content.Intent +import android.net.Uri import android.telecom.CallAudioState import android.telecom.Connection import android.telecom.ConnectionRequest @@ -8,7 +9,6 @@ import android.telecom.ConnectionService import android.telecom.DisconnectCause import android.telecom.PhoneAccountHandle import android.telecom.TelecomManager -import android.net.Uri import java.util.concurrent.ConcurrentHashMap class ConnectionService : ConnectionService() { @@ -64,7 +64,8 @@ class ConnectionService : ConnectionService() { val connection = BaresipConnection(uap, callp) connections[callp] = connection - connection.setAddress(Uri.fromParts("sip", peerUri, null), TelecomManager.PRESENTATION_ALLOWED) + connection.setAddress(Uri.fromParts("sip", peerUri, null), + TelecomManager.PRESENTATION_ALLOWED) connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_HOLD or Connection.CAPABILITY_MERGE_CONFERENCE or @@ -108,6 +109,9 @@ class ConnectionService : ConnectionService() { val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?: nestedExtras?.getBoolean("conferenceCall") ?: false + val pstnCall = rootExtras?.getBoolean("pstnCall", false) ?: + nestedExtras?.getBoolean("pstnCall") ?: false + val onHoldCallp = rootExtras?.getLong("onHoldCallp", 0L).takeIf { it != 0L } ?: nestedExtras?.getLong("onHoldCallp") ?: 0L @@ -123,20 +127,15 @@ class ConnectionService : ConnectionService() { Connection.CAPABILITY_HOLD or Connection.CAPABILITY_MERGE_CONFERENCE or Connection.CAPABILITY_SWAP_CONFERENCE - connection.audioModeIsVoip = true - // Start the SIP connection logic - if (uap != 0L) { + if (!pstnCall) { + connection.audioModeIsVoip = true val sipUri = if (destination.startsWith("sip:")) destination else "sip:$destination" BaresipService.instance?.runCall(uap, sipUri, conferenceCall, onHoldCallp) - } else { - Log.e(TAG, "Cannot start outgoing call: uap is 0") - connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR, "No Account")) - connection.destroy() - pendingOutgoingConnection = null } connection.setDialing() + return connection } diff --git a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt index b65e7d0e..eae8fcd4 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt @@ -1,24 +1,35 @@ package com.tutpro.baresip -import android.app.Service -import android.content.Intent -import android.os.IBinder - -// This is needed in order to allow choosing baresip as default Phone app -class InCallService : Service() { - override fun onBind(intent: Intent): IBinder? { - Log.d(TAG, "InCallService onBind with intent: ${intent.action}") - return null - } -} - -/*import android.telecom.InCallService import android.telecom.Call +import android.telecom.InCallService class InCallService : InCallService() { + override fun onCallAdded(call: Call) { super.onCallAdded(call) - // This is triggered when the system wants YOU to show the call UI - Log.d("Baresip", "InCallService: Call added") + Log.d(TAG, "InCallService: Call added") + + val handle = call.details.accountHandle + val baresipHandle = BaresipService.getPhoneAccountHandle(this) + + if (handle == baresipHandle) { + Log.d(TAG, "InCallService: Identified as SIP call") + // The SIP call is already managed by ConnectionService/BaresipService. + // We just need to ensure the InCallService stays bound. + } else { + Log.d(TAG, "InCallService: Identified as PSTN call from $handle") + // This is a cellular call. We need to wrap it so MainScreen can show it. + BaresipService.instance?.handleExternalCall(call) + } } -}*/ + + override fun onCallRemoved(call: Call) { + super.onCallRemoved(call) + Log.d(TAG, "InCallService: Call removed") + BaresipService.instance?.handleExternalCallRemoved(call) + } + + companion object { + private const val TAG = "Baresip" + } +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 99392ac6..aed0ee9b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -12,12 +12,14 @@ import android.content.res.Configuration import android.media.AudioManager import android.net.Uri import android.os.Build.VERSION +import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.Process import android.os.SystemClock import android.provider.DocumentsContract import android.provider.MediaStore +import android.telecom.TelecomManager import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -1473,12 +1475,14 @@ private fun CallRow( horizontalArrangement = Arrangement.Absolute.SpaceBetween ) { if (isDialer) { + dialerState.showCallPstnButton.value = Utils.pstnAccountHandle(ctx) != null if (dialerState.showCallButton.value) IconButton( modifier = Modifier.size(48.dp), enabled = dialerState.callButtonsEnabled.value, onClick = { if (!dialerState.callButtonsEnabled.value) return@IconButton + dialerState.showCallPstnButton.value = false dialerState.showCallConferenceButton.value = false dialerState.showSuggestions.value = false callClick(ctx, viewModel, dialerState) @@ -1494,6 +1498,28 @@ private fun CallRow( contentDescription = null, ) } + if (dialerState.showCallPstnButton.value) + IconButton( + modifier = Modifier.size(48.dp), + enabled = dialerState.callButtonsEnabled.value, + onClick = { + if (!dialerState.callButtonsEnabled.value) return@IconButton + dialerState.showCallButton.value = false + dialerState.showCallConferenceButton.value = false + dialerState.showSuggestions.value = false + callClick(ctx, viewModel, dialerState) + }, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.call_tel), + modifier = Modifier.size(42.dp), + tint = colorResource(if (dialerState.callButtonsEnabled.value) + R.color.colorTrafficGreen + else + R.color.colorTrafficYellow), + contentDescription = null, + ) + } if (dialerState.showCallConferenceButton.value) { Spacer(modifier = Modifier.weight(1f, true)) IconButton( @@ -1502,6 +1528,7 @@ private fun CallRow( onClick = { if (!dialerState.callButtonsEnabled.value) return@IconButton dialerState.showCallButton.value = false + dialerState.showCallPstnButton.value = false dialerState.showSuggestions.value = false callClick(ctx, viewModel, dialerState) } @@ -2061,14 +2088,14 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel ctx, viewModel, uriText, - dialerState.showCallConferenceButton.value + dialerState ) else if (uris.size == 1) makeCall( ctx, viewModel, uris[0], - dialerState.showCallConferenceButton.value + dialerState ) else { selectItems.value = uris @@ -2077,7 +2104,7 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel ctx, viewModel, uris[index], - dialerState.showCallConferenceButton.value + dialerState ) } showSelectItemDialog.value = true @@ -2095,8 +2122,8 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel } } -private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean, - onHoldCallp: Long = 0L) { +private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, + dialerState: ViewModel.DialerState, onHoldCallp: Long = 0L) { val aor = viewModel.selectedAor.value val ua = UserAgent.ofAor(aor)!! val peerUri = if (Utils.isTelNumber(uriText)) @@ -2104,13 +2131,16 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer else uriText val uri = if (Utils.isTelUri(peerUri)) { - if (ua.account.telProvider == "") { + if (dialerState.showCallPstnButton.value) + peerUri + else if (ua.account.telProvider == "") { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), aor) showAlert.value = true return } - Utils.telToSip(peerUri, ua.account) + else + Utils.telToSip(peerUri, ua.account) } else Utils.uriComplete(peerUri, aor) @@ -2118,28 +2148,62 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri) showAlert.value = true + return + } + else if (dialerState.showCallPstnButton.value && !Utils.isTelUri(uri)) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = "Telephone call can only be made to telephone number" + showAlert.value = true + return } else if (Utils.isAudioMode(ctx,AudioManager.MODE_IN_CALL) && !Call.calls().any { it.ua.account.aor == ua.account.aor }) Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show() else { viewModel.dialerState.callButtonsEnabled.value = false + var error = "" 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}") + val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + if (dialerState.showCallPstnButton.value) { + val phoneAccountHandle = Utils.pstnAccountHandle(ctx) + if (phoneAccountHandle != null) { + val extras = Bundle().apply { + putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle) + } + val callExtras = Bundle() + callExtras.putBoolean("pstnCall", dialerState.showCallPstnButton.value) + extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) + try { + Log.i(TAG, "Placing Telecom PSTN call to $uri with uap=${ua.uap}") + tm.placeCall(uri.toUri(), extras) + } catch (e: SecurityException) { + error = "placeCall failed: ${e.message}" + } + } + else + error = "no phone account" + } + else { + val extras = Bundle() + extras.putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + BaresipService.getPhoneAccountHandle(ctx) + ) + val callExtras = Bundle() + callExtras.putBoolean("conferenceCall", dialerState.showCallConferenceButton.value) + callExtras.putLong("uap", ua.uap) + if (onHoldCallp != 0L) + callExtras.putLong("onHoldCallp", onHoldCallp) + extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) + try { + Log.d(TAG, "Placing Telecom SIP call to $uri with uap=${ua.uap}") + tm.placeCall(uri.toUri(), extras) + } catch (e: SecurityException) { + error = "placeCall failed: ${e.message}" + } + } + if (error != "") { + Log.e(TAG, error) viewModel.dialerState.callButtonsEnabled.value = true } } @@ -2148,7 +2212,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer intent.action = "Start Call" intent.putExtra("uap", ua.uap) intent.putExtra("uri", uri) - intent.putExtra("conferenceCall", conferenceCall) + intent.putExtra("conferenceCall", dialerState.showCallConferenceButton.value) intent.putExtra("onHoldCallp", onHoldCallp) ctx.startService(intent) } @@ -2207,7 +2271,7 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText: if (success) { call.onhold = true call.referTo = uri - makeCall(ctx, viewModel, uri, false, call.callp) + makeCall(ctx, viewModel, uri, viewModel.dialerState, call.callp) showCall(ctx, viewModel, ua, call) } } @@ -2237,6 +2301,7 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal viewModel.dialerState.callUriEnabled.value = true }, 100) viewModel.dialerState.showCallButton.value = true + viewModel.dialerState.showCallPstnButton.value = true viewModel.dialerState.showCallConferenceButton.value = true viewModel.dialerState.callButtonsEnabled.value = true viewModel.dialerState.showSuggestions.value = false @@ -2521,7 +2586,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params if (call in Call.calls()) acceptTransfer(ctx, viewModel, ua, call!!, ev[1]) else - makeCall(ctx, viewModel, ev[1], false) + makeCall(ctx, viewModel, ev[1], viewModel.dialerState) } showDialog.value = true } @@ -2530,7 +2595,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params val call = Call.ofCallp(callp) if (call in Call.calls()) Api.ua_hangup(uap, callp, 487, "Request Terminated") - makeCall(ctx, viewModel, ev[1], false) + makeCall(ctx, viewModel, ev[1], viewModel.dialerState) showCall(ctx, viewModel, ua) } "transfer failed" -> { diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 1256dedf..5b764110 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -1,5 +1,6 @@ package com.tutpro.baresip +import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.app.KeyguardManager @@ -1323,6 +1324,26 @@ object Utils { return file } + fun pstnAccountHandle(ctx: Context): PhoneAccountHandle? { + if (ctx.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == + PackageManager.PERMISSION_GRANTED) { + val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + if (Build.VERSION.SDK_INT >= 29) { + val preferredHandle: PhoneAccountHandle? = tm.userSelectedOutgoingPhoneAccount + if (preferredHandle != null) + return preferredHandle + } + val baresipHandle = BaresipService.getPhoneAccountHandle(ctx) + val phoneAccounts = tm.callCapablePhoneAccounts.filter { it != baresipHandle } + return if (phoneAccounts.isNotEmpty()) + phoneAccounts[0] + else + null + } + else + return null + } + @Suppress("unused") fun listFilesInDirectory(directoryPath: String): List { val directory = File(directoryPath) diff --git a/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt b/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt index eb6d068a..a45ed327 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt @@ -44,6 +44,7 @@ class ViewModel: ViewModel() { val callUriLabel: MutableState = mutableStateOf(""), val showSuggestions: MutableState = mutableStateOf(false), val showCallButton: MutableState = mutableStateOf(true), + val showCallPstnButton: MutableState = mutableStateOf(true), val showCallConferenceButton: MutableState = mutableStateOf(true), val callButtonsEnabled: MutableState = mutableStateOf(true), val conferenceCall: MutableState = mutableStateOf(false), diff --git a/app/src/main/res/drawable/call_tel.xml b/app/src/main/res/drawable/call_tel.xml new file mode 100644 index 00000000..2a6314c7 --- /dev/null +++ b/app/src/main/res/drawable/call_tel.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + From 6a6ccba10e5550bb9dca6e3c50eed95784dce15d Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sat, 9 May 2026 19:59:34 +0300 Subject: [PATCH 02/25] Added virtual mobile account --- .../main/kotlin/com/tutpro/baresip/Account.kt | 111 +++++++++--------- .../com/tutpro/baresip/AccountScreen.kt | 15 ++- .../com/tutpro/baresip/BaresipService.kt | 43 ++++++- .../com/tutpro/baresip/InCallService.kt | 4 +- .../kotlin/com/tutpro/baresip/MainScreen.kt | 41 ++----- .../kotlin/com/tutpro/baresip/UserAgent.kt | 8 +- .../main/kotlin/com/tutpro/baresip/Utils.kt | 32 +++++ .../kotlin/com/tutpro/baresip/ViewModel.kt | 1 - app/src/main/res/values-fi/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 + 10 files changed, 159 insertions(+), 99 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 7a7fd54c..4ad306e4 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -5,87 +5,92 @@ import java.net.URLDecoder import java.net.URLEncoder import java.util.Locale -class Account(val accp: Long) { +class Account(val accp: Long, virtualAor: String? = null) { + var isMobile = false var nickName = "" - var displayName = Api.account_display_name(accp) - val aor = Api.account_aor(accp) - val luri = Api.account_luri(accp) - var authUser = Api.account_auth_user(accp) - var authPass = Api.account_auth_pass(accp) + var displayName = if (accp != 0L) Api.account_display_name(accp) else "" + val aor = if (accp != 0L) Api.account_aor(accp) else (virtualAor ?: "") + val luri = if (accp != 0L) Api.account_luri(accp) else (virtualAor ?: "") + var authUser = if (accp != 0L) Api.account_auth_user(accp) else "" + var authPass = if (accp != 0L) Api.account_auth_pass(accp) else "" var outbound = ArrayList() - var mediaNat = Api.account_medianat(accp) - var stunServer = Api.account_stun_uri(accp) - var stunUser = Api.account_stun_user(accp) - var stunPass = Api.account_stun_pass(accp) + var mediaNat = if (accp != 0L) Api.account_medianat(accp) else "" + var stunServer = if (accp != 0L) Api.account_stun_uri(accp) else "" + var stunUser = if (accp != 0L) Api.account_stun_user(accp) else "" + var stunPass = if (accp != 0L) Api.account_stun_pass(accp) else "" var audioCodec = ArrayList() var videoCodec = ArrayList() - var regint = Api.account_regint(accp) - var checkOrigin = Api.account_check_origin(accp) + var regint = if (accp != 0L) Api.account_regint(accp) else 0 + var checkOrigin = if (accp != 0L) Api.account_check_origin(accp) else true var configuredRegInt = REGISTRATION_INTERVAL - var mediaEnc = Api.account_mediaenc(accp) - var rtcpMux = Api.account_rtcp_mux(accp) - var rel100Mode = Api.account_rel100_mode(accp) - var dtmfMode = Api.account_dtmfmode(accp) - var answerMode = Api.account_answermode(accp) - var autoRedirect = Api.account_sip_autoredirect(accp) + var mediaEnc = if (accp != 0L) Api.account_mediaenc(accp) else "" + var rtcpMux = if (accp != 0L) Api.account_rtcp_mux(accp) else false + var rel100Mode = if (accp != 0L) Api.account_rel100_mode(accp) else Api.REL100_DISABLED + var dtmfMode = if (accp != 0L) Api.account_dtmfmode(accp) else Api.DTMFMODE_AUTO + var answerMode = if (accp != 0L) Api.account_answermode(accp) else Api.ANSWERMODE_MANUAL + var autoRedirect = if (accp != 0L) Api.account_sip_autoredirect(accp) else false var blockUnknown = false - var vmUri = Api.account_vm_uri(accp) + var vmUri = if (accp != 0L) Api.account_vm_uri(accp) else "" var vmNew = 0 var vmOld = 0 var missedCalls = false var unreadMessages = false var callHistory = true var countryCode = "" - var telProvider = Utils.aorDomain(aor) + var telProvider = if (accp != 0L) Utils.aorDomain(aor) else "" var resumeUri = "" var numericKeypad = false var customParams = "" init { - if (authPass == "") - authPass = NO_AUTH_PASS + if (accp != 0L) { + if (authPass == "") + authPass = NO_AUTH_PASS - var i = 0 - while (true) { - val ob = Api.account_outbound(accp, i) - if (ob != "") { - outbound.add(ob) - i++ - } else { - break + var i = 0 + while (true) { + val ob = Api.account_outbound(accp, i) + if (ob != "") { + outbound.add(ob) + i++ + } else { + break + } } - } - i = 0 - while (true) { - val ac = Api.account_audio_codec(accp, i) - if (ac != "") { - audioCodec.add(ac) - i++ - } else { - break + i = 0 + while (true) { + val ac = Api.account_audio_codec(accp, i) + if (ac != "") { + audioCodec.add(ac) + i++ + } else { + break + } } - } - val extra = Api.account_extra(accp) - if (Utils.paramExists(extra, "nickname")) - nickName = Utils.paramValue(extra, "nickname") - if (Utils.paramExists(extra, "regint")) - configuredRegInt = Utils.paramValue(extra, "regint").toInt() - callHistory = Utils.paramValue(extra, "call_history") == "" - blockUnknown= Utils.paramExists(extra, "block_unknown") - if (Utils.paramExists(extra, "country_code")) - countryCode = Utils.paramValue(extra, "country_code") - if (Utils.paramExists(extra, "tel_provider")) - telProvider = URLDecoder.decode(Utils.paramValue(extra, "tel_provider"), "UTF-8") - numericKeypad = Utils.paramExists(extra, "numeric_keypad") - customParams = extra.substringAfter("last=empty").substringAfter(";") + val extra = Api.account_extra(accp) + if (Utils.paramExists(extra, "nickname")) + nickName = Utils.paramValue(extra, "nickname") + if (Utils.paramExists(extra, "regint")) + configuredRegInt = Utils.paramValue(extra, "regint").toInt() + callHistory = Utils.paramValue(extra, "call_history") == "" + blockUnknown = Utils.paramExists(extra, "block_unknown") + if (Utils.paramExists(extra, "country_code")) + countryCode = Utils.paramValue(extra, "country_code") + if (Utils.paramExists(extra, "tel_provider")) + telProvider = URLDecoder.decode(Utils.paramValue(extra, "tel_provider"), "UTF-8") + numericKeypad = Utils.paramExists(extra, "numeric_keypad") + customParams = extra.substringAfter("last=empty").substringAfter(";") + } } fun print() : String { + if (isMobile) return "" + var res = if (displayName != "") "\"${displayName}\" " else diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt index 20403c95..cf3971fe 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt @@ -232,14 +232,25 @@ private fun AccountContent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { + val aorText = if (ua.account.isMobile) { + if (ua.account.aor == "tel:mobile") + stringResource(R.string.not_available) + else + ua.account.aor + } else + ua.account.luri + OutlinedTextField( - value = ua.account.luri, + value = aorText, enabled = false, onValueChange = {}, modifier = Modifier.fillMaxWidth(), textStyle = TextStyle(fontSize = 18.sp), label = { - Text(text = stringResource(R.string.sip_uri), fontWeight = FontWeight.Bold) + Text( + text = stringResource(if (ua.account.isMobile) R.string.tel_uri else R.string.sip_uri), + fontWeight = FontWeight.Bold + ) }, colors = OutlinedTextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 2e74485a..ca1dde7b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -491,9 +491,10 @@ class BaresipService: Service() { activeNetwork = cm.activeNetwork Log.i(TAG, "Active network: $activeNetwork") - if (telecom) + if (telecom) { registerPhoneAccount() - else + addMobileUserAgent() + } else if (btAdapter != null) { Log.i(TAG, "Registering bluetooth receiver") val filter = IntentFilter() @@ -1512,6 +1513,7 @@ class BaresipService: Service() { fun started() { Log.d(TAG, "Received 'started' from baresip") isNativeReady = true + if (telecom) addMobileUserAgent() Api.net_debug() postServiceEvent(ServiceEvent("started", arrayListOf(callActionUri), System.nanoTime())) callActionUri = "" @@ -1748,15 +1750,18 @@ class BaresipService: Service() { } } - fun handleExternalCall(telecomCall: android.telecom.Call) { + fun handleExternalCall(telecomCall: android.telecom.Call, preferredAor: String? = null) { val uri = telecomCall.details.handle?.schemeSpecificPart ?: "Unknown" - Log.d(TAG, "Handling external call from $uri") + Log.d(TAG, "Handling external call from $uri (preferredAor=$preferredAor)") if (uas.value.isEmpty()) { Log.e(TAG, "No User Agents available to handle external call") return } - val ua = UserAgent.statusMap().keys.firstOrNull()?.let { UserAgent.ofAor(it) } ?: uas.value[0] + + val ua = preferredAor?.let { UserAgent.ofAor(it) } + ?: uas.value.find { it.account.isMobile } + ?: uas.value[0] val telecomState = if (VERSION.SDK_INT >= 31) telecomCall.details.state @@ -1819,6 +1824,34 @@ class BaresipService: Service() { messageUpdate.postValue(System.currentTimeMillis()) } + private fun addMobileUserAgent() { + if (!telecom || Utils.pstnAccountHandle(this) == null) return + + val mobileAor = Utils.getLine1Number(this)?.let { "tel:$it" } ?: "tel:mobile" + + val existingMobileUa = uas.value.find { it.account.isMobile } + if (existingMobileUa != null) { + // If we previously had tel:mobile but now have a real number, replace it + if (existingMobileUa.account.aor == "tel:mobile" && mobileAor != "tel:mobile") { + val updatedUas = uas.value.toMutableList() + updatedUas.remove(existingMobileUa) + uas.value = updatedUas.toList() + } else { + return + } + } + + val account = Account(0L, mobileAor) + account.isMobile = true + account.nickName = "Mobile" + val mobileUa = UserAgent(0L, account) + + val updatedUas = uas.value.toMutableList() + updatedUas.add(mobileUa) + uas.value = updatedUas.toList() + uasStatus.value = UserAgent.statusMap() + } + private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { Handler(Looper.getMainLooper()).post { Toast.makeText(this@BaresipService.applicationContext, message, length).show() diff --git a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt index eae8fcd4..8fdaf96f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt @@ -18,8 +18,8 @@ class InCallService : InCallService() { // We just need to ensure the InCallService stays bound. } else { Log.d(TAG, "InCallService: Identified as PSTN call from $handle") - // This is a cellular call. We need to wrap it so MainScreen can show it. - BaresipService.instance?.handleExternalCall(call) + val aor = call.details.intentExtras?.getString("aor") + BaresipService.instance?.handleExternalCall(call, aor) } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index aed0ee9b..2332d78e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -1472,17 +1472,15 @@ private fun CallRow( Row( modifier = Modifier .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Absolute.SpaceBetween + horizontalArrangement = Arrangement.Center ) { if (isDialer) { - dialerState.showCallPstnButton.value = Utils.pstnAccountHandle(ctx) != null if (dialerState.showCallButton.value) IconButton( modifier = Modifier.size(48.dp), enabled = dialerState.callButtonsEnabled.value, onClick = { if (!dialerState.callButtonsEnabled.value) return@IconButton - dialerState.showCallPstnButton.value = false dialerState.showCallConferenceButton.value = false dialerState.showSuggestions.value = false callClick(ctx, viewModel, dialerState) @@ -1498,37 +1496,14 @@ private fun CallRow( contentDescription = null, ) } - if (dialerState.showCallPstnButton.value) - IconButton( - modifier = Modifier.size(48.dp), - enabled = dialerState.callButtonsEnabled.value, - onClick = { - if (!dialerState.callButtonsEnabled.value) return@IconButton - dialerState.showCallButton.value = false - dialerState.showCallConferenceButton.value = false - dialerState.showSuggestions.value = false - callClick(ctx, viewModel, dialerState) - }, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.call_tel), - modifier = Modifier.size(42.dp), - tint = colorResource(if (dialerState.callButtonsEnabled.value) - R.color.colorTrafficGreen - else - R.color.colorTrafficYellow), - contentDescription = null, - ) - } if (dialerState.showCallConferenceButton.value) { - Spacer(modifier = Modifier.weight(1f, true)) + Spacer(modifier = Modifier.width(32.dp)) IconButton( modifier = Modifier.size(48.dp), enabled = dialerState.callButtonsEnabled.value, onClick = { if (!dialerState.callButtonsEnabled.value) return@IconButton dialerState.showCallButton.value = false - dialerState.showCallPstnButton.value = false dialerState.showSuggestions.value = false callClick(ctx, viewModel, dialerState) } @@ -2131,7 +2106,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, else uriText val uri = if (Utils.isTelUri(peerUri)) { - if (dialerState.showCallPstnButton.value) + if (ua.account.isMobile) peerUri else if (ua.account.telProvider == "") { alertTitle.value = ctx.getString(R.string.notice) @@ -2150,7 +2125,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, showAlert.value = true return } - else if (dialerState.showCallPstnButton.value && !Utils.isTelUri(uri)) { + else if (ua.account.isMobile && !Utils.isTelUri(uri)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = "Telephone call can only be made to telephone number" showAlert.value = true @@ -2164,14 +2139,15 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, var error = "" if (BaresipService.telecom) { val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - if (dialerState.showCallPstnButton.value) { + if (ua.account.isMobile) { val phoneAccountHandle = Utils.pstnAccountHandle(ctx) if (phoneAccountHandle != null) { val extras = Bundle().apply { putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle) } val callExtras = Bundle() - callExtras.putBoolean("pstnCall", dialerState.showCallPstnButton.value) + callExtras.putBoolean("pstnCall", true) + callExtras.putString("aor", aor) extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) try { Log.i(TAG, "Placing Telecom PSTN call to $uri with uap=${ua.uap}") @@ -2301,8 +2277,7 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal viewModel.dialerState.callUriEnabled.value = true }, 100) viewModel.dialerState.showCallButton.value = true - viewModel.dialerState.showCallPstnButton.value = true - viewModel.dialerState.showCallConferenceButton.value = true + viewModel.dialerState.showCallConferenceButton.value = !ua.account.isMobile viewModel.dialerState.callButtonsEnabled.value = true viewModel.dialerState.showSuggestions.value = false dialpadButtonEnabled.value = true diff --git a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt index e62a0c12..b5a07218 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt @@ -5,12 +5,13 @@ import com.tutpro.baresip.BaresipService.Companion.colorblind import com.tutpro.baresip.BaresipService.Companion.uas import com.tutpro.baresip.BaresipService.Companion.uasStatus -class UserAgent(val uap: Long) { +class UserAgent(val uap: Long, virtualAccount: Account? = null) { - val account = Account(Api.ua_account(uap)) - var status = R.drawable.circle_white + val account = virtualAccount ?: Account(Api.ua_account(uap)) + var status = if (uap != 0L) R.drawable.circle_white else R.drawable.circle_green fun callAlloc(xCall: Long, videoMode: Int): Long { + if (uap == 0L) return 0L return Api.ua_call_alloc(uap, xCall, videoMode) } @@ -49,6 +50,7 @@ class UserAgent(val uap: Long) { } fun reRegister() { + if (uap == 0L) return this.status = circleYellow.getValue(colorblind) if (this.account.regint == 0) Api.ua_unregister(this.uap) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 5b764110..1fe824b7 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -27,6 +27,8 @@ import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore import android.provider.OpenableColumns +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager import android.telecom.TelecomManager import android.telecom.PhoneAccountHandle import android.text.format.DateUtils @@ -1344,6 +1346,36 @@ object Utils { return null } + @SuppressLint("HardwareIds") + fun getLine1Number(ctx: Context): String? { + try { + if (Build.VERSION.SDK_INT >= 33) { + if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED) { + val sm = ctx.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager + val number = sm.getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) + if (number != "") { + Log.i(TAG, "Retrieved SIM number via SubscriptionManager") + return number + } + } + } else { + if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + val tm = ctx.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + @Suppress("DEPRECATION") + val number = tm.line1Number + if (number != null) { + Log.i(TAG, "Retrieved SIM number via TelephonyManager") + return number + } + } + } + } catch (e: Exception) { + Log.w(TAG, "getLine1Number failed: ${e.message}") + } + return null + } + @Suppress("unused") fun listFilesInDirectory(directoryPath: String): List { val directory = File(directoryPath) diff --git a/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt b/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt index a45ed327..eb6d068a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt @@ -44,7 +44,6 @@ class ViewModel: ViewModel() { val callUriLabel: MutableState = mutableStateOf(""), val showSuggestions: MutableState = mutableStateOf(false), val showCallButton: MutableState = mutableStateOf(true), - val showCallPstnButton: MutableState = mutableStateOf(true), val showCallConferenceButton: MutableState = mutableStateOf(true), val callButtonsEnabled: MutableState = mutableStateOf(true), val conferenceCall: MutableState = mutableStateOf(false), diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a99003f3..49b41fb8 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -530,6 +530,7 @@ Puhelu soi Puhelu on pidossa Puhelu on yhdistetty + Ei saatavilla Tallennus voidaan asettaa päälle tai pois vain silloin, kun puhelu ei ole yhdistetty Puhelun siirto diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c418e07..ec665ca1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -480,6 +480,7 @@ Accept Deny SIP URI + TEL URI Add Delete Edit @@ -513,6 +514,7 @@ Call is ringing Call is on hold Call is connected + Not available Recording can be turned on or off only when call is not connected Call Transfer From cb0b87950fcdb9fc4374b34e51314c0166da7362 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 10 May 2026 10:07:30 +0300 Subject: [PATCH 03/25] tel: URI related bug fixes and improvements --- app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt | 4 +++- .../main/kotlin/com/tutpro/baresip/ConnectionService.kt | 2 +- app/src/main/kotlin/com/tutpro/baresip/Utils.kt | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt index 09c9ce9f..6b5921d9 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt @@ -2,6 +2,7 @@ package com.tutpro.baresip import android.content.Context import android.content.Intent +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -369,7 +370,8 @@ private fun Calls( ) secondButtonText.value = ctx.getString(R.string.add_contact) secondAction.value = { - navController.navigate("baresip_contact/$peerUri/new") + val uri = Utils.sipToTel(peerUri) + navController.navigate("baresip_contact/$uri/new") } lastButtonText.value = ctx.getString(R.string.delete) lastAction.value = { diff --git a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt index c80fe272..64b4b862 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt @@ -115,7 +115,7 @@ class ConnectionService : ConnectionService() { val onHoldCallp = rootExtras?.getLong("onHoldCallp", 0L).takeIf { it != 0L } ?: nestedExtras?.getLong("onHoldCallp") ?: 0L - val destination = request?.address?.schemeSpecificPart ?: "" + val destination = request?.address?.encodedSchemeSpecificPart ?: "" Log.d(TAG, "onCreateOutgoingConnection to $destination (uap=$uap)") diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 1fe824b7..cdceed46 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -380,6 +380,15 @@ object Utils { "@" + hostPart + ";user=phone" } + fun sipToTel(sipUri: String): String { + if (sipUri.contains(";user=phone")) { + val user = uriUserPart(sipUri) + if (user != "") + return "tel:${user.replace("%23", "#")}" + } + return sipUri + } + fun checkName(name: String): Boolean { return name.isNotEmpty() && name == String(name.toByteArray(), Charsets.UTF_8) && name.lines().size == 1 && !name.contains('"') From 84edc5e12435e594ecb1f1c679e11ba309ee9309 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 10 May 2026 12:22:02 +0300 Subject: [PATCH 04/25] Handle mobile call answer/hangup/reject/history --- .../com/tutpro/baresip/BaresipService.kt | 42 ++++++++----- .../main/kotlin/com/tutpro/baresip/Call.kt | 32 ++++++++++ .../kotlin/com/tutpro/baresip/MainScreen.kt | 63 +++---------------- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index ca1dde7b..f1bf7868 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -545,18 +545,15 @@ class BaresipService: Service() { } "Call Answer" -> { - val uap = intent!!.getLongExtra("uap", 0L) - val callp = intent.getLongExtra("callp", 0L) + val callp = intent!!.getLongExtra("callp", 0L) + val call = Call.ofCallp(callp) stopRinging() stopMediaPlayer() setCallVolume() proximitySensing(proximitySensing) - if (telecom) - Api.ua_answer(uap, callp, Api.VIDMODE_OFF) - else { - Api.ua_answer(uap, callp, Api.VIDMODE_OFF) + call?.answer() + if (!telecom) ensureCommunicationMode() - } } "Call Reject" -> { @@ -569,20 +566,15 @@ class BaresipService: Service() { val aor = call.ua.account.aor Log.d(TAG, "Aor $aor rejected incoming call $callp from $peerUri") call.rejected = true - Api.ua_hangup(call.ua.uap, callp, 486, "Rejected") + call.reject() } } "Call Hangup" -> { val callp = intent!!.getLongExtra("callp", 0L) Log.d(TAG, "onStartCommand Hangup action for $callp") - val connection = ConnectionService.connections[callp] - if (connection != null) { - connection.onDisconnect() // This calls Api.ua_hangup(..., 0, "") - } else { - val call = Call.ofCallp(callp) - if (call != null) Api.ua_hangup(call.ua.uap, callp, 0, "") - } + val call = Call.ofCallp(callp) + call?.hangup(0, "") } "Transfer Deny" -> { @@ -1798,6 +1790,8 @@ class BaresipService: Service() { calls.find { it.callp == call.hashCode().toLong() }?.let { if (it.status.value != newStatus) { it.status.value = newStatus + if (newStatus == "connected") + it.startTime = GregorianCalendar() postServiceEvent(ServiceEvent( "call update", arrayListOf(it.ua.uap, it.callp), @@ -1820,7 +1814,23 @@ class BaresipService: Service() { fun handleExternalCallRemoved(telecomCall: android.telecom.Call) { val callp = telecomCall.hashCode().toLong() - calls.removeAll { it.callp == callp } + val call = calls.find { it.callp == callp } + if (call != null) { + if (call.ua.account.callHistory) { + val history = CallHistoryNew(call.ua.account.aor, call.peerUri, call.dir) + history.stopTime = GregorianCalendar() + history.startTime = call.startTime + history.rejected = call.rejected + history.add() + if (call.dir == "in" && call.startTime == null && !call.rejected) + call.ua.account.missedCalls = true + } + calls.remove(call) + } + if (!Call.inCall()) { + proximitySensing(false) + stopMediaPlayer() + } messageUpdate.postValue(System.currentTimeMillis()) } diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index 643c0f27..5a9f4638 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -150,6 +150,26 @@ open class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir Api.call_destroy(callp) } + open fun hangup(code: Int, reason: String) { + if (BaresipService.telecom) { + val connection = ConnectionService.connections[callp] + if (connection != null) + connection.onDisconnect() + else + Api.ua_hangup(ua.uap, callp, code, reason) + } else { + Api.ua_hangup(ua.uap, callp, code, reason) + } + } + + open fun answer() { + Api.ua_answer(ua.uap, callp, Api.VIDMODE_OFF) + } + + open fun reject() { + hangup(486, "Busy Here") + } + class ExternalCall( val telecomCall: android.telecom.Call, ua: UserAgent, @@ -163,6 +183,10 @@ open class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir return true } + override fun answer() { + telecomCall.answer(android.telecom.VideoProfile.STATE_AUDIO_ONLY) + } + override fun hold(): Boolean { telecomCall.hold() onhold = true @@ -177,6 +201,14 @@ open class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir return true } + override fun hangup(code: Int, reason: String) { + telecomCall.disconnect() + } + + override fun reject() { + telecomCall.disconnect() + } + override fun destroy() { telecomCall.disconnect() } diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 2332d78e..14490f91 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -1532,18 +1532,8 @@ private fun CallRow( onClick = { if (call.terminated.value) return@IconButton call.terminated.value = true - 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") - } + Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}") + call.hangup(487, "Request Terminated") }, ) { Icon( @@ -1564,18 +1554,8 @@ private fun CallRow( onClick = { if (call.terminated.value) return@IconButton call.terminated.value = true - 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") - } + Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}") + call.hangup(487, "Request Terminated") } ) { Icon( @@ -1623,8 +1603,7 @@ private fun CallRow( showAlert.value = true } else { - val connection = ConnectionService.connections[call.callp] - connection?.onHold() + call.hold() if (!call.executeTransfer()) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = ctx.getString(R.string.transfer_failed) @@ -2197,8 +2176,6 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, private fun answer(ctx: Context, call: Call) { Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}") - 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) @@ -2208,19 +2185,7 @@ 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}") - 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") - } + call.reject() } private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText: String, attended: Boolean) { @@ -2237,14 +2202,7 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText: val call = ua.currentCall() if (call != null) { if (attended) { - val connection = ConnectionService.connections[call.callp] - val success = if (connection != null) { - connection.onHold() - true - } else { - call.hold() - } - if (success) { + if (call.hold()) { call.onhold = true call.referTo = uri makeCall(ctx, viewModel, uri, viewModel.dialerState, call.callp) @@ -2252,8 +2210,7 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText: } } else { - val connection = ConnectionService.connections[call.callp] - connection?.onHold() + call.hold() if (!call.transfer(uri)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = ctx.getString(R.string.transfer_failed) @@ -2353,7 +2310,7 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal call.callUriLabel.value = ctx.getString(R.string.incoming_call_from_dots) call.callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account) } - call.transferButtonEnabled.value = true + call.transferButtonEnabled.value = !ua.account.isMobile } call.callUri2.value = "" call.callTransfer.value = call.onHoldCall != null @@ -2569,7 +2526,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params val callp = params[1] as Long val call = Call.ofCallp(callp) if (call in Call.calls()) - Api.ua_hangup(uap, callp, 487, "Request Terminated") + call!!.hangup(487, "Request Terminated") makeCall(ctx, viewModel, ev[1], viewModel.dialerState) showCall(ctx, viewModel, ua) } From 0f6aaf05632040e944da729669e1c1dd0010fc22 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 10 May 2026 14:20:01 +0300 Subject: [PATCH 05/25] Add blocked mobile call to history --- .../com/tutpro/baresip/BaresipService.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index f1bf7868..a445b24d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -1765,6 +1765,24 @@ class BaresipService: Service() { else -> "connected" } + if (initialStatus == "incoming") { + if (ua.account.blockUnknown && Contact.contactName(uri) == uri) { + Log.d(TAG, "Auto-rejecting incoming PSTN call from $uri") + telecomCall.disconnect() + toast(String.format(getString(R.string.call_blocked), + Utils.friendlyUri(this, uri, ua.account))) + if (ua.account.callHistory) { + Blocked( + ua.account.aor, + uri, + "invite", + GregorianCalendar().timeInMillis + ).add() + } + return + } + } + val call = Call.ExternalCall( telecomCall, ua, From 5f696118689c7086e8cd4bf1e9594005dbf3f226 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 10 May 2026 15:39:25 +0300 Subject: [PATCH 06/25] InCallService improvements --- .../main/kotlin/com/tutpro/baresip/InCallService.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt index 8fdaf96f..19199d0f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt @@ -1,10 +1,17 @@ package com.tutpro.baresip +import android.content.Intent +import android.os.IBinder import android.telecom.Call import android.telecom.InCallService class InCallService : InCallService() { + override fun onBind(intent: Intent): IBinder? { + instance = this + return super.onBind(intent) + } + override fun onCallAdded(call: Call) { super.onCallAdded(call) Log.d(TAG, "InCallService: Call added") @@ -29,7 +36,13 @@ class InCallService : InCallService() { BaresipService.instance?.handleExternalCallRemoved(call) } + override fun onUnbind(intent: Intent?): Boolean { + instance = null + return super.onUnbind(intent) + } + companion object { private const val TAG = "Baresip" + var instance: InCallService? = null } } From eb18baa08992156307fdefb18c07ee0e26bc80a5 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 10 May 2026 15:40:02 +0300 Subject: [PATCH 07/25] Added fun hasTelecomCall() to Call --- app/src/main/kotlin/com/tutpro/baresip/Call.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index 5a9f4638..637de941 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -247,6 +247,12 @@ open class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir return BaresipService.calls.isNotEmpty() } + fun hasTelecomCall(): Boolean { + return BaresipService.calls.any { + it is ExternalCall || ConnectionService.connections.containsKey(it.callp) + } || ConnectionService.pendingOutgoingConnection != null + } + fun isAnyCallActive(ctx: Context): Boolean { // Check if there exist SIP calls that are not onhold or held if (BaresipService.calls.any { !it.onhold && !it.held }) return true From 4588dabb41763dd6552169ba8693b18611466409 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 10 May 2026 16:12:23 +0300 Subject: [PATCH 08/25] Improved mainscreen call row layout --- .../kotlin/com/tutpro/baresip/MainScreen.kt | 570 +++++++++--------- 1 file changed, 285 insertions(+), 285 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 14490f91..07612197 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -1472,7 +1472,8 @@ private fun CallRow( Row( modifier = Modifier .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + horizontalArrangement = if (isDialer || call?.showCancelButton?.value == true || call?.showAnswerRejectButtons?.value == true) + Arrangement.Center else Arrangement.SpaceBetween ) { if (isDialer) { if (dialerState.showCallButton.value) @@ -1524,8 +1525,6 @@ private fun CallRow( } else { if (call!!.showCancelButton.value) { - if (!call.conferenceCall) - Spacer(modifier = Modifier.weight(1f)) IconButton( modifier = Modifier.size(48.dp), enabled = !call.terminated.value, @@ -1543,11 +1542,9 @@ private fun CallRow( contentDescription = null, ) } - Spacer(modifier = Modifier.width(12.dp)) } if (call.showHangupButton.value) { - IconButton( modifier = Modifier.size(48.dp), enabled = !call.terminated.value, @@ -1565,311 +1562,313 @@ private fun CallRow( contentDescription = null, ) } + } - if (!call.conferenceCall) - IconButton( modifier = Modifier.size(48.dp), - onClick = { - if (call.callOnHold.value) { - Log.d(TAG, "User requested resume for ${call.callp}") - call.resume() // This now automatically holds other calls - } else { - Log.d(TAG, "User requested hold for ${call.callp}") - call.hold() + if (call.showHangupButton.value && !call.conferenceCall) + IconButton( modifier = Modifier.size(48.dp), + onClick = { + if (call.callOnHold.value) { + Log.d(TAG, "User requested resume for ${call.callp}") + call.resume() // This now automatically holds other calls + } else { + Log.d(TAG, "User requested hold for ${call.callp}") + call.hold() + } + }, + ) { + Icon( + imageVector = Icons.Outlined.PauseCircle, + modifier = Modifier.size(42.dp), + tint = if (call.callOnHold.value) + MaterialTheme.colorScheme.error + else + MaterialTheme.colorScheme.secondary, + contentDescription = null, + ) + } + + var showTransferDialog by remember { mutableStateOf(false) } + + if (call.showHangupButton.value && !call.conferenceCall && !call.ua.account.isMobile) + IconButton( + modifier = Modifier.size(48.dp), + enabled = call.transferButtonEnabled.value, + onClick = { + if (call.onHoldCall != null) { + if (!Api.call_supported(call.callp, Api.REPLACES)) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = ctx.getString(R.string.replaces_not_supported) + showAlert.value = true } - }, - ) { - Icon( - imageVector = Icons.Outlined.PauseCircle, - modifier = Modifier.size(42.dp), - tint = if (call.callOnHold.value) - MaterialTheme.colorScheme.error - else - MaterialTheme.colorScheme.secondary, - contentDescription = null, - ) - } - - var showTransferDialog by remember { mutableStateOf(false) } - - if (!call.conferenceCall) - IconButton( - modifier = Modifier.size(48.dp), - enabled = call.transferButtonEnabled.value, - onClick = { - if (call.onHoldCall != null) { - if (!Api.call_supported(call.callp, Api.REPLACES)) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = ctx.getString(R.string.replaces_not_supported) + else { + call.hold() + if (!call.executeTransfer()) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = ctx.getString(R.string.transfer_failed) showAlert.value = true } - else { - call.hold() - if (!call.executeTransfer()) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = ctx.getString(R.string.transfer_failed) - showAlert.value = true + } + } + else + showTransferDialog = true + }, + ) { + Icon( + imageVector = Icons.Outlined.ArrowCircleRight, + modifier = Modifier.size(42.dp), + tint = if (call.callTransfer.value) + MaterialTheme.colorScheme.error + else + MaterialTheme.colorScheme.secondary, + contentDescription = null, + ) + } + + if (showTransferDialog) { + + val showDialog = remember { mutableStateOf(true) } + val blindChecked = remember { mutableStateOf(true) } + + if (showDialog.value) + BasicAlertDialog( + onDismissRequest = { + viewModel.requestHideKeyboard() + showDialog.value = false + showTransferDialog = false + } + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.call_transfer), + fontSize = 20.sp, + color = MaterialTheme.colorScheme.onSurface, + ) + var transferUri by remember { mutableStateOf("") } + val suggestions by remember { contactNames } + var filteredSuggestions by remember { mutableStateOf>(emptyList()) } + val focusRequester = remember { FocusRequester() } + val lazyListState = rememberLazyListState() + OutlinedTextField( + value = transferUri, + singleLine = true, + onValueChange = { + if (it != transferUri) { + transferUri = it + if (it.length > 1) { + val normalizedInput = Utils.unaccent(it) + filteredSuggestions = + suggestions.filter { suggestion -> + Utils.unaccent(suggestion) + .contains(normalizedInput, ignoreCase = true) + } + .map { suggestion -> + Utils.buildAnnotatedStringWithHighlight(suggestion, it) + } + } + call.showSuggestions.value = transferUri.length > 1 + } + }, + trailingIcon = { + if (transferUri.isNotEmpty()) + Icon( + Icons.Outlined.Clear, + contentDescription = null, + modifier = Modifier.clickable { + if (call.showSuggestions.value) + call.showSuggestions.value = false + else + transferUri = "" + }, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 4.dp, + end = 4.dp, + top = 12.dp, + bottom = 2.dp + ) + .focusRequester(focusRequester), + label = { Text(stringResource(R.string.transfer_destination)) }, + textStyle = TextStyle(fontSize = 18.sp), + keyboardOptions = if (isDialpadVisible) + KeyboardOptions(keyboardType = KeyboardType.Phone) + else + KeyboardOptions(keyboardType = KeyboardType.Text) + ) + Spacer(modifier = Modifier.height(8.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .shadow(8.dp, RoundedCornerShape(8.dp)) + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp) + ) + .animateContentSize() + ) { + if (call.showSuggestions.value && filteredSuggestions.isNotEmpty()) { + Box(modifier = Modifier + .fillMaxWidth() + .heightIn(max = 150.dp)) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .verticalScrollbar( + state = lazyListState, + width = 6.dp, + ), + horizontalAlignment = Alignment.Start, + state = lazyListState, + ) { + items( + items = filteredSuggestions, + key = { suggestion -> suggestion.toString() } + ) { suggestion -> + Box( + modifier = Modifier + .fillMaxWidth() + .clickable { + transferUri = + suggestion.toString() + call.showSuggestions.value = + false + } + .padding(12.dp) + ) { + Text( + text = suggestion, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 18.sp + ) + } + } + } + } } } - } - else - showTransferDialog = true - }, - ) { - Icon( - imageVector = Icons.Outlined.ArrowCircleRight, - modifier = Modifier.size(42.dp), - tint = if (call.callTransfer.value) - MaterialTheme.colorScheme.error - else - MaterialTheme.colorScheme.secondary, - contentDescription = null, - ) - } - - if (showTransferDialog) { - - val showDialog = remember { mutableStateOf(true) } - val blindChecked = remember { mutableStateOf(true) } - - if (showDialog.value) - BasicAlertDialog( - onDismissRequest = { - viewModel.requestHideKeyboard() - showDialog.value = false - showTransferDialog = false - } - ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = stringResource(R.string.call_transfer), - fontSize = 20.sp, - color = MaterialTheme.colorScheme.onSurface, - ) - var transferUri by remember { mutableStateOf("") } - val suggestions by remember { contactNames } - var filteredSuggestions by remember { mutableStateOf>(emptyList()) } - val focusRequester = remember { FocusRequester() } - val lazyListState = rememberLazyListState() - OutlinedTextField( - value = transferUri, - singleLine = true, - onValueChange = { - if (it != transferUri) { - transferUri = it - if (it.length > 1) { - val normalizedInput = Utils.unaccent(it) - filteredSuggestions = - suggestions.filter { suggestion -> - Utils.unaccent(suggestion) - .contains(normalizedInput, ignoreCase = true) - } - .map { suggestion -> - Utils.buildAnnotatedStringWithHighlight(suggestion, it) - } - } - call.showSuggestions.value = transferUri.length > 1 - } - }, - trailingIcon = { - if (transferUri.isNotEmpty()) - Icon( - Icons.Outlined.Clear, - contentDescription = null, - modifier = Modifier.clickable { - if (call.showSuggestions.value) - call.showSuggestions.value = false - else - transferUri = "" - }, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - modifier = Modifier - .fillMaxWidth() - .padding( - start = 4.dp, - end = 4.dp, - top = 12.dp, - bottom = 2.dp - ) - .focusRequester(focusRequester), - label = { Text(stringResource(R.string.transfer_destination)) }, - textStyle = TextStyle(fontSize = 18.sp), - keyboardOptions = if (isDialpadVisible) - KeyboardOptions(keyboardType = KeyboardType.Phone) - else - KeyboardOptions(keyboardType = KeyboardType.Text) - ) - Spacer(modifier = Modifier.height(8.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .shadow(8.dp, RoundedCornerShape(8.dp)) - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp) - ) - .animateContentSize() - ) { - if (call.showSuggestions.value && filteredSuggestions.isNotEmpty()) { - Box(modifier = Modifier - .fillMaxWidth() - .heightIn(max = 150.dp)) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .verticalScrollbar( - state = lazyListState, - width = 6.dp, - ), - horizontalAlignment = Alignment.Start, - state = lazyListState, - ) { - items( - items = filteredSuggestions, - key = { suggestion -> suggestion.toString() } - ) { suggestion -> - Box( - modifier = Modifier - .fillMaxWidth() - .clickable { - transferUri = - suggestion.toString() - call.showSuggestions.value = - false - } - .padding(12.dp) - ) { - Text( - text = suggestion, - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 18.sp - ) - } - } - } - } - } - } - if (call.replaces()) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.blind), - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp), - ) - Switch( - checked = blindChecked.value, - onCheckedChange = { - blindChecked.value = true - } - ) - } - Spacer(modifier = Modifier.width(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.attended), - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp), - ) - Switch( - checked = !blindChecked.value, - onCheckedChange = { - blindChecked.value = false - } - ) - } - } + if (call.replaces()) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + horizontalArrangement = Arrangement.Start, ) { - TextButton( - onClick = { - viewModel.requestHideKeyboard() - showDialog.value = false - showTransferDialog = false - }, - modifier = Modifier.padding(end = 32.dp), - ) { + Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = stringResource(R.string.cancel), - color = MaterialTheme.colorScheme.onSurfaceVariant + text = stringResource(R.string.blind), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(8.dp), + ) + Switch( + checked = blindChecked.value, + onCheckedChange = { + blindChecked.value = true + } ) } - TextButton( - onClick = { - call.showSuggestions.value = false - var uriText = transferUri.trim() - if (uriText.isNotEmpty()) { - val uris = Contact.contactUris(uriText) - if (uris.size > 1) { - selectItems.value = uris - selectItemAction.value = { index -> - val uri = uris[index] - transfer( - ctx, - viewModel, - call.ua, - if (Utils.isTelNumber(uri)) "tel:$uri" else uri, - !blindChecked.value - ) - showSelectItemDialog.value = false - } - showSelectItemDialog.value = true - } - else { - if (uris.size == 1) uriText = uris[0] + Spacer(modifier = Modifier.width(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.attended), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(8.dp), + ) + Switch( + checked = !blindChecked.value, + onCheckedChange = { + blindChecked.value = false + } + ) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = { + viewModel.requestHideKeyboard() + showDialog.value = false + showTransferDialog = false + }, + modifier = Modifier.padding(end = 32.dp), + ) { + Text( + text = stringResource(R.string.cancel), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + TextButton( + onClick = { + call.showSuggestions.value = false + var uriText = transferUri.trim() + if (uriText.isNotEmpty()) { + val uris = Contact.contactUris(uriText) + if (uris.size > 1) { + selectItems.value = uris + selectItemAction.value = { index -> + val uri = uris[index] transfer( ctx, viewModel, call.ua, - if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText, + if (Utils.isTelNumber(uri)) "tel:$uri" else uri, !blindChecked.value ) + showSelectItemDialog.value = false } - viewModel.requestHideKeyboard() - showDialog.value = false - showTransferDialog = false + showSelectItemDialog.value = true } - }, - modifier = Modifier.padding(end = 16.dp), - ) { - Text( - text = stringResource( - if (blindChecked.value) - R.string.transfer - else - R.string.call - ).uppercase(), - color = MaterialTheme.colorScheme.primary - ) - } + else { + if (uris.size == 1) uriText = uris[0] + transfer( + ctx, + viewModel, + call.ua, + if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText, + !blindChecked.value + ) + } + viewModel.requestHideKeyboard() + showDialog.value = false + showTransferDialog = false + } + }, + modifier = Modifier.padding(end = 16.dp), + ) { + Text( + text = stringResource( + if (blindChecked.value) + R.string.transfer + else + R.string.call + ).uppercase(), + color = MaterialTheme.colorScheme.primary + ) } } } } + } - } + } - val focusRequester = remember { FocusRequester() } - val shouldRequestFocus by call.focusDtmf - val interactionSource = remember { MutableInteractionSource() } + val focusRequester = remember { FocusRequester() } + val shouldRequestFocus by call.focusDtmf + val interactionSource = remember { MutableInteractionSource() } + if (call.showHangupButton.value) BasicTextField( value = call.dtmfText.value, onValueChange = { newText -> @@ -1924,6 +1923,7 @@ private fun CallRow( ) } ) + if (call.showHangupButton.value) LaunchedEffect(shouldRequestFocus) { if (shouldRequestFocus) { focusRequester.requestFocus() @@ -1931,6 +1931,7 @@ private fun CallRow( } } + if (call.showHangupButton.value && !call.ua.account.isMobile) IconButton( modifier = Modifier.size(48.dp), onClick = { @@ -1970,7 +1971,6 @@ private fun CallRow( contentDescription = null, ) } - } if (call.showAnswerRejectButtons.value) { From efc3e0deddf4747980eba71612a62a9eca193f8c Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sun, 10 May 2026 17:58:37 +0300 Subject: [PATCH 09/25] Improved finding of tel URI's contact --- app/src/main/kotlin/com/tutpro/baresip/Contact.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Contact.kt b/app/src/main/kotlin/com/tutpro/baresip/Contact.kt index 2333f174..7d1b03e5 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Contact.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Contact.kt @@ -76,9 +76,10 @@ sealed class Contact { fun contactName(uri: String): String { var contact = findContact(uri) if (contact == null) { - val userPart = Utils.uriUserPart(uri) - if (Utils.isTelNumber(userPart)) + val userPart = Utils.uriUserPart(uri).replace("%23", "#") + if (Utils.isTelNumber(userPart)) { contact = findContact("tel:$userPart") + } } if (contact != null) return contact.name() From f9cab09b3d358410a51a268acb234cd3792505c1 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 08:40:39 +0300 Subject: [PATCH 10/25] Avoid lint warning in incallservice --- .../main/kotlin/com/tutpro/baresip/InCallService.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt index 19199d0f..40970583 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.IBinder import android.telecom.Call import android.telecom.InCallService +import java.lang.ref.WeakReference class InCallService : InCallService() { @@ -21,8 +22,7 @@ class InCallService : InCallService() { if (handle == baresipHandle) { Log.d(TAG, "InCallService: Identified as SIP call") - // The SIP call is already managed by ConnectionService/BaresipService. - // We just need to ensure the InCallService stays bound. + // SIP call is already managed by ConnectionService/BaresipService } else { Log.d(TAG, "InCallService: Identified as PSTN call from $handle") val aor = call.details.intentExtras?.getString("aor") @@ -43,6 +43,11 @@ class InCallService : InCallService() { companion object { private const val TAG = "Baresip" - var instance: InCallService? = null + private var _instance = WeakReference(null) + var instance: InCallService? + get() = _instance.get() + set(value) { + _instance = WeakReference(value) + } } } From 3d16b467f12516f02ed3e8d86e58eec51590ab3f Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 08:41:24 +0300 Subject: [PATCH 11/25] Removed unused drawable --- app/src/main/res/drawable/call_tel.xml | 32 -------------------------- 1 file changed, 32 deletions(-) delete mode 100644 app/src/main/res/drawable/call_tel.xml diff --git a/app/src/main/res/drawable/call_tel.xml b/app/src/main/res/drawable/call_tel.xml deleted file mode 100644 index 2a6314c7..00000000 --- a/app/src/main/res/drawable/call_tel.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - From c74d780619316fc6876d64b299ae3b58a9a59f15 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 09:01:22 +0300 Subject: [PATCH 12/25] Use Call.hasTelecomCall() to check is telecom call exists Apply default call volume to all types of calls --- .../com/tutpro/baresip/BaresipService.kt | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index a445b24d..cab10f92 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -871,12 +871,8 @@ class BaresipService: Service() { break speakerPhone = speakerPhoneAuto stopMediaPlayer() - val hasTelecom = ConnectionService.connections.containsKey(callp) || - ConnectionService.pendingOutgoingConnection != null - if (!hasTelecom) { - setCallVolume() - ensureCommunicationMode() - } + setCallVolume() + ensureCommunicationMode() proximitySensing(proximitySensing) } "call ringing" -> { @@ -1467,9 +1463,7 @@ class BaresipService: Service() { }, audioDelay) } - val isTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } || - ConnectionService.pendingOutgoingConnection != null - if (isTelecom) + if (Call.hasTelecomCall()) executeCall() else if (VERSION.SDK_INT < 31) { Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION") @@ -1823,6 +1817,8 @@ class BaresipService: Service() { }) calls.add(call) + setCallVolume() + ensureCommunicationMode() postServiceEvent(ServiceEvent( "call incoming", arrayListOf(ua.uap, call.callp), @@ -1854,12 +1850,10 @@ class BaresipService: Service() { private fun addMobileUserAgent() { if (!telecom || Utils.pstnAccountHandle(this) == null) return - val mobileAor = Utils.getLine1Number(this)?.let { "tel:$it" } ?: "tel:mobile" - val existingMobileUa = uas.value.find { it.account.isMobile } if (existingMobileUa != null) { - // If we previously had tel:mobile but now have a real number, replace it + // Replace previous tel:mobile with real number if (existingMobileUa.account.aor == "tel:mobile" && mobileAor != "tel:mobile") { val updatedUas = uas.value.toMutableList() updatedUas.remove(existingMobileUa) @@ -2181,8 +2175,7 @@ class BaresipService: Service() { cleanupRunnable = null } if (isSpeakerphoneOn == speakerPhone) { - val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } - if (hasTelecom || currentMode == MODE_IN_COMMUNICATION) { + if (Call.hasTelecomCall() || currentMode == MODE_IN_COMMUNICATION) { Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.") return } @@ -2209,13 +2202,17 @@ class BaresipService: Service() { val runnable = Runnable { cleanupRunnable = null if (Call.inCall()) { - val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } + val hasTelecom = Call.hasTelecomCall() 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 (!hasTelecom) { + if (InCallService.instance != null) { + Log.d(TAG, "Using InCallService for audio route: $speakerPhone") + @Suppress("DEPRECATION") + InCallService.instance!!.setAudioRoute(if (speakerPhone) android.telecom.CallAudioState.ROUTE_SPEAKER else android.telecom.CallAudioState.ROUTE_EARPIECE) + } else if (!hasTelecom) { Log.d(TAG, "No Telecom connection, using AudioManager for speaker") Utils.setSpeakerPhone(mainExecutor, am, speakerPhone) } else { @@ -2250,7 +2247,7 @@ class BaresipService: Service() { ) ) } - if (!Call.calls().any { ConnectionService.connections.containsKey(it.callp) }) + if (!Call.hasTelecomCall()) resetCallVolume() proximitySensing(false) } From dfd33ad79170ba59378805486e2629b825f61121 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 12:16:33 +0300 Subject: [PATCH 13/25] Mobile account improvements --- .../main/kotlin/com/tutpro/baresip/Account.kt | 58 +++++++++++-------- .../com/tutpro/baresip/AccountScreen.kt | 57 ++++++++++-------- .../com/tutpro/baresip/BaresipService.kt | 25 +++++--- 3 files changed, 82 insertions(+), 58 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 4ad306e4..84de4bb9 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -44,7 +44,6 @@ class Account(val accp: Long, virtualAor: String? = null) { var customParams = "" init { - if (accp != 0L) { if (authPass == "") authPass = NO_AUTH_PASS @@ -70,35 +69,40 @@ class Account(val accp: Long, virtualAor: String? = null) { break } } - - val extra = Api.account_extra(accp) - if (Utils.paramExists(extra, "nickname")) - nickName = Utils.paramValue(extra, "nickname") - if (Utils.paramExists(extra, "regint")) - configuredRegInt = Utils.paramValue(extra, "regint").toInt() - callHistory = Utils.paramValue(extra, "call_history") == "" - blockUnknown = Utils.paramExists(extra, "block_unknown") - if (Utils.paramExists(extra, "country_code")) - countryCode = Utils.paramValue(extra, "country_code") - if (Utils.paramExists(extra, "tel_provider")) - telProvider = URLDecoder.decode(Utils.paramValue(extra, "tel_provider"), "UTF-8") - numericKeypad = Utils.paramExists(extra, "numeric_keypad") - customParams = extra.substringAfter("last=empty").substringAfter(";") } + + val extra = Api.account_extra(accp) + if (Utils.paramExists(extra, "nickname")) + nickName = Utils.paramValue(extra, "nickname") + isMobile = Utils.paramExists(extra, "is_mobile") + if (Utils.paramExists(extra, "regint")) + configuredRegInt = Utils.paramValue(extra, "regint").toInt() + callHistory = Utils.paramValue(extra, "call_history") == "" + blockUnknown = Utils.paramExists(extra, "block_unknown") + if (Utils.paramExists(extra, "country_code")) + countryCode = Utils.paramValue(extra, "country_code") + if (Utils.paramExists(extra, "tel_provider")) + telProvider = URLDecoder.decode(Utils.paramValue(extra, "tel_provider"), "UTF-8") + numericKeypad = Utils.paramExists(extra, "numeric_keypad") + customParams = extra.substringAfter("last=empty").substringAfter(";") } fun print() : String { - if (isMobile) return "" + var res = if (isMobile) { + "<${aor};transport=udp>" + } else { + if (displayName != "") + "\"${displayName}\" " + else + "" + } - var res = if (displayName != "") - "\"${displayName}\" " - else - "" + if (!isMobile) { + res = "$res<$luri>" - res = "$res<$luri>" - - if (authUser != "") res += ";auth_user=\"${authUser}\"" + if (authUser != "") res += ";auth_user=\"${authUser}\"" + } if ((authPass != "") && !BaresipService.aorPasswords.containsKey(aor)) res += ";auth_pass=\"${authPass}\"" @@ -160,13 +164,19 @@ class Account(val accp: Long, virtualAor: String? = null) { if (autoRedirect) res += ";sip_autoredirect=yes" - res += ";ptime=20;regint=${regint};regq=0.5;pubint=0;inreq_allowed=yes;call_transfer=yes" + res += ";ptime=20;regint=${regint};regq=0.5;pubint=0;inreq_allowed=yes" + + if (isMobile) + res += ";call_transfer=no" var extra = "" if (nickName != "") extra += ";nickname=${nickName}" + if (isMobile) + extra += ";is_mobile=yes" + if (!callHistory) extra += ";call_history=no" diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt index cf3971fe..86ad2a97 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt @@ -233,7 +233,7 @@ private fun AccountContent( horizontalArrangement = Arrangement.Start ) { val aorText = if (ua.account.isMobile) { - if (ua.account.aor == "tel:mobile") + if (ua.account.aor == "sip:mobile@pstn") stringResource(R.string.not_available) else ua.account.aor @@ -1258,37 +1258,44 @@ private fun AccountContent( ) { AoR() Nickname() - DisplayName() - AuthUser() - AuthPass() - if (showPasswordDialog.value) - AskPassword(ctx, navController, ua) - Outbound() - Register() - if (viewModel.register.collectAsState().value) { - RegInt() - CheckOrigin() + if (!ua.account.isMobile) { + DisplayName() + AuthUser() + AuthPass() + if (showPasswordDialog.value) + AskPassword(ctx, navController, ua) + Outbound() + Register() + if (viewModel.register.collectAsState().value) { + RegInt() + CheckOrigin() + } } BlockUnknown() - AudioCodecs(navController, aor) - MediaEnc() - MediaNat() - if (showStun) { - StunServer() - StunUser() - StunPass() + if (!ua.account.isMobile) { + AudioCodecs(navController, aor) + MediaEnc() + MediaNat() + if (showStun) { + StunServer() + StunUser() + StunPass() + } + RtcpMux() + Rel100() + Dtmf() + Redirect() } - RtcpMux() - Rel100() - Dtmf() Answer() - Redirect() Voicemail() - CountryCode() - TelProvider() + if (!ua.account.isMobile) { + CountryCode() + TelProvider() + } NumericKeypad() DefaultAccount() - CustomParams() + if (!ua.account.isMobile) + CustomParams() } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index cab10f92..299efba1 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -491,10 +491,9 @@ class BaresipService: Service() { activeNetwork = cm.activeNetwork Log.i(TAG, "Active network: $activeNetwork") - if (telecom) { + if (telecom) registerPhoneAccount() - addMobileUserAgent() - } else + else if (btAdapter != null) { Log.i(TAG, "Registering bluetooth receiver") val filter = IntentFilter() @@ -726,7 +725,9 @@ class BaresipService: Service() { if (ev[0] == "create") { val ua = UserAgent(uap) - ua.status = if (ua.account.regint == 0) + ua.status = if (ua.account.isMobile) + R.drawable.circle_green + else if (ua.account.regint == 0) R.drawable.circle_white else circleYellow.getValue(colorblind) @@ -1850,28 +1851,34 @@ class BaresipService: Service() { private fun addMobileUserAgent() { if (!telecom || Utils.pstnAccountHandle(this) == null) return - val mobileAor = Utils.getLine1Number(this)?.let { "tel:$it" } ?: "tel:mobile" + + val userPart = Utils.getLine1Number(this) ?: "mobile" + val mobileAor = "sip:$userPart@pstn" + val existingMobileUa = uas.value.find { it.account.isMobile } if (existingMobileUa != null) { - // Replace previous tel:mobile with real number - if (existingMobileUa.account.aor == "tel:mobile" && mobileAor != "tel:mobile") { + // Replace previous sip:mobile@pstn with real number if available + if (existingMobileUa.account.aor == "sip:mobile@pstn" && mobileAor != "sip:mobile@pstn") { val updatedUas = uas.value.toMutableList() updatedUas.remove(existingMobileUa) uas.value = updatedUas.toList() - } else { + } else return - } } val account = Account(0L, mobileAor) account.isMobile = true account.nickName = "Mobile" + account.regint = 0 + account.telProvider = "" val mobileUa = UserAgent(0L, account) val updatedUas = uas.value.toMutableList() updatedUas.add(mobileUa) uas.value = updatedUas.toList() uasStatus.value = UserAgent.statusMap() + + Account.saveAccounts() } private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { From 75683291f4ee95a9a7cdf6784103a5b30aee9250 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 12:46:30 +0300 Subject: [PATCH 14/25] Mobile account configuration enhancements --- app/src/main/kotlin/com/tutpro/baresip/Account.kt | 1 - .../main/kotlin/com/tutpro/baresip/AccountScreen.kt | 11 ++++++++--- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 84de4bb9..3180e44f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -100,7 +100,6 @@ class Account(val accp: Long, virtualAor: String? = null) { if (!isMobile) { res = "$res<$luri>" - if (authUser != "") res += ";auth_user=\"${authUser}\"" } diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt index 86ad2a97..e77cb5df 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt @@ -1285,8 +1285,8 @@ private fun AccountContent( Rel100() Dtmf() Redirect() + Answer() } - Answer() Voicemail() if (!ua.account.isMobile) { CountryCode() @@ -1616,8 +1616,13 @@ private fun checkOnClick(ctx: Context, viewModel: AccountViewModel, ua: UserAgen var newVmUri = viewModel.vmUri.value.trim() if (newVmUri != acc.vmUri) { if (newVmUri != "") { - if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri" - if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}" + if (acc.isMobile) { + if (!newVmUri.startsWith("tel:")) newVmUri = "tel:$newVmUri" + } + else { + if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri" + if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}" + } if (!Utils.checkUri(newVmUri)) { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec665ca1..ab7a16b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,7 +206,7 @@ Block Unknown Block calls and messages from peers that are not found in contacts. Voicemail URI - SIP URI for checking of voicemail messages. If left empty, voicemail + SIP or TEL URI for checking of voicemail messages. If left empty, voicemail messages (Message Waiting Indications) are not subscribed to. Country Code From 5ec745950dd0a1a2749c9760ed1845389bd3307d Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 16:10:11 +0300 Subject: [PATCH 15/25] Always use Android Telecom Framework Automatically add/remove Mobile account when Default Phone App setting is turned on/off --- .../main/kotlin/com/tutpro/baresip/Account.kt | 2 +- .../com/tutpro/baresip/AccountScreen.kt | 2 +- .../kotlin/com/tutpro/baresip/AudioScreen.kt | 43 ------ .../com/tutpro/baresip/BaresipService.kt | 132 +++++------------- .../main/kotlin/com/tutpro/baresip/Call.kt | 12 +- .../main/kotlin/com/tutpro/baresip/Config.kt | 7 - .../kotlin/com/tutpro/baresip/MainScreen.kt | 77 +++++----- .../com/tutpro/baresip/SettingsScreen.kt | 6 +- .../main/kotlin/com/tutpro/baresip/Utils.kt | 15 +- app/src/main/res/values/strings.xml | 6 +- 10 files changed, 90 insertions(+), 212 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 3180e44f..6930feee 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -285,7 +285,7 @@ class Account(val accp: Long, virtualAor: String? = null) { BaresipService.filesPath + "/accounts", accounts.toByteArray(Charsets.UTF_8) ) - Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'") + // Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'") } fun ofAor(aor: String): Account? { diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt index e77cb5df..e3b9905d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt @@ -1677,7 +1677,7 @@ private fun checkOnClick(ctx: Context, viewModel: AccountViewModel, ua: UserAgen if (viewModel.defaultAccount.value) ua.makeDefault() - Api.account_debug(acc.accp) + // Api.account_debug(acc.accp) Account.saveAccounts() diff --git a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt index fe7df0e8..3583cd3f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt @@ -164,8 +164,6 @@ 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 @@ -178,8 +176,6 @@ 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") @@ -206,7 +202,6 @@ private fun AudioContent(contentPadding: PaddingValues) { .verticalScroll(state = scrollState), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Telecom() Ringtone() ToneCountry() SpeakerPhone() @@ -219,37 +214,6 @@ 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 Ringtone() { val ringToneTitle = stringResource(R.string.ringtone) @@ -629,13 +593,6 @@ private fun checkOnClick(ctx: Context): Result { var restart = false - if (newTelecom != oldTelecom) { - Config.replaceVariable("telecom", if (newTelecom) "yes" else "no") - BaresipService.telecom = newTelecom - restart = true - save = true - } - if (Preferences(ctx).ringtoneUri != newRingtoneUri) { Preferences(ctx).ringtoneUri = newRingtoneUri BaresipService.rt = RingtoneManager.getRingtone(ctx, newRingtoneUri.toUri()) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 299efba1..a12c6614 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -1,6 +1,5 @@ package com.tutpro.baresip -import android.Manifest import android.Manifest.permission.RECORD_AUDIO import android.annotation.SuppressLint import android.app.Notification @@ -61,7 +60,6 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.Person @@ -491,18 +489,7 @@ class BaresipService: Service() { activeNetwork = cm.activeNetwork Log.i(TAG, "Active network: $activeNetwork") - if (telecom) - registerPhoneAccount() - else - if (btAdapter != null) { - Log.i(TAG, "Registering bluetooth receiver") - val filter = IntentFilter() - filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) - filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED) - filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED) - this.registerReceiver(bluetoothReceiver, filter) - bluetoothReceiverRegistered = true - } + registerPhoneAccount() Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable") @@ -551,8 +538,6 @@ class BaresipService: Service() { setCallVolume() proximitySensing(proximitySensing) call?.answer() - if (!telecom) - ensureCommunicationMode() } "Call Reject" -> { @@ -877,12 +862,10 @@ class BaresipService: Service() { proximitySensing(proximitySensing) } "call ringing" -> { - if (!telecom) ensureCommunicationMode() playRingBack() return } "call progress" -> { - if (!telecom) ensureCommunicationMode() if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() else { @@ -926,7 +909,6 @@ class BaresipService: Service() { ) if (resourceId != 0) { ensureCommunicationMode() - if (!telecom) playUnInterrupted(resourceId, 1) } else { Log.e(TAG, "Callwaiting tone $name.wav not found") } @@ -946,23 +928,14 @@ class BaresipService: Service() { Log.d(TAG, "Incoming call $uap/$callp/$peerUri") if (Call.ofCallp(callp) == null) Call(callp, ua, peerUri, "in", "incoming").add() - 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)!!) + 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}") } return } @@ -1023,12 +996,6 @@ class BaresipService: Service() { if (call.state() == Api.CALL_STATE_EARLY) { if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() - else { - if (!telecom) { - ConnectionService.connections[callp]?.setRinging() - playRingBack() - } - } } if (call.status.value == "connected" && !call.held && !call.onhold) { if (call.callOnHold.value || call.showOnHoldNotice.value) { @@ -1160,13 +1127,8 @@ class BaresipService: Service() { val tone = ev[2] if (tone == "busy") playBusy() - else { - if (!telecom && !Call.inCall()) - abandonAudioFocus(applicationContext) + else ensureCommunicationMode() - } - if (!telecom && !Call.inCall()) - abandonAudioFocus(applicationContext) if (call.dir == "out") call.rejected = call.startTime == null && !reason.startsWith("408") && @@ -1500,7 +1462,7 @@ class BaresipService: Service() { fun started() { Log.d(TAG, "Received 'started' from baresip") isNativeReady = true - if (telecom) addMobileUserAgent() + addMobileUserAgent() Api.net_debug() postServiceEvent(ServiceEvent("started", arrayListOf(callActionUri), System.nanoTime())) callActionUri = "" @@ -1849,23 +1811,41 @@ class BaresipService: Service() { messageUpdate.postValue(System.currentTimeMillis()) } - private fun addMobileUserAgent() { - if (!telecom || Utils.pstnAccountHandle(this) == null) return + fun addMobileUserAgent() { + if (VERSION.SDK_INT < 29) return + + val mobileAccountHandle = Utils.pstnAccountHandle(this) + val existingMobileUa = uas.value.find { it.account.isMobile } + + // If mobile account should not exist (role lost or no SIM), remove it if it exists + if (mobileAccountHandle == null) { + if (existingMobileUa != null) { + Log.d(TAG, "Removing Mobile account (role lost or SIM missing)") + existingMobileUa.remove() + Account.saveAccounts() + } + return + } val userPart = Utils.getLine1Number(this) ?: "mobile" val mobileAor = "sip:$userPart@pstn" - val existingMobileUa = uas.value.find { it.account.isMobile } if (existingMobileUa != null) { - // Replace previous sip:mobile@pstn with real number if available + // Update AOR if it previously was sip:mobile@pstn but now a real number if (existingMobileUa.account.aor == "sip:mobile@pstn" && mobileAor != "sip:mobile@pstn") { - val updatedUas = uas.value.toMutableList() - updatedUas.remove(existingMobileUa) - uas.value = updatedUas.toList() - } else - return + Log.d(TAG, "Updating existing Mobile account AOR to $mobileAor") + val aorField = Account::class.java.getDeclaredField("aor") + aorField.isAccessible = true + aorField.set(existingMobileUa.account, mobileAor) + val luriField = Account::class.java.getDeclaredField("luri") + luriField.isAccessible = true + luriField.set(existingMobileUa.account, mobileAor) + Account.saveAccounts() + } + return } + Log.d(TAG, "Injecting new virtual Mobile account: $mobileAor") val account = Account(0L, mobileAor) account.isMobile = true account.nickName = "Mobile" @@ -2515,7 +2495,6 @@ class BaresipService: Service() { var isRecOn = false var toneCountry = "us" var proximitySensing = true - var telecom = true val uas = mutableStateOf(emptyList()) val uasStatus = mutableStateOf(emptyMap()) @@ -2609,8 +2588,6 @@ class BaresipService: Service() { ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED ) { Log.d(TAG, "requestAudioFocus granted") - if (!telecom && isBluetoothHeadsetConnected(ctx)) - startBluetoothSco(ctx, 250L, 3) } else { Log.w(TAG, "requestAudioFocus denied") @@ -2619,39 +2596,6 @@ class BaresipService: Service() { 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 - if (!telecom && isBluetoothHeadsetConnected(ctx)) - stopBluetoothSco(ctx) - } - else - Log.e(TAG, "Failed to abandon audio focus") - } - am.mode = MODE_NORMAL - } - - private fun isBluetoothHeadsetConnected(ctx: Context): Boolean { - if (VERSION.SDK_INT >= 31 && - ActivityCompat.checkSelfPermission( - ctx, - Manifest.permission.BLUETOOTH_CONNECT - ) == PackageManager.PERMISSION_DENIED - ) - return false - return btAdapter != null && btAdapter!!.isEnabled && - btAdapter!!.getProfileConnectionState(BluetoothHeadset.HEADSET) == - BluetoothAdapter.STATE_CONNECTED - } - private fun isBluetoothScoOn(am: AudioManager): Boolean { return if (VERSION.SDK_INT < 31) @Suppress("DEPRECATION") diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index 637de941..8b59122d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -151,15 +151,11 @@ open class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir } open fun hangup(code: Int, reason: String) { - if (BaresipService.telecom) { - val connection = ConnectionService.connections[callp] - if (connection != null) - connection.onDisconnect() - else - Api.ua_hangup(ua.uap, callp, code, reason) - } else { + val connection = ConnectionService.connections[callp] + if (connection != null) + connection.onDisconnect() + else Api.ua_hangup(ua.uap, callp, code, reason) - } } open fun answer() { diff --git a/app/src/main/kotlin/com/tutpro/baresip/Config.kt b/app/src/main/kotlin/com/tutpro/baresip/Config.kt index efbedab2..da95803a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Config.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Config.kt @@ -226,13 +226,6 @@ 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 07612197..dd2f5446 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -2116,60 +2116,49 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, else { viewModel.dialerState.callButtonsEnabled.value = false var error = "" - if (BaresipService.telecom) { - val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - if (ua.account.isMobile) { - val phoneAccountHandle = Utils.pstnAccountHandle(ctx) - if (phoneAccountHandle != null) { - val extras = Bundle().apply { - putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle) - } - val callExtras = Bundle() - callExtras.putBoolean("pstnCall", true) - callExtras.putString("aor", aor) - extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) - try { - Log.i(TAG, "Placing Telecom PSTN call to $uri with uap=${ua.uap}") - tm.placeCall(uri.toUri(), extras) - } catch (e: SecurityException) { - error = "placeCall failed: ${e.message}" - } + val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + if (ua.account.isMobile) { + val phoneAccountHandle = Utils.pstnAccountHandle(ctx) + if (phoneAccountHandle != null) { + val extras = Bundle().apply { + putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle) } - else - error = "no phone account" - } - else { - val extras = Bundle() - extras.putParcelable( - TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, - BaresipService.getPhoneAccountHandle(ctx) - ) val callExtras = Bundle() - callExtras.putBoolean("conferenceCall", dialerState.showCallConferenceButton.value) - callExtras.putLong("uap", ua.uap) - if (onHoldCallp != 0L) - callExtras.putLong("onHoldCallp", onHoldCallp) + callExtras.putBoolean("pstnCall", true) + callExtras.putString("aor", aor) extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) try { - Log.d(TAG, "Placing Telecom SIP call to $uri with uap=${ua.uap}") + Log.i(TAG, "Placing Telecom PSTN call to $uri with uap=${ua.uap}") tm.placeCall(uri.toUri(), extras) } catch (e: SecurityException) { error = "placeCall failed: ${e.message}" } } - if (error != "") { - Log.e(TAG, error) - viewModel.dialerState.callButtonsEnabled.value = true - } + else + error = "no phone account" } else { - val intent = Intent(ctx, BaresipService::class.java) - intent.action = "Start Call" - intent.putExtra("uap", ua.uap) - intent.putExtra("uri", uri) - intent.putExtra("conferenceCall", dialerState.showCallConferenceButton.value) - intent.putExtra("onHoldCallp", onHoldCallp) - ctx.startService(intent) + val extras = Bundle() + extras.putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + BaresipService.getPhoneAccountHandle(ctx) + ) + val callExtras = Bundle() + callExtras.putBoolean("conferenceCall", dialerState.showCallConferenceButton.value) + callExtras.putLong("uap", ua.uap) + if (onHoldCallp != 0L) + callExtras.putLong("onHoldCallp", onHoldCallp) + extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) + try { + Log.d(TAG, "Placing Telecom SIP call to $uri with uap=${ua.uap}") + tm.placeCall(uri.toUri(), extras) + } catch (e: SecurityException) { + error = "placeCall failed: ${e.message}" + } + } + if (error != "") { + Log.e(TAG, error) + viewModel.dialerState.callButtonsEnabled.value = true } } } @@ -2392,8 +2381,6 @@ 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/kotlin/com/tutpro/baresip/SettingsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt index 9b6b3cea..2434a3fa 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt @@ -80,6 +80,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.tutpro.baresip.BaresipService.Companion.uas import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import com.tutpro.baresip.Utils.copyInputStreamToFile @@ -1031,6 +1032,7 @@ private fun SettingsContent( ) { result -> Log.d(TAG, "dialerRoleRequest result: $result") viewModel.defaultDialer.value = roleManager.isRoleHeld(RoleManager.ROLE_DIALER) + BaresipService.instance?.addMobileUserAgent() } Switch( checked = defaultDialer, @@ -1181,14 +1183,14 @@ private fun SettingsContent( UserAgent() UniqueContactUri() AudioSettings(navController) + if (VERSION.SDK_INT >= 29) + DefaultDialer() BatteryOptimizations() DarkTheme() if (VERSION.SDK_INT >= 31) DynamicColors() ColorBlind() ProximitySensing() - if (VERSION.SDK_INT >= 29) - DefaultDialer() Debug() SipTrace() Reset(onRestartApp) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index cdceed46..3dd7de9b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -4,8 +4,10 @@ import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.app.KeyguardManager +import android.app.role.RoleManager import android.content.ContentResolver import android.content.Context +import android.content.Context.ROLE_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration @@ -1335,15 +1337,16 @@ object Utils { return file } + @RequiresApi(29) fun pstnAccountHandle(ctx: Context): PhoneAccountHandle? { + val roleManager = ctx.getSystemService(ROLE_SERVICE) as RoleManager if (ctx.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == - PackageManager.PERMISSION_GRANTED) { + PackageManager.PERMISSION_GRANTED && + roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) { val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - if (Build.VERSION.SDK_INT >= 29) { - val preferredHandle: PhoneAccountHandle? = tm.userSelectedOutgoingPhoneAccount - if (preferredHandle != null) - return preferredHandle - } + val preferredHandle: PhoneAccountHandle? = tm.userSelectedOutgoingPhoneAccount + if (preferredHandle != null) + return preferredHandle val baresipHandle = BaresipService.getPhoneAccountHandle(ctx) val phoneAccounts = tm.callCapablePhoneAccounts.filter { it != baresipHandle } return if (phoneAccounts.isNotEmpty()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ab7a16b0..4ff482af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -321,8 +321,7 @@ to standby state. Default Phone App Dialer role is not available - If checked, baresip is the default phone app. Do not check - if your device may need to handle also other than SIP calls or messages. + If checked, baresip is the default phone app. Listen Address IP address and port of form \'address:port\' at which baresip listens for incoming SIP requests. If IP address is an IPv6 address, it must be written inside @@ -408,9 +407,6 @@ Use colorblind friendly registration status icons Proximity Sensing If checked, proximity sensing is active during calls. - Telecom Framework - Use Android Telecom framework for calls. If you experience audio - related issues, try if it helps when you turn Telecom Framework off. Video Frame Size Size of transmitted video frames (width x height) Video Frames Per Second From eed03f378777e149fd767fab1578a672f6a5c3ad Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 16:15:24 +0300 Subject: [PATCH 16/25] Removed unused import --- app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt index 2434a3fa..5eff76db 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt @@ -80,7 +80,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.tutpro.baresip.BaresipService.Companion.uas import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import com.tutpro.baresip.Utils.copyInputStreamToFile From c70c04bc5aaba745afea4f2ca45bc64525829d4b Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 18:59:13 +0300 Subject: [PATCH 17/25] Update status notification when call is answered Turn proximity sensing off immediately when last call is closed --- app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index a12c6614..c17ca7de 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -538,6 +538,7 @@ class BaresipService: Service() { setCallVolume() proximitySensing(proximitySensing) call?.answer() + updateStatusNotification() } "Call Reject" -> { @@ -1070,6 +1071,8 @@ class BaresipService: Service() { if (call != null) { call.terminated.value = true call.remove() + if (!Call.inCall()) + proximitySensing(false) } ConnectionService.lastDisconnectTime = System.currentTimeMillis() val connection = ConnectionService.connections[callp] @@ -2236,7 +2239,6 @@ class BaresipService: Service() { } if (!Call.hasTelecomCall()) resetCallVolume() - proximitySensing(false) } } cleanupRunnable = runnable From 85cfd15d53e4f7731dcba4e3ed20962054fccf39 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 19:00:37 +0300 Subject: [PATCH 18/25] Translated using Weblate (Hebrew) --- app/src/main/res/values-iw/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 84bbad5f..3efea256 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -264,7 +264,7 @@ הפעל אוטומטית אם האפשרות מסומנת, baresip יופעל אוטומטית לאחר אתחול המכשיר או לאחר התקנת גרסה חדשה של baresip. ההפעלה תתבצע רק לאחר ביטול נעילת המכשיר. הפעלה אוטומטית דורשת הרשאת \"הצג מעל יישומים אחרים\". - השבת מיטובי סוללה (מומלץ) אם ברצונך להפחית את הסיכוי שאנדרואיד תגביל את הגישה של baresip לרשת או תעביר את baresip למצב המתנה. + השבת מיטובי סוללה אם ברצונך להפחית את הסיכוי שאנדרואיד תגביל את הגישה של baresip לרשת או תעביר את baresip למצב המתנה. יישומון הטלפון המוגדר כברירת מחדל תפקיד חייגן אינו זמין אם האפשרות מסומנת, baresip יוגדר כיישומון טלפון ברירת מחדל. אין לסמן אפשרות זו אם המכשיר שלך צריך לטפל גם בשיחות או הודעות שאינן מסוג SIP. @@ -439,4 +439,12 @@ אם האפשרות מופעלת, השמע יושמע דרך הרמקול של המכשיר. URI ייחודי של איש קשר אם מסומן, URI של איש הקשר מובטח להיות ייחודי. יש לסמן זאת אם קיימים יותר מחשבון אחד עם חלק המשתמש זהה ב־SIP URI, אך גם משפר את ההגנה על החשבונות מפני תקיפות. + פרמטרים מותאמים אישית + רשימה מופרדת בנקודה־פסיק של פרמטרי חשבון מותאמים אישית + מסגרת טלקום + השתמש במסגרת טלקום של אנדרואיד עבור שיחות. אם אתם חווים בעיות הקשורות לאודיו, נסו לבדוק אם כיבוי מסגרת הטלקום עוזר. + הועברה ע\"י + שיחה מצלצלת + שיחה מחוברת + הצד השני אינו תומך בתכונת REPLACES From 1c3d9a8829f65eaf4c3dc62a56560321a5d80ec6 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Mon, 11 May 2026 21:06:46 +0300 Subject: [PATCH 19/25] Version upgrade to match master --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6707f79c..91191f43 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ configure { applicationId = "com.tutpro.baresip" minSdk = 28 targetSdk = 36 - versionCode = 501 - versionName = "80.1.0" + versionCode = 502 + versionName = "80.1.1" @Suppress("UnstableApiUsage") externalNativeBuild { cmake { From ca1d991c9819366d50e400f9d0e28b5d2c3d5027 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Tue, 12 May 2026 07:48:18 +0300 Subject: [PATCH 20/25] Keep screen on when there is active call Avoid lint warning --- app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index dd2f5446..2a2188cf 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -20,6 +20,7 @@ import android.os.SystemClock import android.provider.DocumentsContract import android.provider.MediaStore import android.telecom.TelecomManager +import android.view.WindowManager import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -239,6 +240,12 @@ private fun MainScreen( BaresipService.isMainVisible = true viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone) viewModel.updateCalls(Call.calls().toList()) + + if (Call.inCall()) + (ctx as? Activity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + else + (ctx as? Activity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + (Call.call("incoming") ?: Call.calls().lastOrNull())?.let { spinToAor(viewModel, it.ua.account.aor) } ?: run { @@ -2117,7 +2124,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, viewModel.dialerState.callButtonsEnabled.value = false var error = "" val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - if (ua.account.isMobile) { + if (VERSION.SDK_INT >= 29 && ua.account.isMobile) { val phoneAccountHandle = Utils.pstnAccountHandle(ctx) if (phoneAccountHandle != null) { val extras = Bundle().apply { @@ -2423,6 +2430,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params showCall(ctx, viewModel, ua) } "call established" -> { + (ctx as? Activity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) if (aor == viewModel.selectedAor.value) { viewModel.dialerState.callButtonsEnabled.value = true // Re-enable dialer val callp = params[1] as Long @@ -2521,6 +2529,8 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params showCall(ctx, viewModel, ua) } "call closed" -> { + if (Call.calls().isEmpty()) + (ctx as? Activity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) viewModel.updateCalls(Call.calls().toList()) val activity = ctx as? Activity if (activity != null) { From 294d30055d233404e6ec29eb93c5710d61b85065 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Tue, 12 May 2026 08:33:44 +0300 Subject: [PATCH 21/25] Do not show chat button on mainscreen of mobile account Call voicemail directly when voicemail button it touched on mainscreen of mobile account Properly handle canceling of mobile call --- .../com/tutpro/baresip/AccountScreen.kt | 5 +- .../com/tutpro/baresip/BaresipService.kt | 2 +- .../kotlin/com/tutpro/baresip/MainScreen.kt | 59 +++++++++++-------- app/src/main/res/values/strings.xml | 3 +- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt index e3b9905d..b53c1a5f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt @@ -1067,7 +1067,10 @@ private fun AccountContent( @Composable fun Voicemail() { val voicemailUriTitle = stringResource(R.string.voicemail_uri) - val voicemailUriHelp = stringResource(R.string.voicemain_uri_help) + val voicemailUriHelp = if (ua.account.isMobile) + stringResource(R.string.voicemain_tel_uri_help) + else + stringResource(R.string.voicemain_uri_help) val vmUri by viewModel.vmUri.collectAsState() Row( Modifier.fillMaxWidth().padding(end = 10.dp), diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index c17ca7de..5aee597f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -1758,7 +1758,7 @@ class BaresipService: Service() { android.telecom.Call.STATE_RINGING -> "incoming" android.telecom.Call.STATE_DIALING, android.telecom.Call.STATE_CONNECTING -> "outgoing" android.telecom.Call.STATE_ACTIVE -> "connected" - android.telecom.Call.STATE_DISCONNECTED -> "closed" + android.telecom.Call.STATE_DISCONNECTED, android.telecom.Call.STATE_DISCONNECTING -> "closed" android.telecom.Call.STATE_HOLDING -> { calls.find { it.callp == call.hashCode().toLong() }?.onhold = true "connected" diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 2a2188cf..6e320541 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -740,6 +740,9 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont val hasNewVoicemail = remember(aor, accountUpdate) { if (aor.isNotEmpty()) (Account.ofAor(aor)?.vmNew ?: 0) > 0 else false } + val isMobile = remember(aor, accountUpdate) { + if (aor.isNotEmpty()) Account.ofAor(aor)?.isMobile ?: false else false + } val hasUnreadMessages = remember(aor, accountUpdate) { if (aor.isNotEmpty()) Account.ofAor(aor)?.unreadMessages ?: false else false } @@ -768,19 +771,26 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont val ua = UserAgent.ofAor(aor)!! val acc = ua.account if (acc.vmUri.isNotEmpty()) { - dialogTitle.value = ctx.getString(R.string.voicemail_messages) - dialogMessage.value = acc.vmMessages(ctx) - firstText.value = ctx.getString(R.string.cancel) - onFirstClicked.value = {} - secondText.value = "" - lastText.value = ctx.getString(R.string.listen) - onLastClicked.value = { + if (isMobile) { val intent = Intent(ctx, MainActivity::class.java) intent.putExtra("uap", ua.uap) intent.putExtra("peer", acc.vmUri) handleIntent(ctx, viewModel, intent, "call") + } else { + dialogTitle.value = ctx.getString(R.string.voicemail_messages) + dialogMessage.value = acc.vmMessages(ctx) + firstText.value = ctx.getString(R.string.cancel) + onFirstClicked.value = {} + secondText.value = "" + lastText.value = ctx.getString(R.string.listen) + onLastClicked.value = { + val intent = Intent(ctx, MainActivity::class.java) + intent.putExtra("uap", ua.uap) + intent.putExtra("peer", acc.vmUri) + handleIntent(ctx, viewModel, intent, "call") + } + showDialog.value = true } - showDialog.value = true } }, modifier = Modifier @@ -809,22 +819,23 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont ) } - IconButton( - enabled = aor.isNotEmpty(), - onClick = { - navController.navigate("chats/$aor") - }, - modifier = Modifier - .weight(1f) - .size(buttonSize) - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Chat, - contentDescription = null, - Modifier.size(buttonSize), - tint = if (hasUnreadMessages) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary - ) - } + if (!isMobile) + IconButton( + enabled = aor.isNotEmpty(), + onClick = { + navController.navigate("chats/$aor") + }, + modifier = Modifier + .weight(1f) + .size(buttonSize) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Chat, + contentDescription = null, + Modifier.size(buttonSize), + tint = if (hasUnreadMessages) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary + ) + } IconButton( enabled = aor.isNotEmpty(), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ff482af..fa54d400 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,9 +206,10 @@ Block Unknown Block calls and messages from peers that are not found in contacts. Voicemail URI - SIP or TEL URI for checking of voicemail messages. If left empty, voicemail + SIP URI for checking of voicemail messages. If left empty, voicemail messages (Message Waiting Indications) are not subscribed to. + TEL URI for checking of voicemail messages. Country Code E.164 country code of this account. If From URI userpart of incoming call or message contains a telephone number that does not start with \'+\' sign and if contact From 8fbb73a21c2c6c7a72c6127458a14d938995254f Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Tue, 12 May 2026 10:17:34 +0300 Subject: [PATCH 22/25] Fixed adding of mobile call to history Fixed outgoing mobile call event Do not show new call card when mobile call is placed on hold --- .../com/tutpro/baresip/AccountScreen.kt | 5 ++--- .../com/tutpro/baresip/BaresipService.kt | 20 ++++++++++++++----- .../kotlin/com/tutpro/baresip/MainScreen.kt | 8 +++++++- .../main/kotlin/com/tutpro/baresip/Utils.kt | 9 ++++++--- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt index b53c1a5f..df40a49d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt @@ -1291,10 +1291,9 @@ private fun AccountContent( Answer() } Voicemail() - if (!ua.account.isMobile) { - CountryCode() + CountryCode() + if (!ua.account.isMobile) TelProvider() - } NumericKeypad() DefaultAccount() if (!ua.account.isMobile) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 5aee597f..a968e6ab 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -68,6 +68,7 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData +import com.tutpro.baresip.Utils.e164Uri import com.tutpro.baresip.Utils.toCircle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -1703,8 +1704,12 @@ class BaresipService: Service() { } fun handleExternalCall(telecomCall: android.telecom.Call, preferredAor: String? = null) { - val uri = telecomCall.details.handle?.schemeSpecificPart ?: "Unknown" - Log.d(TAG, "Handling external call from $uri (preferredAor=$preferredAor)") + val rawUri = telecomCall.details.handle?.toString() ?: "Unknown" + val uri = try { + java.net.URLDecoder.decode(rawUri, "UTF-8") + } catch (_: Exception) { + rawUri + } if (uas.value.isEmpty()) { Log.e(TAG, "No User Agents available to handle external call") @@ -1719,13 +1724,17 @@ class BaresipService: Service() { telecomCall.details.state else @Suppress("DEPRECATION") telecomCall.state + + val isIncoming = telecomState == android.telecom.Call.STATE_RINGING + Log.d(TAG, "Handling external call ${if (isIncoming) "from" else "to"} $uri (preferredAor=$preferredAor)") + val initialStatus = when (telecomState) { android.telecom.Call.STATE_RINGING -> "incoming" android.telecom.Call.STATE_DIALING, android.telecom.Call.STATE_CONNECTING -> "outgoing" else -> "connected" } - if (initialStatus == "incoming") { + if (isIncoming) { if (ua.account.blockUnknown && Contact.contactName(uri) == uri) { Log.d(TAG, "Auto-rejecting incoming PSTN call from $uri") telecomCall.disconnect() @@ -1786,7 +1795,7 @@ class BaresipService: Service() { setCallVolume() ensureCommunicationMode() postServiceEvent(ServiceEvent( - "call incoming", + if (isIncoming) "call incoming" else "call outgoing", arrayListOf(ua.uap, call.callp), System.nanoTime()) ) @@ -1797,7 +1806,8 @@ class BaresipService: Service() { val call = calls.find { it.callp == callp } if (call != null) { if (call.ua.account.callHistory) { - val history = CallHistoryNew(call.ua.account.aor, call.peerUri, call.dir) + val historyPeerUri = e164Uri(call.peerUri, call.ua.account.countryCode) + val history = CallHistoryNew(call.ua.account.aor, historyPeerUri, call.dir) history.stopTime = GregorianCalendar() history.startTime = call.startTime history.rejected = call.rejected diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 6e320541..d721b0ed 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -916,6 +916,7 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont val calls by viewModel.calls.collectAsState() val selectedAor by viewModel.selectedAor.collectAsState() + val ua = uas.value.find { it.account.aor == selectedAor } val aorCalls = calls.filter { it.ua.account.aor == selectedAor } val hasActiveCalls = aorCalls.any { !it.callOnHold.value } val conferenceCall = aorCalls.any { it.conferenceCall } @@ -1027,7 +1028,12 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont } } - if (!hasActiveCalls || conferenceCall) + val showEmptyCard = if (ua?.account?.isMobile == true) + aorCalls.isEmpty() + else + !hasActiveCalls || conferenceCall + + if (showEmptyCard) CallCard(ctx = ctx, viewModel = viewModel, call = null, dialerState = viewModel.dialerState) Indicator( diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 3dd7de9b..a6dbbd83 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -133,7 +133,10 @@ object Utils { return if (uri.contains("@")) uri.substringAfter(":").substringBefore("@") else - "" + if (isTelUri(uri)) + uri.substringAfter(":").substringBefore(";") + else + "" } fun uriMatch(firstUri: String, secondUri: String): Boolean { @@ -185,14 +188,14 @@ object Utils { return u } - private fun e164Uri(uri: String, countryCode: String): String { + fun e164Uri(uri: String, countryCode: String): String { if (countryCode == "") return uri val scheme = uri.take(4) val userPart = uriUserPart(uri) return if (userPart.isDigitsOnly()) { when { userPart.startsWith("00") -> uri.replace("$scheme$userPart", - scheme + userPart.substring(2)) + scheme + "+" + userPart.substring(2)) userPart.startsWith("0") -> uri.replace("${scheme}0", "$scheme$countryCode") else -> uri.replace(scheme, "$scheme$countryCode") From 3b7862959bee96d3592a20a11f83887e3d1e5081 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Tue, 12 May 2026 10:24:32 +0300 Subject: [PATCH 23/25] Improved country_code_help --- app/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa54d400..47ff8073 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -211,10 +211,10 @@ TEL URI for checking of voicemail messages. Country Code - E.164 country code of this account. If From URI userpart of - incoming call or message contains a telephone number that does not start with \'+\' sign and if contact + E.164 country code of this account. If peer URI userpart of + call or message contains a telephone number that does not start with \'+\' sign and if contact lookup fails, the number is prefixed with this country code and contact lookup is - tried again. If the telephone number starts with a single digit \'0\', digit \'0\' is removed + tried again. If the number starts with one or two \'0\' digits, they are removed before the number is prefixed. Invalid Country Code \'%1$s\' From 8cdac0bd77f3c70213ac0d4ba3755506c7839a89 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Tue, 12 May 2026 10:45:21 +0300 Subject: [PATCH 24/25] Impoved rejection of blocked mobile calls --- app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index a968e6ab..2bbe231b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -1735,7 +1735,8 @@ class BaresipService: Service() { } if (isIncoming) { - if (ua.account.blockUnknown && Contact.contactName(uri) == uri) { + val e164Uri = e164Uri(uri, ua.account.countryCode) + if (ua.account.blockUnknown && Contact.contactName(e164Uri) == e164Uri) { Log.d(TAG, "Auto-rejecting incoming PSTN call from $uri") telecomCall.disconnect() toast(String.format(getString(R.string.call_blocked), From 8f784e819bb8f4b4b5a9cefc2a35bbb6082d48da Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Tue, 12 May 2026 11:37:30 +0300 Subject: [PATCH 25/25] en and fi strings update --- app/src/main/res/values-fi/strings.xml | 19 +++++++++---------- app/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 49b41fb8..352f2c6c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -109,6 +109,7 @@ Tili + Ei saatavana Lempinimi (jos annettu) millä tämä tili identifioidaan baresip sovelluksessa. Lempinimi @@ -215,15 +216,16 @@ Valitsee toteutetaanko puhelun uudelleenohjauspyyntö automaattisesti vai kysytäänkö vahvistusta. Puhepostin URI - SIP URI, jota käytetään + SIP URI jota käytetään puhepostiviestien kuunteluun. Jos URI:a ei ole annettu, tietoa mahdollista puhepostiviesteistä (Message Waiting Indications) ei tilata. + TEL URI jota käytetään puhepostiviestien kuunteluun. Maakoodi - Tämän tilin E.164-maakoodi. Jos tulevan puhelun tai viestin - From URI:n käyttäjäosa sisältää puhelinnumeron, joka ei ala \'+\' merkillä, ja jos sitä + Tämän tilin E.164-maakoodi. Jos puhelun tai viestin toisen + osapuolen URI:n käyttäjäosa sisältää puhelinnumeron, joka ei ala \'+\' merkillä, ja jos sitä ei löydy yhteystiedoista, niin tämä maakoodi lisätään numeron eteen ja etsintä tehdään - uudelleen. Jos puhelinnumero alkaa yhdellä numerolla \'0\', niin numero \'0\' + uudelleen. Jos puhelinnumero alkaa yhdellä tai kahdella numerolla \'0\', niin ne poistetaan ennen maakoodin lisäämistä. Virheellinen maakoodi \'%1$s\' @@ -334,9 +336,9 @@ Ota akun käytön optimointi pois päältä, jos haluat vähentää todennäköisyyttä, että Android rajoittaa baresip-sovelluksen toimintaa ja pääsyä verkkoon. - Oletuspuhelinsovellus - Puhelinrooli ei ole saatavana - Jos merkity, baresip on oletuspuhelinsovellus. Älä merkitse, jos laitteesi täytyy hallita myös muita kuin SIP-puheluita tai -viestejä. + Oletus puhelusovellus + Oletus puhelusovellusrooli ei ole saatavana + Jos merkitty, baresip on oletus puhelusovellus. Kuunteluosoite IP-osoite ja portti muotoa \'osoite:portti\', missä baresip kuuntelee sisään tulevia @@ -435,9 +437,6 @@ Läheisyyden tunnistus Jos merkitty, läheisyyden tunnistus on aktiivinen puhelun aikana. - Telecom-kehys - Käytä Android Telecom-kehystä puheluihin. Jos sinulla on audioon - liittyviä ongelmia, kokeile auttaako, kun poistat Telecom-kehyksen käytöstä. 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 47ff8073..7758f0a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,6 +113,7 @@ Account + Not available Nickname (if any) used to identify this account within baresip app. Nickname @@ -321,7 +322,7 @@ to reduce likelihood that Android restricts baresip\'s access to network or enters baresip to standby state. Default Phone App - Dialer role is not available + Default phone app role is not available If checked, baresip is the default phone app. Listen Address IP address and port of form \'address:port\' at which baresip listens @@ -511,7 +512,6 @@ Call is ringing Call is on hold Call is connected - Not available Recording can be turned on or off only when call is not connected Call Transfer