Added virtual mobile account
This commit is contained in:
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user