Fix Bluetooth Profile triggering illegal state exception due to already resumed coroutine when the profile service disconnects.

This commit is contained in:
darken
2022-01-20 15:49:49 +01:00
parent 91a16f55d9
commit 5d72d0116b
2 changed files with 86 additions and 94 deletions
@@ -16,20 +16,13 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.IOException
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
@Singleton
class BluetoothManager2 @Inject constructor(
@@ -68,90 +61,95 @@ class BluetoothManager2 @Inject constructor(
awaitClose { context.unregisterReceiver(receiver) }
}
suspend fun getBluetoothProfile(
profile: Int = BluetoothProfile.HEADSET
): BluetoothProfile2 = withContext(dispatcherProvider.IO) {
fun getBluetoothProfile(profile: Int = BluetoothProfile.HEADSET): Flow<BluetoothProfile2> = callbackFlow {
log(TAG, VERBOSE) { "getBluetoothProfile(profile=$profile)" }
suspendCancellableCoroutine {
val connectionState = AtomicBoolean(false)
manager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
log(TAG, VERBOSE) { "onServiceConnected(profile=$profile, proxy=$proxy)" }
connectionState.set(true)
BluetoothProfile2(
profileType = profile,
profileProxy = proxy,
isConnectedAtomic = connectionState
).run { it.resume(this) }
}
var profileProxy: BluetoothProfile2? = null
manager.adapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
log(TAG, VERBOSE) { "onServiceConnected(profile=$profile, proxy=$proxy)" }
override fun onServiceDisconnected(profile: Int) {
log(TAG, WARN) { "onServiceDisconnected(profile=$profile" }
connectionState.set(false)
it.cancel(IOException("BluetoothProfile service disconnected (profile=$profile)"))
}
profileProxy = BluetoothProfile2(
profileType = profile,
profileProxy = proxy,
).also { trySend(it) }
}
}, profile)
override fun onServiceDisconnected(profile: Int) {
log(TAG, WARN) { "onServiceDisconnected(profile=$profile" }
throw CancellationException("BluetoothProfile service disconnected (profile=$profile)")
}
}, profile)
awaitClose {
log(TAG) { "Closing BluetoothProfile: $profileProxy" }
profileProxy?.let {
manager.adapter.closeProfileProxy(it.profileType, it.proxy)
}
}
}
private fun connectedDevicesForProfile(profile: Int): Flow<Set<BluetoothDevice>> = callbackFlow {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" }
trySend(getBluetoothProfile(profile).connectedDevices)
private fun monitorDevicesForProfile(
profile: Int = BluetoothProfile.HEADSET
): Flow<Set<BluetoothDevice>> = getBluetoothProfile(profile).flatMapLatest { bluetoothProfile ->
callbackFlow {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" }
trySend(bluetoothProfile.connectedDevices)
val filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}
val filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}
val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
start()
}
val handler = Handler(handlerThread.looper)
val handlerThread = HandlerThread("BluetoothEventReceiver").apply {
start()
}
val handler = Handler(handlerThread.looper)
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
log(TAG, VERBOSE) { "Bluetooth event (intent=$intent, extras=${intent.extras})" }
val action = intent.action
if (action == null) {
log(TAG, ERROR) { "Bluetooth event without action, how did we get this?" }
return
}
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
if (device == null) {
log(TAG, ERROR) { "Connection event is missing EXTRA_DEVICE: ${intent.extras}" }
return
}
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
log(TAG, VERBOSE) { "Bluetooth event (intent=$intent, extras=${intent.extras})" }
val action = intent.action
if (action == null) {
log(TAG, ERROR) { "Bluetooth event without action, how did we get this?" }
return
}
val device = intent.getParcelableExtra<BluetoothDevice?>(BluetoothDevice.EXTRA_DEVICE)
if (device == null) {
log(TAG, ERROR) { "Connection event is missing EXTRA_DEVICE: ${intent.extras}" }
return
}
this@callbackFlow.launch {
val currentDevices = getBluetoothProfile(profile).connectedDevices
this@callbackFlow.launch {
val currentDevices = bluetoothProfile.connectedDevices
when (action) {
BluetoothDevice.ACTION_ACL_CONNECTED -> {
log(TAG) { "Adding $device to current devices $currentDevices" }
trySend(currentDevices.plus(device))
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
log(TAG) { "Removing $device from current devices $currentDevices" }
trySend(currentDevices.minus(device))
when (action) {
BluetoothDevice.ACTION_ACL_CONNECTED -> {
log(TAG) { "Adding $device to current devices $currentDevices" }
trySend(currentDevices.plus(device))
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
log(TAG) { "Removing $device from current devices $currentDevices" }
trySend(currentDevices.minus(device))
}
}
}
}
}
}
context.registerReceiver(receiver, filter, null, handler)
context.registerReceiver(receiver, filter, null, handler)
awaitClose {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) closed." }
context.unregisterReceiver(receiver)
awaitClose {
log(TAG, VERBOSE) { "connectedDevices(profile=$profile) closed." }
context.unregisterReceiver(receiver)
}
}
}
fun connectedDevices(
featureFilter: Set<ParcelUuid> = ContinuityProtocol.BLE_FEATURE_UUIDS
): Flow<List<BluetoothDevice>> = isBluetoothEnabled
.flatMapLatest { connectedDevicesForProfile(BluetoothProfile.HEADSET) }
.flatMapLatest { monitorDevicesForProfile(BluetoothProfile.HEADSET) }
.map { devices ->
devices.filter { device ->
featureFilter.any { feature ->
@@ -162,21 +160,23 @@ class BluetoothManager2 @Inject constructor(
fun bondedDevices(): Set<BluetoothDevice> = adapter.bondedDevices
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = try {
log(TAG) { "Nudging Android connection to $device" }
suspend fun nudgeConnection(device: BluetoothDevice): Boolean = getBluetoothProfile().map { bluetoothProfile ->
try {
log(TAG) { "Nudging Android connection to $device" }
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
"connect", BluetoothDevice::class.java
).apply { isAccessible = true }
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
"connect", BluetoothDevice::class.java
).apply { isAccessible = true }
connectMethod.invoke(getBluetoothProfile().profile, device)
connectMethod.invoke(bluetoothProfile.proxy, device)
log(TAG) { "Nudged connection to $device" }
true
} catch (e: Exception) {
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
false
}
log(TAG) { "Nudged connection to $device" }
true
} catch (e: Exception) {
Bugs.report(tag = TAG, "BluetoothHeadset.connect(device) is unavailable", exception = e)
false
}
}.first()
companion object {
private val TAG = logTag("Bluetooth", "Manager2")
@@ -2,23 +2,15 @@ package eu.darken.capod.common.bluetooth
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothProfile
import java.util.concurrent.atomic.AtomicBoolean
data class BluetoothProfile2(
private val profileType: Int,
internal val profileType: Int,
private val profileProxy: BluetoothProfile,
private val isConnectedAtomic: AtomicBoolean,
) {
val profile: BluetoothProfile
get() {
if (!isConnected) throw IllegalStateException("Proxy is not connected")
return profileProxy
}
val proxy: BluetoothProfile
get() = profileProxy
val connectedDevices: Set<BluetoothDevice>
get() = profile.connectedDevices.toSet()
val isConnected: Boolean
get() = isConnectedAtomic.get()
get() = proxy.connectedDevices.toSet()
}