Merge branch 'calling_app'

This commit is contained in:
Juha Heinanen
2026-04-03 15:10:09 +03:00
11 changed files with 767 additions and 532 deletions
+17 -1
View File
@@ -9,6 +9,7 @@
tools:ignore="ForegroundServicesPolicy" /> tools:ignore="ForegroundServicesPolicy" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" /> <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
@@ -25,6 +26,8 @@
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" tools:ignore="FullScreenIntentPolicy" /> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" tools:ignore="FullScreenIntentPolicy" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -83,8 +86,21 @@
<service <service
android:name=".BaresipService" android:name=".BaresipService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="microphone|phoneCall" android:foregroundServiceType="microphone|phoneCall|specialUse"
android:stopWithTask="false" > android:stopWithTask="false" >
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Maintaining SIP registration for incoming VoIP calls" />
</service>
<service
android:name=".ConnectionService"
android:enabled="true"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" >
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service> </service>
<service <service
+8
View File
@@ -1414,6 +1414,14 @@ JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1ismuted(
return audio_ismuted(call_audio((struct call *)call)); return audio_ismuted(call_audio((struct call *)call));
} }
JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1supported(
JNIEnv *env, jobject obj, jlong call, jint tags)
{
(void)env;
(void)obj;
return call_supported((struct call *)call, tags);
}
JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1transfer( JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1transfer(
JNIEnv *env, jobject obj, jlong call, jstring jPeer) JNIEnv *env, jobject obj, jlong call, jstring jPeer)
{ {
@@ -20,6 +20,8 @@ object Api {
const val CALL_STATE_EARLY = 4 const val CALL_STATE_EARLY = 4
const val REPLACES = 1
external fun account_set_display_name(acc: Long, dn: String): Int external fun account_set_display_name(acc: Long, dn: String): Int
external fun account_display_name(acc: Long): String external fun account_display_name(acc: Long): String
external fun account_aor(acc: Long): String external fun account_aor(acc: Long): String
@@ -104,6 +106,8 @@ object Api {
external fun call_replace_transfer(xfer_callp: Long, callp: Long): Boolean external fun call_replace_transfer(xfer_callp: Long, callp: Long): Boolean
external fun call_peer_uri(callp: Long): String external fun call_peer_uri(callp: Long): String
external fun call_diverter_uri(callp: Long): String external fun call_diverter_uri(callp: Long): String
external fun call_supported(callp: Long, header: Int): Boolean
external fun call_destroy(callp: Long) external fun call_destroy(callp: Long)
external fun calls_mute(mute: Boolean) external fun calls_mute(mute: Boolean)
@@ -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
@@ -8,24 +7,20 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
import android.content.res.Configuration import android.content.res.Configuration
import android.database.ContentObserver import android.database.ContentObserver
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager import android.media.AudioManager
import android.media.AudioManager.MODE_IN_COMMUNICATION import android.media.AudioManager.MODE_IN_COMMUNICATION
import android.media.AudioManager.MODE_NORMAL
import android.media.MediaPlayer import android.media.MediaPlayer
import android.media.Ringtone import android.media.Ringtone
import android.media.RingtoneManager import android.media.RingtoneManager
@@ -51,6 +46,8 @@ import android.os.VibratorManager
import android.provider.ContactsContract import android.provider.ContactsContract
import android.provider.Settings import android.provider.Settings
import android.system.OsConstants import android.system.OsConstants
import android.telecom.DisconnectCause
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager import android.telecom.TelecomManager
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
@@ -60,7 +57,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
@@ -69,9 +65,6 @@ 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 androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
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
@@ -98,12 +91,10 @@ class BaresipService: Service() {
private lateinit var pm: PowerManager private lateinit var pm: PowerManager
private lateinit var wm: WifiManager private lateinit var wm: WifiManager
private lateinit var tm: TelecomManager private lateinit var tm: TelecomManager
private lateinit var btm: BluetoothManager
private lateinit var vibrator: Vibrator private lateinit var vibrator: Vibrator
private lateinit var partialWakeLock: PowerManager.WakeLock private lateinit var partialWakeLock: PowerManager.WakeLock
private lateinit var proximityWakeLock: PowerManager.WakeLock private lateinit var proximityWakeLock: PowerManager.WakeLock
private lateinit var wifiLock: WifiManager.WifiLock private lateinit var wifiLock: WifiManager.WifiLock
private lateinit var bluetoothReceiver: BroadcastReceiver
private lateinit var hotSpotReceiver: BroadcastReceiver private lateinit var hotSpotReceiver: BroadcastReceiver
private lateinit var androidContactsObserver: ContentObserver private lateinit var androidContactsObserver: ContentObserver
private lateinit var networkCallback: ConnectivityManager.NetworkCallback private lateinit var networkCallback: ConnectivityManager.NetworkCallback
@@ -119,8 +110,8 @@ class BaresipService: Service() {
private var hotSpotAddresses = mapOf<String, String>() private var hotSpotAddresses = mapOf<String, String>()
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var androidContactsObserverRegistered = false private var androidContactsObserverRegistered = false
private var bluetoothReceiverRegistered = false
private var hotSpotReceiverRegistered = false private var hotSpotReceiverRegistered = false
private var isNotificationInCall = false
private var isServiceClean = false private var isServiceClean = false
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
@@ -128,6 +119,7 @@ class BaresipService: Service() {
super.onCreate() super.onCreate()
Log.i(TAG, "BaresipService onCreate") Log.i(TAG, "BaresipService onCreate")
instance = this
intent = Intent("com.tutpro.baresip.EVENT") intent = Intent("com.tutpro.baresip.EVENT")
intent.setPackage("com.tutpro.baresip") intent.setPackage("com.tutpro.baresip")
@@ -279,9 +271,7 @@ class BaresipService: Service() {
hotSpotReceiverRegistered = true hotSpotReceiverRegistered = true
tm = getSystemService(TELECOM_SERVICE) as TelecomManager tm = getSystemService(TELECOM_SERVICE) as TelecomManager
registerPhoneAccount()
btm = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
btAdapter = btm.adapter
proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
"com.tutpro.baresip:proximity_wakelock") "com.tutpro.baresip:proximity_wakelock")
@@ -291,74 +281,8 @@ class BaresipService: Service() {
wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Baresip") wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Baresip")
else else
wm.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Baresip") wm.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Baresip")
wifiLock.setReferenceCounted(false) wifiLock.setReferenceCounted(false)
bluetoothReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
when (intent.action) {
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED -> {
val state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_DISCONNECTED)
when (state) {
BluetoothHeadset.STATE_CONNECTED -> {
Log.d(TAG, "Bluetooth headset is connected")
if (audioFocusRequest != null)
startBluetoothSco(applicationContext, 1000L, 3)
}
BluetoothHeadset.STATE_DISCONNECTED -> {
Log.d(TAG, "Bluetooth headset is disconnected")
if (audioFocusRequest != null)
stopBluetoothSco(applicationContext)
}
}
}
BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED -> {
val state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED)
when (state) {
BluetoothHeadset.STATE_AUDIO_CONNECTED -> {
Log.d(TAG, "Bluetooth headset audio is connected")
}
BluetoothHeadset.STATE_AUDIO_DISCONNECTED -> {
Log.d(TAG, "Bluetooth headset audio is disconnected")
}
}
}
AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED -> {
val state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
AudioManager.SCO_AUDIO_STATE_DISCONNECTED)
when (state) {
AudioManager.SCO_AUDIO_STATE_CONNECTING -> {
Log.d(TAG, "Bluetooth headset SCO is connecting")
}
AudioManager.SCO_AUDIO_STATE_CONNECTED -> {
Log.d(TAG, "Bluetooth headset SCO is connected")
}
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> {
Log.d(TAG, "Bluetooth headset SCO is disconnected")
resetCallVolume()
}
AudioManager.SCO_AUDIO_STATE_ERROR -> {
Log.d(TAG, "Bluetooth headset SCO state ERROR")
}
}
}
}
}
}
if (btAdapter != null) {
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)
ContextCompat.registerReceiver(applicationContext, bluetoothReceiver, filter,
ContextCompat.RECEIVER_NOT_EXPORTED)
bluetoothReceiverRegistered = true
}
androidContactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { androidContactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(self: Boolean) { override fun onChange(self: Boolean) {
Log.d(TAG, "Android contacts change") Log.d(TAG, "Android contacts change")
@@ -566,6 +490,18 @@ class BaresipService: Service() {
} }
} }
"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, "")
}
}
"Transfer Deny" -> { "Transfer Deny" -> {
val callp = intent!!.getLongExtra("callp", 0L) val callp = intent!!.getLongExtra("callp", 0L)
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
@@ -686,6 +622,7 @@ class BaresipService: Service() {
super.onDestroy() super.onDestroy()
Log.d(TAG, "onDestroy at Baresip Service") Log.d(TAG, "onDestroy at Baresip Service")
cleanService() cleanService()
instance = null
if (isServiceRunning) if (isServiceRunning)
sendBroadcast(Intent("com.tutpro.baresip.Restart")) sendBroadcast(Intent("com.tutpro.baresip.Restart"))
} }
@@ -845,19 +782,20 @@ class BaresipService: Service() {
break break
stopMediaPlayer() stopMediaPlayer()
setCallVolume() setCallVolume()
if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am)
proximitySensing(proximitySensing) proximitySensing(proximitySensing)
} }
"call ringing" -> { "call ringing" -> {
ConnectionService.connections[callp]?.setRinging()
playRingBack() playRingBack()
return return
} }
"call progress" -> { "call progress" -> {
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
stopMediaPlayer() stopMediaPlayer()
else else {
ConnectionService.connections[callp]?.setRinging()
playRingBack() playRingBack()
}
return return
} }
"incoming call" -> { "incoming call" -> {
@@ -871,8 +809,6 @@ class BaresipService: Service() {
Utils.friendlyUri(this, peerUri, ua.account)) Utils.friendlyUri(this, peerUri, ua.account))
else if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO))) else if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO)))
getString(R.string.no_calls) getString(R.string.no_calls)
else if (!requestAudioFocus(applicationContext))
getString(R.string.audio_focus_denied)
else else
"" ""
if (toastMsg != "") { if (toastMsg != "") {
@@ -915,86 +851,21 @@ class BaresipService: Service() {
"call incoming" -> { "call incoming" -> {
val peerUri = ev[1] val peerUri = ev[1]
Log.d(TAG, "Incoming call $uap/$callp/$peerUri") Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
if (Call.ofCallp(callp) == null)
Call(callp, ua, peerUri, "in", "incoming").add() Call(callp, ua, peerUri, "in", "incoming").add()
if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am) val extras = android.os.Bundle()
if (ua.account.answerMode == Api.ANSWERMODE_AUTO) { extras.putLong("uap", uap)
val newIntent = Intent(this, MainActivity::class.java) extras.putLong("callp", callp)
newIntent.flags = extras.putString("peerUri", peerUri)
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK
newIntent.putExtra("action", "call answer")
newIntent.putExtra("callp", callp)
startActivity(newIntent)
return
}
val channelId = HIGH_CHANNEL_ID
val callerNumber = peerUri.split(":")[1].split("@")[0]
if (shouldStartRinging(callerNumber))
startRinging()
if (!Utils.isVisible()) {
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val intent = Intent(applicationContext, MainActivity::class.java)
.putExtra("action", "call show")
.putExtra("callp", callp)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
val nb = NotificationCompat.Builder(this, channelId)
val caller = Utils.friendlyUri(this, peerUri, ua.account)
val callerContact = Contact.findContact(peerUri)
val personBuilder = Person.Builder().setName(caller)
val contactColor = callerContact?.color() ?: "#B0B0B0"
val initial = if (caller.isNotEmpty()) caller.take(1) else "?"
val textAvatarBitmap = Utils.createTextAvatar(initial,contactColor)
var icon = IconCompat.createWithBitmap(textAvatarBitmap)
if (callerContact is Contact.BaresipContact) {
if (callerContact.avatarImage != null)
icon = IconCompat.createWithBitmap(callerContact.avatarImage!!.toCircle())
}
else if (callerContact is Contact.AndroidContact) {
if (callerContact.thumbnailUri != null) {
try { try {
val source = ImageDecoder.createSource(contentResolver, tm.addNewIncomingCall(getPhoneAccountHandle(this), extras)
callerContact.thumbnailUri!!)
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
// decoder.setTargetSize(256, 256)
}
icon = IconCompat.createWithBitmap(bitmap.toCircle())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to load Android contact avatar: $e") Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}")
} }
}
}
val person = personBuilder.setIcon(icon).build()
nb.setSmallIcon(R.drawable.ic_notification_call)
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setContentIntent(pi)
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(false)
.setOngoing(true)
.setContentText(getString(R.string.is_calling))
.setWhen(System.currentTimeMillis())
.setShowWhen(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_MAX)
if (VERSION.SDK_INT < 34 || nm.canUseFullScreenIntent())
nb.setFullScreenIntent(pi, true)
val answerIntent = Intent(applicationContext, MainActivity::class.java)
.putExtra("action", "call answer")
.putExtra("callp", callp)
val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE,
answerIntent, piFlags)
val rejectIntent = Intent(this, BaresipService::class.java)
rejectIntent.action = "Call Reject"
rejectIntent.putExtra("callp", callp)
val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, piFlags)
nb.setStyle(NotificationCompat.CallStyle.forIncomingCall(person, rpi, api))
nm.notify(CALL_NOTIFICATION_ID, nb.build())
return return
} }
}
"call answered" -> { "call answered" -> {
stopMediaPlayer() stopMediaPlayer()
if (call!!.status.value == "incoming") if (call!!.status.value == "incoming")
@@ -1006,28 +877,32 @@ class BaresipService: Service() {
stopMediaPlayer() stopMediaPlayer()
} }
"call established" -> { "call established" -> {
ConnectionService.connections[callp]?.setActive()
nm.cancel(CALL_NOTIFICATION_ID) nm.cancel(CALL_NOTIFICATION_ID)
Log.d(TAG, "AoR $aor call $callp established in mode ${am.mode}") Log.d(TAG, "AoR $aor call $callp established")
if (am.mode != MODE_IN_COMMUNICATION)
am.mode = MODE_IN_COMMUNICATION
call!!.status.value = "connected" call!!.status.value = "connected"
call.onhold = false call.onhold = false
if (ua.account.callHistory)
call.startTime = GregorianCalendar() call.startTime = GregorianCalendar()
updateStatusNotification()
if (!isMainVisible) if (!isMainVisible)
return return
} }
"call update" -> { "call update" -> {
call!!.held = when (ev[1].toInt()) { val held = when (ev[1].toInt()) {
Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true
else /* Api.SDP_SENDONLY, Api.SDP_SENDRECV */ -> false else -> false
} }
call!!.held = held
val connection = ConnectionService.connections[callp]
if (held) connection?.setOnHold() else connection?.setActive()
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 else {
ConnectionService.connections[callp]?.setRinging()
playRingBack() playRingBack()
} }
}
if (!isMainVisible || call.status.value != "connected") if (!isMainVisible || call.status.value != "connected")
return return
} }
@@ -1090,6 +965,11 @@ class BaresipService: Service() {
} }
"call closed" -> { "call closed" -> {
Log.d(TAG, "AoR $aor call $callp is closed prm: ${ev[1]}") Log.d(TAG, "AoR $aor call $callp is closed prm: ${ev[1]}")
ConnectionService.connections[callp]?.let {
it.setDisconnected(DisconnectCause(DisconnectCause.REMOTE))
it.destroy()
ConnectionService.connections.remove(callp)
}
nm.cancel(CALL_NOTIFICATION_ID) nm.cancel(CALL_NOTIFICATION_ID)
if (call != null) { if (call != null) {
stopRinging() stopRinging()
@@ -1112,16 +992,15 @@ class BaresipService: Service() {
call.onHoldCall = null call.onHoldCall = null
} }
call.remove() call.remove()
updateStatusNotification()
if (call.conferenceCall && ua.calls().isEmpty()) if (call.conferenceCall && ua.calls().isEmpty())
Api.module_unload("mixminus") Api.module_unload("mixminus")
val reason = ev[1] val reason = ev[1]
val tone = ev[2] val tone = ev[2]
if (tone == "busy") { if (tone == "busy")
playBusy() playBusy()
}
else if (!Call.inCall()) { else if (!Call.inCall()) {
resetCallVolume() resetCallVolume()
abandonAudioFocus(applicationContext)
proximitySensing(false) proximitySensing(false)
} }
if (call.dir == "out") if (call.dir == "out")
@@ -1410,6 +1289,40 @@ class BaresipService: Service() {
} }
} }
fun runCall(uap: Long, uri: String, conferenceCall: Boolean, onHoldCallp: Long) {
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({
val ua = UserAgent.ofUap(uap)
if (ua != null) {
if (conferenceCall && ua.calls().isEmpty())
Api.module_load("mixminus")
val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
if (callp != 0L) {
ConnectionService.promoteOutgoingConnection(callp)
val onHoldCall = Call.ofCallp(onHoldCallp)
val call = Call(callp, ua, uri, "out", "outgoing")
call.onHoldCall = onHoldCall
call.conferenceCall = conferenceCall
call.add()
if (onHoldCall != null)
onHoldCall.newCall = call
if (!call.connect(uri)) {
Log.w(TAG, "call_connect $callp failed")
ConnectionService.onCallClosed(callp)
call.remove()
call.destroy()
}
}
else
ConnectionService.pendingOutgoingConnection?.let {
it.setDisconnected(DisconnectCause(DisconnectCause.ERROR))
it.destroy()
ConnectionService.pendingOutgoingConnection = null
}
}
}, audioDelay)
}
@Suppress("unused") @Suppress("unused")
@Keep @Keep
fun started() { fun started() {
@@ -1466,7 +1379,6 @@ class BaresipService: Service() {
if (VERSION.SDK_INT >= 31) if (VERSION.SDK_INT >= 31)
snb.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE snb.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE
snb.setOngoing(true) snb.setOngoing(true)
snb.setCategory(Notification.CATEGORY_SERVICE)
val notification = snb.build() val notification = snb.build()
notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or
Notification.FLAG_ONGOING_EVENT Notification.FLAG_ONGOING_EVENT
@@ -1494,22 +1406,14 @@ class BaresipService: Service() {
.setContentIntent(pi) .setContentIntent(pi)
.setDeleteIntent(dpi) .setDeleteIntent(dpi)
.setOngoing(true) .setOngoing(true)
.setCategory(Notification.CATEGORY_SERVICE)
.setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notificationLayout) .setCustomContentView(notificationLayout)
val notification = buildStatusNotification() val notification = buildStatusNotification()
try { try {
if (VERSION.SDK_INT >= 29) if (VERSION.SDK_INT >= 34)
startForeground( startForeground(STATUS_NOTIFICATION_ID, notification,
STATUS_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
if (VERSION.SDK_INT >= 30) {
if (ContextCompat.checkSelfPermission(this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED)
FOREGROUND_SERVICE_TYPE_PHONE_CALL or FOREGROUND_SERVICE_TYPE_MICROPHONE
else
FOREGROUND_SERVICE_TYPE_PHONE_CALL
}
else
FOREGROUND_SERVICE_TYPE_PHONE_CALL
)
else else
startForeground(STATUS_NOTIFICATION_ID, notification) startForeground(STATUS_NOTIFICATION_ID, notification)
} catch (e: Exception) { } catch (e: Exception) {
@@ -1517,9 +1421,57 @@ class BaresipService: Service() {
} }
} }
private fun updateStatusNotification() { fun updateStatusNotification() {
val activeCall = Call.calls().find {
it.status.value == "connected" || it.status.value == "outgoing" || it.status.value == "answered"
}
val builder = NotificationCompat.Builder(this, LOW_CHANNEL_ID)
val intent = Intent(applicationContext, MainActivity::class.java)
.setAction(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
val pi = PendingIntent.getActivity(applicationContext, STATUS_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE)
val deleteIntent = Intent(this, BaresipService::class.java).setAction("Notification Dismissed")
val dpi = PendingIntent.getService(this, STATUS_REQ_CODE, deleteIntent, PendingIntent.FLAG_IMMUTABLE)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_notification_b)
.setContentIntent(pi)
.setDeleteIntent(dpi)
.setOngoing(true)
if (activeCall != null) {
val peerUri = activeCall.peerUri
val caller = Utils.friendlyUri(this, peerUri, activeCall.ua.account)
val person = Person.Builder().setName(caller).build()
val hangupIntent = Intent(this, BaresipService::class.java)
hangupIntent.action = "Call Hangup"
hangupIntent.putExtra("callp", activeCall.callp)
val hpi = PendingIntent.getService(this, REJECT_REQ_CODE, hangupIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
builder.setStyle(NotificationCompat.CallStyle.forOngoingCall(person, hpi))
.setCategory(Notification.CATEGORY_CALL)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setWhen(activeCall.startTime?.timeInMillis ?: System.currentTimeMillis())
.setUsesChronometer(true)
.setContentText(if (activeCall.onhold) getString(R.string.call_is_on_hold) else getString(R.string.call_is_connected))
} else {
// IMPORTANT: Explicitly clear the style first to ensure CallStyle is removed
builder.setStyle(null)
builder.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setWhen(0)
.setShowWhen(false)
.setUsesChronometer(false)
.setContentTitle("")
.setContentText("")
val notificationLayout = RemoteViews(packageName, R.layout.status_notification) val notificationLayout = RemoteViews(packageName, R.layout.status_notification)
for (i: Int in 0..3) { for (i in 0..3) {
val resId = when (i) { val resId = when (i) {
0 -> R.id.status0 0 -> R.id.status0
1 -> R.id.status1 1 -> R.id.status1
@@ -1537,19 +1489,59 @@ class BaresipService: Service() {
notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE) notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE)
else else
notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE) notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE)
snb.setCustomContentView(notificationLayout)
// Don't know why, but without the delay the notification is not always updated builder.setCustomContentView(notificationLayout)
Timer().schedule(250) {
nm.notify(STATUS_NOTIFICATION_ID, buildStatusNotification())
} }
if (VERSION.SDK_INT >= 31)
builder.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE
builder.setOngoing(true)
val notification = builder.build()
notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT
try {
if (activeCall != null) {
val type = if (VERSION.SDK_INT >= 30) {
if (ContextCompat.checkSelfPermission(this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED)
FOREGROUND_SERVICE_TYPE_PHONE_CALL or FOREGROUND_SERVICE_TYPE_MICROPHONE
else
FOREGROUND_SERVICE_TYPE_PHONE_CALL
} else 0
if (VERSION.SDK_INT >= 29)
startForeground(STATUS_NOTIFICATION_ID, notification, type)
else
startForeground(STATUS_NOTIFICATION_ID, notification)
isNotificationInCall = true
} else {
if (isNotificationInCall) {
stopForeground(STOP_FOREGROUND_REMOVE)
isNotificationInCall = false
if (VERSION.SDK_INT >= 34)
startForeground(STATUS_NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
else if (VERSION.SDK_INT >= 30)
startForeground(STATUS_NOTIFICATION_ID, notification, 0)
else
startForeground(STATUS_NOTIFICATION_ID, notification)
} else {
// Use standard notify for standby updates.
// This is much more stable for background registration.
nm.notify(STATUS_NOTIFICATION_ID, notification)
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to update foreground notification: ${e.message}")
nm.notify(STATUS_NOTIFICATION_ID, notification)
}
updatePartialWakeLock() updatePartialWakeLock()
} }
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
private fun updatePartialWakeLock() { private fun updatePartialWakeLock() {
val isAnyUaActive = uasStatus.value.values.any { it != R.drawable.circle_white } val isAnyUaActive = uasStatus.value.values.any { it != R.drawable.circle_white }
val isAnyCallActive = calls.isNotEmpty() val needsToStayAwake = isAnyUaActive || calls.isNotEmpty()
val needsToStayAwake = isAnyUaActive || isAnyCallActive
try { try {
if (needsToStayAwake) { if (needsToStayAwake) {
if (!partialWakeLock.isHeld) { if (!partialWakeLock.isHeld) {
@@ -1565,6 +1557,7 @@ class BaresipService: Service() {
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error managing partialWakeLock: ${e.message}") Log.e(TAG, "Error managing partialWakeLock: ${e.message}")
} }
updatePartialWakeLock()
} }
private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
@@ -1573,6 +1566,84 @@ class BaresipService: Service() {
} }
} }
@SuppressLint("FullScreenIntentPolicy")
fun handleIncomingCall(call: Call) {
val ua = call.ua
val peerUri = call.peerUri
val callp = call.callp
val callerNumber = peerUri.split(":")[1].split("@")[0]
if (shouldStartRinging(callerNumber))
startRinging()
if (!Utils.isVisible()) {
val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val intent = Intent(applicationContext, MainActivity::class.java)
.putExtra("action", "call show")
.putExtra("callp", callp)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
val channelId = HIGH_CHANNEL_ID
val nb = NotificationCompat.Builder(this, channelId)
val caller = Utils.friendlyUri(this, peerUri, ua.account)
val callerContact = Contact.findContact(peerUri)
val personBuilder = Person.Builder().setName(caller)
val contactColor = callerContact?.color() ?: "#B0B0B0"
val initial = if (caller.isNotEmpty()) caller.take(1) else "?"
val textAvatarBitmap = Utils.createTextAvatar(initial, contactColor)
var icon = IconCompat.createWithBitmap(textAvatarBitmap)
if (callerContact is Contact.BaresipContact) {
if (callerContact.avatarImage != null)
icon = IconCompat.createWithBitmap(callerContact.avatarImage!!.toCircle())
} else if (callerContact is Contact.AndroidContact) {
if (callerContact.thumbnailUri != null) {
try {
val source = ImageDecoder.createSource(contentResolver, callerContact.thumbnailUri!!)
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
}
icon = IconCompat.createWithBitmap(bitmap.toCircle())
} catch (e: Exception) {
Log.e(TAG, "Failed to load Android contact avatar: $e")
}
}
}
val person = personBuilder.setIcon(icon).build()
nb.setSmallIcon(R.drawable.ic_notification_call)
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setContentIntent(pi)
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(false)
.setOngoing(true)
.setContentText(getString(R.string.is_calling))
.setWhen(System.currentTimeMillis())
.setShowWhen(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_MAX)
if (VERSION.SDK_INT < 34 || nm.canUseFullScreenIntent())
nb.setFullScreenIntent(pi, true)
val answerIntent = Intent(applicationContext, MainActivity::class.java)
.putExtra("action", "call answer")
.putExtra("callp", callp)
val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE, answerIntent, piFlags)
val rejectIntent = Intent(this, BaresipService::class.java)
rejectIntent.action = "Call Reject"
rejectIntent.putExtra("callp", callp)
val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, piFlags)
nb.setStyle(NotificationCompat.CallStyle.forIncomingCall(person, rpi, api))
nm.notify(CALL_NOTIFICATION_ID, nb.build())
}
postServiceEvent(ServiceEvent("call incoming", arrayListOf(ua.uap, callp), System.nanoTime()))
}
private fun startRinging() { private fun startRinging() {
am.mode = AudioManager.MODE_RINGTONE am.mode = AudioManager.MODE_RINGTONE
rt!!.isLooping = true rt!!.isLooping = true
@@ -1621,6 +1692,7 @@ class BaresipService: Service() {
// First, check if the ringer volume is actually non-zero // First, check if the ringer volume is actually non-zero
if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false
// Finally, check the system "Vibrate for calls" setting. // Finally, check the system "Vibrate for calls" setting.
// Although deprecated, it is the standard way to check the "Also vibrate for calls" toggle.
return try { return try {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0 Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0
@@ -1695,7 +1767,6 @@ class BaresipService: Service() {
stopMediaPlayer() stopMediaPlayer()
if (!Call.inCall()) { if (!Call.inCall()) {
resetCallVolume() resetCallVolume()
abandonAudioFocus(applicationContext)
proximitySensing(false) proximitySensing(false)
} }
} }
@@ -1761,15 +1832,9 @@ class BaresipService: Service() {
} }
private fun updateNetwork() { private fun updateNetwork() {
/* for (n in allNetworks)
Log.d(TAG, "NETWORK $n with caps ${cm.getNetworkCapabilities(n)} and props " +
"${cm.getLinkProperties(n)} is active ${isNetworkActive(n)}") */
updateDnsServers() updateDnsServers()
val addresses = linkAddresses() val addresses = linkAddresses()
Log.d(TAG, "Old/new link addresses $linkAddresses/$addresses") Log.d(TAG, "Old/new link addresses $linkAddresses/$addresses")
var added = 0 var added = 0
@@ -1780,6 +1845,7 @@ class BaresipService: Service() {
else else
added++ added++
} }
var removed = 0 var removed = 0
for (a in linkAddresses) for (a in linkAddresses)
if (!addresses.containsKey(a.key)) { if (!addresses.containsKey(a.key)) {
@@ -1895,19 +1961,36 @@ class BaresipService: Service() {
} }
} }
private fun registerPhoneAccount() {
val phoneAccountHandle = getPhoneAccountHandle(this)
val phoneAccount = android.telecom.PhoneAccount.builder(phoneAccountHandle, getString(R.string.app_name))
.setCapabilities(android.telecom.PhoneAccount.CAPABILITY_SELF_MANAGED)
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_SIP)
.addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_TEL)
.build()
tm.registerPhoneAccount(phoneAccount)
}
private fun cleanService() { private fun cleanService() {
if (!isServiceClean) { if (!isServiceClean) {
if (bluetoothReceiverRegistered) { try {
this.unregisterReceiver(bluetoothReceiver)
bluetoothReceiverRegistered = false
}
if (hotSpotReceiverRegistered) { if (hotSpotReceiverRegistered) {
unregisterReceiver(hotSpotReceiver) applicationContext.unregisterReceiver(hotSpotReceiver)
hotSpotReceiverRegistered = false hotSpotReceiverRegistered = false
} }
} catch (_: IllegalArgumentException) {
Log.e(TAG, "hotSpotReceiver was not registered with applicationContext")
}
val callps = ConnectionService.connections.keys.toList()
for (callp in callps)
ConnectionService.onCallClosed(callp)
ConnectionService.pendingOutgoingConnection?.let {
it.setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
it.destroy()
ConnectionService.pendingOutgoingConnection = null
}
stopRinging() stopRinging()
stopMediaPlayer() stopMediaPlayer()
abandonAudioFocus(applicationContext)
uas.value = emptyList() uas.value = emptyList()
uasStatus.value = emptyMap() uasStatus.value = emptyMap()
callHistory.clear() callHistory.clear()
@@ -1940,6 +2023,7 @@ class BaresipService: Service() {
@SuppressLint("MutableCollectionMutableState") @SuppressLint("MutableCollectionMutableState")
companion object { companion object {
var instance: BaresipService? = null
var isServiceRunning = false var isServiceRunning = false
var isStartReceived = false var isStartReceived = false
var isConfigInitialized = false var isConfigInitialized = false
@@ -1984,7 +2068,6 @@ class BaresipService: Service() {
var dnsServers = listOf<InetAddress>() var dnsServers = listOf<InetAddress>()
// <aor, password> of those accounts that have auth username without auth password // <aor, password> of those accounts that have auth username without auth password
val aorPasswords = mutableMapOf<String, String>() val aorPasswords = mutableMapOf<String, String>()
var audioFocusRequest: AudioFocusRequestCompat? = null
var aecAvailable = false var aecAvailable = false
private var aec: AcousticEchoCanceler? = null private var aec: AcousticEchoCanceler? = null
var agcAvailable = false var agcAvailable = false
@@ -2001,10 +2084,15 @@ class BaresipService: Service() {
private var agc: AutomaticGainControl? = null private var agc: AutomaticGainControl? = null
private val nsAvailable = NoiseSuppressor.isAvailable() private val nsAvailable = NoiseSuppressor.isAvailable()
private var ns: NoiseSuppressor? = null private var ns: NoiseSuppressor? = null
private var btAdapter: BluetoothAdapter? = null
private var recorderSessionId = 0 private var recorderSessionId = 0
internal const val KEY_TEXT_REPLY = "key_text_reply_baresip" internal const val KEY_TEXT_REPLY = "key_text_reply_baresip"
private const val PHONE_ACCOUNT_ID = "baresip_phone_account"
fun getPhoneAccountHandle(ctx: Context): PhoneAccountHandle {
val componentName = android.content.ComponentName(ctx, ConnectionService::class.java)
return PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID)
}
fun postServiceEvent(event: ServiceEvent) { fun postServiceEvent(event: ServiceEvent) {
serviceEvents.add(event) serviceEvents.add(event)
@@ -2015,105 +2103,5 @@ class BaresipService: Service() {
Log.d(TAG, "Added service event ${event.event}") Log.d(TAG, "Added service event ${event.event}")
} }
} }
fun requestAudioFocus(ctx: Context): Boolean {
Log.d(TAG, "Requesting audio focus")
if (audioFocusRequest != null) {
Log.d(TAG, "Already focused")
return true
}
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
val attributes = AudioAttributesCompat.Builder()
.setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
.build()
audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.setAudioAttributes(attributes)
.setOnAudioFocusChangeListener { }
.build()
if (AudioManagerCompat.requestAudioFocus(am, audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "requestAudioFocus granted")
if (isBluetoothHeadsetConnected(ctx))
startBluetoothSco(ctx, 250L, 3)
} else {
Log.w(TAG, "requestAudioFocus denied")
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 (AudioManagerCompat.abandonAudioFocusRequest(am, audioFocusRequest!!) ==
AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
audioFocusRequest = null
if (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")
am.isBluetoothScoOn
else
if (am.communicationDevice != null)
am.communicationDevice!!.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
else
false
}
private fun startBluetoothSco(ctx: Context, delay: Long, count: Int) {
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
if (isBluetoothScoOn(am)) {
Log.d(TAG, "Bluetooth SCO is already on")
return
}
Log.d(TAG, "Starting Bluetooth SCO at count $count")
Handler(Looper.getMainLooper()).postDelayed({
if (VERSION.SDK_INT < 31) {
@Suppress("DEPRECATION")
am.startBluetoothSco()
} else {
Utils.setCommunicationDevice(am, AudioDeviceInfo.TYPE_BLUETOOTH_SCO)
}
if (!isBluetoothScoOn(am) && count > 1)
startBluetoothSco(ctx, delay, count - 1)
else
am.isBluetoothScoOn = true
}, delay)
}
private fun stopBluetoothSco(ctx: Context) {
Log.d(TAG, "Stopping Bluetooth SCO")
val am = ctx.getSystemService(AUDIO_SERVICE) as AudioManager
if (!isBluetoothScoOn(am)) {
Log.d(TAG, "Bluetooth SCO is already off")
return
}
Handler(Looper.getMainLooper()).postDelayed({
if (VERSION.SDK_INT < 31)
@Suppress("DEPRECATION")
am.stopBluetoothSco()
else
am.clearCommunicationDevice()
am.isBluetoothScoOn = false
}, 100)
}
} }
} }
@@ -0,0 +1,231 @@
package com.tutpro.baresip
import android.content.Intent
import android.telecom.CallAudioState
import android.telecom.Connection
import android.telecom.ConnectionRequest
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() {
private val TAG = "BaresipConnection"
companion object {
val connections = ConcurrentHashMap<Long, BaresipConnection>()
var pendingOutgoingConnection: BaresipConnection? = null
fun promoteOutgoingConnection(callp: Long) {
pendingOutgoingConnection?.let {
it.callp = callp
connections[callp] = it
pendingOutgoingConnection = null
}
}
fun onCallClosed(callp: Long) {
connections[callp]?.let {
it.setDisconnected(DisconnectCause(DisconnectCause.REMOTE))
it.destroy()
connections.remove(callp)
}
}
}
override fun onCreateIncomingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: ConnectionRequest?
): Connection {
val extras = request?.extras
val uap = extras?.getLong("uap") ?: 0L
val callp = extras?.getLong("callp") ?: 0L
val peerUri = extras?.getString("peerUri") ?: ""
Log.d(TAG, "onCreateIncomingConnection for $peerUri")
val connection = BaresipConnection(uap, callp)
connections[callp] = connection
connection.setAddress(Uri.fromParts("sip", peerUri, null), TelecomManager.PRESENTATION_ALLOWED)
connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_HOLD
val call = Call.ofCallp(callp)
if (call != null)
BaresipService.instance?.handleIncomingCall(call)
val ua = UserAgent.ofUap(uap)
if (ua != null) {
// Check speakerphone setting
if (BaresipService.speakerPhone) {
@Suppress("DEPRECATION")
connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER)
}
// Check for Auto-Answer
if (ua.account.answerMode == Api.ANSWERMODE_AUTO) {
Log.d(TAG, "Auto-answering call $callp")
connection.onAnswer()
} else {
connection.setRinging()
}
}
return connection
}
override fun onCreateIncomingConnectionFailed(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: ConnectionRequest?
) {
Log.e(TAG, "onCreateIncomingConnectionFailed")
}
override fun onCreateOutgoingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: ConnectionRequest?
): Connection {
val rootExtras = request?.extras
val nestedExtras = rootExtras?.getBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)
// Try root extras first, then fallback to nested
val uap = rootExtras?.getLong("uap", 0L).takeIf { it != 0L }
?: nestedExtras?.getLong("uap") ?: 0L
val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?:
nestedExtras?.getBoolean("conferenceCall") ?: false
val onHoldCallp = rootExtras?.getLong("onHoldCallp", 0L).takeIf { it != 0L }
?: nestedExtras?.getLong("onHoldCallp") ?: 0L
val destination = request?.address?.schemeSpecificPart ?: ""
Log.d(TAG, "onCreateOutgoingConnection to $destination (uap=$uap)")
val connection = BaresipConnection(uap, 0L)
pendingOutgoingConnection = connection
if (BaresipService.speakerPhone) {
@Suppress("DEPRECATION")
connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER)
}
connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED)
connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_HOLD
// Start the SIP connection logic
if (uap != 0L) {
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
}
override fun onCreateOutgoingConnectionFailed(
connectionManagerPhoneAccount: PhoneAccountHandle?,
request: ConnectionRequest?
) {
Log.e(TAG, "onCreateOutgoingConnectionFailed")
pendingOutgoingConnection = null
}
inner class BaresipConnection(val uap: Long, var callp: Long) : Connection() {
override fun onAnswer() {
Log.d(TAG, "Telecom Connection onAnswer $callp")
val intent = Intent(this@ConnectionService, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra("action", "call answer")
intent.putExtra("callp", callp)
startActivity(intent)
BaresipService.instance?.updateStatusNotification()
setActive()
}
override fun onReject() {
Log.d(TAG, "Telecom Connection onReject $callp")
Api.ua_hangup(uap, callp, 486, "Rejected")
setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
connections.remove(callp)
destroy()
}
override fun onDisconnect() {
Log.d(TAG, "Telecom Connection onDisconnect $callp")
if (callp != 0L) {
Api.ua_hangup(uap, callp, 0, "")
connections.remove(callp)
} else {
pendingOutgoingConnection = null
}
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
destroy()
}
override fun onAbort() {
Log.d(TAG, "Telecom Connection onAbort $callp")
if (callp != 0L) {
Api.ua_hangup(uap, callp, 0, "")
connections.remove(callp)
} else {
pendingOutgoingConnection = null
}
setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
destroy()
}
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onCallAudioStateChanged(state: CallAudioState?) {
super.onCallAudioStateChanged(state)
Log.d(TAG, "onCallAudioStateChanged: $state")
state?.let {
if (BaresipService.isMicMuted != it.isMuted) {
BaresipService.isMicMuted = it.isMuted
Api.calls_mute(it.isMuted)
BaresipService.postServiceEvent(
ServiceEvent("mic muted,${it.isMuted}", arrayListOf(uap, callp),
System.nanoTime())
)
}
val isSpeaker = it.route == CallAudioState.ROUTE_SPEAKER
BaresipService.postServiceEvent(
ServiceEvent("speaker,${isSpeaker}", arrayListOf(uap, callp), System.nanoTime())
)
}
}
override fun onHold() {
Log.d(TAG, "Telecom Connection onHold $callp")
val c = Call.ofCallp(callp)
if (c?.conferenceCall != true)
c?.hold()
setOnHold()
}
override fun onUnhold() {
Log.d(TAG, "Telecom Connection onUnhold $callp")
val c = Call.ofCallp(callp)
if (c?.conferenceCall != true)
c?.resume()
setActive()
}
override fun onPlayDtmfTone(c: Char) {
Log.d(TAG, "Telecom Connection onPlayDtmfTone $c")
if (callp != 0L) {
val call = Call.ofCallp(callp)
call?.sendDigit(c)
}
}
}
}
@@ -5,11 +5,20 @@ 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 // This is needed in order to allow choosing baresip as default Phone app
class InCallService : Service() { class InCallService : Service() {
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
// TODO("Return the communication channel to the service.") Log.d(TAG, "InCallService onBind with intent: ${intent.action}")
Log.d("Baresip", "InCallService onBind with intent: ${intent.action}")
return null return null
} }
} }
/*import android.telecom.InCallService
import android.telecom.Call
class InCallService : InCallService() {
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")
}
}*/
@@ -177,9 +177,6 @@ private var password = mutableStateOf("")
private val selectItems = mutableStateOf(listOf<String>()) private val selectItems = mutableStateOf(listOf<String>())
private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} }) private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
private val showSelectItemDialog = mutableStateOf(false) private val showSelectItemDialog = mutableStateOf(false)
private var callRunnable: Runnable? = null
private var callHandler: Handler = Handler(Looper.getMainLooper())
private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null
fun NavGraphBuilder.mainScreenRoute( fun NavGraphBuilder.mainScreenRoute(
navController: NavController, navController: NavController,
@@ -529,7 +526,7 @@ private fun TopAppBar(
val recOffImage = Icons.Filled.VoiceOverOff val recOffImage = Icons.Filled.VoiceOverOff
val recOnImage = Icons.Filled.RecordVoiceOver val recOnImage = Icons.Filled.RecordVoiceOver
var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) } var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) }
val isSpeakerOn = remember { mutableStateOf(Utils.isSpeakerPhoneOn(am)) } val isSpeakerOn by viewModel.isSpeakerOn.collectAsState()
var menuExpanded by remember { mutableStateOf(false) } var menuExpanded by remember { mutableStateOf(false) }
val about = stringResource(R.string.about) val about = stringResource(R.string.about)
@@ -648,11 +645,22 @@ private fun TopAppBar(
.clip(CircleShape) .clip(CircleShape)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
if (Build.VERSION.SDK_INT >= 31) val isCurrentlyOn = isSpeakerOn
Log.d(TAG, "Toggling speakerphone when dev/mode is " + val aor = viewModel.selectedAor.value
"${am.communicationDevice!!.type}/${am.mode}") val ua = uas.value.find { it.account.aor == aor }
isSpeakerOn.value = !Utils.isSpeakerPhoneOn(am) val call = ua?.currentCall()
val connection = if (call != null) ConnectionService.connections[call.callp] else null
if (connection != null) {
@Suppress("DEPRECATION")
connection.setAudioRoute(
if (isCurrentlyOn)
android.telecom.CallAudioState.ROUTE_EARPIECE
else
android.telecom.CallAudioState.ROUTE_SPEAKER
)
} else {
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am) Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
}
}, },
onLongClick = { onLongClick = {
alertTitle.value = speakerPhoneTitle alertTitle.value = speakerPhoneTitle
@@ -664,7 +672,7 @@ private fun TopAppBar(
Icon( Icon(
imageVector = Icons.Filled.SpeakerPhone, imageVector = Icons.Filled.SpeakerPhone,
contentDescription = null, contentDescription = null,
tint = if (isSpeakerOn.value) tint = if (isSpeakerOn)
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
else else
MaterialTheme.colorScheme.onPrimary, MaterialTheme.colorScheme.onPrimary,
@@ -884,8 +892,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 filteredCalls = calls.filter { it.ua.account.aor == selectedAor } val filteredCalls = calls.filter { it.ua.account.aor == selectedAor && it.status.value != "disconnecting" }
val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" } val dialingOrRinging = filteredCalls.any { it.status.value == "outgoing" || it.status.value == "incoming" }
val conferenceCall = filteredCalls.any { it.conferenceCall } val conferenceCall = filteredCalls.any { it.conferenceCall }
@@ -1483,12 +1490,13 @@ private fun CallRow(
onClick = { onClick = {
if (call.terminated.value) return@IconButton if (call.terminated.value) return@IconButton
call.terminated.value = true call.terminated.value = true
abandonAudioFocus(ctx) val connection = ConnectionService.connections[call.callp]
Log.d( if (connection != null)
TAG, connection.onDisconnect()
"AoR ${call.ua.account.aor} canceling call ${call.callp} with ${call.callUri.value}" else {
) Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}")
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
}
}, },
) { ) {
Icon( Icon(
@@ -1509,10 +1517,14 @@ private fun CallRow(
onClick = { onClick = {
if (call.terminated.value) return@IconButton if (call.terminated.value) return@IconButton
call.terminated.value = true call.terminated.value = true
abandonAudioFocus(ctx) val connection = ConnectionService.connections[call.callp]
Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp} with ${call.callUri.value}") 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") Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
} }
}
) { ) {
Icon( Icon(
imageVector = Icons.Filled.CallEnd, imageVector = Icons.Filled.CallEnd,
@@ -1522,20 +1534,28 @@ private fun CallRow(
) )
} }
if (!call.conferenceCall)
IconButton( IconButton(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
onClick = { onClick = {
val connection = ConnectionService.connections[call.callp]
if (call.onhold) { if (call.onhold) {
Log.d( Log.d(
TAG, TAG,
"AoR ${call.ua.account.aor} resuming call ${call.callp} with ${call.callUri.value}" "AoR ${call.ua.account.aor} resuming call ${call.callp} with ${call.callUri.value}"
) )
if (connection != null)
connection.onUnhold()
else
call.resume() call.resume()
} else { } else {
Log.d( Log.d(
TAG, TAG,
"AoR ${call.ua.account.aor} holding call ${call.callp} with ${call.callUri.value}" "AoR ${call.ua.account.aor} holding call ${call.callp} with ${call.callUri.value}"
) )
if (connection != null)
connection.onHold()
else
call.hold() call.hold()
} }
}, },
@@ -1559,12 +1579,22 @@ private fun CallRow(
enabled = call.transferButtonEnabled.value, enabled = call.transferButtonEnabled.value,
onClick = { onClick = {
if (call.onHoldCall != null) { 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
}
else {
val connection = ConnectionService.connections[call.callp]
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)
showAlert.value = true showAlert.value = true
} }
} else }
}
else
showTransferDialog = true showTransferDialog = true
}, },
) { ) {
@@ -1961,6 +1991,7 @@ private fun CallRow(
@Composable @Composable
private fun OnHoldNotice() { private fun OnHoldNotice() {
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
OutlinedButton( OutlinedButton(
onClick = {}, onClick = {},
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
@@ -1973,6 +2004,7 @@ private fun OnHoldNotice() {
) )
} }
} }
}
private fun spinToAor(viewModel: ViewModel, aor: String) { private fun spinToAor(viewModel: ViewModel, aor: String) {
if (aor != viewModel.selectedAor.value) if (aor != viewModel.selectedAor.value)
@@ -2011,10 +2043,10 @@ 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, conferenceCall: Boolean,
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager onHoldCallp: Long = 0L) {
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!! val aor = viewModel.selectedAor.value
val aor = ua.account.aor val ua = UserAgent.ofAor(aor)!!
val peerUri = if (Utils.isTelNumber(uriText)) val peerUri = if (Utils.isTelNumber(uriText))
"tel:$uriText" "tel:$uriText"
else else
@@ -2035,44 +2067,31 @@ private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, confer
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
} }
else if (Call.isAnyCallActive(ctx)) else if (Utils.isPSTNCallActive(ctx) || 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 if (!BaresipService.requestAudioFocus(ctx))
Toast.makeText(ctx, R.string.audio_focus_denied, Toast.LENGTH_SHORT).show()
else { else {
viewModel.dialerState.callButtonsEnabled.value = false val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager
if (Build.VERSION.SDK_INT < 31) { val extras = android.os.Bundle()
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION") extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
am.mode = AudioManager.MODE_IN_COMMUNICATION BaresipService.getPhoneAccountHandle(ctx))
runCall(ctx, viewModel, ua, uri, conferenceCall) val callExtras = android.os.Bundle()
} else { callExtras.putBoolean("conferenceCall", conferenceCall)
if (am.mode == AudioManager.MODE_IN_COMMUNICATION) { callExtras.putLong("uap", ua.uap)
runCall(ctx, viewModel, ua, uri, conferenceCall) if (onHoldCallp != 0L)
} else { callExtras.putLong("onHoldCallp", onHoldCallp)
audioModeChangedListener = AudioManager.OnModeChangedListener { mode -> extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
if (mode == AudioManager.MODE_IN_COMMUNICATION) { try {
Log.d(TAG, "Audio mode changed to MODE_IN_COMMUNICATION using " + Log.d(TAG, "Placing Telecom call to $uri with uap=${ua.uap}")
"device ${am.communicationDevice!!.type}") tm.placeCall(uri.toUri(), extras)
if (audioModeChangedListener != null) { } catch (e: SecurityException) {
am.removeOnModeChangedListener(audioModeChangedListener!!) Log.e(TAG, "placeCall failed: ${e.message}")
audioModeChangedListener = null
}
runCall(ctx, viewModel, ua, uri, conferenceCall)
} else {
Log.d(TAG, "Audio mode changed to mode ${am.mode} using " +
"device ${am.communicationDevice!!.type}")
}
}
am.addOnModeChangedListener(ctx.mainExecutor, audioModeChangedListener!!)
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
am.mode = AudioManager.MODE_IN_COMMUNICATION
}
} }
} }
} }
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}")
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)
@@ -2082,60 +2101,13 @@ 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}")
val connection = ConnectionService.connections[call.callp]
if (connection != null)
connection.onReject()
else {
call.rejected = true call.rejected = true
Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here") Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here")
} }
private fun runCall(ctx: Context, viewModel: ViewModel, ua: UserAgent, uri: String, conferenceCall: Boolean) {
callRunnable = Runnable {
callRunnable = null
val newCall = call(ctx, viewModel, ua, uri, conferenceCall)
if (newCall == null) {
BaresipService.abandonAudioFocus(ctx)
viewModel.dialerState.callButtonsEnabled.value = true
}
}
callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay)
}
private fun call(
ctx: Context,
viewModel: ViewModel,
ua: UserAgent,
uri: String,
conferenceCall: Boolean,
onHoldCall: Call? = null
): Call? {
spinToAor(viewModel, ua.account.aor)
if (conferenceCall && ua.calls().isEmpty())
Api.module_load("mixminus")
val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
return if (callp != 0L) {
Log.d(TAG, "Adding outgoing call ${ua.uap}/$callp/$uri")
val call = Call(callp, ua, uri, "out", "outgoing")
call.onHoldCall = onHoldCall
call.conferenceCall = conferenceCall
call.add()
if (onHoldCall != null)
onHoldCall.newCall = call
if (call.connect(uri)) {
showCall(ctx, viewModel, ua)
call
} else {
Log.w(TAG, "call_connect $callp failed")
if (onHoldCall != null)
onHoldCall.newCall = null
call.remove()
call.destroy()
showCall(ctx, viewModel, ua)
null
}
} else {
Log.w(TAG, "callAlloc for ${ua.uap} to $uri failed")
if (conferenceCall && ua.calls().isEmpty())
Api.module_unload("mixminus")
null
}
} }
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) {
@@ -2152,13 +2124,23 @@ 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) {
if (call.hold()) { val connection = ConnectionService.connections[call.callp]
val success = if (connection != null) {
connection.onHold()
true
} else {
call.hold()
}
if (success) {
call.onhold = true
call.referTo = uri call.referTo = uri
call(ctx, viewModel, ua, uri, false,call) makeCall(ctx, viewModel, uri, false, call.callp)
showCall(ctx, viewModel, ua, call) showCall(ctx, viewModel, ua, call)
} }
} }
else { else {
val connection = ConnectionService.connections[call.callp]
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)
@@ -2205,8 +2187,9 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
call.focusDtmf.value = true call.focusDtmf.value = true
viewModel.requestShowKeyboard() viewModel.requestShowKeyboard()
} }
Log.d(TAG, "Showing call ${call.callp} from ${call.ua.account.aor} with status ${call.status.value}")
when (call.status.value) { when (call.status.value) {
"outgoing", "transferring", "answered" -> { "outgoing", "transferring", "answered", "disconnecting" -> {
call.callUriLabel.value = if (call.status.value == "answered") call.callUriLabel.value = if (call.status.value == "answered")
ctx.getString(R.string.incoming_call_from_dots) ctx.getString(R.string.incoming_call_from_dots)
else else
@@ -2373,6 +2356,9 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
"call update" -> { "call update" -> {
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
"update calls" -> {
viewModel.updateCalls(Call.calls().toList())
}
"call verify" -> { "call verify" -> {
val callp = params[1] as Long val callp = params[1] as Long
val call = Call.ofCallp(callp) val call = Call.ofCallp(callp)
@@ -2449,13 +2435,14 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
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") Api.ua_hangup(uap, callp, 487, "Request Terminated")
call(ctx, viewModel, ua, ev[1], false) makeCall(ctx, viewModel, ev[1], false)
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
"transfer failed" -> { "transfer failed" -> {
showCall(ctx, viewModel, ua) showCall(ctx, viewModel, ua)
} }
"call closed" -> { "call closed" -> {
viewModel.updateCalls(Call.calls().toList())
val activity = ctx as? Activity val activity = ctx as? Activity
if (activity != null) { if (activity != null) {
val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
@@ -2490,6 +2477,16 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
if (aor == viewModel.selectedAor.value) if (aor == viewModel.selectedAor.value)
viewModel.triggerAccountUpdate() viewModel.triggerAccountUpdate()
} }
"mic muted" -> {
val muted = ev[1].toBoolean()
if (muted)
viewModel.updateMicIcon(Icons.Filled.MicOff)
else
viewModel.updateMicIcon(Icons.Filled.Mic)
}
"speaker" -> {
viewModel.updateSpeakerPhoneStatus(ev[1].toBoolean())
}
else -> Log.e(TAG, "Unknown event '${ev[0]}'") else -> Log.e(TAG, "Unknown event '${ev[0]}'")
} }
@@ -2769,19 +2766,3 @@ private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) {
downloadsOutputUri = null downloadsOutputUri = null
} }
private fun abandonAudioFocus(ctx: Context) {
if (Build.VERSION.SDK_INT < 31) {
if (callRunnable != null) {
callHandler.removeCallbacks(callRunnable!!)
callRunnable = null
BaresipService.abandonAudioFocus(ctx)
}
} else {
if (audioModeChangedListener != null) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
am.removeOnModeChangedListener(audioModeChangedListener!!)
audioModeChangedListener = null
BaresipService.abandonAudioFocus(ctx)
}
}
}
@@ -41,6 +41,7 @@ import androidx.core.text.isDigitsOnly
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.navigation.NavController import androidx.navigation.NavController
import com.tutpro.baresip.Call.Companion.inCall
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
@@ -841,6 +842,12 @@ object Utils {
Configuration.UI_MODE_NIGHT_YES Configuration.UI_MODE_NIGHT_YES
} }
fun isPSTNCallActive(ctx: Context): Boolean {
// MODE_IN_CALL indicates a PSTN call is active
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
return am.mode == AudioManager.MODE_IN_CALL
}
fun relativeTime(ctx: Context, time: GregorianCalendar): String { fun relativeTime(ctx: Context, time: GregorianCalendar): String {
return if (DateUtils.isToday(time.timeInMillis)) { return if (DateUtils.isToday(time.timeInMillis)) {
val fmt = DateFormat.getTimeInstance(DateFormat.SHORT) val fmt = DateFormat.getTimeInstance(DateFormat.SHORT)
@@ -859,14 +866,6 @@ object Utils {
} }
} }
fun isSpeakerPhoneOn(am: AudioManager): Boolean {
return if (Build.VERSION.SDK_INT >= 31)
am.communicationDevice!!.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
else
@Suppress("DEPRECATION")
am.isSpeakerphoneOn
}
private fun setSpeakerPhone(executor: Executor, am: AudioManager, enable: Boolean) { private fun setSpeakerPhone(executor: Executor, am: AudioManager, enable: Boolean) {
if (Build.VERSION.SDK_INT >= 31) { if (Build.VERSION.SDK_INT >= 31) {
val current = am.communicationDevice!!.type val current = am.communicationDevice!!.type
@@ -895,7 +894,7 @@ object Utils {
Log.d(TAG, "Setting com device to TYPE_BUILTIN_EARPIECE") Log.d(TAG, "Setting com device to TYPE_BUILTIN_EARPIECE")
if (!am.setCommunicationDevice(speakerDevice)) if (!am.setCommunicationDevice(speakerDevice))
Log.e(TAG, "Could not set com device") Log.e(TAG, "Could not set com device")
if (BaresipService.audioFocusRequest != null && am.mode == AudioManager.MODE_NORMAL) { if (inCall() && am.mode == AudioManager.MODE_NORMAL) {
Log.d(TAG, "Setting mode to communication") Log.d(TAG, "Setting mode to communication")
am.mode = AudioManager.MODE_IN_COMMUNICATION am.mode = AudioManager.MODE_IN_COMMUNICATION
} }
@@ -945,18 +944,6 @@ object Utils {
} }
} }
@RequiresApi(Build.VERSION_CODES.S)
fun setCommunicationDevice(am: AudioManager, type: Int) {
val current = am.communicationDevice!!.type
Log.d(TAG, "Current com dev/mode $current/${am.mode}")
for (device in am.availableCommunicationDevices)
if (device.type == type) {
am.setCommunicationDevice(device)
break
}
Log.d(TAG, "New com dev/mode ${am.communicationDevice!!.type}/${am.mode}")
}
private fun clearCommunicationDevice(am: AudioManager) { private fun clearCommunicationDevice(am: AudioManager) {
if (Build.VERSION.SDK_INT >= 31) { if (Build.VERSION.SDK_INT >= 31) {
am.clearCommunicationDevice() am.clearCommunicationDevice()
@@ -63,6 +63,9 @@ class ViewModel: ViewModel() {
private val _micIcon = MutableStateFlow(Icons.Filled.Mic) private val _micIcon = MutableStateFlow(Icons.Filled.Mic)
val micIcon = _micIcon.asStateFlow() val micIcon = _micIcon.asStateFlow()
private val _isSpeakerOn = MutableStateFlow(false)
val isSpeakerOn = _isSpeakerOn.asStateFlow()
private val _isDialpadVisible = MutableStateFlow(false) private val _isDialpadVisible = MutableStateFlow(false)
val isDialpadVisible = _isDialpadVisible.asStateFlow() val isDialpadVisible = _isDialpadVisible.asStateFlow()
@@ -109,6 +112,10 @@ class ViewModel: ViewModel() {
_micIcon.value = icon _micIcon.value = icon
} }
fun updateSpeakerPhoneStatus(on: Boolean) {
_isSpeakerOn.value = on
}
fun toggleDialpadVisibility() { fun toggleDialpadVisibility() {
_isDialpadVisible.value = !_isDialpadVisible.value _isDialpadVisible.value = !_isDialpadVisible.value
} }
+2
View File
@@ -521,6 +521,7 @@
<string name="allow_video_send">Salli video videon lähetys puhelussa \'%1$s\'\?</string> <string name="allow_video_send">Salli video videon lähetys puhelussa \'%1$s\'\?</string>
<string name="allow_video_recv">Salli video videon vastaanotto puhelussa \'%1$s\'\?</string> <string name="allow_video_recv">Salli video videon vastaanotto puhelussa \'%1$s\'\?</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="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>
@@ -530,6 +531,7 @@
<string name="choose_destination_uri">Valitse kohteen URI</string> <string name="choose_destination_uri">Valitse kohteen URI</string>
<string name="transfer">Siirto</string> <string name="transfer">Siirto</string>
<string name="transfer_failed">Siirto epäonnistui</string> <string name="transfer_failed">Siirto epäonnistui</string>
<string name="replaces_not_supported">Toinen osapuoli ei tue REPLACES ominaisuutta</string>
<string name="dtmf">DTMF</string> <string name="dtmf">DTMF</string>
<string name="call_info">Puhelutiedot</string> <string name="call_info">Puhelutiedot</string>
<string name="call_info_not_available">Ei saatavilla</string> <string name="call_info_not_available">Ei saatavilla</string>
+2
View File
@@ -505,6 +505,7 @@
<string name="allow_video_send">Accept sending of video to \'%1$s\'\?</string> <string name="allow_video_send">Accept sending of video to \'%1$s\'\?</string>
<string name="allow_video_recv">Accept receiving video from \'%1$s\'\?</string> <string name="allow_video_recv">Accept receiving video from \'%1$s\'\?</string>
<string name="call_is_on_hold">Call is on hold</string> <string name="call_is_on_hold">Call is on hold</string>
<string name="call_is_connected">Call is connected</string>
<string name="rec_in_call">Recording can be turned on or off only when call is not <string name="rec_in_call">Recording can be turned on or off only when call is not
connected</string> connected</string>
<string name="call_transfer">Call Transfer</string> <string name="call_transfer">Call Transfer</string>
@@ -514,6 +515,7 @@
<string name="choose_destination_uri">Choose destination URI</string> <string name="choose_destination_uri">Choose destination URI</string>
<string name="transfer">Transfer</string> <string name="transfer">Transfer</string>
<string name="transfer_failed">Transfer failed</string> <string name="transfer_failed">Transfer failed</string>
<string name="replaces_not_supported">Peer does not support REPLACES feature</string>
<string name="dtmf">DTMF</string> <string name="dtmf">DTMF</string>
<string name="call_info">Call Info</string> <string name="call_info">Call Info</string>
<string name="call_info_not_available">No info available</string> <string name="call_info_not_available">No info available</string>