Work on calling app

This commit is contained in:
Juha Heinanen
2026-03-30 18:02:52 +03:00
parent 2a38408b76
commit 9c02de17cf
5 changed files with 466 additions and 453 deletions

View File

@ -9,6 +9,7 @@
tools:ignore="ForegroundServicesPolicy" />
<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_SPECIAL_USE" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.INTERNET" />
<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.WRITE_CONTACTS" />
<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.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@ -83,8 +86,21 @@
<service
android:name=".BaresipService"
android:enabled="true"
android:foregroundServiceType="microphone|phoneCall"
android:foregroundServiceType="microphone|phoneCall|specialUse"
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

View File

@ -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
@ -8,24 +7,20 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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_PHONE_CALL
import android.content.res.Configuration
import android.database.ContentObserver
import android.graphics.ImageDecoder
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.MODE_IN_COMMUNICATION
import android.media.AudioManager.MODE_NORMAL
import android.media.MediaPlayer
import android.media.Ringtone
import android.media.RingtoneManager
@ -51,6 +46,8 @@ import android.os.VibratorManager
import android.provider.ContactsContract
import android.provider.Settings
import android.system.OsConstants
import android.telecom.DisconnectCause
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.view.View
import android.widget.RemoteViews
@ -60,7 +57,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
@ -98,11 +94,9 @@ class BaresipService: Service() {
private lateinit var pm: PowerManager
private lateinit var wm: WifiManager
private lateinit var tm: TelecomManager
private lateinit var btm: BluetoothManager
private lateinit var vibrator: Vibrator
private lateinit var proximityWakeLock: PowerManager.WakeLock
private lateinit var wifiLock: WifiManager.WifiLock
private lateinit var bluetoothReceiver: BroadcastReceiver
private lateinit var hotSpotReceiver: BroadcastReceiver
private lateinit var androidContactsObserver: ContentObserver
private lateinit var networkCallback: ConnectivityManager.NetworkCallback
@ -118,7 +112,6 @@ class BaresipService: Service() {
private var hotSpotAddresses = mapOf<String, String>()
private var mediaPlayer: MediaPlayer? = null
private var androidContactsObserverRegistered = false
private var bluetoothReceiverRegistered = false
private var hotSpotReceiverRegistered = false
private var isServiceClean = false
@ -127,6 +120,7 @@ class BaresipService: Service() {
super.onCreate()
Log.i(TAG, "BaresipService onCreate")
instance = this
intent = Intent("com.tutpro.baresip.EVENT")
intent.setPackage("com.tutpro.baresip")
@ -272,9 +266,7 @@ class BaresipService: Service() {
hotSpotReceiverRegistered = true
tm = getSystemService(TELECOM_SERVICE) as TelecomManager
btm = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
btAdapter = btm.adapter
registerPhoneAccount()
proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
"com.tutpro.baresip:proximity_wakelock")
@ -287,71 +279,6 @@ class BaresipService: Service() {
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())) {
override fun onChange(self: Boolean) {
Log.d(TAG, "Android contacts change")
@ -559,6 +486,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" -> {
val callp = intent!!.getLongExtra("callp", 0L)
val call = Call.ofCallp(callp)
@ -679,6 +618,7 @@ class BaresipService: Service() {
super.onDestroy()
Log.d(TAG, "onDestroy at Baresip Service")
cleanService()
instance = null
if (isServiceRunning)
sendBroadcast(Intent("com.tutpro.baresip.Restart"))
}
@ -838,19 +778,20 @@ class BaresipService: Service() {
break
stopMediaPlayer()
setCallVolume()
if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am)
proximitySensing(proximitySensing)
}
"call ringing" -> {
ConnectionService.connections[callp]?.setRinging()
playRingBack()
return
}
"call progress" -> {
if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
stopMediaPlayer()
else
else {
ConnectionService.connections[callp]?.setRinging()
playRingBack()
}
return
}
"incoming call" -> {
@ -910,85 +851,20 @@ class BaresipService: Service() {
"call incoming" -> {
val peerUri = ev[1]
Log.d(TAG, "Incoming call $uap/$callp/$peerUri")
Call(callp, ua, peerUri, "in", "incoming").add()
if (speakerPhone && !Utils.isSpeakerPhoneOn(am))
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(this), am)
if (ua.account.answerMode == Api.ANSWERMODE_AUTO) {
val newIntent = Intent(this, MainActivity::class.java)
newIntent.flags =
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 {
val source = ImageDecoder.createSource(contentResolver,
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) {
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())
return
if (Call.ofCallp(callp) == null)
Call(callp, ua, peerUri, "in", "incoming").add()
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
}
"call answered" -> {
stopMediaPlayer()
@ -1001,27 +877,31 @@ class BaresipService: Service() {
stopMediaPlayer()
}
"call established" -> {
ConnectionService.connections[callp]?.setActive()
nm.cancel(CALL_NOTIFICATION_ID)
Log.d(TAG, "AoR $aor call $callp established in mode ${am.mode}")
if (am.mode != MODE_IN_COMMUNICATION)
am.mode = MODE_IN_COMMUNICATION
Log.d(TAG, "AoR $aor call $callp established")
call!!.status.value = "connected"
call.onhold = false
if (ua.account.callHistory)
call.startTime = GregorianCalendar()
call.startTime = GregorianCalendar()
updateStatusNotification()
if (!isMainVisible)
return
}
"call update" -> {
call!!.held = when (ev[1].toInt()) {
val held = when (ev[1].toInt()) {
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 ((ev[1].toInt() and Api.SDP_RECVONLY) != 0)
stopMediaPlayer()
else
else {
ConnectionService.connections[callp]?.setRinging()
playRingBack()
}
}
if (!isMainVisible || call.status.value != "connected")
return
@ -1085,6 +965,11 @@ class BaresipService: Service() {
}
"call closed" -> {
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)
if (call != null) {
stopRinging()
@ -1107,16 +992,15 @@ class BaresipService: Service() {
call.onHoldCall = null
}
call.remove()
updateStatusNotification()
if (call.conferenceCall && ua.calls().isEmpty())
Api.module_unload("mixminus")
val reason = ev[1]
val tone = ev[2]
if (tone == "busy") {
if (tone == "busy")
playBusy()
}
else if (!Call.inCall()) {
resetCallVolume()
abandonAudioFocus(applicationContext)
proximitySensing(false)
}
if (call.dir == "out")
@ -1405,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")
@Keep
fun started() {
@ -1414,7 +1332,7 @@ class BaresipService: Service() {
callActionUri = ""
Log.d(TAG, "Battery optimizations are ignored: " +
"${pm.isIgnoringBatteryOptimizations(packageName)}")
Log.d(TAG, "Wifi lock is held: ${wifiLock.isHeld}")
Log.d(TAG, "WiFi lock is held: ${wifiLock.isHeld}")
updateStatusNotification()
}
@ -1460,7 +1378,6 @@ class BaresipService: Service() {
if (VERSION.SDK_INT >= 31)
snb.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE
snb.setOngoing(true)
snb.setCategory(Notification.CATEGORY_SERVICE)
val notification = snb.build()
notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or
Notification.FLAG_ONGOING_EVENT
@ -1470,8 +1387,8 @@ class BaresipService: Service() {
@SuppressLint("UnspecifiedImmutableFlag")
private fun showStatusNotification() {
val intent = Intent(applicationContext, MainActivity::class.java)
.setAction(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.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)
@ -1484,26 +1401,18 @@ class BaresipService: Service() {
)
val notificationLayout = RemoteViews(packageName, R.layout.status_notification)
snb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_notification_b)
.setContentIntent(pi)
.setDeleteIntent(dpi)
.setOngoing(true)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notificationLayout)
.setSmallIcon(R.drawable.ic_notification_b)
.setContentIntent(pi)
.setDeleteIntent(dpi)
.setOngoing(true)
.setCategory(Notification.CATEGORY_SERVICE)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notificationLayout)
val notification = buildStatusNotification()
try {
if (VERSION.SDK_INT >= 29)
startForeground(
STATUS_NOTIFICATION_ID, notification,
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
)
if (VERSION.SDK_INT >= 34)
startForeground(STATUS_NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
else
startForeground(STATUS_NOTIFICATION_ID, notification)
} catch (e: Exception) {
@ -1511,39 +1420,194 @@ class BaresipService: Service() {
}
}
private fun updateStatusNotification() {
val notificationLayout = RemoteViews(packageName, R.layout.status_notification)
for (i: Int in 0..3) {
val resId = when (i) {
0-> R.id.status0
1-> R.id.status1
2-> R.id.status2
else -> R.id.status3
}
if (i < uas.value.size) {
notificationLayout.setImageViewResource(resId, uas.value[i].status)
notificationLayout.setViewVisibility(resId, View.VISIBLE)
} else {
notificationLayout.setViewVisibility(resId, View.INVISIBLE)
}
fun updateStatusNotification() {
val activeCall = Call.calls().find {
it.status.value == "connected" || it.status.value == "outgoing" || it.status.value == "answered"
}
if (uas.value.size > 4)
notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE)
else
notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE)
snb.setCustomContentView(notificationLayout)
// Don't know why, but without the delay the notification is not always updated
Timer().schedule(250) {
nm.notify(STATUS_NOTIFICATION_ID, buildStatusNotification())
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)
for (i in 0..3) {
val resId = when (i) {
0 -> R.id.status0
1 -> R.id.status1
2 -> R.id.status2
else -> R.id.status3
}
if (i < uas.value.size) {
notificationLayout.setImageViewResource(resId, uas.value[i].status)
notificationLayout.setViewVisibility(resId, View.VISIBLE)
} else {
notificationLayout.setViewVisibility(resId, View.INVISIBLE)
}
}
if (uas.value.size > 4)
notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE)
else
notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE)
builder.setCustomContentView(notificationLayout)
}
if (VERSION.SDK_INT >= 31)
builder.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE
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)
} else {
stopForeground(STOP_FOREGROUND_REMOVE)
if (VERSION.SDK_INT >= 34)
startForeground(STATUS_NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
else
startForeground(STATUS_NOTIFICATION_ID, notification)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to update foreground notification: ${e.message}")
nm.notify(STATUS_NOTIFICATION_ID, notification)
}
}
private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) {
Handler(Looper.getMainLooper()).post {
Toast.makeText(this@BaresipService.applicationContext, message, length).show()
}
}
@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() {
am.mode = AudioManager.MODE_RINGTONE
rt!!.isLooping = true
@ -1592,6 +1656,7 @@ class BaresipService: Service() {
// First, check if the ringer volume is actually non-zero
if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false
// 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 {
@Suppress("DEPRECATION")
Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0
@ -1666,7 +1731,6 @@ class BaresipService: Service() {
stopMediaPlayer()
if (!Call.inCall()) {
resetCallVolume()
abandonAudioFocus(applicationContext)
proximitySensing(false)
}
}
@ -1732,15 +1796,9 @@ class BaresipService: Service() {
}
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()
val addresses = linkAddresses()
Log.d(TAG, "Old/new link addresses $linkAddresses/$addresses")
var added = 0
@ -1751,6 +1809,7 @@ class BaresipService: Service() {
else
added++
}
var removed = 0
for (a in linkAddresses)
if (!addresses.containsKey(a.key)) {
@ -1771,16 +1830,25 @@ class BaresipService: Service() {
Api.net_debug()
if (activeNetwork != null) {
val caps = cm.getNetworkCapabilities(activeNetwork)
if (caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
// Robust Wi-Fi Lock Logic:
// Check ALL networks, not just the 'active' one, to ensure we keep the radio
// awake if Wi-Fi is available at all.
val hasWifi = allNetworks.any { network ->
val caps = cm.getNetworkCapabilities(network)
caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
if (hasWifi) {
if (!wifiLock.isHeld) {
Log.d(TAG, "Acquiring WiFi Lock")
wifiLock.acquire()
return
}
} else {
if (wifiLock.isHeld) {
Log.d(TAG, "Releasing WiFi Lock")
wifiLock.release()
}
}
Log.d(TAG, "Releasing WiFi Lock")
wifiLock.release()
}
private fun linkAddresses(): MutableMap<String, String> {
@ -1860,19 +1928,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() {
if (!isServiceClean) {
if (bluetoothReceiverRegistered) {
this.unregisterReceiver(bluetoothReceiver)
bluetoothReceiverRegistered = false
try {
if (hotSpotReceiverRegistered) {
applicationContext.unregisterReceiver(hotSpotReceiver)
hotSpotReceiverRegistered = false
}
} catch (_: IllegalArgumentException) {
Log.e(TAG, "hotSpotReceiver was not registered with applicationContext")
}
if (hotSpotReceiverRegistered) {
unregisterReceiver(hotSpotReceiver)
hotSpotReceiverRegistered = false
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()
stopMediaPlayer()
abandonAudioFocus(applicationContext)
uas.value = emptyList()
uasStatus.value = emptyMap()
callHistory.clear()
@ -1903,6 +1988,7 @@ class BaresipService: Service() {
@SuppressLint("MutableCollectionMutableState")
companion object {
var instance: BaresipService? = null
var isServiceRunning = false
var isStartReceived = false
var isConfigInitialized = false
@ -1964,10 +2050,15 @@ class BaresipService: Service() {
private var agc: AutomaticGainControl? = null
private val nsAvailable = NoiseSuppressor.isAvailable()
private var ns: NoiseSuppressor? = null
private var btAdapter: BluetoothAdapter? = null
private var recorderSessionId = 0
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) {
serviceEvents.add(event)
@ -1996,8 +2087,6 @@ class BaresipService: Service() {
.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
@ -2005,78 +2094,5 @@ 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 (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)
}
}
}

View File

@ -177,9 +177,6 @@ private var password = mutableStateOf("")
private val selectItems = mutableStateOf(listOf<String>())
private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
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(
navController: NavController,
@ -536,7 +533,7 @@ private fun TopAppBar(
val recOffImage = Icons.Filled.VoiceOverOff
val recOnImage = Icons.Filled.RecordVoiceOver
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) }
val about = stringResource(R.string.about)
@ -655,11 +652,22 @@ private fun TopAppBar(
.clip(CircleShape)
.combinedClickable(
onClick = {
if (Build.VERSION.SDK_INT >= 31)
Log.d(TAG, "Toggling speakerphone when dev/mode is " +
"${am.communicationDevice!!.type}/${am.mode}")
isSpeakerOn.value = !Utils.isSpeakerPhoneOn(am)
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
val isCurrentlyOn = isSpeakerOn
val aor = viewModel.selectedAor.value
val ua = uas.value.find { it.account.aor == aor }
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)
}
},
onLongClick = {
alertTitle.value = speakerPhoneTitle
@ -671,7 +679,7 @@ private fun TopAppBar(
Icon(
imageVector = Icons.Filled.SpeakerPhone,
contentDescription = null,
tint = if (isSpeakerOn.value)
tint = if (isSpeakerOn)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.onPrimary,
@ -891,8 +899,7 @@ private fun MainContent(navController: NavController, viewModel: ViewModel, cont
val calls by viewModel.calls.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 conferenceCall = filteredCalls.any { it.conferenceCall }
@ -1490,12 +1497,13 @@ private fun CallRow(
onClick = {
if (call.terminated.value) return@IconButton
call.terminated.value = true
abandonAudioFocus(ctx)
Log.d(
TAG,
"AoR ${call.ua.account.aor} canceling call ${call.callp} with ${call.callUri.value}"
)
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
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")
}
},
) {
Icon(
@ -1516,9 +1524,13 @@ private fun CallRow(
onClick = {
if (call.terminated.value) return@IconButton
call.terminated.value = true
abandonAudioFocus(ctx)
Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp} with ${call.callUri.value}")
Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated")
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")
}
}
) {
Icon(
@ -1532,19 +1544,26 @@ private fun CallRow(
IconButton(
modifier = Modifier.size(48.dp),
onClick = {
val connection = ConnectionService.connections[call.callp]
if (call.onhold) {
Log.d(
TAG,
"AoR ${call.ua.account.aor} resuming call ${call.callp} with ${call.callUri.value}"
)
call.resume()
if (connection != null)
connection.onUnhold()
else
call.resume()
call.onhold = false
} else {
Log.d(
TAG,
"AoR ${call.ua.account.aor} holding call ${call.callp} with ${call.callUri.value}"
)
call.hold()
if (connection != null)
connection.onHold()
else
call.hold()
call.onhold = true
}
},
@ -1568,12 +1587,15 @@ private fun CallRow(
enabled = call.transferButtonEnabled.value,
onClick = {
if (call.onHoldCall != null) {
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
}
else
showTransferDialog = true
},
) {
@ -1970,16 +1992,18 @@ private fun CallRow(
@Composable
private fun OnHoldNotice() {
OutlinedButton(
onClick = {},
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
modifier = Modifier.padding(16.dp),
shape = RoundedCornerShape(20)
) {
Text(
text = stringResource(R.string.call_is_on_hold),
fontSize = 18.sp
)
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
OutlinedButton(
onClick = {},
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
modifier = Modifier.padding(16.dp),
shape = RoundedCornerShape(20)
) {
Text(
text = stringResource(R.string.call_is_on_hold),
fontSize = 18.sp
)
}
}
}
@ -2020,10 +2044,10 @@ private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel
}
}
private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
val aor = ua.account.aor
private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean,
onHoldCallp: Long = 0L) {
val aor = viewModel.selectedAor.value
val ua = UserAgent.ofAor(aor)!!
val peerUri = if (Utils.isTelNumber(uriText))
"tel:$uriText"
else
@ -2044,44 +2068,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)
showAlert.value = true
}
else if (!BaresipService.requestAudioFocus(ctx))
Toast.makeText(ctx, R.string.audio_focus_denied, Toast.LENGTH_SHORT).show()
else if (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 (Build.VERSION.SDK_INT < 31) {
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
am.mode = AudioManager.MODE_IN_COMMUNICATION
runCall(ctx, viewModel, ua, uri, conferenceCall)
} else {
if (am.mode == AudioManager.MODE_IN_COMMUNICATION) {
runCall(ctx, viewModel, ua, uri, conferenceCall)
} else {
audioModeChangedListener = AudioManager.OnModeChangedListener { mode ->
if (mode == AudioManager.MODE_IN_COMMUNICATION) {
Log.d(TAG, "Audio mode changed to MODE_IN_COMMUNICATION using " +
"device ${am.communicationDevice!!.type}")
if (audioModeChangedListener != null) {
am.removeOnModeChangedListener(audioModeChangedListener!!)
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
}
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)
callExtras.putLong("uap", ua.uap)
if (onHoldCallp != 0L)
callExtras.putLong("onHoldCallp", onHoldCallp)
extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
try {
Log.d(TAG, "Placing Telecom call to $uri with uap=${ua.uap}")
tm.placeCall(uri.toUri(), extras)
} catch (e: SecurityException) {
Log.e(TAG, "placeCall failed: ${e.message}")
}
}
}
private fun answer(ctx: Context, call: Call) {
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)
intent.action = "Call Answer"
intent.putExtra("uap", call.ua.uap)
@ -2091,59 +2102,12 @@ 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}")
call.rejected = true
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
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")
}
}
@ -2161,12 +2125,22 @@ private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText:
val call = ua.currentCall()
if (call != null) {
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(ctx, viewModel, ua, uri, false,call)
makeCall(ctx, viewModel, uri, false, call.callp)
}
}
else {
val connection = ConnectionService.connections[call.callp]
connection?.onHold()
if (!call.transfer(uri)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = ctx.getString(R.string.transfer_failed)
@ -2213,8 +2187,9 @@ private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCal
call.focusDtmf.value = true
viewModel.requestShowKeyboard()
}
Log.d(TAG, "Showing call ${call.callp} from ${call.ua.account.aor} with status ${call.status.value}")
when (call.status.value) {
"outgoing", "transferring", "answered" -> {
"outgoing", "transferring", "answered", "disconnecting" -> {
call.callUriLabel.value = if (call.status.value == "answered")
ctx.getString(R.string.incoming_call_from_dots)
else
@ -2381,6 +2356,9 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
"call update" -> {
showCall(ctx, viewModel, ua)
}
"update calls" -> {
viewModel.updateCalls(Call.calls().toList())
}
"call verify" -> {
val callp = params[1] as Long
val call = Call.ofCallp(callp)
@ -2457,13 +2435,14 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
val call = Call.ofCallp(callp)
if (call in Call.calls())
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)
}
"transfer failed" -> {
showCall(ctx, viewModel, ua)
}
"call closed" -> {
viewModel.updateCalls(Call.calls().toList())
val activity = ctx as? Activity
if (activity != null) {
val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
@ -2498,6 +2477,16 @@ fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params
if (aor == viewModel.selectedAor.value)
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]}'")
}
@ -2777,19 +2766,3 @@ private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) {
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)
}
}
}

View File

@ -63,6 +63,9 @@ class ViewModel: ViewModel() {
private val _micIcon = MutableStateFlow(Icons.Filled.Mic)
val micIcon = _micIcon.asStateFlow()
private val _isSpeakerOn = MutableStateFlow(false)
val isSpeakerOn = _isSpeakerOn.asStateFlow()
private val _isDialpadVisible = MutableStateFlow(false)
val isDialpadVisible = _isDialpadVisible.asStateFlow()
@ -109,6 +112,10 @@ class ViewModel: ViewModel() {
_micIcon.value = icon
}
fun updateSpeakerPhoneStatus(on: Boolean) {
_isSpeakerOn.value = on
}
fun toggleDialpadVisibility() {
_isDialpadVisible.value = !_isDialpadVisible.value
}

View File

@ -505,6 +505,7 @@
<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="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
connected</string>
<string name="call_transfer">Call Transfer</string>