Added virtual mobile account

This commit is contained in:
Juha Heinanen
2026-05-09 19:59:34 +03:00
parent 0a04a3e132
commit 6a6ccba10e
10 changed files with 159 additions and 99 deletions

View File

@ -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<String>()
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<String>()
var videoCodec = ArrayList<String>()
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

View File

@ -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,

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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<File> {
val directory = File(directoryPath)

View File

@ -44,7 +44,6 @@ class ViewModel: ViewModel() {
val callUriLabel: MutableState<String> = mutableStateOf(""),
val showSuggestions: MutableState<Boolean> = mutableStateOf(false),
val showCallButton: MutableState<Boolean> = mutableStateOf(true),
val showCallPstnButton: MutableState<Boolean> = mutableStateOf(true),
val showCallConferenceButton: MutableState<Boolean> = mutableStateOf(true),
val callButtonsEnabled: MutableState<Boolean> = mutableStateOf(true),
val conferenceCall: MutableState<Boolean> = mutableStateOf(false),

View File

@ -530,6 +530,7 @@
<string name="call_is_ringing">Puhelu soi</string>
<string name="call_is_on_hold">Puhelu on pidossa</string>
<string name="call_is_connected">Puhelu on yhdistetty</string>
<string name="not_available">Ei saatavilla</string>
<string name="rec_in_call">Tallennus voidaan asettaa päälle tai pois vain silloin, kun puhelu
ei ole yhdistetty</string>
<string name="call_transfer">Puhelun siirto</string>

View File

@ -480,6 +480,7 @@
<string name="accept">Accept</string>
<string name="deny">Deny</string>
<string name="sip_uri" translatable="false">SIP URI</string>
<string name="tel_uri" translatable="false">TEL URI</string>
<string name="add">Add</string>
<string name="delete">Delete</string>
<string name="edit">Edit</string>
@ -513,6 +514,7 @@
<string name="call_is_ringing">Call is ringing</string>
<string name="call_is_on_hold">Call is on hold</string>
<string name="call_is_connected">Call is connected</string>
<string name="not_available">Not available</string>
<string name="rec_in_call">Recording can be turned on or off only when call is not
connected</string>
<string name="call_transfer">Call Transfer</string>