Mobile account and calling related fixes and enhancements

Search and show also contact URIs in suggestions
This commit is contained in:
Juha Heinanen
2026-08-14 09:14:33 +03:00
parent a8edd0f234
commit 3af45d8efd
6 changed files with 171 additions and 113 deletions

View File

@ -892,7 +892,8 @@ class BaresipService: Service() {
Blocked.clear(ua.account.aor) Blocked.clear(ua.account.aor)
BlockRule.clear(ua.account.aor) BlockRule.clear(ua.account.aor)
Api.ua_destroy(uap) Api.ua_destroy(uap)
if (isNativeReady) Account.saveAccounts() ua.remove()
Account.saveAccounts()
return return
} }
@ -903,10 +904,11 @@ class BaresipService: Service() {
R.drawable.circle_white R.drawable.circle_white
else else
circleRed.getValue(colorblind) circleRed.getValue(colorblind)
} else {
R.drawable.circle_white
} }
} else if (ua.account.regint == 0) else
R.drawable.circle_white
}
else if (ua.account.regint == 0)
R.drawable.circle_white R.drawable.circle_white
else else
circleYellow.getValue(colorblind) circleYellow.getValue(colorblind)
@ -1076,7 +1078,7 @@ class BaresipService: Service() {
"incoming call" -> { "incoming call" -> {
if (speakerPhoneAuto) if (speakerPhoneAuto)
speakerPhone = true speakerPhone = true
val peerUri = ev[1] val peerUri = Utils.uriUnescape(ev[1])
var blockedCall = false var blockedCall = false
val toastMsg = if (Call.isAnyCallActive(this)) val toastMsg = if (Call.isAnyCallActive(this))
String.format( String.format(
@ -1143,7 +1145,7 @@ class BaresipService: Service() {
} }
"call incoming" -> { "call incoming" -> {
val peerUri = ev[1] val peerUri = Utils.uriUnescape(ev[1])
Log.d(TAG, "Incoming call $uap/$callp/$peerUri") Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
if (Call.ofCallp(callp) == null) if (Call.ofCallp(callp) == null)
Call(callp, ua, peerUri, "in", "incoming").add() Call(callp, ua, peerUri, "in", "incoming").add()
@ -1714,7 +1716,9 @@ class BaresipService: Service() {
private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null
fun runCall(uap: Long, uri: String, conferenceCall: Boolean, onHoldCallp: Long) { fun runCall(uap: Long, uriText: String, conferenceCall: Boolean, onHoldCallp: Long) {
val uri = Utils.uriUnescape(uriText)
val ua = UserAgent.ofUap(uap) val ua = UserAgent.ofUap(uap)
if (ua != null && ua.account.isMobile) { if (ua != null && ua.account.isMobile) {
@ -3047,13 +3051,27 @@ class BaresipService: Service() {
} }
private fun registerPhoneAccount() { private fun registerPhoneAccount() {
val phoneAccountHandle = getPhoneAccountHandle(this) val sipHandle = getPhoneAccountHandle(this, SIP_ACCOUNT_ID)
val phoneAccount = android.telecom.PhoneAccount.builder(phoneAccountHandle, getString(R.string.app_name)) val pstnHandle = getPhoneAccountHandle(this, PSTN_ACCOUNT_ID)
.setCapabilities(android.telecom.PhoneAccount.CAPABILITY_SELF_MANAGED)
val sipAccount = android.telecom.PhoneAccount.builder(sipHandle, getString(R.string.app_name))
.setCapabilities(android.telecom.PhoneAccount.CAPABILITY_SELF_MANAGED or
android.telecom.PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
.setIcon(android.graphics.drawable.Icon.createWithResource(this, R.mipmap.ic_launcher))
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_SIP) .addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_SIP)
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_TEL) .addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_TEL)
.build() .build()
tm.registerPhoneAccount(phoneAccount)
val pstnAccount = android.telecom.PhoneAccount.builder(pstnHandle, getString(R.string.app_name) + " Mobile")
.setCapabilities(android.telecom.PhoneAccount.CAPABILITY_CALL_PROVIDER or
android.telecom.PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
.setIcon(android.graphics.drawable.Icon.createWithResource(this, R.mipmap.ic_launcher))
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_SIP)
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_TEL)
.build()
tm.registerPhoneAccount(sipAccount)
tm.registerPhoneAccount(pstnAccount)
} }
fun isSimReady(): Boolean { fun isSimReady(): Boolean {
@ -3197,7 +3215,7 @@ class BaresipService: Service() {
var instance: BaresipService? = null var instance: BaresipService? = null
var isServiceRunning = false var isServiceRunning = false
var isNativeReady = false var isNativeReady = false
var mobileAccount = true var mobileAccount = false
var isStartReceived = false var isStartReceived = false
var isConfigInitialized = false var isConfigInitialized = false
var libraryLoaded = false var libraryLoaded = false
@ -3295,11 +3313,12 @@ class BaresipService: Service() {
) )
internal const val KEY_TEXT_REPLY = "key_text_reply_baresip" internal const val KEY_TEXT_REPLY = "key_text_reply_baresip"
private const val PHONE_ACCOUNT_ID = "baresip_phone_account" const val SIP_ACCOUNT_ID = "baresip_sip_account"
const val PSTN_ACCOUNT_ID = "baresip_phone_account"
fun getPhoneAccountHandle(ctx: Context): PhoneAccountHandle { fun getPhoneAccountHandle(ctx: Context, id: String = SIP_ACCOUNT_ID): PhoneAccountHandle {
val componentName = android.content.ComponentName(ctx, ConnectionService::class.java) val componentName = android.content.ComponentName(ctx, ConnectionService::class.java)
return PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID) return PhoneAccountHandle(componentName, id)
} }
fun setMicMute(mute: Boolean) { fun setMicMute(mute: Boolean) {

View File

@ -460,7 +460,8 @@ private fun NewChatPeer(navController: NavController, account: Account) {
String.format(noTelephonyProviderText, account.aor) String.format(noTelephonyProviderText, account.aor)
showAlert.value = true showAlert.value = true
"" ""
} else }
else
Utils.telToSip(peerUri, account) Utils.telToSip(peerUri, account)
} }
else else
@ -532,7 +533,7 @@ private fun NewChatPeer(navController: NavController, account: Account) {
) { ) {
items( items(
items = filteredSuggestions, items = filteredSuggestions,
key = { (contact, _, _) -> "${contact.id()}" } key = { (contact, _, matchingUri) -> "${contact.id()}:${matchingUri?.uri ?: ""}" }
) { (contact, annotatedName, matchingUri) -> ) { (contact, annotatedName, matchingUri) ->
Box( Box(
modifier = Modifier modifier = Modifier
@ -551,15 +552,17 @@ private fun NewChatPeer(navController: NavController, account: Account) {
fontSize = 18.sp fontSize = 18.sp
) )
if (matchingUri != null) { if (matchingUri != null) {
val tel = matchingUri.uri.substring(4) val uriPart = matchingUri.uri.substringAfter(":")
val annotatedTel = Utils.buildAnnotatedStringWithHighlight( val highlightPart = if (matchingUri.uri.startsWith("tel:"))
tel, newPeer.filter { c -> c.isDigit() || c == '+' }
newPeer.filter { c -> c.isDigit() || c == '+' }) else
newPeer
val annotatedUri = Utils.buildAnnotatedStringWithHighlight(uriPart, highlightPart)
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
if (matchingUri.label.isNotEmpty()) if (matchingUri.label.isNotEmpty())
append("${matchingUri.label} ") append("${matchingUri.label} ")
append(annotatedTel) append(annotatedUri)
}, },
fontSize = 14.sp, fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
@ -576,31 +579,32 @@ private fun NewChatPeer(navController: NavController, account: Account) {
OutlinedTextField( OutlinedTextField(
value = newPeer, value = newPeer,
placeholder = { Text(stringResource(R.string.new_chat_peer)) }, placeholder = { Text(stringResource(R.string.new_chat_peer)) },
onValueChange = { onValueChange = { input ->
newPeer = it newPeer = input
showSuggestions = newPeer.length > 1 showSuggestions = newPeer.length > 1
filteredSuggestions = if (it.isEmpty()) filteredSuggestions = if (input.length <= 1)
emptyList() emptyList()
else { else {
val normalizedInput = Utils.unaccent(it) val normalizedInput = Utils.unaccent(input)
val numericInput = it.filter { c -> c.isDigit() || c == '+' } val numericInput = input.filter { c -> c.isDigit() || c == '+' }
BaresipService.contacts.mapNotNull { contact -> BaresipService.contacts.flatMap { contact ->
val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true) val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true)
var matchingUri: Contact.ContactUri? = null val uris = contact.uris()
if (numericInput.isNotEmpty()) { val matchingUris = uris.filter { u ->
matchingUri = contact.uris().find { u -> (u.uri.startsWith("tel:") && numericInput.isNotEmpty() && u.uri.substring(4).contains(numericInput)) ||
u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput) (u.uri.startsWith("sip:") && u.uri.substring(4).contains(normalizedInput, ignoreCase = true))
}
} }
if (nameMatch || matchingUri != null) { if (nameMatch) {
val annotatedName = if (nameMatch) val annotatedName = Utils.buildAnnotatedStringWithHighlight(contact.name(), input)
Utils.buildAnnotatedStringWithHighlight(contact.name(), it) if (uris.isEmpty())
listOf(Triple(contact, annotatedName, null))
else else
AnnotatedString(contact.name()) uris.map { Triple(contact, annotatedName, it) }
Triple(contact, annotatedName, matchingUri)
} else {
null
} }
else if (matchingUris.isNotEmpty())
matchingUris.map { Triple(contact, AnnotatedString(contact.name()), it) }
else
emptyList()
} }
} }
}, },
@ -643,9 +647,9 @@ private fun NewChatPeer(navController: NavController, account: Account) {
else else
String.format(contactNoSipOrTelUriText, peerText) String.format(contactNoSipOrTelUriText, peerText)
showAlert.value = true showAlert.value = true
} else {
makeChat(navController, account, peerText)
} }
else
makeChat(navController, account, peerText)
} }
else if (uris.size == 1) else if (uris.size == 1)
makeChat(navController, account, uris[0].uri) makeChat(navController, account, uris[0].uri)

View File

@ -44,11 +44,10 @@ class ConnectionService : ConnectionService() {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val currentRoute = it.callAudioState?.route ?: CallAudioState.ROUTE_EARPIECE val currentRoute = it.callAudioState?.route ?: CallAudioState.ROUTE_EARPIECE
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
if (speaker) { if (speaker)
it.setAudioRoute(CallAudioState.ROUTE_SPEAKER) it.setAudioRoute(CallAudioState.ROUTE_SPEAKER)
} else if (currentRoute == CallAudioState.ROUTE_SPEAKER) { else if (currentRoute == CallAudioState.ROUTE_SPEAKER)
it.setAudioRoute(CallAudioState.ROUTE_EARPIECE) it.setAudioRoute(CallAudioState.ROUTE_EARPIECE)
}
} }
} }
} }
@ -72,6 +71,8 @@ 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
if (request?.accountHandle?.id == BaresipService.SIP_ACCOUNT_ID)
connection.connectionProperties = Connection.PROPERTY_SELF_MANAGED
connection.audioModeIsVoip = true connection.audioModeIsVoip = true
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
@ -110,9 +111,14 @@ class ConnectionService : ConnectionService() {
val rootExtras = request?.extras val rootExtras = request?.extras
val nestedExtras = rootExtras?.getBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS) val nestedExtras = rootExtras?.getBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)
val uap = rootExtras?.getLong("uap", 0L).takeIf { it != 0L } var uap = rootExtras?.getLong("uap", 0L).takeIf { it != 0L }
?: nestedExtras?.getLong("uap") ?: 0L ?: nestedExtras?.getLong("uap") ?: 0L
if (uap == 0L && BaresipService.uas.value.isNotEmpty()) {
uap = BaresipService.uas.value[0].uap
Log.d(TAG, "Outgoing connection request from system, using default uap $uap")
}
val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?: val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?:
nestedExtras?.getBoolean("conferenceCall") ?: false nestedExtras?.getBoolean("conferenceCall") ?: false
@ -122,7 +128,7 @@ class ConnectionService : ConnectionService() {
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
val destination = request?.address?.encodedSchemeSpecificPart ?: "" val destination = request?.address?.schemeSpecificPart ?: ""
Log.d(TAG, "onCreateOutgoingConnection to $destination (uap=$uap)") Log.d(TAG, "onCreateOutgoingConnection to $destination (uap=$uap)")
@ -143,6 +149,8 @@ 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
if (request?.accountHandle?.id == BaresipService.SIP_ACCOUNT_ID)
connection.connectionProperties = Connection.PROPERTY_SELF_MANAGED
if (!pstnCall) { if (!pstnCall) {
connection.audioModeIsVoip = true connection.audioModeIsVoip = true
@ -225,9 +233,9 @@ class ConnectionService : ConnectionService() {
if (callp != 0L) { if (callp != 0L) {
Api.ua_hangup(uap, callp, 0, "") Api.ua_hangup(uap, callp, 0, "")
connections.remove(callp) connections.remove(callp)
} else {
pendingOutgoingConnection = null
} }
else
pendingOutgoingConnection = null
setDisconnected(DisconnectCause(DisconnectCause.CANCELED)) setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
destroy() destroy()
} }
@ -281,9 +289,9 @@ class ConnectionService : ConnectionService() {
call.showOnHoldNotice.value = true call.showOnHoldNotice.value = true
// 3. Tell Telecom the move is complete // 3. Tell Telecom the move is complete
setOnHold() setOnHold()
} else {
Log.e(TAG, "SIP Hold failed for $callp")
} }
else
Log.e(TAG, "SIP Hold failed for $callp")
} }
} }
@ -299,9 +307,9 @@ class ConnectionService : ConnectionService() {
call.showOnHoldNotice.value = false call.showOnHoldNotice.value = false
// 3. Tell Telecom we are active // 3. Tell Telecom we are active
setActive() setActive()
} else {
Log.e(TAG, "SIP Resume failed for $callp")
} }
else
Log.e(TAG, "SIP Resume failed for $callp")
} }
} }

View File

@ -18,9 +18,10 @@ class InCallService : InCallService() {
Log.d(TAG, "InCallService: Call added") Log.d(TAG, "InCallService: Call added")
val handle = call.details.accountHandle val handle = call.details.accountHandle
val baresipHandle = BaresipService.getPhoneAccountHandle(this) val baresipSipHandle = BaresipService.getPhoneAccountHandle(this, BaresipService.SIP_ACCOUNT_ID)
val baresipPstnHandle = BaresipService.getPhoneAccountHandle(this, BaresipService.PSTN_ACCOUNT_ID)
if (handle == baresipHandle) { if (handle == baresipSipHandle || handle == baresipPstnHandle) {
Log.d(TAG, "InCallService: Identified as SIP call") Log.d(TAG, "InCallService: Identified as SIP call")
// SIP call is already managed by ConnectionService/BaresipService // SIP call is already managed by ConnectionService/BaresipService
} }

View File

@ -169,6 +169,8 @@ import com.tutpro.baresip.CustomElements.PasswordDialog
import com.tutpro.baresip.CustomElements.SelectableAlertDialog import com.tutpro.baresip.CustomElements.SelectableAlertDialog
import com.tutpro.baresip.CustomElements.verticalScrollbar import com.tutpro.baresip.CustomElements.verticalScrollbar
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -1204,38 +1206,39 @@ private fun CallUriRow(
value = if (isDialer) dialerState.callUri.value else call!!.callUri.value, value = if (isDialer) dialerState.callUri.value else call!!.callUri.value,
readOnly = if (isDialer) !dialerState.callUriEnabled.value else !call!!.callUriEnabled.value, readOnly = if (isDialer) !dialerState.callUriEnabled.value else !call!!.callUriEnabled.value,
singleLine = true, singleLine = true,
onValueChange = { onValueChange = { input ->
if (isDialer) if (isDialer)
if (it != dialerState.callUri.value) { if (input != dialerState.callUri.value) {
dialerState.callUri.value = it dialerState.callUri.value = input
dialerState.redialUri = "" dialerState.redialUri = ""
if (it == "") { if (input == "") {
dialerState.showCallButton.value = true dialerState.showCallButton.value = true
dialerState.showCallConferenceButton.value = true dialerState.showCallConferenceButton.value = true
} }
if (it.length > 1) { if (input.length > 1) {
val normalizedInput = Utils.unaccent(it) val normalizedInput = Utils.unaccent(input)
val numericInput = it.filter { c -> c.isDigit() || c == '+' } val numericInput = input.filter { c -> c.isDigit() || c == '+' }
filteredSuggestions = BaresipService.contacts.mapNotNull { contact -> filteredSuggestions = BaresipService.contacts.flatMap { contact ->
val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true) val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true)
var matchingUri: Contact.ContactUri? = null val uris = contact.uris()
if (numericInput.isNotEmpty()) { val matchingUris = uris.filter { u ->
matchingUri = contact.uris().find { u -> (u.uri.startsWith("tel:") && numericInput.isNotEmpty() && u.uri.substring(4).contains(numericInput)) ||
u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput) (u.uri.startsWith("sip:") && u.uri.substring(4).contains(normalizedInput, ignoreCase = true))
}
} }
if (nameMatch || matchingUri != null) { if (nameMatch) {
val annotatedName = if (nameMatch) val annotatedName = Utils.buildAnnotatedStringWithHighlight(contact.name(), input)
Utils.buildAnnotatedStringWithHighlight(contact.name(), it) if (uris.isEmpty())
listOf(Triple(contact, annotatedName, null))
else else
AnnotatedString(contact.name()) uris.map { Triple(contact, annotatedName, it) }
Triple(contact, annotatedName, matchingUri)
} else {
null
} }
else if (matchingUris.isNotEmpty())
matchingUris.map { Triple(contact, AnnotatedString(contact.name()), it) }
else
emptyList()
} }
} }
dialerState.showSuggestions.value = it.length > 1 dialerState.showSuggestions.value = input.length > 1
} }
}, },
trailingIcon = { trailingIcon = {
@ -1329,7 +1332,7 @@ private fun CallUriRow(
) { ) {
items( items(
items = filteredSuggestions, items = filteredSuggestions,
key = { (contact, _, _) -> "${contact.id()}" } key = { (contact, _, matchingUri) -> "${contact.id()}:${matchingUri?.uri ?: ""}" }
) { (contact, annotatedName, matchingUri) -> ) { (contact, annotatedName, matchingUri) ->
Box( Box(
modifier = Modifier modifier = Modifier
@ -1348,15 +1351,17 @@ private fun CallUriRow(
fontSize = 18.sp fontSize = 18.sp
) )
if (matchingUri != null) { if (matchingUri != null) {
val tel = matchingUri.uri.substring(4) val uriPart = matchingUri.uri.substringAfter(":")
val annotatedTel = Utils.buildAnnotatedStringWithHighlight( val highlightPart = if (matchingUri.uri.startsWith("tel:"))
tel, dialerState.callUri.value.filter { c -> c.isDigit() || c == '+' }
dialerState.callUri.value.filter { c -> c.isDigit() || c == '+' }) else
dialerState.callUri.value
val annotatedUri = Utils.buildAnnotatedStringWithHighlight(uriPart, highlightPart)
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
if (matchingUri.label.isNotEmpty()) if (matchingUri.label.isNotEmpty())
append("${matchingUri.label} ") append("${matchingUri.label} ")
append(annotatedTel) append(annotatedUri)
}, },
fontSize = 14.sp, fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
@ -1664,29 +1669,30 @@ private fun CallRow(
OutlinedTextField( OutlinedTextField(
value = transferUri, value = transferUri,
singleLine = true, singleLine = true,
onValueChange = { onValueChange = { input ->
if (it != transferUri) { if (input != transferUri) {
transferUri = it transferUri = input
if (it.length > 1) { if (input.length > 1) {
val normalizedInput = Utils.unaccent(it) val normalizedInput = Utils.unaccent(input)
val numericInput = it.filter { c -> c.isDigit() || c == '+' } val numericInput = input.filter { c -> c.isDigit() || c == '+' }
filteredSuggestions = BaresipService.contacts.mapNotNull { contact -> filteredSuggestions = BaresipService.contacts.flatMap { contact ->
val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true) val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true)
var matchingUri: Contact.ContactUri? = null val uris = contact.uris()
if (numericInput.isNotEmpty()) { val matchingUris = uris.filter { u ->
matchingUri = contact.uris().find { u -> (u.uri.startsWith("tel:") && numericInput.isNotEmpty() && u.uri.substring(4).contains(numericInput)) ||
u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput) (u.uri.startsWith("sip:") && u.uri.substring(4).contains(normalizedInput, ignoreCase = true))
}
} }
if (nameMatch || matchingUri != null) { if (nameMatch) {
val annotatedName = if (nameMatch) val annotatedName = Utils.buildAnnotatedStringWithHighlight(contact.name(), input)
Utils.buildAnnotatedStringWithHighlight(contact.name(), it) if (uris.isEmpty())
listOf(Triple(contact, annotatedName, null))
else else
AnnotatedString(contact.name()) uris.map { Triple(contact, annotatedName, it) }
Triple(contact, annotatedName, matchingUri)
} else {
null
} }
else if (matchingUris.isNotEmpty())
matchingUris.map { Triple(contact, AnnotatedString(contact.name()), it) }
else
emptyList()
} }
} }
call.showSuggestions.value = transferUri.length > 1 call.showSuggestions.value = transferUri.length > 1
@ -1739,7 +1745,7 @@ private fun CallRow(
) { ) {
items( items(
items = filteredSuggestions, items = filteredSuggestions,
key = { (contact, _, _) -> "${contact.id()}" } key = { (contact, _, matchingUri) -> "${contact.id()}:${matchingUri?.uri ?: ""}" }
) { (contact, annotatedName, matchingUri) -> ) { (contact, annotatedName, matchingUri) ->
Box( Box(
modifier = Modifier modifier = Modifier
@ -1758,15 +1764,17 @@ private fun CallRow(
fontSize = 18.sp fontSize = 18.sp
) )
if (matchingUri != null) { if (matchingUri != null) {
val tel = matchingUri.uri.substring(4) val uriPart = matchingUri.uri.substringAfter(":")
val annotatedTel = Utils.buildAnnotatedStringWithHighlight( val highlightPart = if (matchingUri.uri.startsWith("tel:"))
tel, transferUri.filter { c -> c.isDigit() || c == '+' }
transferUri.filter { c -> c.isDigit() || c == '+' }) else
transferUri
val annotatedUri = Utils.buildAnnotatedStringWithHighlight(uriPart, highlightPart)
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
if (matchingUri.label.isNotEmpty()) if (matchingUri.label.isNotEmpty())
append("${matchingUri.label} ") append("${matchingUri.label} ")
append(annotatedTel) append(annotatedUri)
}, },
fontSize = 14.sp, fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
@ -2150,6 +2158,14 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String,
extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
try { try {
Log.i(TAG, "Placing Telecom PSTN call to $uri with uap=${ua.uap}") Log.i(TAG, "Placing Telecom PSTN call to $uri with uap=${ua.uap}")
val extras = Bundle().apply {
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
BaresipService.getPhoneAccountHandle(ctx, BaresipService.PSTN_ACCOUNT_ID))
}
val callExtras = Bundle()
callExtras.putBoolean("pstnCall", true)
callExtras.putString("aor", aor)
extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
val telecomUri = if (uri.startsWith("tel:")) val telecomUri = if (uri.startsWith("tel:"))
Uri.fromParts("tel", uri.substring(4), null) Uri.fromParts("tel", uri.substring(4), null)
else else
@ -2166,7 +2182,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String,
val extras = Bundle() val extras = Bundle()
extras.putParcelable( extras.putParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
BaresipService.getPhoneAccountHandle(ctx) BaresipService.getPhoneAccountHandle(ctx, BaresipService.SIP_ACCOUNT_ID)
) )
val callExtras = Bundle() val callExtras = Bundle()
callExtras.putBoolean("conferenceCall", dialerState.showCallConferenceButton.value) callExtras.putBoolean("conferenceCall", dialerState.showCallConferenceButton.value)
@ -2185,6 +2201,14 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String,
Log.e(TAG, error) Log.e(TAG, error)
viewModel.dialerState.callButtonsEnabled.value = true viewModel.dialerState.callButtonsEnabled.value = true
} }
else
viewModel.viewModelScope.launch {
delay(5000.milliseconds)
if (Call.calls().isEmpty()) {
Log.d(TAG, "Re-enabling dialer buttons after timeout (no calls)")
viewModel.dialerState.callButtonsEnabled.value = true
}
}
} }
} }
@ -2467,7 +2491,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
spinToAor(viewModel, aor, Call.ofCallp(callp)) spinToAor(viewModel, aor, Call.ofCallp(callp))
} }
"call redirect" -> { "call redirect" -> {
val redirectUri = ev[1] val redirectUri = Utils.uriUnescape(ev[1])
val target = Utils.friendlyUri(ctx, redirectUri, acc) val target = Utils.friendlyUri(ctx, redirectUri, acc)
if (acc.autoRedirect) { if (acc.autoRedirect) {
redirect(ctx, viewModel, ua, redirectUri) redirect(ctx, viewModel, ua, redirectUri)
@ -2545,7 +2569,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
viewModel.navigateToHome() viewModel.navigateToHome()
val callp = params[1] as Long val callp = params[1] as Long
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
val target = Utils.friendlyUri(ctx, ev[1], acc) val target = Utils.friendlyUri(ctx, Utils.uriUnescape(ev[1]), acc)
dialogTitle.value = if (call != null) dialogTitle.value = if (call != null)
ctx.getString(R.string.transfer_request) ctx.getString(R.string.transfer_request)
else else

View File

@ -212,10 +212,12 @@ object Utils {
return u return u
} }
fun e164Uri(uri: String, countryCode: String): String { fun e164Uri(uriText: String, countryCode: String): String {
val uri = uriUnescape(uriText)
val scheme = uri.take(4) val scheme = uri.take(4)
val userPart = uriUserPart(uri) val userPart = uriUserPart(uri)
return if (userPart.isNotEmpty() && userPart.isDigitsOnly()) val digitsOnlyUserPart = userPart.filter { it.isDigit() }
return if (digitsOnlyUserPart.isNotEmpty() && digitsOnlyUserPart.length == userPart.filterNot { it == ' ' || it == '-' || it == '(' || it == ')' || it == '+' }.length)
when { when {
userPart.startsWith("00") -> uri.replace("$scheme$userPart", userPart.startsWith("00") -> uri.replace("$scheme$userPart",
scheme + "+" + userPart.substring(2)) scheme + "+" + userPart.substring(2))
@ -238,7 +240,7 @@ object Utils {
fun uriUnescape(uri: String): String { fun uriUnescape(uri: String): String {
return uri.replace("%2B" to "+", "%3A" to ":", "%3B" to ";", "%40" to "@", "%3D" to "=", return uri.replace("%2B" to "+", "%3A" to ":", "%3B" to ";", "%40" to "@", "%3D" to "=",
"%23" to "#", "%2A" to "*") "%23" to "#", "%2A" to "*", "%20" to "")
} }
fun aorDomain(aor: String): String { fun aorDomain(aor: String): String {