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).
This commit is contained in:
darken
2026-04-03 13:27:52 +02:00
committed by Matthias Urhahn
parent adfedc1cce
commit fea1092a95
5 changed files with 103 additions and 68 deletions
@@ -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<List<PodDevice>> = combine(
blePodMonitor.devices,
aapManager.allStates,
@@ -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<BluetoothAddress> → Flow<Unit>
.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
}
@@ -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")
}
}
@@ -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" }
@@ -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