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

View File

@ -1,5 +1,6 @@
package com.tutpro.baresip
import android.Manifest
import android.Manifest.permission.RECORD_AUDIO
import android.annotation.SuppressLint
import android.app.Notification
@ -7,6 +8,9 @@ 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
@ -59,6 +63,7 @@ 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
@ -93,10 +98,12 @@ 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 partialWakeLock: PowerManager.WakeLock
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
@ -113,6 +120,7 @@ class BaresipService: Service() {
private var mediaPlayer: MediaPlayer? = null
private var androidContactsObserverRegistered = false
private var hotSpotReceiverRegistered = false
private var bluetoothReceiverRegistered = false
private var isNotificationInCall = false
private var isServiceClean = false
private var cleanupRunnable: Runnable? = null
@ -281,7 +289,64 @@ class BaresipService: Service() {
hotSpotReceiverRegistered = true
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,
"com.tutpro.baresip:proximity_wakelock")
@ -374,8 +439,9 @@ class BaresipService: Service() {
} else {
Log.i(TAG, "Asset '$a' already copied")
}
if (a == "config")
if (a == "config") {
Config.initialize(applicationContext)
}
}
if (contactsMode != "android")
@ -427,6 +493,19 @@ class BaresipService: Service() {
activeNetwork = cm.activeNetwork
Log.i(TAG, "Active network: $activeNetwork")
if (telecom)
registerPhoneAccount()
else
if (btAdapter != null) {
Log.i(TAG, "Registering bluetooth receiver")
val filter = IntentFilter()
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
this.registerReceiver(bluetoothReceiver, filter)
bluetoothReceiverRegistered = true
}
Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable")
val userAgent = Config.variable("user_agent")
@ -800,7 +879,6 @@ class BaresipService: Service() {
if (call!!.status.value == "transferring")
break
speakerPhone = speakerPhoneAuto
// stopRinging()
stopMediaPlayer()
val hasTelecom = ConnectionService.connections.containsKey(callp) ||
ConnectionService.pendingOutgoingConnection != null
@ -2238,6 +2316,12 @@ class BaresipService: Service() {
} catch (_: IllegalArgumentException) {
Log.e(TAG, "hotSpotReceiver was not registered with applicationContext")
}
if (bluetoothReceiverRegistered) {
try {
unregisterReceiver(bluetoothReceiver)
bluetoothReceiverRegistered = false
} catch (_: IllegalArgumentException) {}
}
val callps = ConnectionService.connections.keys.toList()
for (callp in callps)
ConnectionService.onCallClosed(callp)
@ -2289,6 +2373,7 @@ class BaresipService: Service() {
var isConfigInitialized = false
var libraryLoaded = false
var callVolume = 0
@Volatile
var speakerPhone = false
var speakerPhoneAuto = false
@ -2329,6 +2414,7 @@ class BaresipService: Service() {
var contactsMode by mutableStateOf("baresip")
var addressFamily = ""
var dnsServers = listOf<InetAddress>()
// <aor, password> of those accounts that have auth username without auth password
val aorPasswords = mutableMapOf<String, String>()
var aecAvailable = false
@ -2338,17 +2424,24 @@ class BaresipService: Service() {
private var agc: AutomaticGainControl? = null
private var ns: NoiseSuppressor? = null
private var recorderSessionId = 0
private var btAdapter: BluetoothAdapter? = null
private var audioFocusRequest: androidx.media.AudioFocusRequestCompat? = null
var rt: Ringtone? = null
var colorblind = false
val circleGreen = mapOf(true to R.drawable.circle_green_blind,
false to R.drawable.circle_green)
val circleYellow = mapOf(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)
val circleGreen = mapOf(
true to R.drawable.circle_green_blind,
false to R.drawable.circle_green
)
val circleYellow = mapOf(
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"
private const val PHONE_ACCOUNT_ID = "baresip_phone_account"
@ -2363,9 +2456,9 @@ class BaresipService: Service() {
if (serviceEvents.size == 1) {
Log.d(TAG, "Posted service event ${event.event} at ${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 {
@ -2390,7 +2483,10 @@ class BaresipService: Service() {
) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
) {
Log.d(TAG, "requestAudioFocus granted")
} else {
if (!telecom && isBluetoothHeadsetConnected(ctx))
startBluetoothSco(ctx, 250L, 3)
}
else {
Log.w(TAG, "requestAudioFocus denied")
audioFocusRequest = null
}
@ -2408,11 +2504,75 @@ class BaresipService: Service() {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED
) {
audioFocusRequest = null
} else {
Log.e(TAG, "Failed to abandon audio focus")
if (!telecom && isBluetoothHeadsetConnected(ctx))
stopBluetoothSco(ctx)
}
else
Log.e(TAG, "Failed to abandon audio focus")
}
am.mode = MODE_NORMAL
}
private fun isBluetoothHeadsetConnected(ctx: Context): Boolean {
if (VERSION.SDK_INT >= 31 &&
ActivityCompat.checkSelfPermission(
ctx,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_DENIED
)
return false
return btAdapter != null && btAdapter!!.isEnabled &&
btAdapter!!.getProfileConnectionState(BluetoothHeadset.HEADSET) ==
BluetoothAdapter.STATE_CONNECTED
}
private fun isBluetoothScoOn(am: AudioManager): Boolean {
return if (VERSION.SDK_INT < 31)
@Suppress("DEPRECATION")
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

@ -40,7 +40,13 @@ class ConnectionService : ConnectionService() {
connections[callp]?.let {
Log.d(TAG, "Setting audio route for $callp to speaker=$speaker")
@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)
}
}
}
}

View File

@ -957,20 +957,22 @@ object Utils {
}
}
fun toggleSpeakerPhone(executor: Executor, am: AudioManager) {
val isSpeakerOn = if (Build.VERSION.SDK_INT >= 31) {
am.communicationDevice?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
} else {
@Suppress("DEPRECATION")
am.isSpeakerphoneOn
}
setSpeakerPhone(executor, am, !isSpeakerOn)
@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}")
}
fun clearCommunicationDevice(am: AudioManager) {
if (Build.VERSION.SDK_INT >= 31) {
if (Build.VERSION.SDK_INT >= 31)
am.clearCommunicationDevice()
} else {
else {
@Suppress("DEPRECATION")
am.isSpeakerphoneOn = false
}

View File

@ -436,8 +436,8 @@
<string name="proximity_sensing_help">Jos merkitty, läheisyyden tunnistus on aktiivinen
puhelun aikana.</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.
Bluetooth on tuettu vain, kun Telecom-kehys on käytössä.</string>
<string name="telecom_help">Käytä Android Telecom-kehystä puheluihin. Poista valinta, jos
puhelun toinen osapuoli ei kuule ääntäsi.</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_fps">Videokehysten lähetystaajuus</string>

View File

@ -410,7 +410,7 @@
<string name="proximity_sensing_help">If checked, proximity sensing is active during calls.</string>
<string name="telecom">Telecom Framework</string>
<string name="telecom_help">Use Android Telecom framework for calls. 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_help">Size of transmitted video frames (width x height)</string>
<string name="video_fps">Video Frames Per Second</string>