diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 7a7fd54c..6930feee 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -5,77 +5,80 @@ import java.net.URLDecoder import java.net.URLEncoder import java.util.Locale -class Account(val accp: Long) { +class Account(val accp: Long, virtualAor: String? = null) { + var isMobile = false var nickName = "" - var displayName = Api.account_display_name(accp) - val aor = Api.account_aor(accp) - val luri = Api.account_luri(accp) - var authUser = Api.account_auth_user(accp) - var authPass = Api.account_auth_pass(accp) + var displayName = if (accp != 0L) Api.account_display_name(accp) else "" + val aor = if (accp != 0L) Api.account_aor(accp) else (virtualAor ?: "") + val luri = if (accp != 0L) Api.account_luri(accp) else (virtualAor ?: "") + var authUser = if (accp != 0L) Api.account_auth_user(accp) else "" + var authPass = if (accp != 0L) Api.account_auth_pass(accp) else "" var outbound = ArrayList() - var mediaNat = Api.account_medianat(accp) - var stunServer = Api.account_stun_uri(accp) - var stunUser = Api.account_stun_user(accp) - var stunPass = Api.account_stun_pass(accp) + var mediaNat = if (accp != 0L) Api.account_medianat(accp) else "" + var stunServer = if (accp != 0L) Api.account_stun_uri(accp) else "" + var stunUser = if (accp != 0L) Api.account_stun_user(accp) else "" + var stunPass = if (accp != 0L) Api.account_stun_pass(accp) else "" var audioCodec = ArrayList() var videoCodec = ArrayList() - var regint = Api.account_regint(accp) - var checkOrigin = Api.account_check_origin(accp) + var regint = if (accp != 0L) Api.account_regint(accp) else 0 + var checkOrigin = if (accp != 0L) Api.account_check_origin(accp) else true var configuredRegInt = REGISTRATION_INTERVAL - var mediaEnc = Api.account_mediaenc(accp) - var rtcpMux = Api.account_rtcp_mux(accp) - var rel100Mode = Api.account_rel100_mode(accp) - var dtmfMode = Api.account_dtmfmode(accp) - var answerMode = Api.account_answermode(accp) - var autoRedirect = Api.account_sip_autoredirect(accp) + var mediaEnc = if (accp != 0L) Api.account_mediaenc(accp) else "" + var rtcpMux = if (accp != 0L) Api.account_rtcp_mux(accp) else false + var rel100Mode = if (accp != 0L) Api.account_rel100_mode(accp) else Api.REL100_DISABLED + var dtmfMode = if (accp != 0L) Api.account_dtmfmode(accp) else Api.DTMFMODE_AUTO + var answerMode = if (accp != 0L) Api.account_answermode(accp) else Api.ANSWERMODE_MANUAL + var autoRedirect = if (accp != 0L) Api.account_sip_autoredirect(accp) else false var blockUnknown = false - var vmUri = Api.account_vm_uri(accp) + var vmUri = if (accp != 0L) Api.account_vm_uri(accp) else "" var vmNew = 0 var vmOld = 0 var missedCalls = false var unreadMessages = false var callHistory = true var countryCode = "" - var telProvider = Utils.aorDomain(aor) + var telProvider = if (accp != 0L) Utils.aorDomain(aor) else "" var resumeUri = "" var numericKeypad = false var customParams = "" init { + if (accp != 0L) { + if (authPass == "") + authPass = NO_AUTH_PASS - 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") + isMobile = Utils.paramExists(extra, "is_mobile") if (Utils.paramExists(extra, "regint")) configuredRegInt = Utils.paramValue(extra, "regint").toInt() callHistory = Utils.paramValue(extra, "call_history") == "" - blockUnknown= Utils.paramExists(extra, "block_unknown") + blockUnknown = Utils.paramExists(extra, "block_unknown") if (Utils.paramExists(extra, "country_code")) countryCode = Utils.paramValue(extra, "country_code") if (Utils.paramExists(extra, "tel_provider")) @@ -86,14 +89,19 @@ class Account(val accp: Long) { fun print() : String { - var res = if (displayName != "") - "\"${displayName}\" " - else - "" + var res = if (isMobile) { + "<${aor};transport=udp>" + } else { + if (displayName != "") + "\"${displayName}\" " + else + "" + } - res = "$res<$luri>" - - if (authUser != "") res += ";auth_user=\"${authUser}\"" + if (!isMobile) { + res = "$res<$luri>" + if (authUser != "") res += ";auth_user=\"${authUser}\"" + } if ((authPass != "") && !BaresipService.aorPasswords.containsKey(aor)) res += ";auth_pass=\"${authPass}\"" @@ -155,13 +163,19 @@ class Account(val accp: Long) { if (autoRedirect) 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 = "" if (nickName != "") extra += ";nickname=${nickName}" + if (isMobile) + extra += ";is_mobile=yes" + if (!callHistory) extra += ";call_history=no" @@ -271,7 +285,7 @@ class Account(val accp: Long) { BaresipService.filesPath + "/accounts", 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? { diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt index 20403c95..df40a49d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt @@ -232,14 +232,25 @@ private fun AccountContent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { + val aorText = if (ua.account.isMobile) { + if (ua.account.aor == "sip:mobile@pstn") + 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, @@ -1056,7 +1067,10 @@ private fun AccountContent( @Composable fun Voicemail() { 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() Row( Modifier.fillMaxWidth().padding(end = 10.dp), @@ -1247,37 +1261,43 @@ private fun AccountContent( ) { AoR() Nickname() - DisplayName() - AuthUser() - AuthPass() - if (showPasswordDialog.value) - AskPassword(ctx, navController, ua) - Outbound() - Register() - if (viewModel.register.collectAsState().value) { - RegInt() - CheckOrigin() + if (!ua.account.isMobile) { + DisplayName() + AuthUser() + AuthPass() + if (showPasswordDialog.value) + AskPassword(ctx, navController, ua) + Outbound() + Register() + if (viewModel.register.collectAsState().value) { + RegInt() + CheckOrigin() + } } BlockUnknown() - AudioCodecs(navController, aor) - MediaEnc() - MediaNat() - if (showStun) { - StunServer() - StunUser() - StunPass() + if (!ua.account.isMobile) { + AudioCodecs(navController, aor) + MediaEnc() + MediaNat() + if (showStun) { + StunServer() + StunUser() + StunPass() + } + RtcpMux() + Rel100() + Dtmf() + Redirect() + Answer() } - RtcpMux() - Rel100() - Dtmf() - Answer() - Redirect() Voicemail() CountryCode() - TelProvider() + if (!ua.account.isMobile) + TelProvider() NumericKeypad() DefaultAccount() - CustomParams() + if (!ua.account.isMobile) + CustomParams() } } @@ -1598,8 +1618,13 @@ private fun checkOnClick(ctx: Context, viewModel: AccountViewModel, ua: UserAgen var newVmUri = viewModel.vmUri.value.trim() if (newVmUri != acc.vmUri) { if (newVmUri != "") { - if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri" - if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}" + if (acc.isMobile) { + if (!newVmUri.startsWith("tel:")) newVmUri = "tel:$newVmUri" + } + else { + if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri" + if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}" + } if (!Utils.checkUri(newVmUri)) { alertTitle.value = noticeTitle 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() - Api.account_debug(acc.accp) + // Api.account_debug(acc.accp) Account.saveAccounts() diff --git a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt index fe7df0e8..3583cd3f 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt @@ -164,8 +164,6 @@ private var newOpusPacketLoss = oldOpusPacketLoss private var newAudioDelay = BaresipService.audioDelay.toString() private var newToneCountry = BaresipService.toneCountry private var newRingtoneUri = "" -private var oldTelecom = BaresipService.telecom -private var newTelecom = oldTelecom private var save = false @@ -178,8 +176,6 @@ private fun AudioContent(contentPadding: PaddingValues) { oldSpeakerPhone = Config.variable("speaker_phone") == "yes" newSpeakerPhone = oldSpeakerPhone - oldTelecom = Config.variable("telecom") == "yes" - newTelecom = oldTelecom oldAudioModules = Config.variables("module") oldOpusBitrate = Config.variable("opus_bitrate") oldOpusPacketLoss = Config.variable("opus_packet_loss") @@ -206,7 +202,6 @@ private fun AudioContent(contentPadding: PaddingValues) { .verticalScroll(state = scrollState), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Telecom() Ringtone() ToneCountry() 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 private fun Ringtone() { val ringToneTitle = stringResource(R.string.ringtone) @@ -629,13 +593,6 @@ private fun checkOnClick(ctx: Context): Result { 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) { Preferences(ctx).ringtoneUri = newRingtoneUri BaresipService.rt = RingtoneManager.getRingtone(ctx, newRingtoneUri.toUri()) diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index 5060ab07..2bbe231b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -1,6 +1,5 @@ package com.tutpro.baresip -import android.Manifest import android.Manifest.permission.RECORD_AUDIO import android.annotation.SuppressLint import android.app.Notification @@ -61,7 +60,6 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.Person @@ -70,6 +68,7 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData +import com.tutpro.baresip.Utils.e164Uri import com.tutpro.baresip.Utils.toCircle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -491,18 +490,7 @@ class BaresipService: Service() { activeNetwork = cm.activeNetwork Log.i(TAG, "Active network: $activeNetwork") - if (telecom) - 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 - } + registerPhoneAccount() Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable") @@ -544,16 +532,14 @@ class BaresipService: Service() { } "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() stopMediaPlayer() setCallVolume() proximitySensing(proximitySensing) - Api.ua_answer(uap, callp, Api.VIDMODE_OFF) + call?.answer() updateStatusNotification() - if (!telecom) - ensureCommunicationMode() } "Call Reject" -> { @@ -566,20 +552,15 @@ class BaresipService: Service() { val aor = call.ua.account.aor Log.d(TAG, "Aor $aor rejected incoming call $callp from $peerUri") call.rejected = true - Api.ua_hangup(call.ua.uap, callp, 486, "Rejected") + call.reject() } } "Call Hangup" -> { val callp = intent!!.getLongExtra("callp", 0L) 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) - if (call != null) Api.ua_hangup(call.ua.uap, callp, 0, "") - } + val call = Call.ofCallp(callp) + call?.hangup(0, "") } "Transfer Deny" -> { @@ -731,7 +712,9 @@ class BaresipService: Service() { if (ev[0] == "create") { 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 else circleYellow.getValue(colorblind) @@ -876,21 +859,15 @@ class BaresipService: Service() { break speakerPhone = speakerPhoneAuto stopMediaPlayer() - val hasTelecom = ConnectionService.connections.containsKey(callp) || - ConnectionService.pendingOutgoingConnection != null - if (!hasTelecom) { - setCallVolume() - ensureCommunicationMode() - } + setCallVolume() + ensureCommunicationMode() proximitySensing(proximitySensing) } "call ringing" -> { - if (!telecom) ensureCommunicationMode() playRingBack() return } "call progress" -> { - if (!telecom) ensureCommunicationMode() if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() else { @@ -934,7 +911,6 @@ class BaresipService: Service() { ) if (resourceId != 0) { ensureCommunicationMode() - if (!telecom) playUnInterrupted(resourceId, 1) } else { Log.e(TAG, "Callwaiting tone $name.wav not found") } @@ -954,23 +930,14 @@ class BaresipService: Service() { Log.d(TAG, "Incoming call $uap/$callp/$peerUri") if (Call.ofCallp(callp) == null) Call(callp, ua, peerUri, "in", "incoming").add() - if (telecom) { - val extras = android.os.Bundle() - extras.putLong("uap", uap) - extras.putLong("callp", callp) - extras.putString("peerUri", peerUri) - try { - tm.addNewIncomingCall(getPhoneAccountHandle(this), extras) - } catch (e: Exception) { - 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)!!) + val extras = android.os.Bundle() + extras.putLong("uap", uap) + extras.putLong("callp", callp) + extras.putString("peerUri", peerUri) + try { + tm.addNewIncomingCall(getPhoneAccountHandle(this), extras) + } catch (e: Exception) { + Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}") } return } @@ -1031,12 +998,6 @@ class BaresipService: Service() { if (call.state() == Api.CALL_STATE_EARLY) { if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() - else { - if (!telecom) { - ConnectionService.connections[callp]?.setRinging() - playRingBack() - } - } } if (call.status.value == "connected" && !call.held && !call.onhold) { if (call.callOnHold.value || call.showOnHoldNotice.value) { @@ -1111,11 +1072,8 @@ class BaresipService: Service() { if (call != null) { call.terminated.value = true call.remove() - if (!Call.inCall()) { + if (!Call.inCall()) proximitySensing(false) - if (!telecom) - abandonAudioFocus(applicationContext) - } } ConnectionService.lastDisconnectTime = System.currentTimeMillis() val connection = ConnectionService.connections[callp] @@ -1174,8 +1132,7 @@ class BaresipService: Service() { if (tone == "busy") playBusy() else - if (!Call.inCall()) - ensureCommunicationMode() + ensureCommunicationMode() if (call.dir == "out") call.rejected = call.startTime == null && !reason.startsWith("408") && @@ -1473,9 +1430,7 @@ class BaresipService: Service() { }, audioDelay) } - val isTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } || - ConnectionService.pendingOutgoingConnection != null - if (isTelecom) + if (Call.hasTelecomCall()) executeCall() else if (VERSION.SDK_INT < 31) { Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION") @@ -1511,6 +1466,7 @@ class BaresipService: Service() { fun started() { Log.d(TAG, "Received 'started' from baresip") isNativeReady = true + addMobileUserAgent() Api.net_debug() postServiceEvent(ServiceEvent("started", arrayListOf(callActionUri), System.nanoTime())) 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) { Handler(Looper.getMainLooper()).post { Toast.makeText(this@BaresipService.applicationContext, message, length).show() @@ -2048,8 +2176,7 @@ class BaresipService: Service() { cleanupRunnable = null } if (isSpeakerphoneOn == speakerPhone) { - val hasTelecom = Call.calls().any { ConnectionService.connections.containsKey(it.callp) } - if (hasTelecom || currentMode == MODE_IN_COMMUNICATION) { + if (Call.hasTelecomCall() || currentMode == MODE_IN_COMMUNICATION) { Log.d(TAG, "Already in valid call mode ($currentMode) with correct speaker state.") return } @@ -2076,13 +2203,17 @@ class BaresipService: Service() { val runnable = Runnable { cleanupRunnable = null 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) { am.mode = MODE_IN_COMMUNICATION Log.d(TAG, "Manual Mode Guard: Setting MODE_IN_COMMUNICATON from ${am.mode}") } 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") Utils.setSpeakerPhone(mainExecutor, am, speakerPhone) } else { @@ -2117,7 +2248,7 @@ class BaresipService: Service() { ) ) } - if (!Call.calls().any { ConnectionService.connections.containsKey(it.callp) }) + if (!Call.hasTelecomCall()) resetCallVolume() } } @@ -2377,7 +2508,6 @@ class BaresipService: Service() { var isRecOn = false var toneCountry = "us" var proximitySensing = true - var telecom = true val uas = mutableStateOf(emptyList()) val uasStatus = mutableStateOf(emptyMap()) @@ -2471,8 +2601,6 @@ class BaresipService: Service() { ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED ) { Log.d(TAG, "requestAudioFocus granted") - if (!telecom && isBluetoothHeadsetConnected(ctx)) - startBluetoothSco(ctx, 250L, 3) } else { Log.w(TAG, "requestAudioFocus denied") @@ -2481,39 +2609,6 @@ class BaresipService: Service() { 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 { return if (VERSION.SDK_INT < 31) @Suppress("DEPRECATION") diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index 3dbeb1a4..8b59122d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.core.net.toUri 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 = mutableStateOf(initialStatus) @@ -55,11 +55,11 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str BaresipService.calls.remove(this) } - fun connect(uri: String): Boolean { + open fun connect(uri: String): Boolean { return Api.call_connect(callp, uri) == 0 } - fun hold(): Boolean { + open fun hold(): Boolean { if (onhold) return true if (Api.call_hold(callp, true)) { onhold = true @@ -71,7 +71,7 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str return false } - fun resume(): Boolean { + open fun resume(): Boolean { if (!onhold && !held) return true // 1. Hold other calls first 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 } - fun transfer(uri: String): Boolean { + open fun transfer(uri: String): Boolean { if (!onhold) hold() Log.d(TAG, "Transferring call $callp to $uri") return Api.call_transfer(callp, uri) == 0 } - fun executeTransfer(): Boolean { + open fun executeTransfer(): Boolean { return if (onHoldCall != null) { if (Api.call_hold(callp, true)) 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 } - fun sendDigit(digit: Char): Int { + open fun sendDigit(digit: Char): Int { 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) } - fun duration(): Int { + open fun duration(): Int { return Api.call_duration(callp) } - fun stats(stream: String): String { + open fun stats(stream: String): String { return Api.call_stats(callp, stream) } - fun state(): Int { + open fun state(): Int { return Api.call_state(callp) } - fun audioCodecs(): String { + open fun audioCodecs(): String { return Api.call_audio_codecs(callp) } - fun replaces(): Boolean { + open fun replaces(): Boolean { 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 } - fun destroy() { + open fun destroy() { 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 { fun calls(): ArrayList { @@ -172,6 +243,12 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str 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 { // Check if there exist SIP calls that are not onhold or held if (BaresipService.calls.any { !it.onhold && !it.held }) return true diff --git a/app/src/main/kotlin/com/tutpro/baresip/Config.kt b/app/src/main/kotlin/com/tutpro/baresip/Config.kt index efbedab2..da95803a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Config.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Config.kt @@ -226,13 +226,6 @@ object Config { BaresipService.toneCountry = toneCountry 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() BaresipService.isConfigInitialized = true diff --git a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt index a7b42e98..64b4b862 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt @@ -1,6 +1,7 @@ package com.tutpro.baresip import android.content.Intent +import android.net.Uri import android.telecom.CallAudioState import android.telecom.Connection import android.telecom.ConnectionRequest @@ -8,7 +9,6 @@ import android.telecom.ConnectionService import android.telecom.DisconnectCause import android.telecom.PhoneAccountHandle import android.telecom.TelecomManager -import android.net.Uri import java.util.concurrent.ConcurrentHashMap class ConnectionService : ConnectionService() { @@ -64,7 +64,8 @@ class ConnectionService : ConnectionService() { val connection = BaresipConnection(uap, callp) 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.CAPABILITY_HOLD or Connection.CAPABILITY_MERGE_CONFERENCE or @@ -108,6 +109,9 @@ class ConnectionService : ConnectionService() { val conferenceCall = rootExtras?.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 } ?: nestedExtras?.getLong("onHoldCallp") ?: 0L @@ -123,20 +127,15 @@ class ConnectionService : ConnectionService() { Connection.CAPABILITY_HOLD or Connection.CAPABILITY_MERGE_CONFERENCE or Connection.CAPABILITY_SWAP_CONFERENCE - connection.audioModeIsVoip = true - // Start the SIP connection logic - if (uap != 0L) { + if (!pstnCall) { + connection.audioModeIsVoip = true val sipUri = if (destination.startsWith("sip:")) destination else "sip:$destination" 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() + return connection } diff --git a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt index b65e7d0e..40970583 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/InCallService.kt @@ -1,24 +1,53 @@ package com.tutpro.baresip -import android.app.Service import android.content.Intent 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.InCallService +import java.lang.ref.WeakReference class InCallService : InCallService() { + + override fun onBind(intent: Intent): IBinder? { + instance = this + return super.onBind(intent) + } + override fun onCallAdded(call: Call) { super.onCallAdded(call) - // This is triggered when the system wants YOU to show the call UI - Log.d("Baresip", "InCallService: Call added") + Log.d(TAG, "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(null) + var instance: InCallService? + get() = _instance.get() + set(value) { + _instance = WeakReference(value) + } + } +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt index 99392ac6..d721b0ed 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt @@ -12,12 +12,15 @@ import android.content.res.Configuration import android.media.AudioManager import android.net.Uri import android.os.Build.VERSION +import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.Process import android.os.SystemClock import android.provider.DocumentsContract import android.provider.MediaStore +import android.telecom.TelecomManager +import android.view.WindowManager import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -237,6 +240,12 @@ private fun MainScreen( BaresipService.isMainVisible = true viewModel.updateSpeakerPhoneStatus(BaresipService.speakerPhone) 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 { spinToAor(viewModel, it.ua.account.aor) } ?: run { @@ -731,6 +740,9 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont val hasNewVoicemail = remember(aor, accountUpdate) { 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) { if (aor.isNotEmpty()) Account.ofAor(aor)?.unreadMessages ?: false else false } @@ -759,19 +771,26 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont val ua = UserAgent.ofAor(aor)!! val acc = ua.account if (acc.vmUri.isNotEmpty()) { - dialogTitle.value = ctx.getString(R.string.voicemail_messages) - dialogMessage.value = acc.vmMessages(ctx) - firstText.value = ctx.getString(R.string.cancel) - onFirstClicked.value = {} - secondText.value = "" - lastText.value = ctx.getString(R.string.listen) - onLastClicked.value = { + 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) + dialogMessage.value = acc.vmMessages(ctx) + firstText.value = ctx.getString(R.string.cancel) + onFirstClicked.value = {} + secondText.value = "" + lastText.value = ctx.getString(R.string.listen) + onLastClicked.value = { + val intent = Intent(ctx, MainActivity::class.java) + intent.putExtra("uap", ua.uap) + intent.putExtra("peer", acc.vmUri) + handleIntent(ctx, viewModel, intent, "call") + } + showDialog.value = true } - showDialog.value = true } }, modifier = Modifier @@ -800,22 +819,23 @@ private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavCont ) } - IconButton( - enabled = aor.isNotEmpty(), - onClick = { - navController.navigate("chats/$aor") - }, - modifier = Modifier - .weight(1f) - .size(buttonSize) - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Chat, - contentDescription = null, - Modifier.size(buttonSize), - tint = if (hasUnreadMessages) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary - ) - } + if (!isMobile) + IconButton( + enabled = aor.isNotEmpty(), + onClick = { + navController.navigate("chats/$aor") + }, + modifier = Modifier + .weight(1f) + .size(buttonSize) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Chat, + contentDescription = null, + Modifier.size(buttonSize), + tint = if (hasUnreadMessages) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary + ) + } IconButton( enabled = aor.isNotEmpty(), @@ -896,6 +916,7 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont val calls by viewModel.calls.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 hasActiveCalls = aorCalls.any { !it.callOnHold.value } 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) Indicator( @@ -1470,7 +1496,8 @@ private fun CallRow( Row( modifier = Modifier .fillMaxWidth(), 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 (dialerState.showCallButton.value) @@ -1495,7 +1522,7 @@ private fun CallRow( ) } 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, @@ -1522,26 +1549,14 @@ private fun CallRow( } else { if (call!!.showCancelButton.value) { - if (!call.conferenceCall) - Spacer(modifier = Modifier.weight(1f)) IconButton( modifier = Modifier.size(48.dp), enabled = !call.terminated.value, onClick = { if (call.terminated.value) return@IconButton 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}") - Api.ua_hangup(call.ua.uap, call.callp, 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") - } + Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}") + call.hangup(487, "Request Terminated") }, ) { Icon( @@ -1551,29 +1566,17 @@ private fun CallRow( contentDescription = null, ) } - Spacer(modifier = Modifier.width(12.dp)) } if (call.showHangupButton.value) { - IconButton( modifier = Modifier.size(48.dp), enabled = !call.terminated.value, onClick = { if (call.terminated.value) return@IconButton 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}") - Api.ua_hangup(call.ua.uap, call.callp, 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") - } + Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}") + call.hangup(487, "Request Terminated") } ) { Icon( @@ -1583,312 +1586,313 @@ private fun CallRow( contentDescription = null, ) } + } - if (!call.conferenceCall) - IconButton( modifier = Modifier.size(48.dp), - onClick = { - if (call.callOnHold.value) { - Log.d(TAG, "User requested resume for ${call.callp}") - call.resume() // This now automatically holds other calls - } else { - Log.d(TAG, "User requested hold for ${call.callp}") - call.hold() + if (call.showHangupButton.value && !call.conferenceCall) + IconButton( modifier = Modifier.size(48.dp), + onClick = { + if (call.callOnHold.value) { + Log.d(TAG, "User requested resume for ${call.callp}") + call.resume() // This now automatically holds other calls + } else { + Log.d(TAG, "User requested hold for ${call.callp}") + call.hold() + } + }, + ) { + Icon( + imageVector = Icons.Outlined.PauseCircle, + modifier = Modifier.size(42.dp), + tint = if (call.callOnHold.value) + MaterialTheme.colorScheme.error + else + MaterialTheme.colorScheme.secondary, + contentDescription = null, + ) + } + + var showTransferDialog by remember { mutableStateOf(false) } + + if (call.showHangupButton.value && !call.conferenceCall && !call.ua.account.isMobile) + IconButton( + modifier = Modifier.size(48.dp), + enabled = call.transferButtonEnabled.value, + onClick = { + if (call.onHoldCall != null) { + if (!Api.call_supported(call.callp, Api.REPLACES)) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = ctx.getString(R.string.replaces_not_supported) + showAlert.value = true } - }, - ) { - Icon( - imageVector = Icons.Outlined.PauseCircle, - modifier = Modifier.size(42.dp), - tint = if (call.callOnHold.value) - MaterialTheme.colorScheme.error - else - MaterialTheme.colorScheme.secondary, - contentDescription = null, - ) - } - - var showTransferDialog by remember { mutableStateOf(false) } - - if (!call.conferenceCall) - IconButton( - modifier = Modifier.size(48.dp), - enabled = call.transferButtonEnabled.value, - onClick = { - if (call.onHoldCall != null) { - if (!Api.call_supported(call.callp, Api.REPLACES)) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = ctx.getString(R.string.replaces_not_supported) + else { + call.hold() + if (!call.executeTransfer()) { + alertTitle.value = ctx.getString(R.string.notice) + alertMessage.value = ctx.getString(R.string.transfer_failed) showAlert.value = true } - else { - val connection = ConnectionService.connections[call.callp] - connection?.onHold() - if (!call.executeTransfer()) { - alertTitle.value = ctx.getString(R.string.notice) - alertMessage.value = ctx.getString(R.string.transfer_failed) - showAlert.value = true + } + } + else + showTransferDialog = true + }, + ) { + Icon( + imageVector = Icons.Outlined.ArrowCircleRight, + modifier = Modifier.size(42.dp), + tint = if (call.callTransfer.value) + MaterialTheme.colorScheme.error + else + MaterialTheme.colorScheme.secondary, + contentDescription = null, + ) + } + + if (showTransferDialog) { + + val showDialog = remember { mutableStateOf(true) } + val blindChecked = remember { mutableStateOf(true) } + + if (showDialog.value) + BasicAlertDialog( + onDismissRequest = { + viewModel.requestHideKeyboard() + showDialog.value = false + showTransferDialog = false + } + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.call_transfer), + fontSize = 20.sp, + color = MaterialTheme.colorScheme.onSurface, + ) + var transferUri by remember { mutableStateOf("") } + val suggestions by remember { contactNames } + var filteredSuggestions by remember { mutableStateOf>(emptyList()) } + val focusRequester = remember { FocusRequester() } + val lazyListState = rememberLazyListState() + OutlinedTextField( + value = transferUri, + singleLine = true, + onValueChange = { + if (it != transferUri) { + transferUri = it + if (it.length > 1) { + val normalizedInput = Utils.unaccent(it) + filteredSuggestions = + suggestions.filter { suggestion -> + Utils.unaccent(suggestion) + .contains(normalizedInput, ignoreCase = true) + } + .map { suggestion -> + Utils.buildAnnotatedStringWithHighlight(suggestion, it) + } + } + call.showSuggestions.value = transferUri.length > 1 + } + }, + trailingIcon = { + if (transferUri.isNotEmpty()) + Icon( + Icons.Outlined.Clear, + contentDescription = null, + modifier = Modifier.clickable { + if (call.showSuggestions.value) + call.showSuggestions.value = false + else + transferUri = "" + }, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 4.dp, + end = 4.dp, + top = 12.dp, + bottom = 2.dp + ) + .focusRequester(focusRequester), + label = { Text(stringResource(R.string.transfer_destination)) }, + textStyle = TextStyle(fontSize = 18.sp), + keyboardOptions = if (isDialpadVisible) + KeyboardOptions(keyboardType = KeyboardType.Phone) + else + KeyboardOptions(keyboardType = KeyboardType.Text) + ) + Spacer(modifier = Modifier.height(8.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .shadow(8.dp, RoundedCornerShape(8.dp)) + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp) + ) + .animateContentSize() + ) { + if (call.showSuggestions.value && filteredSuggestions.isNotEmpty()) { + Box(modifier = Modifier + .fillMaxWidth() + .heightIn(max = 150.dp)) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .verticalScrollbar( + state = lazyListState, + width = 6.dp, + ), + horizontalAlignment = Alignment.Start, + state = lazyListState, + ) { + items( + items = filteredSuggestions, + key = { suggestion -> suggestion.toString() } + ) { suggestion -> + Box( + modifier = Modifier + .fillMaxWidth() + .clickable { + transferUri = + suggestion.toString() + call.showSuggestions.value = + false + } + .padding(12.dp) + ) { + Text( + text = suggestion, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 18.sp + ) + } + } + } + } } } - } - else - showTransferDialog = true - }, - ) { - Icon( - imageVector = Icons.Outlined.ArrowCircleRight, - modifier = Modifier.size(42.dp), - tint = if (call.callTransfer.value) - MaterialTheme.colorScheme.error - else - MaterialTheme.colorScheme.secondary, - contentDescription = null, - ) - } - - if (showTransferDialog) { - - val showDialog = remember { mutableStateOf(true) } - val blindChecked = remember { mutableStateOf(true) } - - if (showDialog.value) - BasicAlertDialog( - onDismissRequest = { - viewModel.requestHideKeyboard() - showDialog.value = false - showTransferDialog = false - } - ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = stringResource(R.string.call_transfer), - fontSize = 20.sp, - color = MaterialTheme.colorScheme.onSurface, - ) - var transferUri by remember { mutableStateOf("") } - val suggestions by remember { contactNames } - var filteredSuggestions by remember { mutableStateOf>(emptyList()) } - val focusRequester = remember { FocusRequester() } - val lazyListState = rememberLazyListState() - OutlinedTextField( - value = transferUri, - singleLine = true, - onValueChange = { - if (it != transferUri) { - transferUri = it - if (it.length > 1) { - val normalizedInput = Utils.unaccent(it) - filteredSuggestions = - suggestions.filter { suggestion -> - Utils.unaccent(suggestion) - .contains(normalizedInput, ignoreCase = true) - } - .map { suggestion -> - Utils.buildAnnotatedStringWithHighlight(suggestion, it) - } - } - call.showSuggestions.value = transferUri.length > 1 - } - }, - trailingIcon = { - if (transferUri.isNotEmpty()) - Icon( - Icons.Outlined.Clear, - contentDescription = null, - modifier = Modifier.clickable { - if (call.showSuggestions.value) - call.showSuggestions.value = false - else - transferUri = "" - }, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - modifier = Modifier - .fillMaxWidth() - .padding( - start = 4.dp, - end = 4.dp, - top = 12.dp, - bottom = 2.dp - ) - .focusRequester(focusRequester), - label = { Text(stringResource(R.string.transfer_destination)) }, - textStyle = TextStyle(fontSize = 18.sp), - keyboardOptions = if (isDialpadVisible) - KeyboardOptions(keyboardType = KeyboardType.Phone) - else - KeyboardOptions(keyboardType = KeyboardType.Text) - ) - Spacer(modifier = Modifier.height(8.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .shadow(8.dp, RoundedCornerShape(8.dp)) - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp) - ) - .animateContentSize() - ) { - if (call.showSuggestions.value && filteredSuggestions.isNotEmpty()) { - Box(modifier = Modifier - .fillMaxWidth() - .heightIn(max = 150.dp)) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .verticalScrollbar( - state = lazyListState, - width = 6.dp, - ), - horizontalAlignment = Alignment.Start, - state = lazyListState, - ) { - items( - items = filteredSuggestions, - key = { suggestion -> suggestion.toString() } - ) { suggestion -> - Box( - modifier = Modifier - .fillMaxWidth() - .clickable { - transferUri = - suggestion.toString() - call.showSuggestions.value = - false - } - .padding(12.dp) - ) { - Text( - text = suggestion, - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 18.sp - ) - } - } - } - } - } - } - if (call.replaces()) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.blind), - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp), - ) - Switch( - checked = blindChecked.value, - onCheckedChange = { - blindChecked.value = true - } - ) - } - Spacer(modifier = Modifier.width(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.attended), - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp), - ) - Switch( - checked = !blindChecked.value, - onCheckedChange = { - blindChecked.value = false - } - ) - } - } + if (call.replaces()) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + horizontalArrangement = Arrangement.Start, ) { - TextButton( - onClick = { - viewModel.requestHideKeyboard() - showDialog.value = false - showTransferDialog = false - }, - modifier = Modifier.padding(end = 32.dp), - ) { + Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = stringResource(R.string.cancel), - color = MaterialTheme.colorScheme.onSurfaceVariant + text = stringResource(R.string.blind), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(8.dp), + ) + Switch( + checked = blindChecked.value, + onCheckedChange = { + blindChecked.value = true + } ) } - TextButton( - onClick = { - call.showSuggestions.value = false - var uriText = transferUri.trim() - if (uriText.isNotEmpty()) { - val uris = Contact.contactUris(uriText) - if (uris.size > 1) { - selectItems.value = uris - selectItemAction.value = { index -> - val uri = uris[index] - transfer( - ctx, - viewModel, - call.ua, - if (Utils.isTelNumber(uri)) "tel:$uri" else uri, - !blindChecked.value - ) - showSelectItemDialog.value = false - } - showSelectItemDialog.value = true - } - else { - if (uris.size == 1) uriText = uris[0] + Spacer(modifier = Modifier.width(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.attended), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(8.dp), + ) + Switch( + checked = !blindChecked.value, + onCheckedChange = { + blindChecked.value = false + } + ) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = { + viewModel.requestHideKeyboard() + showDialog.value = false + showTransferDialog = false + }, + modifier = Modifier.padding(end = 32.dp), + ) { + Text( + text = stringResource(R.string.cancel), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + TextButton( + onClick = { + call.showSuggestions.value = false + var uriText = transferUri.trim() + if (uriText.isNotEmpty()) { + val uris = Contact.contactUris(uriText) + if (uris.size > 1) { + selectItems.value = uris + selectItemAction.value = { index -> + val uri = uris[index] transfer( ctx, viewModel, call.ua, - if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText, + if (Utils.isTelNumber(uri)) "tel:$uri" else uri, !blindChecked.value ) + showSelectItemDialog.value = false } - viewModel.requestHideKeyboard() - showDialog.value = false - showTransferDialog = false + showSelectItemDialog.value = true } - }, - modifier = Modifier.padding(end = 16.dp), - ) { - Text( - text = stringResource( - if (blindChecked.value) - R.string.transfer - else - R.string.call - ).uppercase(), - color = MaterialTheme.colorScheme.primary - ) - } + else { + if (uris.size == 1) uriText = uris[0] + transfer( + ctx, + viewModel, + call.ua, + if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText, + !blindChecked.value + ) + } + viewModel.requestHideKeyboard() + showDialog.value = false + showTransferDialog = false + } + }, + modifier = Modifier.padding(end = 16.dp), + ) { + Text( + text = stringResource( + if (blindChecked.value) + R.string.transfer + else + R.string.call + ).uppercase(), + color = MaterialTheme.colorScheme.primary + ) } } } } + } - } + } - val focusRequester = remember { FocusRequester() } - val shouldRequestFocus by call.focusDtmf - val interactionSource = remember { MutableInteractionSource() } + val focusRequester = remember { FocusRequester() } + val shouldRequestFocus by call.focusDtmf + val interactionSource = remember { MutableInteractionSource() } + if (call.showHangupButton.value) BasicTextField( value = call.dtmfText.value, onValueChange = { newText -> @@ -1943,6 +1947,7 @@ private fun CallRow( ) } ) + if (call.showHangupButton.value) LaunchedEffect(shouldRequestFocus) { if (shouldRequestFocus) { focusRequester.requestFocus() @@ -1950,6 +1955,7 @@ private fun CallRow( } } + if (call.showHangupButton.value && !call.ua.account.isMobile) IconButton( modifier = Modifier.size(48.dp), onClick = { @@ -1989,7 +1995,6 @@ private fun CallRow( contentDescription = null, ) } - } if (call.showAnswerRejectButtons.value) { @@ -2061,14 +2066,14 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel ctx, viewModel, uriText, - dialerState.showCallConferenceButton.value + dialerState ) else if (uris.size == 1) makeCall( ctx, viewModel, uris[0], - dialerState.showCallConferenceButton.value + dialerState ) else { selectItems.value = uris @@ -2077,7 +2082,7 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel ctx, viewModel, uris[index], - dialerState.showCallConferenceButton.value + dialerState ) } 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, - onHoldCallp: Long = 0L) { +private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, + dialerState: ViewModel.DialerState, onHoldCallp: Long = 0L) { val aor = viewModel.selectedAor.value val ua = UserAgent.ofAor(aor)!! val peerUri = if (Utils.isTelNumber(uriText)) @@ -2104,13 +2109,16 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer else uriText 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) alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), aor) showAlert.value = true return } - Utils.telToSip(peerUri, ua.account) + else + Utils.telToSip(peerUri, ua.account) } else Utils.uriComplete(peerUri, aor) @@ -2118,47 +2126,69 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri) 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) && !Call.calls().any { it.ua.account.aor == ua.account.aor }) Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show() else { viewModel.dialerState.callButtonsEnabled.value = false - if (BaresipService.telecom) { - val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager - val extras = android.os.Bundle() - extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, - BaresipService.getPhoneAccountHandle(ctx)) - val callExtras = android.os.Bundle() - callExtras.putBoolean("conferenceCall", conferenceCall) + var error = "" + val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + if (VERSION.SDK_INT >= 29 && 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", 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) if (onHoldCallp != 0L) callExtras.putLong("onHoldCallp", onHoldCallp) - extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) + extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) 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) } catch (e: SecurityException) { - Log.e(TAG, "placeCall failed: ${e.message}") - viewModel.dialerState.callButtonsEnabled.value = true + error = "placeCall failed: ${e.message}" } } - 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) + if (error != "") { + Log.e(TAG, error) + viewModel.dialerState.callButtonsEnabled.value = true } } } private fun answer(ctx: Context, call: Call) { 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) intent.action = "Call Answer" intent.putExtra("uap", call.ua.uap) @@ -2168,19 +2198,7 @@ private fun answer(ctx: Context, call: Call) { private fun reject(call: Call) { Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}") - if (BaresipService.telecom) { - 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") - } + call.reject() } 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() if (call != null) { if (attended) { - val connection = ConnectionService.connections[call.callp] - val success = if (connection != null) { - connection.onHold() - true - } else { - call.hold() - } - if (success) { + if (call.hold()) { call.onhold = true call.referTo = uri - makeCall(ctx, viewModel, uri, false, call.callp) + makeCall(ctx, viewModel, uri, viewModel.dialerState, call.callp) showCall(ctx, viewModel, ua, call) } } else { - val connection = ConnectionService.connections[call.callp] - connection?.onHold() + call.hold() if (!call.transfer(uri)) { alertTitle.value = ctx.getString(R.string.notice) 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 }, 100) viewModel.dialerState.showCallButton.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 @@ -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.callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account) } - call.transferButtonEnabled.value = true + call.transferButtonEnabled.value = !ua.account.isMobile } call.callUri2.value = "" call.callTransfer.value = call.onHoldCall != null @@ -2395,8 +2405,6 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params when (ev[0]) { "call rejected" -> { - if (!BaresipService.telecom) - BaresipService.abandonAudioFocus(ctx) if (aor == viewModel.selectedAor.value) viewModel.triggerAccountUpdate() } @@ -2439,6 +2447,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params showCall(ctx, viewModel, ua) } "call established" -> { + (ctx as? Activity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) if (aor == viewModel.selectedAor.value) { viewModel.dialerState.callButtonsEnabled.value = true // Re-enable dialer val callp = params[1] as Long @@ -2521,7 +2530,7 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params if (call in Call.calls()) acceptTransfer(ctx, viewModel, ua, call!!, ev[1]) else - makeCall(ctx, viewModel, ev[1], false) + makeCall(ctx, viewModel, ev[1], viewModel.dialerState) } showDialog.value = true } @@ -2529,14 +2538,16 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params val callp = params[1] as Long val call = Call.ofCallp(callp) if (call in Call.calls()) - Api.ua_hangup(uap, callp, 487, "Request Terminated") - makeCall(ctx, viewModel, ev[1], false) + call!!.hangup(487, "Request Terminated") + makeCall(ctx, viewModel, ev[1], viewModel.dialerState) showCall(ctx, viewModel, ua) } "transfer failed" -> { showCall(ctx, viewModel, ua) } "call closed" -> { + if (Call.calls().isEmpty()) + (ctx as? Activity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) viewModel.updateCalls(Call.calls().toList()) val activity = ctx as? Activity if (activity != null) { diff --git a/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt index 9b6b3cea..5eff76db 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt @@ -1031,6 +1031,7 @@ private fun SettingsContent( ) { result -> Log.d(TAG, "dialerRoleRequest result: $result") viewModel.defaultDialer.value = roleManager.isRoleHeld(RoleManager.ROLE_DIALER) + BaresipService.instance?.addMobileUserAgent() } Switch( checked = defaultDialer, @@ -1181,14 +1182,14 @@ private fun SettingsContent( UserAgent() UniqueContactUri() AudioSettings(navController) + if (VERSION.SDK_INT >= 29) + DefaultDialer() BatteryOptimizations() DarkTheme() if (VERSION.SDK_INT >= 31) DynamicColors() ColorBlind() ProximitySensing() - if (VERSION.SDK_INT >= 29) - DefaultDialer() Debug() SipTrace() Reset(onRestartApp) diff --git a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt index e62a0c12..b5a07218 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt @@ -5,12 +5,13 @@ import com.tutpro.baresip.BaresipService.Companion.colorblind import com.tutpro.baresip.BaresipService.Companion.uas import com.tutpro.baresip.BaresipService.Companion.uasStatus -class UserAgent(val uap: Long) { +class UserAgent(val uap: Long, virtualAccount: Account? = null) { - val account = Account(Api.ua_account(uap)) - var status = R.drawable.circle_white + val account = virtualAccount ?: Account(Api.ua_account(uap)) + var status = if (uap != 0L) R.drawable.circle_white else R.drawable.circle_green fun callAlloc(xCall: Long, videoMode: Int): Long { + if (uap == 0L) return 0L return Api.ua_call_alloc(uap, xCall, videoMode) } @@ -49,6 +50,7 @@ class UserAgent(val uap: Long) { } fun reRegister() { + if (uap == 0L) return this.status = circleYellow.getValue(colorblind) if (this.account.regint == 0) Api.ua_unregister(this.uap) diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index cee56c2c..a6dbbd83 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -1,10 +1,13 @@ package com.tutpro.baresip +import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.app.KeyguardManager +import android.app.role.RoleManager import android.content.ContentResolver import android.content.Context +import android.content.Context.ROLE_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration @@ -26,6 +29,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 @@ -128,7 +133,10 @@ object Utils { return if (uri.contains("@")) uri.substringAfter(":").substringBefore("@") else - "" + if (isTelUri(uri)) + uri.substringAfter(":").substringBefore(";") + else + "" } fun uriMatch(firstUri: String, secondUri: String): Boolean { @@ -180,14 +188,14 @@ object Utils { return u } - private fun e164Uri(uri: String, countryCode: String): String { + fun e164Uri(uri: String, countryCode: String): String { if (countryCode == "") return uri val scheme = uri.take(4) val userPart = uriUserPart(uri) return if (userPart.isDigitsOnly()) { when { userPart.startsWith("00") -> uri.replace("$scheme$userPart", - scheme + userPart.substring(2)) + scheme + "+" + userPart.substring(2)) userPart.startsWith("0") -> uri.replace("${scheme}0", "$scheme$countryCode") else -> uri.replace(scheme, "$scheme$countryCode") @@ -1332,6 +1340,57 @@ object Utils { 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") fun listFilesInDirectory(directoryPath: String): List { val directory = File(directoryPath) diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a99003f3..352f2c6c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -109,6 +109,7 @@ Tili + Ei saatavana Lempinimi (jos annettu) millä tämä tili identifioidaan baresip sovelluksessa. Lempinimi @@ -215,15 +216,16 @@ Valitsee toteutetaanko puhelun uudelleenohjauspyyntö automaattisesti vai kysytäänkö vahvistusta. Puhepostin URI - SIP URI, jota käytetään + SIP URI jota käytetään puhepostiviestien kuunteluun. Jos URI:a ei ole annettu, tietoa mahdollista puhepostiviesteistä (Message Waiting Indications) ei tilata. + TEL URI jota käytetään puhepostiviestien kuunteluun. Maakoodi - Tämän tilin E.164-maakoodi. Jos tulevan puhelun tai viestin - From URI:n käyttäjäosa sisältää puhelinnumeron, joka ei ala \'+\' merkillä, ja jos sitä + Tämän tilin E.164-maakoodi. Jos puhelun tai viestin toisen + 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 - 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ä. Virheellinen maakoodi \'%1$s\' @@ -334,9 +336,9 @@ Ota akun käytön optimointi pois päältä, jos haluat vähentää todennäköisyyttä, että Android rajoittaa baresip-sovelluksen toimintaa ja pääsyä verkkoon. - Oletuspuhelinsovellus - Puhelinrooli ei ole saatavana - Jos merkity, baresip on oletuspuhelinsovellus. Älä merkitse, jos laitteesi täytyy hallita myös muita kuin SIP-puheluita tai -viestejä. + Oletus puhelusovellus + Oletus puhelusovellusrooli ei ole saatavana + Jos merkitty, baresip on oletus puhelusovellus. Kuunteluosoite IP-osoite ja portti muotoa \'osoite:portti\', missä baresip kuuntelee sisään tulevia @@ -435,9 +437,6 @@ Läheisyyden tunnistus Jos merkitty, läheisyyden tunnistus on aktiivinen puhelun aikana. - Telecom-kehys - Käytä Android Telecom-kehystä puheluihin. Jos sinulla on audioon - liittyviä ongelmia, kokeile auttaako, kun poistat Telecom-kehyksen käytöstä. Videon kehyskoko Lähetettävän videon kehyskoko (leveys x korkeus) Videokehysten lähetystaajuus @@ -530,6 +529,7 @@ Puhelu soi Puhelu on pidossa Puhelu on yhdistetty + Ei saatavilla Tallennus voidaan asettaa päälle tai pois vain silloin, kun puhelu ei ole yhdistetty Puhelun siirto diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c418e07..7758f0a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,6 +113,7 @@ Account + Not available Nickname (if any) used to identify this account within baresip app. Nickname @@ -209,11 +210,12 @@ SIP URI for checking of voicemail messages. If left empty, voicemail messages (Message Waiting Indications) are not subscribed to. + TEL URI for checking of voicemail messages. Country Code - E.164 country code of this account. If From URI userpart of - incoming call or message contains a telephone number that does not start with \'+\' sign and if contact + E.164 country code of this account. If peer URI userpart of + 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 - 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. Invalid Country Code \'%1$s\' @@ -320,9 +322,8 @@ to reduce likelihood that Android restricts baresip\'s access to network or enters baresip to standby state. Default Phone App - Dialer role is not available - If checked, baresip is the default phone app. Do not check - if your device may need to handle also other than SIP calls or messages. + Default phone app role is not available + If checked, baresip is the default phone app. Listen Address 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 @@ -408,9 +409,6 @@ Use colorblind friendly registration status icons Proximity Sensing If checked, proximity sensing is active during calls. - Telecom Framework - Use Android Telecom framework for calls. If you experience audio - related issues, try if it helps when you turn Telecom Framework off. Video Frame Size Size of transmitted video frames (width x height) Video Frames Per Second @@ -480,6 +478,7 @@ Accept Deny SIP URI + TEL URI Add Delete Edit