From 0a04a3e1326ac85bbda55f343d59b5a84f94b874 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Fri, 8 May 2026 17:25:19 +0300 Subject: [PATCH] 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 @@ + + + + + + + + + + + + + + +