Started experimenting with baresip as PSTN dialer

This commit is contained in:
Juha Heinanen
2026-05-08 17:25:19 +03:00
parent 20d1312d0f
commit 0a04a3e132
8 changed files with 309 additions and 66 deletions
@@ -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) { private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Toast.makeText(this@BaresipService.applicationContext, message, length).show() Toast.makeText(this@BaresipService.applicationContext, message, length).show()
+57 -14
View File
@@ -8,7 +8,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.core.net.toUri import androidx.core.net.toUri
import java.util.* 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<String> = mutableStateOf(initialStatus) var status: MutableState<String> = mutableStateOf(initialStatus)
@@ -55,11 +55,11 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
BaresipService.calls.remove(this) BaresipService.calls.remove(this)
} }
fun connect(uri: String): Boolean { open fun connect(uri: String): Boolean {
return Api.call_connect(callp, uri) == 0 return Api.call_connect(callp, uri) == 0
} }
fun hold(): Boolean { open fun hold(): Boolean {
if (onhold) return true if (onhold) return true
if (Api.call_hold(callp, true)) { if (Api.call_hold(callp, true)) {
onhold = true onhold = true
@@ -71,7 +71,7 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
return false return false
} }
fun resume(): Boolean { open fun resume(): Boolean {
if (!onhold && !held) return true if (!onhold && !held) return true
// 1. Hold other calls first // 1. Hold other calls first
for (c in BaresipService.calls) { 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 return false
} }
fun transfer(uri: String): Boolean { open fun transfer(uri: String): Boolean {
if (!onhold) hold() if (!onhold) hold()
Log.d(TAG, "Transferring call $callp to $uri") Log.d(TAG, "Transferring call $callp to $uri")
return Api.call_transfer(callp, uri) == 0 return Api.call_transfer(callp, uri) == 0
} }
fun executeTransfer(): Boolean { open fun executeTransfer(): Boolean {
return if (onHoldCall != null) { return if (onHoldCall != null) {
if (Api.call_hold(callp, true)) if (Api.call_hold(callp, true))
Api.call_replace_transfer(onHoldCall!!.callp, callp) 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 false
} }
fun sendDigit(digit: Char): Int { open fun sendDigit(digit: Char): Int {
return Api.call_send_digit(callp, digit) 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) Api.call_notify_sipfrag(callp, code, reason)
} }
fun duration(): Int { open fun duration(): Int {
return Api.call_duration(callp) return Api.call_duration(callp)
} }
fun stats(stream: String): String { open fun stats(stream: String): String {
return Api.call_stats(callp, stream) return Api.call_stats(callp, stream)
} }
fun state(): Int { open fun state(): Int {
return Api.call_state(callp) return Api.call_state(callp)
} }
fun audioCodecs(): String { open fun audioCodecs(): String {
return Api.call_audio_codecs(callp) return Api.call_audio_codecs(callp)
} }
fun replaces(): Boolean { open fun replaces(): Boolean {
return Api.call_replaces(callp) 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 if (ua.account.mediaEnc != "") security = R.color.colorTrafficRed
} }
fun destroy() { open fun destroy() {
Api.call_destroy(callp) 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 { companion object {
fun calls(): ArrayList<Call> { fun calls(): ArrayList<Call> {
@@ -1,6 +1,7 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.telecom.CallAudioState import android.telecom.CallAudioState
import android.telecom.Connection import android.telecom.Connection
import android.telecom.ConnectionRequest import android.telecom.ConnectionRequest
@@ -8,7 +9,6 @@ import android.telecom.ConnectionService
import android.telecom.DisconnectCause import android.telecom.DisconnectCause
import android.telecom.PhoneAccountHandle import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager import android.telecom.TelecomManager
import android.net.Uri
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class ConnectionService : ConnectionService() { class ConnectionService : ConnectionService() {
@@ -64,7 +64,8 @@ class ConnectionService : ConnectionService() {
val connection = BaresipConnection(uap, callp) val connection = BaresipConnection(uap, callp)
connections[callp] = connection 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.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or
Connection.CAPABILITY_HOLD or Connection.CAPABILITY_HOLD or
Connection.CAPABILITY_MERGE_CONFERENCE or Connection.CAPABILITY_MERGE_CONFERENCE or
@@ -108,6 +109,9 @@ class ConnectionService : ConnectionService() {
val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?: val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?:
nestedExtras?.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 } val onHoldCallp = rootExtras?.getLong("onHoldCallp", 0L).takeIf { it != 0L }
?: nestedExtras?.getLong("onHoldCallp") ?: 0L ?: nestedExtras?.getLong("onHoldCallp") ?: 0L
@@ -123,20 +127,15 @@ class ConnectionService : ConnectionService() {
Connection.CAPABILITY_HOLD or Connection.CAPABILITY_HOLD or
Connection.CAPABILITY_MERGE_CONFERENCE or Connection.CAPABILITY_MERGE_CONFERENCE or
Connection.CAPABILITY_SWAP_CONFERENCE Connection.CAPABILITY_SWAP_CONFERENCE
connection.audioModeIsVoip = true
// Start the SIP connection logic if (!pstnCall) {
if (uap != 0L) { connection.audioModeIsVoip = true
val sipUri = if (destination.startsWith("sip:")) destination else "sip:$destination" val sipUri = if (destination.startsWith("sip:")) destination else "sip:$destination"
BaresipService.instance?.runCall(uap, sipUri, conferenceCall, onHoldCallp) 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() connection.setDialing()
return connection return connection
} }
@@ -1,24 +1,35 @@
package com.tutpro.baresip 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.Call
import android.telecom.InCallService
class InCallService : InCallService() { class InCallService : InCallService() {
override fun onCallAdded(call: Call) { override fun onCallAdded(call: Call) {
super.onCallAdded(call) super.onCallAdded(call)
// This is triggered when the system wants YOU to show the call UI Log.d(TAG, "InCallService: Call added")
Log.d("Baresip", "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"
}
}
@@ -12,12 +12,14 @@ import android.content.res.Configuration
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Process import android.os.Process
import android.os.SystemClock import android.os.SystemClock
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.telecom.TelecomManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -1473,12 +1475,14 @@ private fun CallRow(
horizontalArrangement = Arrangement.Absolute.SpaceBetween horizontalArrangement = Arrangement.Absolute.SpaceBetween
) { ) {
if (isDialer) { if (isDialer) {
dialerState.showCallPstnButton.value = Utils.pstnAccountHandle(ctx) != null
if (dialerState.showCallButton.value) if (dialerState.showCallButton.value)
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
enabled = dialerState.callButtonsEnabled.value, enabled = dialerState.callButtonsEnabled.value,
onClick = { onClick = {
if (!dialerState.callButtonsEnabled.value) return@IconButton if (!dialerState.callButtonsEnabled.value) return@IconButton
dialerState.showCallPstnButton.value = false
dialerState.showCallConferenceButton.value = false dialerState.showCallConferenceButton.value = false
dialerState.showSuggestions.value = false dialerState.showSuggestions.value = false
callClick(ctx, viewModel, dialerState) callClick(ctx, viewModel, dialerState)
@@ -1494,6 +1498,28 @@ private fun CallRow(
contentDescription = null, 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) { if (dialerState.showCallConferenceButton.value) {
Spacer(modifier = Modifier.weight(1f, true)) Spacer(modifier = Modifier.weight(1f, true))
IconButton( IconButton(
@@ -1502,6 +1528,7 @@ private fun CallRow(
onClick = { onClick = {
if (!dialerState.callButtonsEnabled.value) return@IconButton if (!dialerState.callButtonsEnabled.value) return@IconButton
dialerState.showCallButton.value = false dialerState.showCallButton.value = false
dialerState.showCallPstnButton.value = false
dialerState.showSuggestions.value = false dialerState.showSuggestions.value = false
callClick(ctx, viewModel, dialerState) callClick(ctx, viewModel, dialerState)
} }
@@ -2061,14 +2088,14 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
ctx, ctx,
viewModel, viewModel,
uriText, uriText,
dialerState.showCallConferenceButton.value dialerState
) )
else if (uris.size == 1) else if (uris.size == 1)
makeCall( makeCall(
ctx, ctx,
viewModel, viewModel,
uris[0], uris[0],
dialerState.showCallConferenceButton.value dialerState
) )
else { else {
selectItems.value = uris selectItems.value = uris
@@ -2077,7 +2104,7 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
ctx, ctx,
viewModel, viewModel,
uris[index], uris[index],
dialerState.showCallConferenceButton.value dialerState
) )
} }
showSelectItemDialog.value = true 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, private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String,
onHoldCallp: Long = 0L) { dialerState: ViewModel.DialerState, onHoldCallp: Long = 0L) {
val aor = viewModel.selectedAor.value val aor = viewModel.selectedAor.value
val ua = UserAgent.ofAor(aor)!! val ua = UserAgent.ofAor(aor)!!
val peerUri = if (Utils.isTelNumber(uriText)) val peerUri = if (Utils.isTelNumber(uriText))
@@ -2104,13 +2131,16 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer
else else
uriText uriText
val uri = if (Utils.isTelUri(peerUri)) { 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) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), aor) alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), aor)
showAlert.value = true showAlert.value = true
return return
} }
Utils.telToSip(peerUri, ua.account) else
Utils.telToSip(peerUri, ua.account)
} }
else else
Utils.uriComplete(peerUri, aor) 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) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
showAlert.value = true 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) && else if (Utils.isAudioMode(ctx,AudioManager.MODE_IN_CALL) &&
!Call.calls().any { it.ua.account.aor == ua.account.aor }) !Call.calls().any { it.ua.account.aor == ua.account.aor })
Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show() Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show()
else { else {
viewModel.dialerState.callButtonsEnabled.value = false viewModel.dialerState.callButtonsEnabled.value = false
var error = ""
if (BaresipService.telecom) { if (BaresipService.telecom) {
val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val extras = android.os.Bundle() if (dialerState.showCallPstnButton.value) {
extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, val phoneAccountHandle = Utils.pstnAccountHandle(ctx)
BaresipService.getPhoneAccountHandle(ctx)) if (phoneAccountHandle != null) {
val callExtras = android.os.Bundle() val extras = Bundle().apply {
callExtras.putBoolean("conferenceCall", conferenceCall) putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
callExtras.putLong("uap", ua.uap) }
if (onHoldCallp != 0L) val callExtras = Bundle()
callExtras.putLong("onHoldCallp", onHoldCallp) callExtras.putBoolean("pstnCall", dialerState.showCallPstnButton.value)
extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
try { try {
Log.d(TAG, "Placing Telecom 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) tm.placeCall(uri.toUri(), extras)
} catch (e: SecurityException) { } catch (e: SecurityException) {
Log.e(TAG, "placeCall failed: ${e.message}") 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 viewModel.dialerState.callButtonsEnabled.value = true
} }
} }
@@ -2148,7 +2212,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer
intent.action = "Start Call" intent.action = "Start Call"
intent.putExtra("uap", ua.uap) intent.putExtra("uap", ua.uap)
intent.putExtra("uri", uri) intent.putExtra("uri", uri)
intent.putExtra("conferenceCall", conferenceCall) intent.putExtra("conferenceCall", dialerState.showCallConferenceButton.value)
intent.putExtra("onHoldCallp", onHoldCallp) intent.putExtra("onHoldCallp", onHoldCallp)
ctx.startService(intent) ctx.startService(intent)
} }
@@ -2207,7 +2271,7 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText:
if (success) { if (success) {
call.onhold = true call.onhold = true
call.referTo = uri call.referTo = uri
makeCall(ctx, viewModel, uri, false, call.callp) makeCall(ctx, viewModel, uri, viewModel.dialerState, call.callp)
showCall(ctx, viewModel, ua, call) showCall(ctx, viewModel, ua, call)
} }
} }
@@ -2237,6 +2301,7 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
viewModel.dialerState.callUriEnabled.value = true viewModel.dialerState.callUriEnabled.value = true
}, 100) }, 100)
viewModel.dialerState.showCallButton.value = true viewModel.dialerState.showCallButton.value = true
viewModel.dialerState.showCallPstnButton.value = true
viewModel.dialerState.showCallConferenceButton.value = true viewModel.dialerState.showCallConferenceButton.value = true
viewModel.dialerState.callButtonsEnabled.value = true viewModel.dialerState.callButtonsEnabled.value = true
viewModel.dialerState.showSuggestions.value = false viewModel.dialerState.showSuggestions.value = false
@@ -2521,7 +2586,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
if (call in Call.calls()) if (call in Call.calls())
acceptTransfer(ctx, viewModel, ua, call!!, ev[1]) acceptTransfer(ctx, viewModel, ua, call!!, ev[1])
else else
makeCall(ctx, viewModel, ev[1], false) makeCall(ctx, viewModel, ev[1], viewModel.dialerState)
} }
showDialog.value = true showDialog.value = true
} }
@@ -2530,7 +2595,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
if (call in Call.calls()) if (call in Call.calls())
Api.ua_hangup(uap, callp, 487, "Request Terminated") 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) showCall(ctx, viewModel, ua)
} }
"transfer failed" -> { "transfer failed" -> {
@@ -1,5 +1,6 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
@@ -1323,6 +1324,26 @@ object Utils {
return file 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") @Suppress("unused")
fun listFilesInDirectory(directoryPath: String): List<File> { fun listFilesInDirectory(directoryPath: String): List<File> {
val directory = File(directoryPath) val directory = File(directoryPath)
@@ -44,6 +44,7 @@ class ViewModel: ViewModel() {
val callUriLabel: MutableState<String> = mutableStateOf(""), val callUriLabel: MutableState<String> = mutableStateOf(""),
val showSuggestions: MutableState<Boolean> = mutableStateOf(false), val showSuggestions: MutableState<Boolean> = mutableStateOf(false),
val showCallButton: MutableState<Boolean> = mutableStateOf(true), val showCallButton: MutableState<Boolean> = mutableStateOf(true),
val showCallPstnButton: MutableState<Boolean> = mutableStateOf(true),
val showCallConferenceButton: MutableState<Boolean> = mutableStateOf(true), val showCallConferenceButton: MutableState<Boolean> = mutableStateOf(true),
val callButtonsEnabled: MutableState<Boolean> = mutableStateOf(true), val callButtonsEnabled: MutableState<Boolean> = mutableStateOf(true),
val conferenceCall: MutableState<Boolean> = mutableStateOf(false), val conferenceCall: MutableState<Boolean> = mutableStateOf(false),
+32
View File
@@ -0,0 +1,32 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<!-- Phone handle -->
<path
android:fillColor="@android:color/white"
android:pathData="
M798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM241,360L307,294L290,200L201,200Q206,241 215,281Q224,321 241,360ZM599,718Q638,735 678.5,745Q719,755 760,758L760,670L666,651L599,718Z" />
<!-- Letter T -->
<path
android:fillColor="@android:color/white"
android:pathData="
M480,120L600,120L600,160L560,160L560,320L520,320L520,160L480,160Z" />
<!-- Letter E -->
<path
android:fillColor="@android:color/white"
android:pathData="
M620,120L740,120L740,160L660,160L660,200L730,200L730,240L660,240L660,280L740,280L740,320L620,320Z" />
<!-- Letter L -->
<path
android:fillColor="@android:color/white"
android:pathData="
M760,120L800,120L800,280L860,280L860,320L760,320Z" />
</vector>