Always use Android Telecom Framework

Automatically add/remove Mobile account when Default Phone App setting is turned on/off
This commit is contained in:
Juha Heinanen
2026-05-11 16:10:11 +03:00
parent 75683291f4
commit 5ec745950d
10 changed files with 90 additions and 212 deletions

View File

@ -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? {

View File

@ -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()

View File

@ -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())

View File

@ -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<UserAgent>())
val uasStatus = mutableStateOf(emptyMap<String, Int>())
@ -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")

View File

@ -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() {

View File

@ -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

View File

@ -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()
}

View File

@ -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)

View File

@ -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())

View File

@ -321,8 +321,7 @@
to standby state.</string>
<string name="default_phone_app">Default Phone App</string>
<string name="dialer_role_not_available">Dialer role is not available</string>
<string name="default_phone_app_help">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.</string>
<string name="default_phone_app_help">If checked, baresip is the default phone app.</string>
<string name="listen_address">Listen Address</string>
<string name="listen_address_help">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 @@
<string name="colorblind_help">Use colorblind friendly registration status icons</string>
<string name="proximity_sensing">Proximity Sensing</string>
<string name="proximity_sensing_help">If checked, proximity sensing is active during calls.</string>
<string name="telecom">Telecom Framework</string>
<string name="telecom_help">Use Android Telecom framework for calls. If you experience audio
related issues, try if it helps when you turn Telecom Framework off.</string>
<string name="video_size">Video Frame Size</string>
<string name="video_size_help">Size of transmitted video frames (width x height)</string>
<string name="video_fps">Video Frames Per Second</string>