Resolved merge conflict in BaresipService.kt

This commit is contained in:
Juha Heinanen
2026-05-12 11:56:26 +03:00
14 changed files with 961 additions and 700 deletions

View File

@ -5,45 +5,46 @@ import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Locale import java.util.Locale
class Account(val accp: Long) { class Account(val accp: Long, virtualAor: String? = null) {
var isMobile = false
var nickName = "" var nickName = ""
var displayName = Api.account_display_name(accp) var displayName = if (accp != 0L) Api.account_display_name(accp) else ""
val aor = Api.account_aor(accp) val aor = if (accp != 0L) Api.account_aor(accp) else (virtualAor ?: "")
val luri = Api.account_luri(accp) val luri = if (accp != 0L) Api.account_luri(accp) else (virtualAor ?: "")
var authUser = Api.account_auth_user(accp) var authUser = if (accp != 0L) Api.account_auth_user(accp) else ""
var authPass = Api.account_auth_pass(accp) var authPass = if (accp != 0L) Api.account_auth_pass(accp) else ""
var outbound = ArrayList<String>() var outbound = ArrayList<String>()
var mediaNat = Api.account_medianat(accp) var mediaNat = if (accp != 0L) Api.account_medianat(accp) else ""
var stunServer = Api.account_stun_uri(accp) var stunServer = if (accp != 0L) Api.account_stun_uri(accp) else ""
var stunUser = Api.account_stun_user(accp) var stunUser = if (accp != 0L) Api.account_stun_user(accp) else ""
var stunPass = Api.account_stun_pass(accp) var stunPass = if (accp != 0L) Api.account_stun_pass(accp) else ""
var audioCodec = ArrayList<String>() var audioCodec = ArrayList<String>()
var videoCodec = ArrayList<String>() var videoCodec = ArrayList<String>()
var regint = Api.account_regint(accp) var regint = if (accp != 0L) Api.account_regint(accp) else 0
var checkOrigin = Api.account_check_origin(accp) var checkOrigin = if (accp != 0L) Api.account_check_origin(accp) else true
var configuredRegInt = REGISTRATION_INTERVAL var configuredRegInt = REGISTRATION_INTERVAL
var mediaEnc = Api.account_mediaenc(accp) var mediaEnc = if (accp != 0L) Api.account_mediaenc(accp) else ""
var rtcpMux = Api.account_rtcp_mux(accp) var rtcpMux = if (accp != 0L) Api.account_rtcp_mux(accp) else false
var rel100Mode = Api.account_rel100_mode(accp) var rel100Mode = if (accp != 0L) Api.account_rel100_mode(accp) else Api.REL100_DISABLED
var dtmfMode = Api.account_dtmfmode(accp) var dtmfMode = if (accp != 0L) Api.account_dtmfmode(accp) else Api.DTMFMODE_AUTO
var answerMode = Api.account_answermode(accp) var answerMode = if (accp != 0L) Api.account_answermode(accp) else Api.ANSWERMODE_MANUAL
var autoRedirect = Api.account_sip_autoredirect(accp) var autoRedirect = if (accp != 0L) Api.account_sip_autoredirect(accp) else false
var blockUnknown = 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 vmNew = 0
var vmOld = 0 var vmOld = 0
var missedCalls = false var missedCalls = false
var unreadMessages = false var unreadMessages = false
var callHistory = true var callHistory = true
var countryCode = "" var countryCode = ""
var telProvider = Utils.aorDomain(aor) var telProvider = if (accp != 0L) Utils.aorDomain(aor) else ""
var resumeUri = "" var resumeUri = ""
var numericKeypad = false var numericKeypad = false
var customParams = "" var customParams = ""
init { init {
if (accp != 0L) {
if (authPass == "") if (authPass == "")
authPass = NO_AUTH_PASS authPass = NO_AUTH_PASS
@ -68,14 +69,16 @@ class Account(val accp: Long) {
break break
} }
} }
}
val extra = Api.account_extra(accp) val extra = Api.account_extra(accp)
if (Utils.paramExists(extra, "nickname")) if (Utils.paramExists(extra, "nickname"))
nickName = Utils.paramValue(extra, "nickname") nickName = Utils.paramValue(extra, "nickname")
isMobile = Utils.paramExists(extra, "is_mobile")
if (Utils.paramExists(extra, "regint")) if (Utils.paramExists(extra, "regint"))
configuredRegInt = Utils.paramValue(extra, "regint").toInt() configuredRegInt = Utils.paramValue(extra, "regint").toInt()
callHistory = Utils.paramValue(extra, "call_history") == "" callHistory = Utils.paramValue(extra, "call_history") == ""
blockUnknown= Utils.paramExists(extra, "block_unknown") blockUnknown = Utils.paramExists(extra, "block_unknown")
if (Utils.paramExists(extra, "country_code")) if (Utils.paramExists(extra, "country_code"))
countryCode = Utils.paramValue(extra, "country_code") countryCode = Utils.paramValue(extra, "country_code")
if (Utils.paramExists(extra, "tel_provider")) if (Utils.paramExists(extra, "tel_provider"))
@ -86,14 +89,19 @@ class Account(val accp: Long) {
fun print() : String { fun print() : String {
var res = if (displayName != "") var res = if (isMobile) {
"<${aor};transport=udp>"
} else {
if (displayName != "")
"\"${displayName}\" " "\"${displayName}\" "
else else
"" ""
}
if (!isMobile) {
res = "$res<$luri>" res = "$res<$luri>"
if (authUser != "") res += ";auth_user=\"${authUser}\"" if (authUser != "") res += ";auth_user=\"${authUser}\""
}
if ((authPass != "") && !BaresipService.aorPasswords.containsKey(aor)) if ((authPass != "") && !BaresipService.aorPasswords.containsKey(aor))
res += ";auth_pass=\"${authPass}\"" res += ";auth_pass=\"${authPass}\""
@ -155,13 +163,19 @@ class Account(val accp: Long) {
if (autoRedirect) if (autoRedirect)
res += ";sip_autoredirect=yes" res += ";sip_autoredirect=yes"
res += ";ptime=20;regint=${regint};regq=0.5;pubint=0;inreq_allowed=yes;call_transfer=yes" res += ";ptime=20;regint=${regint};regq=0.5;pubint=0;inreq_allowed=yes"
if (isMobile)
res += ";call_transfer=no"
var extra = "" var extra = ""
if (nickName != "") if (nickName != "")
extra += ";nickname=${nickName}" extra += ";nickname=${nickName}"
if (isMobile)
extra += ";is_mobile=yes"
if (!callHistory) if (!callHistory)
extra += ";call_history=no" extra += ";call_history=no"
@ -271,7 +285,7 @@ class Account(val accp: Long) {
BaresipService.filesPath + "/accounts", BaresipService.filesPath + "/accounts",
accounts.toByteArray(Charsets.UTF_8) accounts.toByteArray(Charsets.UTF_8)
) )
Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'") // Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'")
} }
fun ofAor(aor: String): Account? { fun ofAor(aor: String): Account? {

View File

@ -232,14 +232,25 @@ private fun AccountContent(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
val aorText = if (ua.account.isMobile) {
if (ua.account.aor == "sip:mobile@pstn")
stringResource(R.string.not_available)
else
ua.account.aor
} else
ua.account.luri
OutlinedTextField( OutlinedTextField(
value = ua.account.luri, value = aorText,
enabled = false, enabled = false,
onValueChange = {}, onValueChange = {},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(fontSize = 18.sp), textStyle = TextStyle(fontSize = 18.sp),
label = { 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( colors = OutlinedTextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -1056,7 +1067,10 @@ private fun AccountContent(
@Composable @Composable
fun Voicemail() { fun Voicemail() {
val voicemailUriTitle = stringResource(R.string.voicemail_uri) val voicemailUriTitle = stringResource(R.string.voicemail_uri)
val voicemailUriHelp = stringResource(R.string.voicemain_uri_help) val voicemailUriHelp = if (ua.account.isMobile)
stringResource(R.string.voicemain_tel_uri_help)
else
stringResource(R.string.voicemain_uri_help)
val vmUri by viewModel.vmUri.collectAsState() val vmUri by viewModel.vmUri.collectAsState()
Row( Row(
Modifier.fillMaxWidth().padding(end = 10.dp), Modifier.fillMaxWidth().padding(end = 10.dp),
@ -1247,6 +1261,7 @@ private fun AccountContent(
) { ) {
AoR() AoR()
Nickname() Nickname()
if (!ua.account.isMobile) {
DisplayName() DisplayName()
AuthUser() AuthUser()
AuthPass() AuthPass()
@ -1258,7 +1273,9 @@ private fun AccountContent(
RegInt() RegInt()
CheckOrigin() CheckOrigin()
} }
}
BlockUnknown() BlockUnknown()
if (!ua.account.isMobile) {
AudioCodecs(navController, aor) AudioCodecs(navController, aor)
MediaEnc() MediaEnc()
MediaNat() MediaNat()
@ -1270,13 +1287,16 @@ private fun AccountContent(
RtcpMux() RtcpMux()
Rel100() Rel100()
Dtmf() Dtmf()
Answer()
Redirect() Redirect()
Answer()
}
Voicemail() Voicemail()
CountryCode() CountryCode()
if (!ua.account.isMobile)
TelProvider() TelProvider()
NumericKeypad() NumericKeypad()
DefaultAccount() DefaultAccount()
if (!ua.account.isMobile)
CustomParams() CustomParams()
} }
} }
@ -1598,8 +1618,13 @@ private fun checkOnClick(ctx: Context, viewModel: AccountViewModel, ua: UserAgen
var newVmUri = viewModel.vmUri.value.trim() var newVmUri = viewModel.vmUri.value.trim()
if (newVmUri != acc.vmUri) { if (newVmUri != acc.vmUri) {
if (newVmUri != "") { if (newVmUri != "") {
if (acc.isMobile) {
if (!newVmUri.startsWith("tel:")) newVmUri = "tel:$newVmUri"
}
else {
if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri" if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri"
if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}" if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}"
}
if (!Utils.checkUri(newVmUri)) { if (!Utils.checkUri(newVmUri)) {
alertTitle.value = noticeTitle alertTitle.value = noticeTitle
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri),
@ -1654,7 +1679,7 @@ private fun checkOnClick(ctx: Context, viewModel: AccountViewModel, ua: UserAgen
if (viewModel.defaultAccount.value) ua.makeDefault() if (viewModel.defaultAccount.value) ua.makeDefault()
Api.account_debug(acc.accp) // Api.account_debug(acc.accp)
Account.saveAccounts() Account.saveAccounts()

View File

@ -164,8 +164,6 @@ private var newOpusPacketLoss = oldOpusPacketLoss
private var newAudioDelay = BaresipService.audioDelay.toString() private var newAudioDelay = BaresipService.audioDelay.toString()
private var newToneCountry = BaresipService.toneCountry private var newToneCountry = BaresipService.toneCountry
private var newRingtoneUri = "" private var newRingtoneUri = ""
private var oldTelecom = BaresipService.telecom
private var newTelecom = oldTelecom
private var save = false private var save = false
@ -178,8 +176,6 @@ private fun AudioContent(contentPadding: PaddingValues) {
oldSpeakerPhone = Config.variable("speaker_phone") == "yes" oldSpeakerPhone = Config.variable("speaker_phone") == "yes"
newSpeakerPhone = oldSpeakerPhone newSpeakerPhone = oldSpeakerPhone
oldTelecom = Config.variable("telecom") == "yes"
newTelecom = oldTelecom
oldAudioModules = Config.variables("module") oldAudioModules = Config.variables("module")
oldOpusBitrate = Config.variable("opus_bitrate") oldOpusBitrate = Config.variable("opus_bitrate")
oldOpusPacketLoss = Config.variable("opus_packet_loss") oldOpusPacketLoss = Config.variable("opus_packet_loss")
@ -206,7 +202,6 @@ private fun AudioContent(contentPadding: PaddingValues) {
.verticalScroll(state = scrollState), .verticalScroll(state = scrollState),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Telecom()
Ringtone() Ringtone()
ToneCountry() ToneCountry()
SpeakerPhone() SpeakerPhone()
@ -219,37 +214,6 @@ private fun AudioContent(contentPadding: PaddingValues) {
} }
} }
@Composable
private fun Telecom() {
Row(
Modifier
.fillMaxWidth()
.padding(end = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
val telecomTitle = stringResource(R.string.telecom)
val telecomHelp = stringResource(R.string.telecom_help)
Text(text = telecomTitle,
modifier = Modifier
.weight(1f)
.clickable {
alertTitle.value = telecomTitle
alertMessage.value = telecomHelp
showAlert.value = true
},
fontSize = 18.sp)
var telecom by remember { mutableStateOf(oldTelecom) }
Switch(
checked = telecom,
onCheckedChange = {
telecom = it
newTelecom = telecom
}
)
}
}
@Composable @Composable
private fun Ringtone() { private fun Ringtone() {
val ringToneTitle = stringResource(R.string.ringtone) val ringToneTitle = stringResource(R.string.ringtone)
@ -629,13 +593,6 @@ private fun checkOnClick(ctx: Context): Result {
var restart = false var restart = false
if (newTelecom != oldTelecom) {
Config.replaceVariable("telecom", if (newTelecom) "yes" else "no")
BaresipService.telecom = newTelecom
restart = true
save = true
}
if (Preferences(ctx).ringtoneUri != newRingtoneUri) { if (Preferences(ctx).ringtoneUri != newRingtoneUri) {
Preferences(ctx).ringtoneUri = newRingtoneUri Preferences(ctx).ringtoneUri = newRingtoneUri
BaresipService.rt = RingtoneManager.getRingtone(ctx, newRingtoneUri.toUri()) BaresipService.rt = RingtoneManager.getRingtone(ctx, newRingtoneUri.toUri())

View File

@ -1,6 +1,5 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.Manifest
import android.Manifest.permission.RECORD_AUDIO import android.Manifest.permission.RECORD_AUDIO
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
@ -61,7 +60,6 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.NotificationCompat.MessagingStyle
import androidx.core.app.Person import androidx.core.app.Person
@ -70,6 +68,7 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.tutpro.baresip.Utils.e164Uri
import com.tutpro.baresip.Utils.toCircle import com.tutpro.baresip.Utils.toCircle
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -491,18 +490,7 @@ class BaresipService: Service() {
activeNetwork = cm.activeNetwork activeNetwork = cm.activeNetwork
Log.i(TAG, "Active network: $activeNetwork") Log.i(TAG, "Active network: $activeNetwork")
if (telecom)
registerPhoneAccount() registerPhoneAccount()
else
if (btAdapter != null) {
Log.i(TAG, "Registering bluetooth receiver")
val filter = IntentFilter()
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
this.registerReceiver(bluetoothReceiver, filter)
bluetoothReceiverRegistered = true
}
Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable") Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable")
@ -544,16 +532,14 @@ class BaresipService: Service() {
} }
"Call Answer" -> { "Call Answer" -> {
val uap = intent!!.getLongExtra("uap", 0L) val callp = intent!!.getLongExtra("callp", 0L)
val callp = intent.getLongExtra("callp", 0L) val call = Call.ofCallp(callp)
stopRinging() stopRinging()
stopMediaPlayer() stopMediaPlayer()
setCallVolume() setCallVolume()
proximitySensing(proximitySensing) proximitySensing(proximitySensing)
Api.ua_answer(uap, callp, Api.VIDMODE_OFF) call?.answer()
updateStatusNotification() updateStatusNotification()
if (!telecom)
ensureCommunicationMode()
} }
"Call Reject" -> { "Call Reject" -> {
@ -566,20 +552,15 @@ class BaresipService: Service() {
val aor = call.ua.account.aor val aor = call.ua.account.aor
Log.d(TAG, "Aor $aor rejected incoming call $callp from $peerUri") Log.d(TAG, "Aor $aor rejected incoming call $callp from $peerUri")
call.rejected = true call.rejected = true
Api.ua_hangup(call.ua.uap, callp, 486, "Rejected") call.reject()
} }
} }
"Call Hangup" -> { "Call Hangup" -> {
val callp = intent!!.getLongExtra("callp", 0L) val callp = intent!!.getLongExtra("callp", 0L)
Log.d(TAG, "onStartCommand Hangup action for $callp") Log.d(TAG, "onStartCommand Hangup action for $callp")
val connection = ConnectionService.connections[callp]
if (connection != null) {
connection.onDisconnect() // This calls Api.ua_hangup(..., 0, "")
} else {
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
if (call != null) Api.ua_hangup(call.ua.uap, callp, 0, "") call?.hangup(0, "")
}
} }
"Transfer Deny" -> { "Transfer Deny" -> {
@ -731,7 +712,9 @@ class BaresipService: Service() {
if (ev[0] == "create") { if (ev[0] == "create") {
val ua = UserAgent(uap) val ua = UserAgent(uap)
ua.status = if (ua.account.regint == 0) ua.status = if (ua.account.isMobile)
R.drawable.circle_green
else if (ua.account.regint == 0)
R.drawable.circle_white R.drawable.circle_white
else else
circleYellow.getValue(colorblind) circleYellow.getValue(colorblind)
@ -876,21 +859,15 @@ class BaresipService: Service() {
break break
speakerPhone = speakerPhoneAuto speakerPhone = speakerPhoneAuto
stopMediaPlayer() stopMediaPlayer()
val hasTelecom = ConnectionService.connections.containsKey(callp) ||
ConnectionService.pendingOutgoingConnection != null
if (!hasTelecom) {
setCallVolume() setCallVolume()
ensureCommunicationMode() ensureCommunicationMode()
}
proximitySensing(proximitySensing) proximitySensing(proximitySensing)
} }
"call ringing" -> { "call ringing" -> {
if (!telecom) ensureCommunicationMode()
playRingBack() playRingBack()
return return
} }
"call progress" -> { "call progress" -> {
if (!telecom) ensureCommunicationMode()
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
stopMediaPlayer() stopMediaPlayer()
else { else {
@ -934,7 +911,6 @@ class BaresipService: Service() {
) )
if (resourceId != 0) { if (resourceId != 0) {
ensureCommunicationMode() ensureCommunicationMode()
if (!telecom) playUnInterrupted(resourceId, 1)
} else { } else {
Log.e(TAG, "Callwaiting tone $name.wav not found") Log.e(TAG, "Callwaiting tone $name.wav not found")
} }
@ -954,7 +930,6 @@ class BaresipService: Service() {
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()
if (telecom) {
val extras = android.os.Bundle() val extras = android.os.Bundle()
extras.putLong("uap", uap) extras.putLong("uap", uap)
extras.putLong("callp", callp) extras.putLong("callp", callp)
@ -964,14 +939,6 @@ class BaresipService: Service() {
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}") Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}")
} }
} else {
if (!requestAudioFocus(applicationContext)) {
Log.w(TAG, "Audio focus denied for incoming call")
Api.ua_hangup(uap, callp, 486, "Busy Here")
return
}
handleIncomingCall(Call.ofCallp(callp)!!)
}
return return
} }
"call answered" -> { "call answered" -> {
@ -1031,12 +998,6 @@ class BaresipService: Service() {
if (call.state() == Api.CALL_STATE_EARLY) { if (call.state() == Api.CALL_STATE_EARLY) {
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
stopMediaPlayer() stopMediaPlayer()
else {
if (!telecom) {
ConnectionService.connections[callp]?.setRinging()
playRingBack()
}
}
} }
if (call.status.value == "connected" && !call.held && !call.onhold) { if (call.status.value == "connected" && !call.held && !call.onhold) {
if (call.callOnHold.value || call.showOnHoldNotice.value) { if (call.callOnHold.value || call.showOnHoldNotice.value) {
@ -1111,11 +1072,8 @@ class BaresipService: Service() {
if (call != null) { if (call != null) {
call.terminated.value = true call.terminated.value = true
call.remove() call.remove()
if (!Call.inCall()) { if (!Call.inCall())
proximitySensing(false) proximitySensing(false)
if (!telecom)
abandonAudioFocus(applicationContext)
}
} }
ConnectionService.lastDisconnectTime = System.currentTimeMillis() ConnectionService.lastDisconnectTime = System.currentTimeMillis()
val connection = ConnectionService.connections[callp] val connection = ConnectionService.connections[callp]
@ -1174,7 +1132,6 @@ class BaresipService: Service() {
if (tone == "busy") if (tone == "busy")
playBusy() playBusy()
else else
if (!Call.inCall())
ensureCommunicationMode() ensureCommunicationMode()
if (call.dir == "out") if (call.dir == "out")
call.rejected = call.startTime == null && call.rejected = call.startTime == null &&
@ -1473,9 +1430,7 @@ class BaresipService: Service() {
}, audioDelay) }, audioDelay)
} }
val isTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } || if (Call.hasTelecomCall())
ConnectionService.pendingOutgoingConnection != null
if (isTelecom)
executeCall() executeCall()
else if (VERSION.SDK_INT < 31) { else if (VERSION.SDK_INT < 31) {
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION") Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
@ -1511,6 +1466,7 @@ class BaresipService: Service() {
fun started() { fun started() {
Log.d(TAG, "Received 'started' from baresip") Log.d(TAG, "Received 'started' from baresip")
isNativeReady = true isNativeReady = true
addMobileUserAgent()
Api.net_debug() Api.net_debug()
postServiceEvent(ServiceEvent("started", arrayListOf(callActionUri), System.nanoTime())) postServiceEvent(ServiceEvent("started", arrayListOf(callActionUri), System.nanoTime()))
callActionUri = "" callActionUri = ""
@ -1747,6 +1703,178 @@ class BaresipService: Service() {
} }
} }
fun handleExternalCall(telecomCall: android.telecom.Call, preferredAor: String? = null) {
val rawUri = telecomCall.details.handle?.toString() ?: "Unknown"
val uri = try {
java.net.URLDecoder.decode(rawUri, "UTF-8")
} catch (_: Exception) {
rawUri
}
if (uas.value.isEmpty()) {
Log.e(TAG, "No User Agents available to handle external call")
return
}
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
else
@Suppress("DEPRECATION") telecomCall.state
val isIncoming = telecomState == android.telecom.Call.STATE_RINGING
Log.d(TAG, "Handling external call ${if (isIncoming) "from" else "to"} $uri (preferredAor=$preferredAor)")
val initialStatus = when (telecomState) {
android.telecom.Call.STATE_RINGING -> "incoming"
android.telecom.Call.STATE_DIALING, android.telecom.Call.STATE_CONNECTING -> "outgoing"
else -> "connected"
}
if (isIncoming) {
val e164Uri = e164Uri(uri, ua.account.countryCode)
if (ua.account.blockUnknown && Contact.contactName(e164Uri) == e164Uri) {
Log.d(TAG, "Auto-rejecting incoming PSTN call from $uri")
telecomCall.disconnect()
toast(String.format(getString(R.string.call_blocked),
Utils.friendlyUri(this, uri, ua.account)))
if (ua.account.callHistory) {
Blocked(
ua.account.aor,
uri,
"invite",
GregorianCalendar().timeInMillis
).add()
}
return
}
}
val call = Call.ExternalCall(
telecomCall,
ua,
uri,
if (telecomState == android.telecom.Call.STATE_RINGING) "in" else "out",
initialStatus
)
telecomCall.registerCallback(object : android.telecom.Call.Callback() {
override fun onStateChanged(call: android.telecom.Call, state: Int) {
super.onStateChanged(call, state)
val newStatus = when (state) {
android.telecom.Call.STATE_RINGING -> "incoming"
android.telecom.Call.STATE_DIALING, android.telecom.Call.STATE_CONNECTING -> "outgoing"
android.telecom.Call.STATE_ACTIVE -> "connected"
android.telecom.Call.STATE_DISCONNECTED, android.telecom.Call.STATE_DISCONNECTING -> "closed"
android.telecom.Call.STATE_HOLDING -> {
calls.find { it.callp == call.hashCode().toLong() }?.onhold = true
"connected"
}
else -> "connected"
}
calls.find { it.callp == call.hashCode().toLong() }?.let {
if (it.status.value != newStatus) {
it.status.value = newStatus
if (newStatus == "connected")
it.startTime = GregorianCalendar()
postServiceEvent(ServiceEvent(
"call update",
arrayListOf(it.ua.uap, it.callp),
System.nanoTime())
)
if (newStatus == "closed")
handleExternalCallRemoved(call)
}
}
}
})
calls.add(call)
setCallVolume()
ensureCommunicationMode()
postServiceEvent(ServiceEvent(
if (isIncoming) "call incoming" else "call outgoing",
arrayListOf(ua.uap, call.callp),
System.nanoTime())
)
}
fun handleExternalCallRemoved(telecomCall: android.telecom.Call) {
val callp = telecomCall.hashCode().toLong()
val call = calls.find { it.callp == callp }
if (call != null) {
if (call.ua.account.callHistory) {
val historyPeerUri = e164Uri(call.peerUri, call.ua.account.countryCode)
val history = CallHistoryNew(call.ua.account.aor, historyPeerUri, call.dir)
history.stopTime = GregorianCalendar()
history.startTime = call.startTime
history.rejected = call.rejected
history.add()
if (call.dir == "in" && call.startTime == null && !call.rejected)
call.ua.account.missedCalls = true
}
calls.remove(call)
}
if (!Call.inCall()) {
proximitySensing(false)
stopMediaPlayer()
}
messageUpdate.postValue(System.currentTimeMillis())
}
fun addMobileUserAgent() {
if (VERSION.SDK_INT < 29) return
val mobileAccountHandle = Utils.pstnAccountHandle(this)
val existingMobileUa = uas.value.find { it.account.isMobile }
// If mobile account should not exist (role lost or no SIM), remove it if it exists
if (mobileAccountHandle == null) {
if (existingMobileUa != null) {
Log.d(TAG, "Removing Mobile account (role lost or SIM missing)")
existingMobileUa.remove()
Account.saveAccounts()
}
return
}
val userPart = Utils.getLine1Number(this) ?: "mobile"
val mobileAor = "sip:$userPart@pstn"
if (existingMobileUa != null) {
// Update AOR if it previously was sip:mobile@pstn but now a real number
if (existingMobileUa.account.aor == "sip:mobile@pstn" && mobileAor != "sip:mobile@pstn") {
Log.d(TAG, "Updating existing Mobile account AOR to $mobileAor")
val aorField = Account::class.java.getDeclaredField("aor")
aorField.isAccessible = true
aorField.set(existingMobileUa.account, mobileAor)
val luriField = Account::class.java.getDeclaredField("luri")
luriField.isAccessible = true
luriField.set(existingMobileUa.account, mobileAor)
Account.saveAccounts()
}
return
}
Log.d(TAG, "Injecting new virtual Mobile account: $mobileAor")
val account = Account(0L, mobileAor)
account.isMobile = true
account.nickName = "Mobile"
account.regint = 0
account.telProvider = ""
val mobileUa = UserAgent(0L, account)
val updatedUas = uas.value.toMutableList()
updatedUas.add(mobileUa)
uas.value = updatedUas.toList()
uasStatus.value = UserAgent.statusMap()
Account.saveAccounts()
}
private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Toast.makeText(this@BaresipService.applicationContext, message, length).show() Toast.makeText(this@BaresipService.applicationContext, message, length).show()
@ -2048,8 +2176,7 @@ class BaresipService: Service() {
cleanupRunnable = null cleanupRunnable = null
} }
if (isSpeakerphoneOn == speakerPhone) { if (isSpeakerphoneOn == speakerPhone) {
val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } if (Call.hasTelecomCall() || currentMode == MODE_IN_COMMUNICATION) {
if (hasTelecom || currentMode == MODE_IN_COMMUNICATION) {
Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.") Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.")
return return
} }
@ -2076,13 +2203,17 @@ class BaresipService: Service() {
val runnable = Runnable { val runnable = Runnable {
cleanupRunnable = null cleanupRunnable = null
if (Call.inCall()) { if (Call.inCall()) {
val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } val hasTelecom = Call.hasTelecomCall()
if (!hasTelecom && am.mode != MODE_IN_COMMUNICATION && am.mode != AudioManager.MODE_IN_CALL) { if (!hasTelecom && am.mode != MODE_IN_COMMUNICATION && am.mode != AudioManager.MODE_IN_CALL) {
am.mode = MODE_IN_COMMUNICATION am.mode = MODE_IN_COMMUNICATION
Log.d(TAG, "Manual Mode Guard: Setting MODE_IN_COMMUNICATON from ${am.mode}") Log.d(TAG, "Manual Mode Guard: Setting MODE_IN_COMMUNICATON from ${am.mode}")
} }
Log.d(TAG, "Applying speakerphone state: $speakerPhone") Log.d(TAG, "Applying speakerphone state: $speakerPhone")
if (!hasTelecom) { if (InCallService.instance != null) {
Log.d(TAG, "Using InCallService for audio route: $speakerPhone")
@Suppress("DEPRECATION")
InCallService.instance!!.setAudioRoute(if (speakerPhone) android.telecom.CallAudioState.ROUTE_SPEAKER else android.telecom.CallAudioState.ROUTE_EARPIECE)
} else if (!hasTelecom) {
Log.d(TAG, "No Telecom connection, using AudioManager for speaker") Log.d(TAG, "No Telecom connection, using AudioManager for speaker")
Utils.setSpeakerPhone(mainExecutor, am, speakerPhone) Utils.setSpeakerPhone(mainExecutor, am, speakerPhone)
} else { } else {
@ -2117,7 +2248,7 @@ class BaresipService: Service() {
) )
) )
} }
if (!Call.calls().any { ConnectionService.connections.containsKey(it.callp) }) if (!Call.hasTelecomCall())
resetCallVolume() resetCallVolume()
} }
} }
@ -2377,7 +2508,6 @@ class BaresipService: Service() {
var isRecOn = false var isRecOn = false
var toneCountry = "us" var toneCountry = "us"
var proximitySensing = true var proximitySensing = true
var telecom = true
val uas = mutableStateOf(emptyList<UserAgent>()) val uas = mutableStateOf(emptyList<UserAgent>())
val uasStatus = mutableStateOf(emptyMap<String, Int>()) val uasStatus = mutableStateOf(emptyMap<String, Int>())
@ -2471,8 +2601,6 @@ class BaresipService: Service() {
) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
) { ) {
Log.d(TAG, "requestAudioFocus granted") Log.d(TAG, "requestAudioFocus granted")
if (!telecom && isBluetoothHeadsetConnected(ctx))
startBluetoothSco(ctx, 250L, 3)
} }
else { else {
Log.w(TAG, "requestAudioFocus denied") Log.w(TAG, "requestAudioFocus denied")
@ -2481,39 +2609,6 @@ class BaresipService: Service() {
return audioFocusRequest != null return audioFocusRequest != null
} }
fun abandonAudioFocus(ctx: Context) {
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
if (audioFocusRequest != null) {
Log.d(TAG, "Abandoning audio focus")
if (androidx.media.AudioManagerCompat.abandonAudioFocusRequest(
am,
audioFocusRequest!!
) ==
AudioManager.AUDIOFOCUS_REQUEST_GRANTED
) {
audioFocusRequest = null
if (!telecom && isBluetoothHeadsetConnected(ctx))
stopBluetoothSco(ctx)
}
else
Log.e(TAG, "Failed to abandon audio focus")
}
am.mode = MODE_NORMAL
}
private fun isBluetoothHeadsetConnected(ctx: Context): Boolean {
if (VERSION.SDK_INT >= 31 &&
ActivityCompat.checkSelfPermission(
ctx,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_DENIED
)
return false
return btAdapter != null && btAdapter!!.isEnabled &&
btAdapter!!.getProfileConnectionState(BluetoothHeadset.HEADSET) ==
BluetoothAdapter.STATE_CONNECTED
}
private fun isBluetoothScoOn(am: AudioManager): Boolean { private fun isBluetoothScoOn(am: AudioManager): Boolean {
return if (VERSION.SDK_INT < 31) return if (VERSION.SDK_INT < 31)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View File

@ -8,7 +8,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.core.net.toUri import androidx.core.net.toUri
import java.util.* import java.util.*
class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, initialStatus: String) { open class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, initialStatus: String) {
var status: MutableState<String> = mutableStateOf(initialStatus) var status: MutableState<String> = mutableStateOf(initialStatus)
@ -55,11 +55,11 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
BaresipService.calls.remove(this) BaresipService.calls.remove(this)
} }
fun connect(uri: String): Boolean { open fun connect(uri: String): Boolean {
return Api.call_connect(callp, uri) == 0 return Api.call_connect(callp, uri) == 0
} }
fun hold(): Boolean { open fun hold(): Boolean {
if (onhold) return true if (onhold) return true
if (Api.call_hold(callp, true)) { if (Api.call_hold(callp, true)) {
onhold = true onhold = true
@ -71,7 +71,7 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
return false return false
} }
fun resume(): Boolean { open fun resume(): Boolean {
if (!onhold && !held) return true if (!onhold && !held) return true
// 1. Hold other calls first // 1. Hold other calls first
for (c in BaresipService.calls) { for (c in BaresipService.calls) {
@ -94,13 +94,13 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
return false return false
} }
fun transfer(uri: String): Boolean { open fun transfer(uri: String): Boolean {
if (!onhold) hold() if (!onhold) hold()
Log.d(TAG, "Transferring call $callp to $uri") Log.d(TAG, "Transferring call $callp to $uri")
return Api.call_transfer(callp, uri) == 0 return Api.call_transfer(callp, uri) == 0
} }
fun executeTransfer(): Boolean { open fun executeTransfer(): Boolean {
return if (onHoldCall != null) { return if (onHoldCall != null) {
if (Api.call_hold(callp, true)) if (Api.call_hold(callp, true))
Api.call_replace_transfer(onHoldCall!!.callp, callp) Api.call_replace_transfer(onHoldCall!!.callp, callp)
@ -110,31 +110,31 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
false false
} }
fun sendDigit(digit: Char): Int { open fun sendDigit(digit: Char): Int {
return Api.call_send_digit(callp, digit) return Api.call_send_digit(callp, digit)
} }
fun notifySipfrag(code: Int, reason: String) { open fun notifySipfrag(code: Int, reason: String) {
Api.call_notify_sipfrag(callp, code, reason) Api.call_notify_sipfrag(callp, code, reason)
} }
fun duration(): Int { open fun duration(): Int {
return Api.call_duration(callp) return Api.call_duration(callp)
} }
fun stats(stream: String): String { open fun stats(stream: String): String {
return Api.call_stats(callp, stream) return Api.call_stats(callp, stream)
} }
fun state(): Int { open fun state(): Int {
return Api.call_state(callp) return Api.call_state(callp)
} }
fun audioCodecs(): String { open fun audioCodecs(): String {
return Api.call_audio_codecs(callp) return Api.call_audio_codecs(callp)
} }
fun replaces(): Boolean { open fun replaces(): Boolean {
return Api.call_replaces(callp) return Api.call_replaces(callp)
} }
@ -146,10 +146,81 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
if (ua.account.mediaEnc != "") security = R.color.colorTrafficRed if (ua.account.mediaEnc != "") security = R.color.colorTrafficRed
} }
fun destroy() { open fun destroy() {
Api.call_destroy(callp) Api.call_destroy(callp)
} }
open fun hangup(code: Int, reason: String) {
val connection = ConnectionService.connections[callp]
if (connection != null)
connection.onDisconnect()
else
Api.ua_hangup(ua.uap, callp, code, reason)
}
open fun answer() {
Api.ua_answer(ua.uap, callp, Api.VIDMODE_OFF)
}
open fun reject() {
hangup(486, "Busy Here")
}
class ExternalCall(
val telecomCall: android.telecom.Call,
ua: UserAgent,
peerUri: String,
dir: String,
initialStatus: String
) : Call(telecomCall.hashCode().toLong(), ua, peerUri, dir, initialStatus) {
override fun connect(uri: String): Boolean {
telecomCall.answer(android.telecom.VideoProfile.STATE_AUDIO_ONLY)
return true
}
override fun answer() {
telecomCall.answer(android.telecom.VideoProfile.STATE_AUDIO_ONLY)
}
override fun hold(): Boolean {
telecomCall.hold()
onhold = true
callOnHold.value = true
return true
}
override fun resume(): Boolean {
telecomCall.unhold()
onhold = false
callOnHold.value = false
return true
}
override fun hangup(code: Int, reason: String) {
telecomCall.disconnect()
}
override fun reject() {
telecomCall.disconnect()
}
override fun destroy() {
telecomCall.disconnect()
}
override fun sendDigit(digit: Char): Int {
telecomCall.playDtmfTone(digit)
telecomCall.stopDtmfTone()
return 0
}
override fun duration(): Int = 0
override fun stats(stream: String): String = ""
override fun state(): Int = 0
override fun audioCodecs(): String = "PSTN"
}
companion object { companion object {
fun calls(): ArrayList<Call> { fun calls(): ArrayList<Call> {
@ -172,6 +243,12 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str
return BaresipService.calls.isNotEmpty() return BaresipService.calls.isNotEmpty()
} }
fun hasTelecomCall(): Boolean {
return BaresipService.calls.any {
it is ExternalCall || ConnectionService.connections.containsKey(it.callp)
} || ConnectionService.pendingOutgoingConnection != null
}
fun isAnyCallActive(ctx: Context): Boolean { fun isAnyCallActive(ctx: Context): Boolean {
// Check if there exist SIP calls that are not onhold or held // Check if there exist SIP calls that are not onhold or held
if (BaresipService.calls.any { !it.onhold && !it.held }) return true if (BaresipService.calls.any { !it.onhold && !it.held }) return true

View File

@ -226,13 +226,6 @@ object Config {
BaresipService.toneCountry = toneCountry BaresipService.toneCountry = toneCountry
config = "${config}tone_country ${BaresipService.toneCountry}\n" config = "${config}tone_country ${BaresipService.toneCountry}\n"
val telecom = previousVariable("telecom")
if (telecom != "")
BaresipService.telecom = telecom == "yes"
else
BaresipService.telecom = true
config = "${config}telecom ${if (BaresipService.telecom) "yes" else "no"}\n"
save() save()
BaresipService.isConfigInitialized = true BaresipService.isConfigInitialized = true

View File

@ -1,6 +1,7 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.telecom.CallAudioState import android.telecom.CallAudioState
import android.telecom.Connection import android.telecom.Connection
import android.telecom.ConnectionRequest import android.telecom.ConnectionRequest
@ -8,7 +9,6 @@ import android.telecom.ConnectionService
import android.telecom.DisconnectCause import android.telecom.DisconnectCause
import android.telecom.PhoneAccountHandle import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager import android.telecom.TelecomManager
import android.net.Uri
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class ConnectionService : ConnectionService() { class ConnectionService : ConnectionService() {
@ -64,7 +64,8 @@ class ConnectionService : ConnectionService() {
val connection = BaresipConnection(uap, callp) val connection = BaresipConnection(uap, callp)
connections[callp] = connection connections[callp] = connection
connection.setAddress(Uri.fromParts("sip", peerUri, null), TelecomManager.PRESENTATION_ALLOWED) connection.setAddress(Uri.fromParts("sip", peerUri, null),
TelecomManager.PRESENTATION_ALLOWED)
connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or
Connection.CAPABILITY_HOLD or Connection.CAPABILITY_HOLD or
Connection.CAPABILITY_MERGE_CONFERENCE or Connection.CAPABILITY_MERGE_CONFERENCE or
@ -108,6 +109,9 @@ class ConnectionService : ConnectionService() {
val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?: val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?:
nestedExtras?.getBoolean("conferenceCall") ?: false nestedExtras?.getBoolean("conferenceCall") ?: false
val pstnCall = rootExtras?.getBoolean("pstnCall", false) ?:
nestedExtras?.getBoolean("pstnCall") ?: false
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
@ -123,20 +127,15 @@ 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
connection.audioModeIsVoip = true
// Start the SIP connection logic if (!pstnCall) {
if (uap != 0L) { connection.audioModeIsVoip = true
val sipUri = if (destination.startsWith("sip:")) destination else "sip:$destination" val sipUri = if (destination.startsWith("sip:")) destination else "sip:$destination"
BaresipService.instance?.runCall(uap, sipUri, conferenceCall, onHoldCallp) BaresipService.instance?.runCall(uap, sipUri, conferenceCall, onHoldCallp)
} else {
Log.e(TAG, "Cannot start outgoing call: uap is 0")
connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR, "No Account"))
connection.destroy()
pendingOutgoingConnection = null
} }
connection.setDialing() connection.setDialing()
return connection return connection
} }

View File

@ -1,24 +1,53 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
// This is needed in order to allow choosing baresip as default Phone app
class InCallService : Service() {
override fun onBind(intent: Intent): IBinder? {
Log.d(TAG, "InCallService onBind with intent: ${intent.action}")
return null
}
}
/*import android.telecom.InCallService
import android.telecom.Call import android.telecom.Call
import android.telecom.InCallService
import java.lang.ref.WeakReference
class InCallService : InCallService() { class InCallService : InCallService() {
override fun onBind(intent: Intent): IBinder? {
instance = this
return super.onBind(intent)
}
override fun onCallAdded(call: Call) { override fun onCallAdded(call: Call) {
super.onCallAdded(call) super.onCallAdded(call)
// This is triggered when the system wants YOU to show the call UI Log.d(TAG, "InCallService: Call added")
Log.d("Baresip", "InCallService: Call added")
val handle = call.details.accountHandle
val baresipHandle = BaresipService.getPhoneAccountHandle(this)
if (handle == baresipHandle) {
Log.d(TAG, "InCallService: Identified as SIP call")
// SIP call is already managed by ConnectionService/BaresipService
} else {
Log.d(TAG, "InCallService: Identified as PSTN call from $handle")
val aor = call.details.intentExtras?.getString("aor")
BaresipService.instance?.handleExternalCall(call, aor)
} }
}*/ }
override fun onCallRemoved(call: Call) {
super.onCallRemoved(call)
Log.d(TAG, "InCallService: Call removed")
BaresipService.instance?.handleExternalCallRemoved(call)
}
override fun onUnbind(intent: Intent?): Boolean {
instance = null
return super.onUnbind(intent)
}
companion object {
private const val TAG = "Baresip"
private var _instance = WeakReference<InCallService>(null)
var instance: InCallService?
get() = _instance.get()
set(value) {
_instance = WeakReference(value)
}
}
}

View File

@ -12,12 +12,15 @@ import android.content.res.Configuration
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Process import android.os.Process
import android.os.SystemClock import android.os.SystemClock
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.telecom.TelecomManager
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -237,6 +240,12 @@ private fun MainScreen(
BaresipService.isMainVisible = true BaresipService.isMainVisible = true
viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone) viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone)
viewModel.updateCalls(Call.calls().toList()) viewModel.updateCalls(Call.calls().toList())
if (Call.inCall())
(ctx as? Activity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else
(ctx as? Activity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
(Call.call("incoming") ?: Call.calls().lastOrNull())?.let { (Call.call("incoming") ?: Call.calls().lastOrNull())?.let {
spinToAor(viewModel, it.ua.account.aor) spinToAor(viewModel, it.ua.account.aor)
} ?: run { } ?: run {
@ -731,6 +740,9 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
val hasNewVoicemail = remember(aor, accountUpdate) { val hasNewVoicemail = remember(aor, accountUpdate) {
if (aor.isNotEmpty()) (Account.ofAor(aor)?.vmNew ?: 0) > 0 else false if (aor.isNotEmpty()) (Account.ofAor(aor)?.vmNew ?: 0) > 0 else false
} }
val isMobile = remember(aor, accountUpdate) {
if (aor.isNotEmpty()) Account.ofAor(aor)?.isMobile ?: false else false
}
val hasUnreadMessages = remember(aor, accountUpdate) { val hasUnreadMessages = remember(aor, accountUpdate) {
if (aor.isNotEmpty()) Account.ofAor(aor)?.unreadMessages ?: false else false if (aor.isNotEmpty()) Account.ofAor(aor)?.unreadMessages ?: false else false
} }
@ -759,6 +771,12 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
val ua = UserAgent.ofAor(aor)!! val ua = UserAgent.ofAor(aor)!!
val acc = ua.account val acc = ua.account
if (acc.vmUri.isNotEmpty()) { if (acc.vmUri.isNotEmpty()) {
if (isMobile) {
val intent = Intent(ctx, MainActivity::class.java)
intent.putExtra("uap", ua.uap)
intent.putExtra("peer", acc.vmUri)
handleIntent(ctx, viewModel, intent, "call")
} else {
dialogTitle.value = ctx.getString(R.string.voicemail_messages) dialogTitle.value = ctx.getString(R.string.voicemail_messages)
dialogMessage.value = acc.vmMessages(ctx) dialogMessage.value = acc.vmMessages(ctx)
firstText.value = ctx.getString(R.string.cancel) firstText.value = ctx.getString(R.string.cancel)
@ -773,6 +791,7 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
} }
showDialog.value = true showDialog.value = true
} }
}
}, },
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
@ -800,6 +819,7 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont
) )
} }
if (!isMobile)
IconButton( IconButton(
enabled = aor.isNotEmpty(), enabled = aor.isNotEmpty(),
onClick = { onClick = {
@ -896,6 +916,7 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
val calls by viewModel.calls.collectAsState() val calls by viewModel.calls.collectAsState()
val selectedAor by viewModel.selectedAor.collectAsState() val selectedAor by viewModel.selectedAor.collectAsState()
val ua = uas.value.find { it.account.aor == selectedAor }
val aorCalls = calls.filter { it.ua.account.aor == selectedAor } val aorCalls = calls.filter { it.ua.account.aor == selectedAor }
val hasActiveCalls = aorCalls.any { !it.callOnHold.value } val hasActiveCalls = aorCalls.any { !it.callOnHold.value }
val conferenceCall = aorCalls.any { it.conferenceCall } val conferenceCall = aorCalls.any { it.conferenceCall }
@ -1007,7 +1028,12 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
} }
} }
if (!hasActiveCalls || conferenceCall) val showEmptyCard = if (ua?.account?.isMobile == true)
aorCalls.isEmpty()
else
!hasActiveCalls || conferenceCall
if (showEmptyCard)
CallCard(ctx = ctx, viewModel = viewModel, call = null, dialerState = viewModel.dialerState) CallCard(ctx = ctx, viewModel = viewModel, call = null, dialerState = viewModel.dialerState)
Indicator( Indicator(
@ -1470,7 +1496,8 @@ private fun CallRow(
Row( modifier = Modifier Row( modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Absolute.SpaceBetween horizontalArrangement = if (isDialer || call?.showCancelButton?.value == true || call?.showAnswerRejectButtons?.value == true)
Arrangement.Center else Arrangement.SpaceBetween
) { ) {
if (isDialer) { if (isDialer) {
if (dialerState.showCallButton.value) if (dialerState.showCallButton.value)
@ -1495,7 +1522,7 @@ private fun CallRow(
) )
} }
if (dialerState.showCallConferenceButton.value) { if (dialerState.showCallConferenceButton.value) {
Spacer(modifier = Modifier.weight(1f, true)) Spacer(modifier = Modifier.width(32.dp))
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
enabled = dialerState.callButtonsEnabled.value, enabled = dialerState.callButtonsEnabled.value,
@ -1522,26 +1549,14 @@ private fun CallRow(
} }
else { else {
if (call!!.showCancelButton.value) { if (call!!.showCancelButton.value) {
if (!call.conferenceCall)
Spacer(modifier = Modifier.weight(1f))
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
enabled = !call.terminated.value, enabled = !call.terminated.value,
onClick = { onClick = {
if (call.terminated.value) return@IconButton if (call.terminated.value) return@IconButton
call.terminated.value = true call.terminated.value = true
if (BaresipService.telecom) {
val connection = ConnectionService.connections[call.callp]
if (connection != null)
connection.onDisconnect()
else {
Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}") Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}")
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") call.hangup(487, "Request Terminated")
}
} else {
Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}")
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
}
}, },
) { ) {
Icon( Icon(
@ -1551,29 +1566,17 @@ private fun CallRow(
contentDescription = null, contentDescription = null,
) )
} }
Spacer(modifier = Modifier.width(12.dp))
} }
if (call.showHangupButton.value) { if (call.showHangupButton.value) {
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
enabled = !call.terminated.value, enabled = !call.terminated.value,
onClick = { onClick = {
if (call.terminated.value) return@IconButton if (call.terminated.value) return@IconButton
call.terminated.value = true call.terminated.value = true
if (BaresipService.telecom) {
val connection = ConnectionService.connections[call.callp]
if (connection != null)
connection.onDisconnect()
else {
Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}") Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}")
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") call.hangup(487, "Request Terminated")
}
} else {
Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}")
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
}
} }
) { ) {
Icon( Icon(
@ -1583,8 +1586,9 @@ private fun CallRow(
contentDescription = null, contentDescription = null,
) )
} }
}
if (!call.conferenceCall) if (call.showHangupButton.value && !call.conferenceCall)
IconButton( modifier = Modifier.size(48.dp), IconButton( modifier = Modifier.size(48.dp),
onClick = { onClick = {
if (call.callOnHold.value) { if (call.callOnHold.value) {
@ -1609,7 +1613,7 @@ private fun CallRow(
var showTransferDialog by remember { mutableStateOf(false) } var showTransferDialog by remember { mutableStateOf(false) }
if (!call.conferenceCall) if (call.showHangupButton.value && !call.conferenceCall && !call.ua.account.isMobile)
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
enabled = call.transferButtonEnabled.value, enabled = call.transferButtonEnabled.value,
@ -1621,8 +1625,7 @@ private fun CallRow(
showAlert.value = true showAlert.value = true
} }
else { else {
val connection = ConnectionService.connections[call.callp] call.hold()
connection?.onHold()
if (!call.executeTransfer()) { if (!call.executeTransfer()) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.transfer_failed) alertMessage.value = ctx.getString(R.string.transfer_failed)
@ -1889,6 +1892,7 @@ private fun CallRow(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val shouldRequestFocus by call.focusDtmf val shouldRequestFocus by call.focusDtmf
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
if (call.showHangupButton.value)
BasicTextField( BasicTextField(
value = call.dtmfText.value, value = call.dtmfText.value,
onValueChange = { newText -> onValueChange = { newText ->
@ -1943,6 +1947,7 @@ private fun CallRow(
) )
} }
) )
if (call.showHangupButton.value)
LaunchedEffect(shouldRequestFocus) { LaunchedEffect(shouldRequestFocus) {
if (shouldRequestFocus) { if (shouldRequestFocus) {
focusRequester.requestFocus() focusRequester.requestFocus()
@ -1950,6 +1955,7 @@ private fun CallRow(
} }
} }
if (call.showHangupButton.value && !call.ua.account.isMobile)
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
@ -1989,7 +1995,6 @@ private fun CallRow(
contentDescription = null, contentDescription = null,
) )
} }
}
if (call.showAnswerRejectButtons.value) { if (call.showAnswerRejectButtons.value) {
@ -2061,14 +2066,14 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
ctx, ctx,
viewModel, viewModel,
uriText, uriText,
dialerState.showCallConferenceButton.value dialerState
) )
else if (uris.size == 1) else if (uris.size == 1)
makeCall( makeCall(
ctx, ctx,
viewModel, viewModel,
uris[0], uris[0],
dialerState.showCallConferenceButton.value dialerState
) )
else { else {
selectItems.value = uris selectItems.value = uris
@ -2077,7 +2082,7 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
ctx, ctx,
viewModel, viewModel,
uris[index], uris[index],
dialerState.showCallConferenceButton.value dialerState
) )
} }
showSelectItemDialog.value = true showSelectItemDialog.value = true
@ -2095,8 +2100,8 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
} }
} }
private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean, private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String,
onHoldCallp: Long = 0L) { dialerState: ViewModel.DialerState, onHoldCallp: Long = 0L) {
val aor = viewModel.selectedAor.value val aor = viewModel.selectedAor.value
val ua = UserAgent.ofAor(aor)!! val ua = UserAgent.ofAor(aor)!!
val peerUri = if (Utils.isTelNumber(uriText)) val peerUri = if (Utils.isTelNumber(uriText))
@ -2104,12 +2109,15 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer
else else
uriText uriText
val uri = if (Utils.isTelUri(peerUri)) { val uri = if (Utils.isTelUri(peerUri)) {
if (ua.account.telProvider == "") { if (ua.account.isMobile)
peerUri
else if (ua.account.telProvider == "") {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), aor) alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), aor)
showAlert.value = true showAlert.value = true
return return
} }
else
Utils.telToSip(peerUri, ua.account) Utils.telToSip(peerUri, ua.account)
} }
else else
@ -2118,47 +2126,69 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
showAlert.value = true showAlert.value = true
return
}
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
return
} }
else if (Utils.isAudioMode(ctx,AudioManager.MODE_IN_CALL) && else if (Utils.isAudioMode(ctx,AudioManager.MODE_IN_CALL) &&
!Call.calls().any { it.ua.account.aor == ua.account.aor }) !Call.calls().any { it.ua.account.aor == ua.account.aor })
Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show() Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show()
else { else {
viewModel.dialerState.callButtonsEnabled.value = false viewModel.dialerState.callButtonsEnabled.value = false
if (BaresipService.telecom) { var error = ""
val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val extras = android.os.Bundle() if (VERSION.SDK_INT >= 29 && ua.account.isMobile) {
extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, val phoneAccountHandle = Utils.pstnAccountHandle(ctx)
BaresipService.getPhoneAccountHandle(ctx)) if (phoneAccountHandle != null) {
val callExtras = android.os.Bundle() val extras = Bundle().apply {
callExtras.putBoolean("conferenceCall", conferenceCall) putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
}
val callExtras = Bundle()
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}")
tm.placeCall(uri.toUri(), extras)
} catch (e: SecurityException) {
error = "placeCall failed: ${e.message}"
}
}
else
error = "no phone account"
}
else {
val extras = Bundle()
extras.putParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
BaresipService.getPhoneAccountHandle(ctx)
)
val callExtras = Bundle()
callExtras.putBoolean("conferenceCall", dialerState.showCallConferenceButton.value)
callExtras.putLong("uap", ua.uap) callExtras.putLong("uap", ua.uap)
if (onHoldCallp != 0L) if (onHoldCallp != 0L)
callExtras.putLong("onHoldCallp", onHoldCallp) callExtras.putLong("onHoldCallp", onHoldCallp)
extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
try { try {
Log.d(TAG, "Placing Telecom call to $uri with uap=${ua.uap}") Log.d(TAG, "Placing Telecom SIP call to $uri with uap=${ua.uap}")
tm.placeCall(uri.toUri(), extras) tm.placeCall(uri.toUri(), extras)
} catch (e: SecurityException) { } catch (e: SecurityException) {
Log.e(TAG, "placeCall failed: ${e.message}") error = "placeCall failed: ${e.message}"
}
}
if (error != "") {
Log.e(TAG, error)
viewModel.dialerState.callButtonsEnabled.value = true viewModel.dialerState.callButtonsEnabled.value = true
} }
} }
else {
val intent = Intent(ctx, BaresipService::class.java)
intent.action = "Start Call"
intent.putExtra("uap", ua.uap)
intent.putExtra("uri", uri)
intent.putExtra("conferenceCall", conferenceCall)
intent.putExtra("onHoldCallp", onHoldCallp)
ctx.startService(intent)
}
}
} }
private fun answer(ctx: Context, call: Call) { private fun answer(ctx: Context, call: Call) {
Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}") Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}")
if (BaresipService.telecom)
ConnectionService.connections[call.callp]?.setActive()
val intent = Intent(ctx, BaresipService::class.java) val intent = Intent(ctx, BaresipService::class.java)
intent.action = "Call Answer" intent.action = "Call Answer"
intent.putExtra("uap", call.ua.uap) intent.putExtra("uap", call.ua.uap)
@ -2168,19 +2198,7 @@ private fun answer(ctx: Context, call: Call) {
private fun reject(call: Call) { private fun reject(call: Call) {
Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}") Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}")
if (BaresipService.telecom) { call.reject()
val connection = ConnectionService.connections[call.callp]
if (connection != null)
connection.onReject()
else {
call.rejected = true
Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here")
}
}
else {
call.rejected = true
Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here")
}
} }
private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText: String, attended: Boolean) { private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText: String, attended: Boolean) {
@ -2197,23 +2215,15 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText:
val call = ua.currentCall() val call = ua.currentCall()
if (call != null) { if (call != null) {
if (attended) { if (attended) {
val connection = ConnectionService.connections[call.callp] if (call.hold()) {
val success = if (connection != null) {
connection.onHold()
true
} else {
call.hold()
}
if (success) {
call.onhold = true call.onhold = true
call.referTo = uri call.referTo = uri
makeCall(ctx, viewModel, uri, false, call.callp) makeCall(ctx, viewModel, uri, viewModel.dialerState, call.callp)
showCall(ctx, viewModel, ua, call) showCall(ctx, viewModel, ua, call)
} }
} }
else { else {
val connection = ConnectionService.connections[call.callp] call.hold()
connection?.onHold()
if (!call.transfer(uri)) { if (!call.transfer(uri)) {
alertTitle.value = ctx.getString(R.string.notice) alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.transfer_failed) alertMessage.value = ctx.getString(R.string.transfer_failed)
@ -2237,7 +2247,7 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
viewModel.dialerState.callUriEnabled.value = true viewModel.dialerState.callUriEnabled.value = true
}, 100) }, 100)
viewModel.dialerState.showCallButton.value = true viewModel.dialerState.showCallButton.value = true
viewModel.dialerState.showCallConferenceButton.value = true viewModel.dialerState.showCallConferenceButton.value = !ua.account.isMobile
viewModel.dialerState.callButtonsEnabled.value = true viewModel.dialerState.callButtonsEnabled.value = true
viewModel.dialerState.showSuggestions.value = false viewModel.dialerState.showSuggestions.value = false
dialpadButtonEnabled.value = true dialpadButtonEnabled.value = true
@ -2313,7 +2323,7 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
call.callUriLabel.value = ctx.getString(R.string.incoming_call_from_dots) call.callUriLabel.value = ctx.getString(R.string.incoming_call_from_dots)
call.callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account) call.callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account)
} }
call.transferButtonEnabled.value = true call.transferButtonEnabled.value = !ua.account.isMobile
} }
call.callUri2.value = "" call.callUri2.value = ""
call.callTransfer.value = call.onHoldCall != null call.callTransfer.value = call.onHoldCall != null
@ -2395,8 +2405,6 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
when (ev[0]) { when (ev[0]) {
"call rejected" -> { "call rejected" -> {
if (!BaresipService.telecom)
BaresipService.abandonAudioFocus(ctx)
if (aor == viewModel.selectedAor.value) if (aor == viewModel.selectedAor.value)
viewModel.triggerAccountUpdate() viewModel.triggerAccountUpdate()
} }
@ -2439,6 +2447,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
"call established" -> { "call established" -> {
(ctx as? Activity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (aor == viewModel.selectedAor.value) { if (aor == viewModel.selectedAor.value) {
viewModel.dialerState.callButtonsEnabled.value = true // Re-enable dialer viewModel.dialerState.callButtonsEnabled.value = true // Re-enable dialer
val callp = params[1] as Long val callp = params[1] as Long
@ -2521,7 +2530,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
if (call in Call.calls()) if (call in Call.calls())
acceptTransfer(ctx, viewModel, ua, call!!, ev[1]) acceptTransfer(ctx, viewModel, ua, call!!, ev[1])
else else
makeCall(ctx, viewModel, ev[1], false) makeCall(ctx, viewModel, ev[1], viewModel.dialerState)
} }
showDialog.value = true showDialog.value = true
} }
@ -2529,14 +2538,16 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
val callp = params[1] as Long val callp = params[1] as Long
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
if (call in Call.calls()) if (call in Call.calls())
Api.ua_hangup(uap, callp, 487, "Request Terminated") call!!.hangup(487, "Request Terminated")
makeCall(ctx, viewModel, ev[1], false) makeCall(ctx, viewModel, ev[1], viewModel.dialerState)
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
"transfer failed" -> { "transfer failed" -> {
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
"call closed" -> { "call closed" -> {
if (Call.calls().isEmpty())
(ctx as? Activity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
viewModel.updateCalls(Call.calls().toList()) viewModel.updateCalls(Call.calls().toList())
val activity = ctx as? Activity val activity = ctx as? Activity
if (activity != null) { if (activity != null) {

View File

@ -1031,6 +1031,7 @@ private fun SettingsContent(
) { result -> ) { result ->
Log.d(TAG, "dialerRoleRequest result: $result") Log.d(TAG, "dialerRoleRequest result: $result")
viewModel.defaultDialer.value = roleManager.isRoleHeld(RoleManager.ROLE_DIALER) viewModel.defaultDialer.value = roleManager.isRoleHeld(RoleManager.ROLE_DIALER)
BaresipService.instance?.addMobileUserAgent()
} }
Switch( Switch(
checked = defaultDialer, checked = defaultDialer,
@ -1181,14 +1182,14 @@ private fun SettingsContent(
UserAgent() UserAgent()
UniqueContactUri() UniqueContactUri()
AudioSettings(navController) AudioSettings(navController)
if (VERSION.SDK_INT >= 29)
DefaultDialer()
BatteryOptimizations() BatteryOptimizations()
DarkTheme() DarkTheme()
if (VERSION.SDK_INT >= 31) if (VERSION.SDK_INT >= 31)
DynamicColors() DynamicColors()
ColorBlind() ColorBlind()
ProximitySensing() ProximitySensing()
if (VERSION.SDK_INT >= 29)
DefaultDialer()
Debug() Debug()
SipTrace() SipTrace()
Reset(onRestartApp) Reset(onRestartApp)

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.uas
import com.tutpro.baresip.BaresipService.Companion.uasStatus 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)) val account = virtualAccount ?: Account(Api.ua_account(uap))
var status = R.drawable.circle_white var status = if (uap != 0L) R.drawable.circle_white else R.drawable.circle_green
fun callAlloc(xCall: Long, videoMode: Int): Long { fun callAlloc(xCall: Long, videoMode: Int): Long {
if (uap == 0L) return 0L
return Api.ua_call_alloc(uap, xCall, videoMode) return Api.ua_call_alloc(uap, xCall, videoMode)
} }
@ -49,6 +50,7 @@ class UserAgent(val uap: Long) {
} }
fun reRegister() { fun reRegister() {
if (uap == 0L) return
this.status = circleYellow.getValue(colorblind) this.status = circleYellow.getValue(colorblind)
if (this.account.regint == 0) if (this.account.regint == 0)
Api.ua_unregister(this.uap) Api.ua_unregister(this.uap)

View File

@ -1,10 +1,13 @@
package com.tutpro.baresip package com.tutpro.baresip
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.app.role.RoleManager
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Context.ROLE_SERVICE
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
@ -26,6 +29,8 @@ import android.os.Environment
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import android.telecom.TelecomManager import android.telecom.TelecomManager
import android.telecom.PhoneAccountHandle import android.telecom.PhoneAccountHandle
import android.text.format.DateUtils import android.text.format.DateUtils
@ -127,6 +132,9 @@ object Utils {
fun uriUserPart(uri: String): String { fun uriUserPart(uri: String): String {
return if (uri.contains("@")) return if (uri.contains("@"))
uri.substringAfter(":").substringBefore("@") uri.substringAfter(":").substringBefore("@")
else
if (isTelUri(uri))
uri.substringAfter(":").substringBefore(";")
else else
"" ""
} }
@ -180,14 +188,14 @@ object Utils {
return u return u
} }
private fun e164Uri(uri: String, countryCode: String): String { fun e164Uri(uri: String, countryCode: String): String {
if (countryCode == "") return uri if (countryCode == "") return uri
val scheme = uri.take(4) val scheme = uri.take(4)
val userPart = uriUserPart(uri) val userPart = uriUserPart(uri)
return if (userPart.isDigitsOnly()) { return if (userPart.isDigitsOnly()) {
when { when {
userPart.startsWith("00") -> uri.replace("$scheme$userPart", userPart.startsWith("00") -> uri.replace("$scheme$userPart",
scheme + userPart.substring(2)) scheme + "+" + userPart.substring(2))
userPart.startsWith("0") -> uri.replace("${scheme}0", userPart.startsWith("0") -> uri.replace("${scheme}0",
"$scheme$countryCode") "$scheme$countryCode")
else -> uri.replace(scheme, "$scheme$countryCode") else -> uri.replace(scheme, "$scheme$countryCode")
@ -1332,6 +1340,57 @@ object Utils {
return file return file
} }
@RequiresApi(29)
fun pstnAccountHandle(ctx: Context): PhoneAccountHandle? {
val roleManager = ctx.getSystemService(ROLE_SERVICE) as RoleManager
if (ctx.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) ==
PackageManager.PERMISSION_GRANTED &&
roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) {
val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val preferredHandle: PhoneAccountHandle? = tm.userSelectedOutgoingPhoneAccount
if (preferredHandle != null)
return preferredHandle
val baresipHandle = BaresipService.getPhoneAccountHandle(ctx)
val phoneAccounts = tm.callCapablePhoneAccounts.filter { it != baresipHandle }
return if (phoneAccounts.isNotEmpty())
phoneAccounts[0]
else
null
}
else
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") @Suppress("unused")
fun listFilesInDirectory(directoryPath: String): List<File> { fun listFilesInDirectory(directoryPath: String): List<File> {
val directory = File(directoryPath) val directory = File(directoryPath)

View File

@ -109,6 +109,7 @@
</string> </string>
<!-- Account Activity --> <!-- Account Activity -->
<string name="account">Tili</string> <string name="account">Tili</string>
<string name="not_available">Ei saatavana</string>
<string name="account_nickname_help">Lempinimi (jos annettu) millä tämä tili identifioidaan <string name="account_nickname_help">Lempinimi (jos annettu) millä tämä tili identifioidaan
baresip sovelluksessa.</string> baresip sovelluksessa.</string>
<string name="nickname">Lempinimi</string> <string name="nickname">Lempinimi</string>
@ -215,15 +216,16 @@
<string name="redirect_mode_help">Valitsee toteutetaanko puhelun uudelleenohjauspyyntö <string name="redirect_mode_help">Valitsee toteutetaanko puhelun uudelleenohjauspyyntö
automaattisesti vai kysytäänkö vahvistusta.</string> automaattisesti vai kysytäänkö vahvistusta.</string>
<string name="voicemail_uri">Puhepostin URI</string> <string name="voicemail_uri">Puhepostin URI</string>
<string name="voicemain_uri_help">SIP URI, jota käytetään <string name="voicemain_uri_help">SIP URI jota käytetään
puhepostiviestien kuunteluun. Jos URI:a ei ole annettu, tietoa puhepostiviestien kuunteluun. Jos URI:a ei ole annettu, tietoa
mahdollista puhepostiviesteistä (Message Waiting Indications) ei tilata. mahdollista puhepostiviesteistä (Message Waiting Indications) ei tilata.
</string> </string>
<string name="voicemain_tel_uri_help">TEL URI jota käytetään puhepostiviestien kuunteluun.</string>
<string name="country_code">Maakoodi</string> <string name="country_code">Maakoodi</string>
<string name="country_code_help">Tämän tilin E.164-maakoodi. Jos tulevan puhelun tai viestin <string name="country_code_help">Tämän tilin E.164-maakoodi. Jos puhelun tai viestin toisen
From URI:n käyttäjäosa sisältää puhelinnumeron, joka ei ala \'+\' merkillä, ja jos sitä osapuolen URI:n käyttäjäosa sisältää puhelinnumeron, joka ei ala \'+\' merkillä, ja jos sitä
ei löydy yhteystiedoista, niin tämä maakoodi lisätään numeron eteen ja etsintä tehdään ei löydy yhteystiedoista, niin tämä maakoodi lisätään numeron eteen ja etsintä tehdään
uudelleen. Jos puhelinnumero alkaa yhdellä numerolla \'0\', niin numero \'0\' uudelleen. Jos puhelinnumero alkaa yhdellä tai kahdella numerolla \'0\', niin ne
poistetaan ennen maakoodin lisäämistä. poistetaan ennen maakoodin lisäämistä.
</string> </string>
<string name="invalid_country_code">Virheellinen maakoodi \'%1$s\'</string> <string name="invalid_country_code">Virheellinen maakoodi \'%1$s\'</string>
@ -334,9 +336,9 @@
<string name="battery_optimizations_help">Ota akun käytön optimointi pois päältä, <string name="battery_optimizations_help">Ota akun käytön optimointi pois päältä,
jos haluat vähentää todennäköisyyttä, että Android rajoittaa baresip-sovelluksen toimintaa jos haluat vähentää todennäköisyyttä, että Android rajoittaa baresip-sovelluksen toimintaa
ja pääsyä verkkoon.</string> ja pääsyä verkkoon.</string>
<string name="default_phone_app">Oletuspuhelinsovellus</string> <string name="default_phone_app">Oletus puhelusovellus</string>
<string name="dialer_role_not_available">Puhelinrooli ei ole saatavana</string> <string name="dialer_role_not_available">Oletus puhelusovellusrooli ei ole saatavana</string>
<string name="default_phone_app_help">Jos merkity, baresip on oletuspuhelinsovellus. Älä merkitse, jos laitteesi täytyy hallita myös muita kuin SIP-puheluita tai -viestejä.</string> <string name="default_phone_app_help">Jos merkitty, baresip on oletus puhelusovellus.</string>
<string name="listen_address">Kuunteluosoite</string> <string name="listen_address">Kuunteluosoite</string>
<string name="listen_address_help">IP-osoite ja portti muotoa <string name="listen_address_help">IP-osoite ja portti muotoa
\'osoite:portti\', missä baresip kuuntelee sisään tulevia \'osoite:portti\', missä baresip kuuntelee sisään tulevia
@ -435,9 +437,6 @@
<string name="proximity_sensing">Läheisyyden tunnistus</string> <string name="proximity_sensing">Läheisyyden tunnistus</string>
<string name="proximity_sensing_help">Jos merkitty, läheisyyden tunnistus on aktiivinen <string name="proximity_sensing_help">Jos merkitty, läheisyyden tunnistus on aktiivinen
puhelun aikana.</string> puhelun aikana.</string>
<string name="telecom">Telecom-kehys</string>
<string name="telecom_help">Käytä Android Telecom-kehystä puheluihin. Jos sinulla on audioon
liittyviä ongelmia, kokeile auttaako, kun poistat Telecom-kehyksen käytöstä.</string>
<string name="video_size">Videon kehyskoko</string> <string name="video_size">Videon kehyskoko</string>
<string name="video_size_help">Lähetettävän videon kehyskoko (leveys x korkeus)</string> <string name="video_size_help">Lähetettävän videon kehyskoko (leveys x korkeus)</string>
<string name="video_fps">Videokehysten lähetystaajuus</string> <string name="video_fps">Videokehysten lähetystaajuus</string>
@ -530,6 +529,7 @@
<string name="call_is_ringing">Puhelu soi</string> <string name="call_is_ringing">Puhelu soi</string>
<string name="call_is_on_hold">Puhelu on pidossa</string> <string name="call_is_on_hold">Puhelu on pidossa</string>
<string name="call_is_connected">Puhelu on yhdistetty</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 <string name="rec_in_call">Tallennus voidaan asettaa päälle tai pois vain silloin, kun puhelu
ei ole yhdistetty</string> ei ole yhdistetty</string>
<string name="call_transfer">Puhelun siirto</string> <string name="call_transfer">Puhelun siirto</string>

View File

@ -113,6 +113,7 @@
</string> </string>
<!-- Account Activity --> <!-- Account Activity -->
<string name="account">Account</string> <string name="account">Account</string>
<string name="not_available">Not available</string>
<string name="account_nickname_help">Nickname (if any) used to identify this account within <string name="account_nickname_help">Nickname (if any) used to identify this account within
baresip app.</string> baresip app.</string>
<string name="nickname">Nickname</string> <string name="nickname">Nickname</string>
@ -209,11 +210,12 @@
<string name="voicemain_uri_help">SIP URI for checking of voicemail messages. If left empty, voicemail <string name="voicemain_uri_help">SIP URI for checking of voicemail messages. If left empty, voicemail
messages (Message Waiting Indications) are not subscribed to. messages (Message Waiting Indications) are not subscribed to.
</string> </string>
<string name="voicemain_tel_uri_help">TEL URI for checking of voicemail messages.</string>
<string name="country_code">Country Code</string> <string name="country_code">Country Code</string>
<string name="country_code_help">E.164 country code of this account. If From URI userpart of <string name="country_code_help">E.164 country code of this account. If peer URI userpart of
incoming call or message contains a telephone number that does not start with \'+\' sign and if contact call or message contains a telephone number that does not start with \'+\' sign and if contact
lookup fails, the number is prefixed with this country code and contact lookup is lookup fails, the number is prefixed with this country code and contact lookup is
tried again. If the telephone number starts with a single digit \'0\', digit \'0\' is removed tried again. If the number starts with one or two \'0\' digits, they are removed
before the number is prefixed. before the number is prefixed.
</string> </string>
<string name="invalid_country_code">Invalid Country Code \'%1$s\'</string> <string name="invalid_country_code">Invalid Country Code \'%1$s\'</string>
@ -320,9 +322,8 @@
to reduce likelihood that Android restricts baresip\'s access to network or enters baresip to reduce likelihood that Android restricts baresip\'s access to network or enters baresip
to standby state.</string> to standby state.</string>
<string name="default_phone_app">Default Phone App</string> <string name="default_phone_app">Default Phone App</string>
<string name="dialer_role_not_available">Dialer role is not available</string> <string name="dialer_role_not_available">Default phone app role is not available</string>
<string name="default_phone_app_help">If checked, baresip is the default phone app. Do not check <string name="default_phone_app_help">If checked, baresip is the default phone app.</string>
if your device may need to handle also other than SIP calls or messages.</string>
<string name="listen_address">Listen Address</string> <string name="listen_address">Listen Address</string>
<string name="listen_address_help">IP address and port of form \'address:port\' at which baresip listens <string name="listen_address_help">IP address and port of form \'address:port\' at which baresip listens
for incoming SIP requests. If IP address is an IPv6 address, it must be written inside for incoming SIP requests. If IP address is an IPv6 address, it must be written inside
@ -408,9 +409,6 @@
<string name="colorblind_help">Use colorblind friendly registration status icons</string> <string name="colorblind_help">Use colorblind friendly registration status icons</string>
<string name="proximity_sensing">Proximity Sensing</string> <string name="proximity_sensing">Proximity Sensing</string>
<string name="proximity_sensing_help">If checked, proximity sensing is active during calls.</string> <string name="proximity_sensing_help">If checked, proximity sensing is active during calls.</string>
<string name="telecom">Telecom Framework</string>
<string name="telecom_help">Use Android Telecom framework for calls. If you experience audio
related issues, try if it helps when you turn Telecom Framework off.</string>
<string name="video_size">Video Frame Size</string> <string name="video_size">Video Frame Size</string>
<string name="video_size_help">Size of transmitted video frames (width x height)</string> <string name="video_size_help">Size of transmitted video frames (width x height)</string>
<string name="video_fps">Video Frames Per Second</string> <string name="video_fps">Video Frames Per Second</string>
@ -480,6 +478,7 @@
<string name="accept">Accept</string> <string name="accept">Accept</string>
<string name="deny">Deny</string> <string name="deny">Deny</string>
<string name="sip_uri" translatable="false">SIP URI</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="add">Add</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="edit">Edit</string> <string name="edit">Edit</string>