From 6a6ccba10e5550bb9dca6e3c50eed95784dce15d Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Sat, 9 May 2026 19:59:34 +0300 Subject: [PATCH] Added virtual mobile account --- .../main/kotlin/com/tutpro/baresip/Account.kt | 111 +++++++++--------- .../com/tutpro/baresip/AccountScreen.kt | 15 ++- .../com/tutpro/baresip/BaresipService.kt | 43 ++++++- .../com/tutpro/baresip/InCallService.kt | 4 +- .../kotlin/com/tutpro/baresip/MainScreen.kt | 41 ++----- .../kotlin/com/tutpro/baresip/UserAgent.kt | 8 +- .../main/kotlin/com/tutpro/baresip/Utils.kt | 32 +++++ .../kotlin/com/tutpro/baresip/ViewModel.kt | 1 - app/src/main/res/values-fi/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 + 10 files changed, 159 insertions(+), 99 deletions(-) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 7a7fd54c..4ad306e4 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -5,87 +5,92 @@ import java.net.URLDecoder import java.net.URLEncoder import java.util.Locale -class Account(val accp: Long) { +class Account(val accp: Long, virtualAor: String? = null) { + var isMobile = false var nickName = "" - var displayName = Api.account_display_name(accp) - val aor = Api.account_aor(accp) - val luri = Api.account_luri(accp) - var authUser = Api.account_auth_user(accp) - var authPass = Api.account_auth_pass(accp) + var displayName = if (accp != 0L) Api.account_display_name(accp) else "" + val aor = if (accp != 0L) Api.account_aor(accp) else (virtualAor ?: "") + val luri = if (accp != 0L) Api.account_luri(accp) else (virtualAor ?: "") + var authUser = if (accp != 0L) Api.account_auth_user(accp) else "" + var authPass = if (accp != 0L) Api.account_auth_pass(accp) else "" var outbound = ArrayList() - var mediaNat = Api.account_medianat(accp) - var stunServer = Api.account_stun_uri(accp) - var stunUser = Api.account_stun_user(accp) - var stunPass = Api.account_stun_pass(accp) + var mediaNat = if (accp != 0L) Api.account_medianat(accp) else "" + var stunServer = if (accp != 0L) Api.account_stun_uri(accp) else "" + var stunUser = if (accp != 0L) Api.account_stun_user(accp) else "" + var stunPass = if (accp != 0L) Api.account_stun_pass(accp) else "" var audioCodec = ArrayList() var videoCodec = ArrayList() - var regint = Api.account_regint(accp) - var checkOrigin = Api.account_check_origin(accp) + var regint = if (accp != 0L) Api.account_regint(accp) else 0 + var checkOrigin = if (accp != 0L) Api.account_check_origin(accp) else true var configuredRegInt = REGISTRATION_INTERVAL - var mediaEnc = Api.account_mediaenc(accp) - var rtcpMux = Api.account_rtcp_mux(accp) - var rel100Mode = Api.account_rel100_mode(accp) - var dtmfMode = Api.account_dtmfmode(accp) - var answerMode = Api.account_answermode(accp) - var autoRedirect = Api.account_sip_autoredirect(accp) + var mediaEnc = if (accp != 0L) Api.account_mediaenc(accp) else "" + var rtcpMux = if (accp != 0L) Api.account_rtcp_mux(accp) else false + var rel100Mode = if (accp != 0L) Api.account_rel100_mode(accp) else Api.REL100_DISABLED + var dtmfMode = if (accp != 0L) Api.account_dtmfmode(accp) else Api.DTMFMODE_AUTO + var answerMode = if (accp != 0L) Api.account_answermode(accp) else Api.ANSWERMODE_MANUAL + var autoRedirect = if (accp != 0L) Api.account_sip_autoredirect(accp) else false var blockUnknown = false - var vmUri = Api.account_vm_uri(accp) + var vmUri = if (accp != 0L) Api.account_vm_uri(accp) else "" var vmNew = 0 var vmOld = 0 var missedCalls = false var unreadMessages = false var callHistory = true var countryCode = "" - var telProvider = Utils.aorDomain(aor) + var telProvider = if (accp != 0L) Utils.aorDomain(aor) else "" var resumeUri = "" var numericKeypad = false var customParams = "" init { - if (authPass == "") - authPass = NO_AUTH_PASS + if (accp != 0L) { + if (authPass == "") + authPass = NO_AUTH_PASS - var i = 0 - while (true) { - val ob = Api.account_outbound(accp, i) - if (ob != "") { - outbound.add(ob) - i++ - } else { - break + var i = 0 + while (true) { + val ob = Api.account_outbound(accp, i) + if (ob != "") { + outbound.add(ob) + i++ + } else { + break + } } - } - i = 0 - while (true) { - val ac = Api.account_audio_codec(accp, i) - if (ac != "") { - audioCodec.add(ac) - i++ - } else { - break + i = 0 + while (true) { + val ac = Api.account_audio_codec(accp, i) + if (ac != "") { + audioCodec.add(ac) + i++ + } else { + break + } } - } - val extra = Api.account_extra(accp) - if (Utils.paramExists(extra, "nickname")) - nickName = Utils.paramValue(extra, "nickname") - if (Utils.paramExists(extra, "regint")) - configuredRegInt = Utils.paramValue(extra, "regint").toInt() - callHistory = Utils.paramValue(extra, "call_history") == "" - blockUnknown= Utils.paramExists(extra, "block_unknown") - if (Utils.paramExists(extra, "country_code")) - countryCode = Utils.paramValue(extra, "country_code") - if (Utils.paramExists(extra, "tel_provider")) - telProvider = URLDecoder.decode(Utils.paramValue(extra, "tel_provider"), "UTF-8") - numericKeypad = Utils.paramExists(extra, "numeric_keypad") - customParams = extra.substringAfter("last=empty").substringAfter(";") + val extra = Api.account_extra(accp) + if (Utils.paramExists(extra, "nickname")) + nickName = Utils.paramValue(extra, "nickname") + if (Utils.paramExists(extra, "regint")) + configuredRegInt = Utils.paramValue(extra, "regint").toInt() + callHistory = Utils.paramValue(extra, "call_history") == "" + blockUnknown = Utils.paramExists(extra, "block_unknown") + if (Utils.paramExists(extra, "country_code")) + countryCode = Utils.paramValue(extra, "country_code") + if (Utils.paramExists(extra, "tel_provider")) + telProvider = URLDecoder.decode(Utils.paramValue(extra, "tel_provider"), "UTF-8") + numericKeypad = Utils.paramExists(extra, "numeric_keypad") + customParams = extra.substringAfter("last=empty").substringAfter(";") + } } fun print() : String { + if (isMobile) return "" + var res = if (displayName != "") "\"${displayName}\" " else diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt index 20403c95..cf3971fe 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt @@ -232,14 +232,25 @@ private fun AccountContent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { + val aorText = if (ua.account.isMobile) { + if (ua.account.aor == "tel:mobile") + stringResource(R.string.not_available) + else + ua.account.aor + } else + ua.account.luri + OutlinedTextField( - value = ua.account.luri, + value = aorText, enabled = false, onValueChange = {}, modifier = Modifier.fillMaxWidth(), textStyle = TextStyle(fontSize = 18.sp), label = { - Text(text = stringResource(R.string.sip_uri), fontWeight = FontWeight.Bold) + Text( + text = stringResource(if (ua.account.isMobile) R.string.tel_uri else R.string.sip_uri), + fontWeight = FontWeight.Bold + ) }, colors = OutlinedTextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 2e74485a..ca1dde7b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -491,9 +491,10 @@ class BaresipService: Service() { activeNetwork = cm.activeNetwork Log.i(TAG, "Active network: $activeNetwork") - if (telecom) + if (telecom) { registerPhoneAccount() - else + addMobileUserAgent() + } else if (btAdapter != null) { Log.i(TAG, "Registering bluetooth receiver") val filter = IntentFilter() @@ -1512,6 +1513,7 @@ class BaresipService: Service() { fun started() { Log.d(TAG, "Received 'started' from baresip") isNativeReady = true + if (telecom) addMobileUserAgent() Api.net_debug() postServiceEvent(ServiceEvent("started", arrayListOf(callActionUri), System.nanoTime())) callActionUri = "" @@ -1748,15 +1750,18 @@ class BaresipService: Service() { } } - fun handleExternalCall(telecomCall: android.telecom.Call) { + fun handleExternalCall(telecomCall: android.telecom.Call, preferredAor: String? = null) { val uri = telecomCall.details.handle?.schemeSpecificPart ?: "Unknown" - Log.d(TAG, "Handling external call from $uri") + Log.d(TAG, "Handling external call from $uri (preferredAor=$preferredAor)") 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 ua = preferredAor?.let { UserAgent.ofAor(it) } + ?: uas.value.find { it.account.isMobile } + ?: uas.value[0] val telecomState = if (VERSION.SDK_INT >= 31) telecomCall.details.state @@ -1819,6 +1824,34 @@ class BaresipService: Service() { messageUpdate.postValue(System.currentTimeMillis()) } + private fun addMobileUserAgent() { + if (!telecom || Utils.pstnAccountHandle(this) == null) return + + val mobileAor = Utils.getLine1Number(this)?.let { "tel:$it" } ?: "tel:mobile" + + val existingMobileUa = uas.value.find { it.account.isMobile } + if (existingMobileUa != null) { + // If we previously had tel:mobile but now have a real number, replace it + if (existingMobileUa.account.aor == "tel:mobile" && mobileAor != "tel:mobile") { + val updatedUas = uas.value.toMutableList() + updatedUas.remove(existingMobileUa) + uas.value = updatedUas.toList() + } else { + return + } + } + + val account = Account(0L, mobileAor) + account.isMobile = true + account.nickName = "Mobile" + val mobileUa = UserAgent(0L, account) + + val updatedUas = uas.value.toMutableList() + updatedUas.add(mobileUa) + uas.value = updatedUas.toList() + uasStatus.value = UserAgent.statusMap() + } + 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/InCallService.kt b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt index eae8fcd4..8fdaf96f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt @@ -18,8 +18,8 @@ class InCallService : InCallService() { // 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) + val aor = call.details.intentExtras?.getString("aor") + BaresipService.instance?.handleExternalCall(call, aor) } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index aed0ee9b..2332d78e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -1472,17 +1472,15 @@ private fun CallRow( Row( modifier = Modifier .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Absolute.SpaceBetween + horizontalArrangement = Arrangement.Center ) { 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) @@ -1498,37 +1496,14 @@ 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)) + Spacer(modifier = Modifier.width(32.dp)) IconButton( modifier = Modifier.size(48.dp), enabled = dialerState.callButtonsEnabled.value, onClick = { if (!dialerState.callButtonsEnabled.value) return@IconButton dialerState.showCallButton.value = false - dialerState.showCallPstnButton.value = false dialerState.showSuggestions.value = false callClick(ctx, viewModel, dialerState) } @@ -2131,7 +2106,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, else uriText val uri = if (Utils.isTelUri(peerUri)) { - if (dialerState.showCallPstnButton.value) + if (ua.account.isMobile) peerUri else if (ua.account.telProvider == "") { alertTitle.value = ctx.getString(R.string.notice) @@ -2150,7 +2125,7 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, showAlert.value = true return } - else if (dialerState.showCallPstnButton.value && !Utils.isTelUri(uri)) { + else if (ua.account.isMobile && !Utils.isTelUri(uri)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = "Telephone call can only be made to telephone number" showAlert.value = true @@ -2164,14 +2139,15 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, var error = "" if (BaresipService.telecom) { val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - if (dialerState.showCallPstnButton.value) { + if (ua.account.isMobile) { 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) + callExtras.putBoolean("pstnCall", true) + callExtras.putString("aor", aor) extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) try { Log.i(TAG, "Placing Telecom PSTN call to $uri with uap=${ua.uap}") @@ -2301,8 +2277,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.showCallConferenceButton.value = !ua.account.isMobile viewModel.dialerState.callButtonsEnabled.value = true viewModel.dialerState.showSuggestions.value = false dialpadButtonEnabled.value = true diff --git a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt index e62a0c12..b5a07218 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt @@ -5,12 +5,13 @@ import com.tutpro.baresip.BaresipService.Companion.colorblind import com.tutpro.baresip.BaresipService.Companion.uas import com.tutpro.baresip.BaresipService.Companion.uasStatus -class UserAgent(val uap: Long) { +class UserAgent(val uap: Long, virtualAccount: Account? = null) { - val account = Account(Api.ua_account(uap)) - var status = R.drawable.circle_white + val account = virtualAccount ?: Account(Api.ua_account(uap)) + var status = if (uap != 0L) R.drawable.circle_white else R.drawable.circle_green fun callAlloc(xCall: Long, videoMode: Int): Long { + if (uap == 0L) return 0L return Api.ua_call_alloc(uap, xCall, videoMode) } @@ -49,6 +50,7 @@ class UserAgent(val uap: Long) { } fun reRegister() { + if (uap == 0L) return this.status = circleYellow.getValue(colorblind) if (this.account.regint == 0) Api.ua_unregister(this.uap) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index 5b764110..1fe824b7 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -27,6 +27,8 @@ import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore import android.provider.OpenableColumns +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager import android.telecom.TelecomManager import android.telecom.PhoneAccountHandle import android.text.format.DateUtils @@ -1344,6 +1346,36 @@ object Utils { return null } + @SuppressLint("HardwareIds") + fun getLine1Number(ctx: Context): String? { + try { + if (Build.VERSION.SDK_INT >= 33) { + if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED) { + val sm = ctx.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager + val number = sm.getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) + if (number != "") { + Log.i(TAG, "Retrieved SIM number via SubscriptionManager") + return number + } + } + } else { + if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + val tm = ctx.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + @Suppress("DEPRECATION") + val number = tm.line1Number + if (number != null) { + Log.i(TAG, "Retrieved SIM number via TelephonyManager") + return number + } + } + } + } catch (e: Exception) { + Log.w(TAG, "getLine1Number failed: ${e.message}") + } + 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 a45ed327..eb6d068a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt @@ -44,7 +44,6 @@ 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/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a99003f3..49b41fb8 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -530,6 +530,7 @@ Puhelu soi Puhelu on pidossa Puhelu on yhdistetty + Ei saatavilla Tallennus voidaan asettaa päälle tai pois vain silloin, kun puhelu ei ole yhdistetty Puhelun siirto diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c418e07..ec665ca1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -480,6 +480,7 @@ Accept Deny SIP URI + TEL URI Add Delete Edit @@ -513,6 +514,7 @@ Call is ringing Call is on hold Call is connected + Not available Recording can be turned on or off only when call is not connected Call Transfer