Restored bluetooth support when Telecom Framework is not active

This commit is contained in:
Juha Heinanen
2026-04-28 16:24:57 +03:00
parent 46c439e28d
commit b1694762e9
5 changed files with 196 additions and 28 deletions
@@ -1,5 +1,6 @@
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
@@ -7,6 +8,9 @@ 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
@@ -59,6 +63,7 @@ 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
@@ -93,10 +98,12 @@ 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
@@ -113,6 +120,7 @@ class BaresipService: Service() {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var androidContactsObserverRegistered = false private var androidContactsObserverRegistered = false
private var hotSpotReceiverRegistered = false private var hotSpotReceiverRegistered = false
private var bluetoothReceiverRegistered = false
private var isNotificationInCall = false private var isNotificationInCall = false
private var isServiceClean = false private var isServiceClean = false
private var cleanupRunnable: Runnable? = null private var cleanupRunnable: Runnable? = null
@@ -281,7 +289,64 @@ 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
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")
}
}
}
}
}
}
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")
@@ -374,9 +439,10 @@ class BaresipService: Service() {
} else { } else {
Log.i(TAG, "Asset '$a' already copied") Log.i(TAG, "Asset '$a' already copied")
} }
if (a == "config") if (a == "config") {
Config.initialize(applicationContext) Config.initialize(applicationContext)
} }
}
if (contactsMode != "android") if (contactsMode != "android")
Contact.restoreBaresipContacts() Contact.restoreBaresipContacts()
@@ -427,6 +493,19 @@ class BaresipService: Service() {
activeNetwork = cm.activeNetwork activeNetwork = cm.activeNetwork
Log.i(TAG, "Active network: $activeNetwork") Log.i(TAG, "Active network: $activeNetwork")
if (telecom)
registerPhoneAccount()
else
if (btAdapter != null) {
Log.i(TAG, "Registering bluetooth receiver")
val filter = IntentFilter()
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
this.registerReceiver(bluetoothReceiver, filter)
bluetoothReceiverRegistered = true
}
Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable") Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable")
val userAgent = Config.variable("user_agent") val userAgent = Config.variable("user_agent")
@@ -800,7 +879,6 @@ class BaresipService: Service() {
if (call!!.status.value == "transferring") if (call!!.status.value == "transferring")
break break
speakerPhone = speakerPhoneAuto speakerPhone = speakerPhoneAuto
// stopRinging()
stopMediaPlayer() stopMediaPlayer()
val hasTelecom = ConnectionService.connections.containsKey(callp) || val hasTelecom = ConnectionService.connections.containsKey(callp) ||
ConnectionService.pendingOutgoingConnection != null ConnectionService.pendingOutgoingConnection != null
@@ -2238,6 +2316,12 @@ class BaresipService: Service() {
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
Log.e(TAG, "hotSpotReceiver was not registered with applicationContext") Log.e(TAG, "hotSpotReceiver was not registered with applicationContext")
} }
if (bluetoothReceiverRegistered) {
try {
unregisterReceiver(bluetoothReceiver)
bluetoothReceiverRegistered = false
} catch (_: IllegalArgumentException) {}
}
val callps = ConnectionService.connections.keys.toList() val callps = ConnectionService.connections.keys.toList()
for (callp in callps) for (callp in callps)
ConnectionService.onCallClosed(callp) ConnectionService.onCallClosed(callp)
@@ -2289,6 +2373,7 @@ class BaresipService: Service() {
var isConfigInitialized = false var isConfigInitialized = false
var libraryLoaded = false var libraryLoaded = false
var callVolume = 0 var callVolume = 0
@Volatile @Volatile
var speakerPhone = false var speakerPhone = false
var speakerPhoneAuto = false var speakerPhoneAuto = false
@@ -2329,6 +2414,7 @@ class BaresipService: Service() {
var contactsMode by mutableStateOf("baresip") var contactsMode by mutableStateOf("baresip")
var addressFamily = "" var addressFamily = ""
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 aecAvailable = false var aecAvailable = false
@@ -2338,17 +2424,24 @@ class BaresipService: Service() {
private var agc: AutomaticGainControl? = null private var agc: AutomaticGainControl? = null
private var ns: NoiseSuppressor? = null private var ns: NoiseSuppressor? = null
private var recorderSessionId = 0 private var recorderSessionId = 0
private var btAdapter: BluetoothAdapter? = null
private var audioFocusRequest: androidx.media.AudioFocusRequestCompat? = null private var audioFocusRequest: androidx.media.AudioFocusRequestCompat? = null
var rt: Ringtone? = null var rt: Ringtone? = null
var colorblind = false var colorblind = false
val circleGreen = mapOf(true to R.drawable.circle_green_blind, val circleGreen = mapOf(
false to R.drawable.circle_green) true to R.drawable.circle_green_blind,
val circleYellow = mapOf(true to R.drawable.circle_yellow_blind, false to R.drawable.circle_green
false to R.drawable.circle_yellow) )
val circleRed = mapOf(true to R.drawable.circle_red_blind, val circleYellow = mapOf(
false to R.drawable.circle_red) true to R.drawable.circle_yellow_blind,
false to R.drawable.circle_yellow
)
val circleRed = mapOf(
true to R.drawable.circle_red_blind,
false to R.drawable.circle_red
)
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" private const val PHONE_ACCOUNT_ID = "baresip_phone_account"
@@ -2363,9 +2456,9 @@ class BaresipService: Service() {
if (serviceEvents.size == 1) { if (serviceEvents.size == 1) {
Log.d(TAG, "Posted service event ${event.event} at ${event.timeStamp}") Log.d(TAG, "Posted service event ${event.event} at ${event.timeStamp}")
serviceEvent.postValue(Event(event.timeStamp)) serviceEvent.postValue(Event(event.timeStamp))
} else {
Log.d(TAG, "Added service event ${event.event}")
} }
else
Log.d(TAG, "Added service event ${event.event}")
} }
fun requestAudioFocus(ctx: Context): Boolean { fun requestAudioFocus(ctx: Context): Boolean {
@@ -2390,7 +2483,10 @@ class BaresipService: Service() {
) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
) { ) {
Log.d(TAG, "requestAudioFocus granted") Log.d(TAG, "requestAudioFocus granted")
} else { if (!telecom && isBluetoothHeadsetConnected(ctx))
startBluetoothSco(ctx, 250L, 3)
}
else {
Log.w(TAG, "requestAudioFocus denied") Log.w(TAG, "requestAudioFocus denied")
audioFocusRequest = null audioFocusRequest = null
} }
@@ -2408,11 +2504,75 @@ class BaresipService: Service() {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED AudioManager.AUDIOFOCUS_REQUEST_GRANTED
) { ) {
audioFocusRequest = null audioFocusRequest = null
} else { if (!telecom && isBluetoothHeadsetConnected(ctx))
Log.e(TAG, "Failed to abandon audio focus") stopBluetoothSco(ctx)
} }
else
Log.e(TAG, "Failed to abandon audio focus")
} }
am.mode = MODE_NORMAL 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)
}
} }
} }
@@ -40,7 +40,13 @@ class ConnectionService : ConnectionService() {
connections[callp]?.let { connections[callp]?.let {
Log.d(TAG, "Setting audio route for $callp to speaker=$speaker") Log.d(TAG, "Setting audio route for $callp to speaker=$speaker")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
it.setAudioRoute(if (speaker) CallAudioState.ROUTE_SPEAKER else CallAudioState.ROUTE_EARPIECE) val currentRoute = it.callAudioState?.route ?: CallAudioState.ROUTE_EARPIECE
@Suppress("DEPRECATION")
if (speaker) {
it.setAudioRoute(CallAudioState.ROUTE_SPEAKER)
} else if (currentRoute == CallAudioState.ROUTE_SPEAKER) {
it.setAudioRoute(CallAudioState.ROUTE_EARPIECE)
}
} }
} }
} }
@@ -957,20 +957,22 @@ object Utils {
} }
} }
fun toggleSpeakerPhone(executor: Executor, am: AudioManager) { @RequiresApi(Build.VERSION_CODES.S)
val isSpeakerOn = if (Build.VERSION.SDK_INT >= 31) { fun setCommunicationDevice(am: AudioManager, type: Int) {
am.communicationDevice?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER val current = am.communicationDevice!!.type
} else { Log.d(TAG, "Current com dev/mode $current/${am.mode}")
@Suppress("DEPRECATION") for (device in am.availableCommunicationDevices)
am.isSpeakerphoneOn if (device.type == type) {
am.setCommunicationDevice(device)
break
} }
setSpeakerPhone(executor, am, !isSpeakerOn) Log.d(TAG, "New com dev/mode ${am.communicationDevice!!.type}/${am.mode}")
} }
fun clearCommunicationDevice(am: AudioManager) { fun clearCommunicationDevice(am: AudioManager) {
if (Build.VERSION.SDK_INT >= 31) { if (Build.VERSION.SDK_INT >= 31)
am.clearCommunicationDevice() am.clearCommunicationDevice()
} else { else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
am.isSpeakerphoneOn = false am.isSpeakerphoneOn = false
} }
+2 -2
View File
@@ -436,8 +436,8 @@
<string name="proximity_sensing_help">Jos merkitty, läheisyyden tunnistus on aktiivinen <string name="proximity_sensing_help">Jos merkitty, läheisyyden tunnistus on aktiivinen
puhelun aikana.</string> puhelun aikana.</string>
<string name="telecom">Telecom-kehys</string> <string name="telecom">Telecom-kehys</string>
<string name="telecom_help">Käytä Android Telecom -kehystä puheluihin. Poista valinta, jos puhelun toinen osapuoli ei kuule ääntäsi. <string name="telecom_help">Käytä Android Telecom-kehystä puheluihin. Poista valinta, jos
Bluetooth on tuettu vain, kun Telecom-kehys on käytössä.</string> puhelun toinen osapuoli ei kuule ääntäsi.</string>
<string name="video_size">Videon kehyskoko</string> <string name="video_size">Videon kehyskoko</string>
<string name="video_size_help">Lähetettävän videon kehyskoko (leveys x korkeus)</string> <string name="video_size_help">Lähetettävän videon kehyskoko (leveys x korkeus)</string>
<string name="video_fps">Videokehysten lähetystaajuus</string> <string name="video_fps">Videokehysten lähetystaajuus</string>
+1 -1
View File
@@ -410,7 +410,7 @@
<string name="proximity_sensing_help">If checked, proximity sensing is active during calls.</string> <string name="proximity_sensing_help">If checked, proximity sensing is active during calls.</string>
<string name="telecom">Telecom Framework</string> <string name="telecom">Telecom Framework</string>
<string name="telecom_help">Use Android Telecom framework for calls. Uncheck this if peer does <string name="telecom_help">Use Android Telecom framework for calls. Uncheck this if peer does
not hear your audio. Bluetooth is supported only when Telecom Framework is in use.</string> not hear your audio.</string>
<string name="video_size">Video Frame Size</string> <string name="video_size">Video Frame Size</string>
<string name="video_size_help">Size of transmitted video frames (width x height)</string> <string name="video_size_help">Size of transmitted video frames (width x height)</string>
<string name="video_fps">Video Frames Per Second</string> <string name="video_fps">Video Frames Per Second</string>