diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt index a8e5dd75..c6405eb1 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt @@ -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 = 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> = callbackFlow { - log(TAG, VERBOSE) { "connectedDevices(profile=$profile) starting" } - trySend(getBluetoothProfile(profile).connectedDevices) + private fun monitorDevicesForProfile( + profile: Int = BluetoothProfile.HEADSET + ): Flow> = 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.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.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 = ContinuityProtocol.BLE_FEATURE_UUIDS ): Flow> = 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 = 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") diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothProfile2.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothProfile2.kt index 05f8326b..6ab5df72 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothProfile2.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothProfile2.kt @@ -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 - get() = profile.connectedDevices.toSet() - - val isConnected: Boolean - get() = isConnectedAtomic.get() + get() = proxy.connectedDevices.toSet() }