diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothDevice2.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothDevice2.kt index 2c92331c..6517fc0f 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothDevice2.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothDevice2.kt @@ -7,8 +7,11 @@ import kotlinx.parcelize.Parcelize @Parcelize data class BluetoothDevice2( - private val device: BluetoothDevice + val device: BluetoothDevice ) : Parcelable { + val name: String? + get() = device.name + fun hasFeature(uuid: ParcelUuid): Boolean = device.hasFeature(uuid) } \ No newline at end of file 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 8b2ab14d..fc35efeb 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 @@ -1,9 +1,6 @@ package eu.darken.capod.common.bluetooth -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothManager -import android.bluetooth.BluetoothProfile +import android.bluetooth.* import android.bluetooth.le.BluetoothLeScanner import android.content.BroadcastReceiver import android.content.Context @@ -13,16 +10,22 @@ import android.os.Handler import android.os.HandlerThread import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.logging.Logging.Priority.* +import eu.darken.capod.common.debug.logging.asLog 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.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.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 @@ -93,7 +96,7 @@ class BluetoothManager2 @Inject constructor( } } - fun connectedDevices(profile: Int = BluetoothProfile.HEADSET): Flow> = callbackFlow { + private fun connectedDevicesForProfile(profile: Int): Flow> = callbackFlow { log(TAG) { "connectedDevices(profile=$profile) starting" } trySend(getBluetoothProfile(profile).connectedDevices) @@ -147,6 +150,31 @@ class BluetoothManager2 @Inject constructor( } } + fun connectedDevices(): Flow> = isBluetoothEnabled + .flatMapLatest { connectedDevicesForProfile(BluetoothProfile.HEADSET) } + .map { devices -> + devices.filter { device -> + ContinuityProtocol.BLE_FEATURE_UUIDS.any { feature -> + device.hasFeature(feature) + } + } + } + + fun bondedDevices(): List = adapter.bondedDevices + .map { BluetoothDevice2(it) } + + suspend fun nudgeConnection(device: BluetoothDevice2): Boolean = try { + val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod( + "connect", BluetoothDevice::class.java + ).apply { isAccessible = true } + connectMethod.invoke(getBluetoothProfile().profile, device.device) + true + } catch (e: Exception) { + log(TAG, WARN) { "BluetoothHeadset.connect(device) is unavailable:\n${e.asLog()}" } + Bugs.report(e) + false + } + companion object { private val TAG = logTag("Bluetooth", "Manager2") } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PairedDevices.kt b/app/src/main/java/eu/darken/capod/monitor/core/PairedDevices.kt deleted file mode 100644 index fabaa0d4..00000000 --- a/app/src/main/java/eu/darken/capod/monitor/core/PairedDevices.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.darken.capod.monitor.core - -import eu.darken.capod.common.bluetooth.BluetoothDevice2 -import eu.darken.capod.common.bluetooth.BluetoothManager2 -import eu.darken.capod.common.coroutine.AppScope -import eu.darken.capod.common.flow.replayingShare -import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PairedDevices @Inject constructor( - @AppScope private val scope: CoroutineScope, - private val bluetoothManager2: BluetoothManager2, -) { - - fun connectedDevices(): Flow> = bluetoothManager2 - .isBluetoothEnabled - .flatMapLatest { bluetoothManager2.connectedDevices() } - .map { devices -> - devices.filter { device -> - ContinuityProtocol.BLE_FEATURE_UUIDS.any { feature -> - device.hasFeature(feature) - } - } - } - .replayingShare(scope) -} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt index c123a1f3..1b5b47af 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt @@ -8,6 +8,7 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.hilt.EntryPoints +import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.logging.Logging.Priority.* import eu.darken.capod.common.debug.logging.asLog @@ -19,6 +20,7 @@ import eu.darken.capod.main.core.MonitorMode import eu.darken.capod.main.core.PermissionTool import eu.darken.capod.monitor.core.* import eu.darken.capod.monitor.ui.MonitorNotifications +import eu.darken.capod.reactions.core.ReactionsHub import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay @@ -34,9 +36,9 @@ class MonitorWorker @AssistedInject constructor( private val notificationManager: NotificationManager, private val generalSettings: GeneralSettings, private val permissionTool: PermissionTool, - private val pairedDevices: PairedDevices, private val podMonitor: PodMonitor, - private val podReactions: PodReactions, + private val reactionsHub: ReactionsHub, + private val bluetoothManager: BluetoothManager2, ) : CoroutineWorker(context, params) { private val workerScope = MonitorCoroutineScope() @@ -105,7 +107,7 @@ class MonitorWorker @AssistedInject constructor( return@flatMapLatest emptyFlow() } - pairedDevices.connectedDevices().map { knownDevices -> + bluetoothManager.connectedDevices().map { knownDevices -> monitorMode to knownDevices } } @@ -136,7 +138,7 @@ class MonitorWorker @AssistedInject constructor( } .launchIn(workerScope) - podReactions.podReactions() + reactionsHub.monitor() .catch { log(TAG, WARN) { "Pod reactions failed:\n${it.asLog()}" } } diff --git a/app/src/main/java/eu/darken/capod/reactions/core/AutoConnect.kt b/app/src/main/java/eu/darken/capod/reactions/core/AutoConnect.kt new file mode 100644 index 00000000..7e816235 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/reactions/core/AutoConnect.kt @@ -0,0 +1,26 @@ +package eu.darken.capod.reactions.core + +import eu.darken.capod.common.bluetooth.BluetoothManager2 +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.monitor.core.PodMonitor +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AutoConnect @Inject constructor( + private val bluetoothManager: BluetoothManager2, + private val podMonitor: PodMonitor, +) { + + fun monitor(): Flow = podMonitor.mainDevice + .map { + val podDevice = bluetoothManager2.bondedDevices().firstOrNull { it.name?.contains("AirPod") ?: false } + bluetoothManager2.nudgeConnection(podDevice!!) + } + + companion object { + private val TAG = logTag("Reactions", "AutoConnect") + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodReactions.kt b/app/src/main/java/eu/darken/capod/reactions/core/PlayPause.kt similarity index 84% rename from app/src/main/java/eu/darken/capod/monitor/core/PodReactions.kt rename to app/src/main/java/eu/darken/capod/reactions/core/PlayPause.kt index 602fa917..0ea12406 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodReactions.kt +++ b/app/src/main/java/eu/darken/capod/reactions/core/PlayPause.kt @@ -1,12 +1,14 @@ -package eu.darken.capod.monitor.core +package eu.darken.capod.reactions.core import dagger.Reusable import eu.darken.capod.common.MediaControl +import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.setupCommonEventHandlers import eu.darken.capod.common.flow.withPrevious import eu.darken.capod.main.core.GeneralSettings +import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.pods.core.HasEarDetection import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow @@ -15,14 +17,14 @@ import kotlinx.coroutines.flow.onEach import javax.inject.Inject @Reusable -class PodReactions @Inject constructor( +class PlayPause @Inject constructor( private val podMonitor: PodMonitor, - private val pairedDevices: PairedDevices, + private val bluetoothManager: BluetoothManager2, private val generalSettings: GeneralSettings, private val mediaControl: MediaControl, ) { - fun podReactions() = pairedDevices.connectedDevices() + fun monitor() = bluetoothManager.connectedDevices() .flatMapLatest { if (it.isEmpty()) { log(TAG) { "No known devices connected." } @@ -50,6 +52,6 @@ class PodReactions @Inject constructor( } companion object { - private val TAG = logTag("Monitor", "Reactions") + private val TAG = logTag("Reactions", "PlayPause") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/reactions/core/ReactionsHub.kt b/app/src/main/java/eu/darken/capod/reactions/core/ReactionsHub.kt new file mode 100644 index 00000000..f19486d4 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/reactions/core/ReactionsHub.kt @@ -0,0 +1,25 @@ +package eu.darken.capod.reactions.core + +import dagger.Reusable +import eu.darken.capod.common.debug.logging.logTag +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import javax.inject.Inject + +@Reusable +class ReactionsHub @Inject constructor( + private val playPause: PlayPause, + private val autoConnect: AutoConnect, +) { + + fun monitor(): Flow = combine( + playPause.monitor(), + autoConnect.monitor() + ) { _, _ -> + Unit + } + + companion object { + private val TAG = logTag("Reactions", "Hub") + } +} \ No newline at end of file