From 358bf5e2448a349e6d3436601ad2e2f961b73add Mon Sep 17 00:00:00 2001 From: darken Date: Mon, 22 Jun 2026 20:02:59 +0200 Subject: [PATCH] fix(monitor): Drive monitor mode from first addressed profile --- .../capod/monitor/core/MonitorModeResolver.kt | 10 +++- .../monitor/core/MonitorModeResolverTest.kt | 48 ++++++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/darken/capod/monitor/core/MonitorModeResolver.kt b/app/src/main/java/eu/darken/capod/monitor/core/MonitorModeResolver.kt index d8b0d5d9..b77b5d38 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/MonitorModeResolver.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/MonitorModeResolver.kt @@ -26,8 +26,14 @@ class MonitorModeResolver @Inject constructor( profilesRepo.profiles, nudgeCapabilityStore.availability, ) { profiles, nudge -> - val primary = profiles.firstOrNull() ?: return@combine MonitorMode.MANUAL - primary.requiredMode(nudge) + // The mode is driven by the first *addressed* profile, not just the first profile. An + // address-less profile can neither host an AAP session (AapAutoConnect requires an address) + // nor auto-connect, so it can only ever resolve to MANUAL. Letting such a profile sit at the + // top of the list and force MANUAL would silently stop the foreground service that keeps the + // process — and thus background AAP/stem controls — alive for an addressed profile below it. + // No addressed profile at all => genuinely nothing to monitor => MANUAL. + profiles.firstOrNull { !it.address.isNullOrBlank() }?.requiredMode(nudge) + ?: MonitorMode.MANUAL } .distinctUntilChanged() .onEach { log(TAG, VERBOSE) { "effectiveMode = $it" } } diff --git a/app/src/test/java/eu/darken/capod/monitor/core/MonitorModeResolverTest.kt b/app/src/test/java/eu/darken/capod/monitor/core/MonitorModeResolverTest.kt index f4c970ad..32c260d8 100644 --- a/app/src/test/java/eu/darken/capod/monitor/core/MonitorModeResolverTest.kt +++ b/app/src/test/java/eu/darken/capod/monitor/core/MonitorModeResolverTest.kt @@ -100,7 +100,7 @@ class MonitorModeResolverTest : BaseTest() { } @Test - fun `multi-profile primary-only - primary case 2 + secondary case 3 - AUTOMATIC`() = runTest { + fun `first addressed profile controls mode - addressed primary case 2 + addressed secondary case 3 - AUTOMATIC`() = runTest { profilesFlow.value = listOf( profile(id = "primary", autoConnect = false), profile(id = "secondary", autoConnect = true), @@ -111,25 +111,59 @@ class MonitorModeResolverTest : BaseTest() { } @Test - fun `multi-profile primary-only - primary case 4 + secondary case 2 - MANUAL`() = runTest { + fun `unpaired primary is skipped when an addressed secondary exists - AUTOMATIC`() = runTest { profilesFlow.value = listOf( profile(id = "primary", address = null), profile(id = "secondary", address = "AA:BB:CC:DD:EE:FF"), ) + resolver.effectiveMode.first() shouldBe MonitorMode.AUTOMATIC + } + + @Test + fun `unpaired primary is skipped - addressed autoConnect secondary drives ALWAYS`() = runTest { + profilesFlow.value = listOf( + profile(id = "primary", address = null), + profile(id = "secondary", address = "AA:BB:CC:DD:EE:FF", autoConnect = true), + ) + nudgeFlow.value = NudgeAvailability.AVAILABLE + + resolver.effectiveMode.first() shouldBe MonitorMode.ALWAYS + } + + @Test + fun `blank-address primary is skipped when an addressed secondary exists`() = runTest { + profilesFlow.value = listOf( + profile(id = "primary", address = " "), + profile(id = "secondary", address = "AA:BB:CC:DD:EE:FF", autoConnect = true), + ) + nudgeFlow.value = NudgeAvailability.AVAILABLE + + resolver.effectiveMode.first() shouldBe MonitorMode.ALWAYS + } + + @Test + fun `all profiles unpaired - MANUAL`() = runTest { + profilesFlow.value = listOf( + profile(id = "a", address = null, autoConnect = true), + profile(id = "b", address = "", autoConnect = true), + ) + nudgeFlow.value = NudgeAvailability.AVAILABLE + resolver.effectiveMode.first() shouldBe MonitorMode.MANUAL } @Test - fun `list reorder makes the secondary primary - mode flips`() = runTest { - val a = profile(id = "a", autoConnect = true) - val b = profile(id = "b", autoConnect = false) + fun `reorder among addressed profiles still flips the mode, unpaired stays ignored`() = runTest { + val unpaired = profile(id = "unpaired", address = null) + val addressedAuto = profile(id = "addressedAuto", autoConnect = true) + val addressedManual = profile(id = "addressedManual", autoConnect = false) nudgeFlow.value = NudgeAvailability.AVAILABLE - profilesFlow.value = listOf(a, b) + profilesFlow.value = listOf(unpaired, addressedAuto, addressedManual) resolver.effectiveMode.first() shouldBe MonitorMode.ALWAYS - profilesFlow.value = listOf(b, a) + profilesFlow.value = listOf(unpaired, addressedManual, addressedAuto) resolver.effectiveMode.first() shouldBe MonitorMode.AUTOMATIC }