From fea1092a952e4d03d7f6274301fdd40f0c221f61 Mon Sep 17 00:00:00 2001 From: darken Date: Fri, 3 Apr 2026 13:26:59 +0200 Subject: [PATCH] refactor: Move AAP auto-connect from MonitorService to DeviceMonitor scope AAP connections now run in appScope via AapLifecycleManager, independent of MonitorMode. Fixes AAP not connecting when monitor mode is MANUAL. Also fixes reconnect cleanup on flow cancellation (try/finally). --- .../capod/monitor/core/DeviceMonitor.kt | 6 + .../core/aap/AapAutoConnect.kt | 106 +++++++++--------- .../monitor/core/aap/AapLifecycleManager.kt | 42 +++++++ .../monitor/core/worker/MonitorService.kt | 15 --- .../core/aap/AapAutoConnectTest.kt | 2 +- 5 files changed, 103 insertions(+), 68 deletions(-) rename app/src/main/java/eu/darken/capod/{reaction => monitor}/core/aap/AapAutoConnect.kt (74%) create mode 100644 app/src/main/java/eu/darken/capod/monitor/core/aap/AapLifecycleManager.kt rename app/src/test/java/eu/darken/capod/{reaction => monitor}/core/aap/AapAutoConnectTest.kt (99%) 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