fix(android): own Bluetooth proxies across router lifecycle
This commit is contained in:
1 parent
3987295707
commit
de83d7bbf4
2 files changed
+51
-22
No files matched your search
@@ -29,8 +29,9 @@ enum class AudioRoute { EARPIECE, SPEAKER, HEADSET }
|
||||
* BluetoothHeadset profile (классический BT до установки SCO). Без BLUETOOTH_CONNECT
|
||||
* (Android 12+) BT-часть деградирует до «не видно» — остаётся проводная.
|
||||
*
|
||||
* Владелец — CallAudioEngine (start/stop в начале/конце звонка), т.к. именно он
|
||||
* управляет режимом AudioManager (MODE_IN_COMMUNICATION).
|
||||
* Владельцы — CallAudioEngine и MediaAudioController. start/stop и Bluetooth callbacks
|
||||
* сериализованы монитором router: media start может приходить из I/O, callbacks — с main.
|
||||
* Владение profile proxy сохраняется при disconnect сервиса и заканчивается явным close.
|
||||
*/
|
||||
class CallAudioRouter {
|
||||
|
||||
@@ -82,7 +83,7 @@ class CallAudioRouter {
|
||||
ChatApplication.instance.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
private var preferWiredHeadset = false
|
||||
private var started = false
|
||||
@Volatile private var started = false
|
||||
private var globalRouting = true
|
||||
private var desiredRoute = AudioRoute.SPEAKER
|
||||
var telecom: UtunConnection? = null
|
||||
@@ -111,12 +112,13 @@ class CallAudioRouter {
|
||||
}
|
||||
}
|
||||
private var deviceCallback: AudioDeviceCallback? = null
|
||||
private var btHeadset: BluetoothHeadset? = null
|
||||
private var btProfileProxyBound = false
|
||||
@Volatile private var btHeadset: BluetoothHeadset? = null // Владение, не признак подключения сервиса.
|
||||
private var btProfileAdapter: BluetoothAdapter? = null // Adapter, создавший текущий proxy; под монитором router.
|
||||
private var btProfileGeneration = 0L // Поколение запроса, инвалидируется до закрытия; под монитором router.
|
||||
/* Неизменяемый снимок, обновляется на main; capture читает уже найденные устройства. */
|
||||
@Volatile private var devices: Array<AudioDeviceInfo>? = null
|
||||
|
||||
fun start() {
|
||||
@Synchronized fun start() {
|
||||
if (started) return
|
||||
started = true
|
||||
val startedAt = SystemClock.elapsedRealtime()
|
||||
@@ -152,6 +154,7 @@ class CallAudioRouter {
|
||||
}
|
||||
|
||||
@Synchronized fun stop() {
|
||||
++btProfileGeneration
|
||||
if (!started) { bindTelecom(null); return }
|
||||
started = false
|
||||
|
||||
@@ -170,7 +173,7 @@ class CallAudioRouter {
|
||||
}
|
||||
devices = null
|
||||
bindTelecom(null)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "stopped")
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "stopped btGeneration=$btProfileGeneration")
|
||||
}
|
||||
|
||||
fun setRoute(r: AudioRoute, preferWired: Boolean = false, automatic: Boolean = false) {
|
||||
@@ -436,44 +439,69 @@ class CallAudioRouter {
|
||||
}
|
||||
|
||||
/* ── Bluetooth headset profile ── */
|
||||
/** Под монитором start; listener относится только к поколению этого запроса. */
|
||||
private fun bindBtHeadsetProfile() {
|
||||
val generation = ++btProfileGeneration
|
||||
try {
|
||||
val adapter = BluetoothAdapter.getDefaultAdapter() ?: return
|
||||
adapter.getProfileProxy(ChatApplication.instance, object : BluetoothProfile.ServiceListener {
|
||||
val accepted = adapter.getProfileProxy(ChatApplication.instance, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
if (profile != BluetoothProfile.HEADSET) return
|
||||
synchronized(this@CallAudioRouter) {
|
||||
if (!started || generation != btProfileGeneration) {
|
||||
try {
|
||||
adapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "stale BT proxy closed proxy=$proxy " +
|
||||
"generation=$generation current=$btProfileGeneration")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "CallAudioRouter", "stale BT proxy close failed proxy=$proxy " +
|
||||
"generation=$generation current=$btProfileGeneration: ${e.message}")
|
||||
}
|
||||
return
|
||||
}
|
||||
btProfileAdapter = adapter
|
||||
btHeadset = proxy as BluetoothHeadset
|
||||
btProfileProxyBound = true
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "bt headset profile connected")
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "bt headset profile connected proxy=$proxy generation=$generation")
|
||||
onDevicesChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
btHeadset = null
|
||||
btProfileProxyBound = false
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "bt headset profile disconnected")
|
||||
if (profile != BluetoothProfile.HEADSET) return
|
||||
synchronized(this@CallAudioRouter) {
|
||||
if (!started || generation != btProfileGeneration) {
|
||||
LogManager.addLog("DEBUG", "CallAudioRouter", "stale BT disconnect ignored " +
|
||||
"generation=$generation current=$btProfileGeneration")
|
||||
return
|
||||
}
|
||||
// Сервис отключён; proxy остаётся нашим до closeProfileProxy.
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "bt headset profile disconnected, proxy retained " +
|
||||
"proxy=$btHeadset generation=$generation")
|
||||
onDevicesChanged()
|
||||
}
|
||||
}
|
||||
}, BluetoothProfile.HEADSET)
|
||||
LogManager.addLog(if (accepted) "INFO" else "WARN", "CallAudioRouter", "bt profile request generation=$generation accepted=$accepted")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudioRouter", "bt profile proxy failed: ${e.message}")
|
||||
LogManager.addLog("WARN", "CallAudioRouter", "bt profile request failed generation=$generation: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/** Под монитором stop; отсоединить владение до вызова Android, даже если сервис уже отключён. */
|
||||
private fun unbindBtHeadsetProfile() {
|
||||
val adapter = btProfileAdapter
|
||||
val proxy = btHeadset
|
||||
btProfileAdapter = null
|
||||
btHeadset = null
|
||||
try {
|
||||
val adapter = BluetoothAdapter.getDefaultAdapter()
|
||||
if (adapter != null && btProfileProxyBound) {
|
||||
adapter.closeProfileProxy(BluetoothProfile.HEADSET, btHeadset)
|
||||
if (adapter != null && proxy != null) {
|
||||
adapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
|
||||
LogManager.addLog("INFO", "CallAudioRouter", "bt owned proxy closed proxy=$proxy generation=$btProfileGeneration")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "CallAudioRouter", "unbind bt profile failed: ${e.message}")
|
||||
LogManager.addLog("ERROR", "CallAudioRouter", "bt owned proxy close failed proxy=$proxy " +
|
||||
"generation=$btProfileGeneration: ${e.message}")
|
||||
}
|
||||
btHeadset = null
|
||||
btProfileProxyBound = false
|
||||
}
|
||||
|
||||
private fun hasBtConnectPermission(): Boolean =
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.2.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
||||
id("org.jetbrains.kotlin.jvm") version "1.9.20" apply false
|
||||
}
|
||||
Reference in new issue
Block a user