diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 1127f1c0..71ee54f7 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -892,7 +892,8 @@ class BaresipService: Service() { Blocked.clear(ua.account.aor) BlockRule.clear(ua.account.aor) Api.ua_destroy(uap) - if (isNativeReady) Account.saveAccounts() + ua.remove() + Account.saveAccounts() return } @@ -903,10 +904,11 @@ class BaresipService: Service() { R.drawable.circle_white else 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 else circleYellow.getValue(colorblind) @@ -1076,7 +1078,7 @@ class BaresipService: Service() { "incoming call" -> { if (speakerPhoneAuto) speakerPhone = true - val peerUri = ev[1] + val peerUri = Utils.uriUnescape(ev[1]) var blockedCall = false val toastMsg = if (Call.isAnyCallActive(this)) String.format( @@ -1143,7 +1145,7 @@ class BaresipService: Service() { } "call incoming" -> { - val peerUri = ev[1] + val peerUri = Utils.uriUnescape(ev[1]) Log.d(TAG, "Incoming call $uap/$callp/$peerUri") if (Call.ofCallp(callp) == null) Call(callp, ua, peerUri, "in", "incoming").add() @@ -1714,7 +1716,9 @@ class BaresipService: Service() { 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) if (ua != null && ua.account.isMobile) { @@ -3047,13 +3051,27 @@ class BaresipService: Service() { } private fun registerPhoneAccount() { - val phoneAccountHandle = getPhoneAccountHandle(this) - val phoneAccount = android.telecom.PhoneAccount.builder(phoneAccountHandle, getString(R.string.app_name)) - .setCapabilities(android.telecom.PhoneAccount.CAPABILITY_SELF_MANAGED) + val sipHandle = getPhoneAccountHandle(this, SIP_ACCOUNT_ID) + val pstnHandle = getPhoneAccountHandle(this, PSTN_ACCOUNT_ID) + + 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_TEL) .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 { @@ -3197,7 +3215,7 @@ class BaresipService: Service() { var instance: BaresipService? = null var isServiceRunning = false var isNativeReady = false - var mobileAccount = true + var mobileAccount = false var isStartReceived = false var isConfigInitialized = false var libraryLoaded = false @@ -3295,11 +3313,12 @@ class BaresipService: Service() { ) 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) - return PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID) + return PhoneAccountHandle(componentName, id) } fun setMicMute(mute: Boolean) { diff --git a/app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt index f20eb374..192c7982 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt @@ -460,7 +460,8 @@ private fun NewChatPeer(navController: NavController, account: Account) { String.format(noTelephonyProviderText, account.aor) showAlert.value = true "" - } else + } + else Utils.telToSip(peerUri, account) } else @@ -532,7 +533,7 @@ private fun NewChatPeer(navController: NavController, account: Account) { ) { items( items = filteredSuggestions, - key = { (contact, _, _) -> "${contact.id()}" } + key = { (contact, _, matchingUri) -> "${contact.id()}:${matchingUri?.uri ?: ""}" } ) { (contact, annotatedName, matchingUri) -> Box( modifier = Modifier @@ -551,15 +552,17 @@ private fun NewChatPeer(navController: NavController, account: Account) { fontSize = 18.sp ) if (matchingUri != null) { - val tel = matchingUri.uri.substring(4) - val annotatedTel = Utils.buildAnnotatedStringWithHighlight( - tel, - newPeer.filter { c -> c.isDigit() || c == '+' }) + val uriPart = matchingUri.uri.substringAfter(":") + val highlightPart = if (matchingUri.uri.startsWith("tel:")) + newPeer.filter { c -> c.isDigit() || c == '+' } + else + newPeer + val annotatedUri = Utils.buildAnnotatedStringWithHighlight(uriPart, highlightPart) Text( text = buildAnnotatedString { if (matchingUri.label.isNotEmpty()) append("${matchingUri.label} ") - append(annotatedTel) + append(annotatedUri) }, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -576,31 +579,32 @@ private fun NewChatPeer(navController: NavController, account: Account) { OutlinedTextField( value = newPeer, placeholder = { Text(stringResource(R.string.new_chat_peer)) }, - onValueChange = { - newPeer = it + onValueChange = { input -> + newPeer = input showSuggestions = newPeer.length > 1 - filteredSuggestions = if (it.isEmpty()) + filteredSuggestions = if (input.length <= 1) emptyList() else { - val normalizedInput = Utils.unaccent(it) - val numericInput = it.filter { c -> c.isDigit() || c == '+' } - BaresipService.contacts.mapNotNull { contact -> + val normalizedInput = Utils.unaccent(input) + val numericInput = input.filter { c -> c.isDigit() || c == '+' } + BaresipService.contacts.flatMap { contact -> val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true) - var matchingUri: Contact.ContactUri? = null - if (numericInput.isNotEmpty()) { - matchingUri = contact.uris().find { u -> - u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput) - } + val uris = contact.uris() + val matchingUris = uris.filter { u -> + (u.uri.startsWith("tel:") && numericInput.isNotEmpty() && u.uri.substring(4).contains(numericInput)) || + (u.uri.startsWith("sip:") && u.uri.substring(4).contains(normalizedInput, ignoreCase = true)) } - if (nameMatch || matchingUri != null) { - val annotatedName = if (nameMatch) - Utils.buildAnnotatedStringWithHighlight(contact.name(), it) + if (nameMatch) { + val annotatedName = Utils.buildAnnotatedStringWithHighlight(contact.name(), input) + if (uris.isEmpty()) + listOf(Triple(contact, annotatedName, null)) else - AnnotatedString(contact.name()) - Triple(contact, annotatedName, matchingUri) - } else { - null + uris.map { Triple(contact, annotatedName, it) } } + 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 String.format(contactNoSipOrTelUriText, peerText) showAlert.value = true - } else { - makeChat(navController, account, peerText) } + else + makeChat(navController, account, peerText) } else if (uris.size == 1) makeChat(navController, account, uris[0].uri) diff --git a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt index 91d91210..b97e4ed1 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt @@ -44,11 +44,10 @@ class ConnectionService : ConnectionService() { @Suppress("DEPRECATION") val currentRoute = it.callAudioState?.route ?: CallAudioState.ROUTE_EARPIECE @Suppress("DEPRECATION") - if (speaker) { + if (speaker) it.setAudioRoute(CallAudioState.ROUTE_SPEAKER) - } else if (currentRoute == CallAudioState.ROUTE_SPEAKER) { + else if (currentRoute == CallAudioState.ROUTE_SPEAKER) it.setAudioRoute(CallAudioState.ROUTE_EARPIECE) - } } } } @@ -72,6 +71,8 @@ class ConnectionService : ConnectionService() { Connection.CAPABILITY_HOLD or Connection.CAPABILITY_MERGE_CONFERENCE or Connection.CAPABILITY_SWAP_CONFERENCE + if (request?.accountHandle?.id == BaresipService.SIP_ACCOUNT_ID) + connection.connectionProperties = Connection.PROPERTY_SELF_MANAGED connection.audioModeIsVoip = true val call = Call.ofCallp(callp) @@ -110,9 +111,14 @@ class ConnectionService : ConnectionService() { val rootExtras = request?.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 + 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) ?: nestedExtras?.getBoolean("conferenceCall") ?: false @@ -122,7 +128,7 @@ class ConnectionService : ConnectionService() { val onHoldCallp = rootExtras?.getLong("onHoldCallp", 0L).takeIf { it != 0L } ?: nestedExtras?.getLong("onHoldCallp") ?: 0L - val destination = request?.address?.encodedSchemeSpecificPart ?: "" + val destination = request?.address?.schemeSpecificPart ?: "" Log.d(TAG, "onCreateOutgoingConnection to $destination (uap=$uap)") @@ -143,6 +149,8 @@ class ConnectionService : ConnectionService() { Connection.CAPABILITY_HOLD or Connection.CAPABILITY_MERGE_CONFERENCE or Connection.CAPABILITY_SWAP_CONFERENCE + if (request?.accountHandle?.id == BaresipService.SIP_ACCOUNT_ID) + connection.connectionProperties = Connection.PROPERTY_SELF_MANAGED if (!pstnCall) { connection.audioModeIsVoip = true @@ -225,9 +233,9 @@ class ConnectionService : ConnectionService() { if (callp != 0L) { Api.ua_hangup(uap, callp, 0, "") connections.remove(callp) - } else { - pendingOutgoingConnection = null } + else + pendingOutgoingConnection = null setDisconnected(DisconnectCause(DisconnectCause.CANCELED)) destroy() } @@ -281,9 +289,9 @@ class ConnectionService : ConnectionService() { call.showOnHoldNotice.value = true // 3. Tell Telecom the move is complete 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 // 3. Tell Telecom we are active setActive() - } else { - Log.e(TAG, "SIP Resume failed for $callp") } + else + Log.e(TAG, "SIP Resume failed for $callp") } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt index c3b1953d..297242b9 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt @@ -18,9 +18,10 @@ class InCallService : InCallService() { Log.d(TAG, "InCallService: Call added") 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") // SIP call is already managed by ConnectionService/BaresipService } diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 233ff2da..bf0bfb3d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -169,6 +169,8 @@ import com.tutpro.baresip.CustomElements.PasswordDialog import com.tutpro.baresip.CustomElements.SelectableAlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import androidx.lifecycle.viewModelScope import java.io.File import java.text.SimpleDateFormat import java.util.Date @@ -1204,38 +1206,39 @@ private fun CallUriRow( value = if (isDialer) dialerState.callUri.value else call!!.callUri.value, readOnly = if (isDialer) !dialerState.callUriEnabled.value else !call!!.callUriEnabled.value, singleLine = true, - onValueChange = { + onValueChange = { input -> if (isDialer) - if (it != dialerState.callUri.value) { - dialerState.callUri.value = it + if (input != dialerState.callUri.value) { + dialerState.callUri.value = input dialerState.redialUri = "" - if (it == "") { + if (input == "") { dialerState.showCallButton.value = true dialerState.showCallConferenceButton.value = true } - if (it.length > 1) { - val normalizedInput = Utils.unaccent(it) - val numericInput = it.filter { c -> c.isDigit() || c == '+' } - filteredSuggestions = BaresipService.contacts.mapNotNull { contact -> + if (input.length > 1) { + val normalizedInput = Utils.unaccent(input) + val numericInput = input.filter { c -> c.isDigit() || c == '+' } + filteredSuggestions = BaresipService.contacts.flatMap { contact -> val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true) - var matchingUri: Contact.ContactUri? = null - if (numericInput.isNotEmpty()) { - matchingUri = contact.uris().find { u -> - u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput) - } + val uris = contact.uris() + val matchingUris = uris.filter { u -> + (u.uri.startsWith("tel:") && numericInput.isNotEmpty() && u.uri.substring(4).contains(numericInput)) || + (u.uri.startsWith("sip:") && u.uri.substring(4).contains(normalizedInput, ignoreCase = true)) } - if (nameMatch || matchingUri != null) { - val annotatedName = if (nameMatch) - Utils.buildAnnotatedStringWithHighlight(contact.name(), it) + if (nameMatch) { + val annotatedName = Utils.buildAnnotatedStringWithHighlight(contact.name(), input) + if (uris.isEmpty()) + listOf(Triple(contact, annotatedName, null)) else - AnnotatedString(contact.name()) - Triple(contact, annotatedName, matchingUri) - } else { - null + uris.map { Triple(contact, annotatedName, it) } } + 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 = { @@ -1329,7 +1332,7 @@ private fun CallUriRow( ) { items( items = filteredSuggestions, - key = { (contact, _, _) -> "${contact.id()}" } + key = { (contact, _, matchingUri) -> "${contact.id()}:${matchingUri?.uri ?: ""}" } ) { (contact, annotatedName, matchingUri) -> Box( modifier = Modifier @@ -1348,15 +1351,17 @@ private fun CallUriRow( fontSize = 18.sp ) if (matchingUri != null) { - val tel = matchingUri.uri.substring(4) - val annotatedTel = Utils.buildAnnotatedStringWithHighlight( - tel, - dialerState.callUri.value.filter { c -> c.isDigit() || c == '+' }) + val uriPart = matchingUri.uri.substringAfter(":") + val highlightPart = if (matchingUri.uri.startsWith("tel:")) + dialerState.callUri.value.filter { c -> c.isDigit() || c == '+' } + else + dialerState.callUri.value + val annotatedUri = Utils.buildAnnotatedStringWithHighlight(uriPart, highlightPart) Text( text = buildAnnotatedString { if (matchingUri.label.isNotEmpty()) append("${matchingUri.label} ") - append(annotatedTel) + append(annotatedUri) }, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -1664,29 +1669,30 @@ private fun CallRow( OutlinedTextField( value = transferUri, singleLine = true, - onValueChange = { - if (it != transferUri) { - transferUri = it - if (it.length > 1) { - val normalizedInput = Utils.unaccent(it) - val numericInput = it.filter { c -> c.isDigit() || c == '+' } - filteredSuggestions = BaresipService.contacts.mapNotNull { contact -> + onValueChange = { input -> + if (input != transferUri) { + transferUri = input + if (input.length > 1) { + val normalizedInput = Utils.unaccent(input) + val numericInput = input.filter { c -> c.isDigit() || c == '+' } + filteredSuggestions = BaresipService.contacts.flatMap { contact -> val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true) - var matchingUri: Contact.ContactUri? = null - if (numericInput.isNotEmpty()) { - matchingUri = contact.uris().find { u -> - u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput) - } + val uris = contact.uris() + val matchingUris = uris.filter { u -> + (u.uri.startsWith("tel:") && numericInput.isNotEmpty() && u.uri.substring(4).contains(numericInput)) || + (u.uri.startsWith("sip:") && u.uri.substring(4).contains(normalizedInput, ignoreCase = true)) } - if (nameMatch || matchingUri != null) { - val annotatedName = if (nameMatch) - Utils.buildAnnotatedStringWithHighlight(contact.name(), it) + if (nameMatch) { + val annotatedName = Utils.buildAnnotatedStringWithHighlight(contact.name(), input) + if (uris.isEmpty()) + listOf(Triple(contact, annotatedName, null)) else - AnnotatedString(contact.name()) - Triple(contact, annotatedName, matchingUri) - } else { - null + uris.map { Triple(contact, annotatedName, it) } } + else if (matchingUris.isNotEmpty()) + matchingUris.map { Triple(contact, AnnotatedString(contact.name()), it) } + else + emptyList() } } call.showSuggestions.value = transferUri.length > 1 @@ -1739,7 +1745,7 @@ private fun CallRow( ) { items( items = filteredSuggestions, - key = { (contact, _, _) -> "${contact.id()}" } + key = { (contact, _, matchingUri) -> "${contact.id()}:${matchingUri?.uri ?: ""}" } ) { (contact, annotatedName, matchingUri) -> Box( modifier = Modifier @@ -1758,15 +1764,17 @@ private fun CallRow( fontSize = 18.sp ) if (matchingUri != null) { - val tel = matchingUri.uri.substring(4) - val annotatedTel = Utils.buildAnnotatedStringWithHighlight( - tel, - transferUri.filter { c -> c.isDigit() || c == '+' }) + val uriPart = matchingUri.uri.substringAfter(":") + val highlightPart = if (matchingUri.uri.startsWith("tel:")) + transferUri.filter { c -> c.isDigit() || c == '+' } + else + transferUri + val annotatedUri = Utils.buildAnnotatedStringWithHighlight(uriPart, highlightPart) Text( text = buildAnnotatedString { if (matchingUri.label.isNotEmpty()) append("${matchingUri.label} ") - append(annotatedTel) + append(annotatedUri) }, fontSize = 14.sp, 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) try { 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:")) Uri.fromParts("tel", uri.substring(4), null) else @@ -2166,7 +2182,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, val extras = Bundle() extras.putParcelable( TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, - BaresipService.getPhoneAccountHandle(ctx) + BaresipService.getPhoneAccountHandle(ctx, BaresipService.SIP_ACCOUNT_ID) ) val callExtras = Bundle() callExtras.putBoolean("conferenceCall", dialerState.showCallConferenceButton.value) @@ -2185,6 +2201,14 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, Log.e(TAG, error) 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)) } "call redirect" -> { - val redirectUri = ev[1] + val redirectUri = Utils.uriUnescape(ev[1]) val target = Utils.friendlyUri(ctx, redirectUri, acc) if (acc.autoRedirect) { redirect(ctx, viewModel, ua, redirectUri) @@ -2545,7 +2569,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params viewModel.navigateToHome() val callp = params[1] as Long 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) ctx.getString(R.string.transfer_request) else diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 77c0fee7..ac069607 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -212,10 +212,12 @@ object Utils { 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 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 { userPart.startsWith("00") -> uri.replace("$scheme$userPart", scheme + "+" + userPart.substring(2)) @@ -238,7 +240,7 @@ object Utils { fun uriUnescape(uri: String): String { 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 {