Started experimenting with baresip as PSTN dialer
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user