mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
fix: Improve BluetoothManager stability
Enhances robustness by adding extensive error handling around broadcast receiver registration, profile event processing, and service disconnections to prevent crashes. Also ensures device flow is cleared when Bluetooth is disabled.
This commit is contained in:
@@ -34,13 +34,13 @@ import kotlinx.coroutines.flow.filterNotNull
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -98,7 +98,7 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
|
|
||||||
override fun onServiceDisconnected(profile: Int) {
|
override fun onServiceDisconnected(profile: Int) {
|
||||||
log(TAG, WARN) { "onServiceDisconnected(profile=$profile)" }
|
log(TAG, WARN) { "onServiceDisconnected(profile=$profile)" }
|
||||||
close(IOException("BluetoothProfile service disconnected (profile=$profile)"))
|
close() // Close gracefully without exception to prevent crash
|
||||||
}
|
}
|
||||||
|
|
||||||
}, profile)
|
}, profile)
|
||||||
@@ -117,22 +117,27 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
|
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
log(TAG, VERBOSE) { "monitorProfile(): for profile=$profile starting" }
|
log(TAG, VERBOSE) { "monitorProfile(): for profile=$profile starting" }
|
||||||
trySend(bluetoothProfile.connectedDevices)
|
|
||||||
|
try {
|
||||||
|
trySend(bluetoothProfile.connectedDevices)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "monitorProfile(): Error querying initial connected devices: $e" }
|
||||||
|
close(e)
|
||||||
|
return@callbackFlow
|
||||||
|
}
|
||||||
|
|
||||||
val filter = IntentFilter().apply {
|
val filter = IntentFilter().apply {
|
||||||
addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
||||||
}
|
}
|
||||||
|
|
||||||
val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
|
val handlerThread = HandlerThread("BluetoothEventReceiver").apply { start() }
|
||||||
start()
|
|
||||||
}
|
|
||||||
val handler = Handler(handlerThread.looper)
|
val handler = Handler(handlerThread.looper)
|
||||||
|
|
||||||
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
log(TAG, VERBOSE) { "monitorProfile(): Bluetooth event (intent=$intent, extras=${intent.extras})" }
|
log(TAG, VERBOSE) { "monitorProfile(): Bluetooth event (intent=$intent, extras=${intent.extras})" }
|
||||||
val action = intent.action
|
|
||||||
if (action == null) {
|
if (intent.action == null) {
|
||||||
log(TAG, ERROR) { "monitorProfile(): Bluetooth event without action?" }
|
log(TAG, ERROR) { "monitorProfile(): Bluetooth event without action?" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -143,11 +148,8 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
this@callbackFlow.launch {
|
this@callbackFlow.launch {
|
||||||
val currentDevices = bluetoothProfile.connectedDevices.toMutableSet()
|
if (intent.action != BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) {
|
||||||
log(TAG) { "monitorProfile(): currentDevices: $currentDevices" }
|
log(TAG, WARN) { "Unknown action: ${intent.action}" }
|
||||||
|
|
||||||
if (action != BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) {
|
|
||||||
log(TAG, WARN) { "Unknown action: $action" }
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +160,14 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
val stateNow = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1)
|
val stateNow = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1)
|
||||||
log(TAG) { "monitorProfile(): HEADSET profile state changed for $device - now: $stateNow" }
|
log(TAG) { "monitorProfile(): HEADSET profile state changed for $device - now: $stateNow" }
|
||||||
|
|
||||||
|
val currentDevices = try {
|
||||||
|
bluetoothProfile.connectedDevices
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "monitorProfile(): Error handling profile event: $e" }
|
||||||
|
// Log but continue - don't kill the whole Flow for one bad event
|
||||||
|
emptySet()
|
||||||
|
}.toMutableSet()
|
||||||
|
log(TAG) { "monitorProfile(): currentDevices: $currentDevices" }
|
||||||
|
|
||||||
when (stateNow) {
|
when (stateNow) {
|
||||||
BluetoothProfile.STATE_CONNECTING -> {
|
BluetoothProfile.STATE_CONNECTING -> {
|
||||||
@@ -167,7 +177,10 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
BluetoothProfile.STATE_CONNECTED -> {
|
BluetoothProfile.STATE_CONNECTED -> {
|
||||||
log(TAG) { "monitorProfile(): Device has connected $device" }
|
log(TAG) { "monitorProfile(): Device has connected $device" }
|
||||||
if (!currentDevices.contains(device)) {
|
if (!currentDevices.contains(device)) {
|
||||||
log(TAG, WARN) { "monitorProfile(): $device was not in $currentDevices" }
|
log(
|
||||||
|
TAG,
|
||||||
|
VERBOSE
|
||||||
|
) { "monitorProfile(): $device not in proxy yet, adding manually" }
|
||||||
currentDevices.add(device)
|
currentDevices.add(device)
|
||||||
}
|
}
|
||||||
trySend(currentDevices)
|
trySend(currentDevices)
|
||||||
@@ -180,7 +193,10 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||||
log(TAG) { "monitorProfile(): Device has disconnected $device" }
|
log(TAG) { "monitorProfile(): Device has disconnected $device" }
|
||||||
if (currentDevices.contains(device)) {
|
if (currentDevices.contains(device)) {
|
||||||
log(TAG, WARN) { "monitorProfile(): $device WAS in $currentDevices" }
|
log(
|
||||||
|
TAG,
|
||||||
|
VERBOSE
|
||||||
|
) { "monitorProfile(): $device still in proxy, removing manually" }
|
||||||
currentDevices.remove(device)
|
currentDevices.remove(device)
|
||||||
}
|
}
|
||||||
trySend(currentDevices)
|
trySend(currentDevices)
|
||||||
@@ -189,11 +205,24 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.registerReceiver(receiver, filter, null, handler)
|
|
||||||
|
try {
|
||||||
|
context.registerReceiver(receiver, filter, null, handler)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "monitorProfile(): Failed to register receiver: $e" }
|
||||||
|
close(e)
|
||||||
|
return@callbackFlow
|
||||||
|
}
|
||||||
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
log(TAG, VERBOSE) { "monitorProfile(): profile=$profile closed." }
|
log(TAG, VERBOSE) { "monitorProfile(): profile=$profile closed." }
|
||||||
context.unregisterReceiver(receiver)
|
try {
|
||||||
|
context.unregisterReceiver(receiver)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "monitorProfile(): Error unregistering receiver: $e" }
|
||||||
|
} finally {
|
||||||
|
handlerThread.quitSafely()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,7 +231,10 @@ class BluetoothManager2 @Inject constructor(
|
|||||||
private val seenDevicesCache = mutableMapOf<String, Instant>()
|
private val seenDevicesCache = mutableMapOf<String, Instant>()
|
||||||
|
|
||||||
val connectedDevices: Flow<List<BluetoothDevice2>> = isBluetoothEnabled
|
val connectedDevices: Flow<List<BluetoothDevice2>> = isBluetoothEnabled
|
||||||
.flatMapLatest { monitorProfile(BluetoothProfile.HEADSET) }
|
.flatMapLatest { enabled ->
|
||||||
|
if (enabled) monitorProfile(BluetoothProfile.HEADSET)
|
||||||
|
else flowOf(emptySet()) // Return empty when Bluetooth is off
|
||||||
|
}
|
||||||
.map { devices ->
|
.map { devices ->
|
||||||
val currentAddresses = devices.map { it.address }
|
val currentAddresses = devices.map { it.address }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user