diff --git a/app/src/main/java/eu/darken/capod/monitor/core/DeviceMonitor.kt b/app/src/main/java/eu/darken/capod/monitor/core/DeviceMonitor.kt index 9b970bb8..b98b05b5 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/DeviceMonitor.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/DeviceMonitor.kt @@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.replayingShare +import eu.darken.capod.monitor.core.aap.AapLifecycleManager import eu.darken.capod.monitor.core.ble.BlePodMonitor import eu.darken.capod.monitor.core.cache.DeviceStateCache import eu.darken.capod.monitor.core.cache.toCachedState @@ -33,7 +34,12 @@ class DeviceMonitor @Inject constructor( private val aapManager: AapConnectionManager, private val deviceStateCache: DeviceStateCache, private val profilesRepo: DeviceProfilesRepo, + private val aapLifecycleManager: AapLifecycleManager, ) { + init { + aapLifecycleManager.start() + } + val devices: Flow> = combine( blePodMonitor.devices, aapManager.allStates, diff --git a/app/src/main/java/eu/darken/capod/reaction/core/aap/AapAutoConnect.kt b/app/src/main/java/eu/darken/capod/monitor/core/aap/AapAutoConnect.kt similarity index 74% rename from app/src/main/java/eu/darken/capod/reaction/core/aap/AapAutoConnect.kt rename to app/src/main/java/eu/darken/capod/monitor/core/aap/AapAutoConnect.kt index 8a7f1811..a841544f 100644 --- a/app/src/main/java/eu/darken/capod/reaction/core/aap/AapAutoConnect.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/aap/AapAutoConnect.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.reaction.core.aap +package eu.darken.capod.monitor.core.aap import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE @@ -121,59 +121,61 @@ class AapAutoConnect @Inject constructor( return@onEach } - for ((attempt, delayMs) in RETRY_DELAYS.withIndex()) { - delay(delayMs) + try { + for ((attempt, delayMs) in RETRY_DELAYS.withIndex()) { + delay(delayMs) - // Check if still profiled - val profile = profilesRepo.profiles.first() - .firstOrNull { it.address == address } - if (profile == null) { - log(TAG) { "AAP reconnect: $address no longer profiled, stopping" } - break - } - - // Check if still bonded - val bonded = bluetoothManager.bondedDevices().first() - .firstOrNull { it.address == address } - if (bonded == null) { - log(TAG) { "AAP reconnect: $address no longer bonded, stopping" } - break - } - - // Check if still classically connected - val currentConnected = bluetoothManager.connectedDevices.first().map { it.address }.toSet() - if (address !in currentConnected) { - log(TAG) { "AAP reconnect: $address no longer classically connected, stopping" } - break - } - - // Check if still visible in BLE - val bleDevices = blePodMonitor.devices.first() - if (bleDevices.none { it.meta?.profile?.address == address }) { - log(TAG) { "AAP reconnect: $address no longer visible in BLE, stopping" } - break - } - - // Check if already reconnected (e.g., by initialConnect) - val currentState = aapManager.allStates.value[address] - if (currentState != null && currentState.connectionState != AapPodState.ConnectionState.DISCONNECTED) { - log(TAG) { "AAP reconnect: $address already reconnected" } - break - } - - try { - log(TAG) { "AAP reconnect attempt ${attempt + 1} for $address in ${delayMs}ms" } - withTimeout(CONNECT_TIMEOUT) { - aapManager.connect(address, bonded.internal, profile.model) + // Check if still profiled + val profile = profilesRepo.profiles.first() + .firstOrNull { it.address == address } + if (profile == null) { + log(TAG) { "AAP reconnect: $address no longer profiled, stopping" } + break } - log(TAG) { "AAP reconnected to $address" } - break - } catch (e: Exception) { - log(TAG, WARN) { "AAP reconnect attempt ${attempt + 1} failed for $address: ${e.message}" } - } - } - activeReconnects.remove(address) + // Check if still bonded + val bonded = bluetoothManager.bondedDevices().first() + .firstOrNull { it.address == address } + if (bonded == null) { + log(TAG) { "AAP reconnect: $address no longer bonded, stopping" } + break + } + + // Check if still classically connected + val currentConnected = bluetoothManager.connectedDevices.first().map { it.address }.toSet() + if (address !in currentConnected) { + log(TAG) { "AAP reconnect: $address no longer classically connected, stopping" } + break + } + + // Check if still visible in BLE + val bleDevices = blePodMonitor.devices.first() + if (bleDevices.none { it.meta?.profile?.address == address }) { + log(TAG) { "AAP reconnect: $address no longer visible in BLE, stopping" } + break + } + + // Check if already reconnected (e.g., by initialConnect) + val currentState = aapManager.allStates.value[address] + if (currentState != null && currentState.connectionState != AapPodState.ConnectionState.DISCONNECTED) { + log(TAG) { "AAP reconnect: $address already reconnected" } + break + } + + try { + log(TAG) { "AAP reconnect attempt ${attempt + 1} for $address in ${delayMs}ms" } + withTimeout(CONNECT_TIMEOUT) { + aapManager.connect(address, bonded.internal, profile.model) + } + log(TAG) { "AAP reconnected to $address" } + break + } catch (e: Exception) { + log(TAG, WARN) { "AAP reconnect attempt ${attempt + 1} failed for $address: ${e.message}" } + } + } + } finally { + activeReconnects.remove(address) + } } .map { } // SharedFlow → Flow .setupCommonEventHandlers(TAG) { "reconnect" } @@ -227,7 +229,7 @@ class AapAutoConnect @Inject constructor( .setupCommonEventHandlers(TAG) { "correctModel" } companion object { - private val TAG = logTag("Reaction", "AapAutoConnect") + private val TAG = logTag("Monitor", "AapAutoConnect") internal val RETRY_DELAYS = longArrayOf(3_000, 3_000, 3_000, 5_000, 5_000, 10_000, 10_000) private val CONNECT_TIMEOUT = 5.seconds } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/aap/AapLifecycleManager.kt b/app/src/main/java/eu/darken/capod/monitor/core/aap/AapLifecycleManager.kt new file mode 100644 index 00000000..746b65d4 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/monitor/core/aap/AapLifecycleManager.kt @@ -0,0 +1,42 @@ +package eu.darken.capod.monitor.core.aap + +import eu.darken.capod.common.coroutine.AppScope +import eu.darken.capod.common.debug.logging.Logging.Priority.WARN +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.common.flow.setupCommonEventHandlers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.merge +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Launches AAP auto-connect and key persistence in [appScope], + * independent of [eu.darken.capod.monitor.core.worker.MonitorService] lifecycle. + * + * Call [start] from [eu.darken.capod.monitor.core.DeviceMonitor] init to activate. + */ +@Singleton +class AapLifecycleManager @Inject constructor( + @AppScope private val appScope: CoroutineScope, + private val aapAutoConnect: AapAutoConnect, + private val aapKeyPersister: AapKeyPersister, +) { + fun start() { + log(TAG) { "start()" } + merge( + aapAutoConnect.monitor(), + aapKeyPersister.monitor(), + ) + .catch { e -> log(TAG, WARN) { "AAP lifecycle error: ${e.asLog()}" } } + .setupCommonEventHandlers(TAG) { "aapActive" } + .launchIn(appScope) + } + + companion object { + private val TAG = logTag("Monitor", "AapLifecycleManager") + } +} diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt index 32627e10..8d56ed0f 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt @@ -29,14 +29,12 @@ import eu.darken.capod.main.core.MonitorMode import eu.darken.capod.main.core.PermissionTool import eu.darken.capod.monitor.core.DeviceMonitor import eu.darken.capod.monitor.core.MonitorCoroutineScope -import eu.darken.capod.monitor.core.aap.AapKeyPersister import eu.darken.capod.monitor.core.ble.BlePodMonitor import eu.darken.capod.monitor.core.primaryDevice import eu.darken.capod.monitor.ui.MonitorNotifications import eu.darken.capod.pods.core.apple.aap.AapConnectionManager import eu.darken.capod.profiles.core.DeviceProfile import eu.darken.capod.profiles.core.DeviceProfilesRepo -import eu.darken.capod.reaction.core.aap.AapAutoConnect import eu.darken.capod.reaction.core.autoconnect.AutoConnect import eu.darken.capod.reaction.core.playpause.PlayPause import eu.darken.capod.reaction.core.popup.PopUpReaction @@ -75,9 +73,6 @@ class MonitorService : Service() { @Inject lateinit var popUpReaction: PopUpReaction @Inject lateinit var popUpWindow: PopUpWindow @Inject lateinit var profilesRepo: DeviceProfilesRepo - @Inject lateinit var aapAutoConnect: AapAutoConnect - @Inject lateinit var aapKeyPersister: AapKeyPersister - @Inject lateinit var aapConnectionManager: AapConnectionManager private val monitorScope = MonitorCoroutineScope() @@ -298,16 +293,6 @@ class MonitorService : Service() { .catch { log(TAG, WARN) { "autoConnect failed:\n${it.asLog()}" } } .launchIn(monitorScope) - aapAutoConnect.monitor() - .setupCommonEventHandlers(TAG) { "aapAutoConnect" } - .catch { log(TAG, WARN) { "aapAutoConnect failed:\n${it.asLog()}" } } - .launchIn(monitorScope) - - aapKeyPersister.monitor() - .setupCommonEventHandlers(TAG) { "aapKeyPersister" } - .catch { log(TAG, WARN) { "aapKeyPersister failed:\n${it.asLog()}" } } - .launchIn(monitorScope) - log(TAG, VERBOSE) { "Monitor job is active" } monitorJob.join() log(TAG, VERBOSE) { "Monitor job quit" } diff --git a/app/src/test/java/eu/darken/capod/reaction/core/aap/AapAutoConnectTest.kt b/app/src/test/java/eu/darken/capod/monitor/core/aap/AapAutoConnectTest.kt similarity index 99% rename from app/src/test/java/eu/darken/capod/reaction/core/aap/AapAutoConnectTest.kt rename to app/src/test/java/eu/darken/capod/monitor/core/aap/AapAutoConnectTest.kt index d46d1c69..7f2b1b3b 100644 --- a/app/src/test/java/eu/darken/capod/reaction/core/aap/AapAutoConnectTest.kt +++ b/app/src/test/java/eu/darken/capod/monitor/core/aap/AapAutoConnectTest.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.reaction.core.aap +package eu.darken.capod.monitor.core.aap import eu.darken.capod.common.bluetooth.BluetoothDevice2 import eu.darken.capod.common.bluetooth.BluetoothManager2