mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
refactor(monitor): Compute monitor mode automatically from profile state
This commit is contained in:
committed by
Matthias Urhahn
parent
8c9094d85a
commit
a4ba302223
+23
-14
@@ -4,10 +4,13 @@ import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.bluetooth.NudgeAttemptResult
|
||||
import eu.darken.capod.common.bluetooth.NudgeAvailability
|
||||
import eu.darken.capod.common.bluetooth.NudgeCapabilityStore
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||
import eu.darken.capod.monitor.core.MonitorModeResolver
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
|
||||
@@ -43,7 +46,6 @@ import org.junit.jupiter.api.extension.ExtendWith
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.TestTimeSource
|
||||
import testhelpers.coroutine.TestDispatcherProvider
|
||||
import testhelpers.datastore.FakeDataStoreValue
|
||||
import testhelpers.livedata.InstantExecutorExtension
|
||||
|
||||
@ExtendWith(InstantExecutorExtension::class)
|
||||
@@ -60,8 +62,10 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
private lateinit var upgradeRepo: UpgradeRepo
|
||||
private lateinit var bluetoothManager: BluetoothManager2
|
||||
private lateinit var profilesRepo: DeviceProfilesRepo
|
||||
private lateinit var generalSettings: GeneralSettings
|
||||
private lateinit var fakeMonitorMode: FakeDataStoreValue<MonitorMode>
|
||||
private lateinit var monitorModeResolver: MonitorModeResolver
|
||||
private lateinit var nudgeCapabilityStore: NudgeCapabilityStore
|
||||
private lateinit var nudgeAvailabilityFlow: MutableStateFlow<NudgeAvailability>
|
||||
private lateinit var effectiveModeFlow: MutableStateFlow<MonitorMode>
|
||||
private val timeSource: TimeSource = TestTimeSource()
|
||||
|
||||
private lateinit var devicesFlow: MutableStateFlow<List<PodDevice>>
|
||||
@@ -100,9 +104,9 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
}
|
||||
connectedDevicesFlow = MutableStateFlow(emptyList())
|
||||
bluetoothManager = mockk(relaxed = true) {
|
||||
every { isNudgeAvailable } returns true
|
||||
every { bondedDevices() } returns flowOf(emptySet())
|
||||
every { connectedDevices } returns connectedDevicesFlow
|
||||
coEvery { nudgeConnection(any()) } returns NudgeAttemptResult.Rejected
|
||||
}
|
||||
profilesFlow = MutableStateFlow(
|
||||
listOf(
|
||||
@@ -112,9 +116,13 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
profilesRepo = mockk(relaxed = true) {
|
||||
every { profiles } returns profilesFlow
|
||||
}
|
||||
fakeMonitorMode = FakeDataStoreValue(MonitorMode.AUTOMATIC)
|
||||
generalSettings = mockk<GeneralSettings>().also {
|
||||
every { it.monitorMode } returns fakeMonitorMode.mock
|
||||
effectiveModeFlow = MutableStateFlow(MonitorMode.AUTOMATIC)
|
||||
monitorModeResolver = mockk<MonitorModeResolver>().also {
|
||||
every { it.effectiveMode } returns effectiveModeFlow
|
||||
}
|
||||
nudgeAvailabilityFlow = MutableStateFlow(NudgeAvailability.UNKNOWN)
|
||||
nudgeCapabilityStore = mockk<NudgeCapabilityStore>(relaxed = true).also {
|
||||
every { it.availability } returns nudgeAvailabilityFlow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +140,8 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
upgradeRepo = upgradeRepo,
|
||||
bluetoothManager = bluetoothManager,
|
||||
profilesRepo = profilesRepo,
|
||||
generalSettings = generalSettings,
|
||||
monitorModeResolver = monitorModeResolver,
|
||||
nudgeCapabilityStore = nudgeCapabilityStore,
|
||||
timeSource = timeSource,
|
||||
webpageTool = mockk(relaxed = true),
|
||||
).also { vm = it }
|
||||
@@ -150,7 +159,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
fun `forceConnect happy path - bonded exists, nudge accepted, no event emitted`() = runVmTest {
|
||||
val bonded = mockBondedDevice(testAddress)
|
||||
every { bluetoothManager.bondedDevices() } returns flowOf(setOf(bonded))
|
||||
coEvery { bluetoothManager.nudgeConnection(bonded) } returns true
|
||||
coEvery { bluetoothManager.nudgeConnection(bonded) } returns NudgeAttemptResult.Accepted
|
||||
|
||||
val vm = createViewModel()
|
||||
vm.initialize(testAddress)
|
||||
@@ -166,7 +175,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
fun `forceConnect when nudge not accepted - emits OpenBluetoothSettings`() = runVmTest {
|
||||
val bonded = mockBondedDevice(testAddress)
|
||||
every { bluetoothManager.bondedDevices() } returns flowOf(setOf(bonded))
|
||||
coEvery { bluetoothManager.nudgeConnection(bonded) } returns false
|
||||
coEvery { bluetoothManager.nudgeConnection(bonded) } returns NudgeAttemptResult.Rejected
|
||||
|
||||
val vm = createViewModel()
|
||||
vm.initialize(testAddress)
|
||||
@@ -183,7 +192,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
fun `forceConnect when nudge unavailable - emits OpenBluetoothSettings without calling nudge`() = runVmTest {
|
||||
val bonded = mockBondedDevice(testAddress)
|
||||
every { bluetoothManager.bondedDevices() } returns flowOf(setOf(bonded))
|
||||
every { bluetoothManager.isNudgeAvailable } returns false
|
||||
nudgeAvailabilityFlow.value = NudgeAvailability.BROKEN
|
||||
|
||||
val vm = createViewModel()
|
||||
vm.initialize(testAddress)
|
||||
@@ -251,7 +260,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
val bonded = mockBondedDevice(testAddress)
|
||||
every { bluetoothManager.bondedDevices() } returns flowOf(setOf(bonded))
|
||||
|
||||
val gate = CompletableDeferred<Boolean>()
|
||||
val gate = CompletableDeferred<NudgeAttemptResult>()
|
||||
coEvery { bluetoothManager.nudgeConnection(bonded) } coAnswers { gate.await() }
|
||||
|
||||
val vm = createViewModel()
|
||||
@@ -261,7 +270,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
vm.forceConnect()
|
||||
vm.forceConnect()
|
||||
|
||||
gate.complete(true)
|
||||
gate.complete(NudgeAttemptResult.Accepted)
|
||||
|
||||
coVerify(exactly = 1) { bluetoothManager.nudgeConnection(bonded) }
|
||||
vm.state.first().isForceConnecting shouldBe false
|
||||
|
||||
@@ -10,6 +10,7 @@ import eu.darken.capod.main.core.GeneralSettings
|
||||
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.MonitorModeResolver
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
@@ -51,6 +52,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
private lateinit var upgradeRepo: UpgradeRepo
|
||||
private lateinit var bluetoothManager: BluetoothManager2
|
||||
private lateinit var profilesRepo: DeviceProfilesRepo
|
||||
private lateinit var monitorModeResolver: MonitorModeResolver
|
||||
private val timeSource: TimeSource = TestTimeSource()
|
||||
|
||||
private lateinit var missingPermissionsFlow: MutableStateFlow<Set<Permission>>
|
||||
@@ -60,7 +62,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
private lateinit var profilesFlow: MutableStateFlow<List<DeviceProfile>>
|
||||
private lateinit var hadLegacyReactionDataFlow: MutableStateFlow<Boolean>
|
||||
private lateinit var upgradeInfoFlow: MutableStateFlow<UpgradeRepo.Info>
|
||||
private lateinit var fakeMonitorMode: FakeDataStoreValue<MonitorMode>
|
||||
private lateinit var effectiveModeFlow: MutableStateFlow<MonitorMode>
|
||||
private lateinit var fakeDebugMode: FakeDataStoreValue<Boolean>
|
||||
private lateinit var fakeReactionsHintDismissed: FakeDataStoreValue<Boolean>
|
||||
|
||||
@@ -75,7 +77,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
profilesFlow = MutableStateFlow(emptyList())
|
||||
hadLegacyReactionDataFlow = MutableStateFlow(false)
|
||||
upgradeInfoFlow = MutableStateFlow(mockk<UpgradeRepo.Info>(relaxed = true))
|
||||
fakeMonitorMode = FakeDataStoreValue(MonitorMode.AUTOMATIC)
|
||||
effectiveModeFlow = MutableStateFlow(MonitorMode.AUTOMATIC)
|
||||
fakeDebugMode = FakeDataStoreValue(false)
|
||||
fakeReactionsHintDismissed = FakeDataStoreValue(false)
|
||||
|
||||
@@ -93,10 +95,13 @@ class OverviewViewModelTest : BaseTest() {
|
||||
}
|
||||
|
||||
generalSettings = mockk<GeneralSettings>().also {
|
||||
every { it.monitorMode } returns fakeMonitorMode.mock
|
||||
every { it.reactionsHintDismissed } returns fakeReactionsHintDismissed.mock
|
||||
}
|
||||
|
||||
monitorModeResolver = mockk<MonitorModeResolver>().also {
|
||||
every { it.effectiveMode } returns effectiveModeFlow
|
||||
}
|
||||
|
||||
debugSettings = mockk<DebugSettings>().also {
|
||||
every { it.isDebugModeEnabled } returns fakeDebugMode.mock
|
||||
}
|
||||
@@ -132,6 +137,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
bluetoothManager = bluetoothManager,
|
||||
profilesRepo = profilesRepo,
|
||||
aapManager = mockk(relaxed = true),
|
||||
monitorModeResolver = monitorModeResolver,
|
||||
timeSource = timeSource,
|
||||
)
|
||||
|
||||
@@ -347,7 +353,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `MANUAL mode - never starts monitor`() = runTest(testDispatcher) {
|
||||
fakeMonitorMode.value = MonitorMode.MANUAL
|
||||
effectiveModeFlow.value = MonitorMode.MANUAL
|
||||
val vm = createViewModel()
|
||||
|
||||
// Collect workerAutolaunch to trigger the side effect
|
||||
@@ -358,7 +364,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `ALWAYS mode - starts monitor when permissions OK`() = runTest(testDispatcher) {
|
||||
fakeMonitorMode.value = MonitorMode.ALWAYS
|
||||
effectiveModeFlow.value = MonitorMode.ALWAYS
|
||||
val vm = createViewModel()
|
||||
|
||||
vm.workerAutolaunch.first()
|
||||
@@ -368,7 +374,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `ALWAYS mode - does NOT start monitor when permissions missing`() = runTest(testDispatcher) {
|
||||
fakeMonitorMode.value = MonitorMode.ALWAYS
|
||||
effectiveModeFlow.value = MonitorMode.ALWAYS
|
||||
missingPermissionsFlow.value = setOf(Permission.BLUETOOTH)
|
||||
val vm = createViewModel()
|
||||
|
||||
@@ -379,7 +385,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `AUTOMATIC mode - starts monitor when connected devices exist`() = runTest(testDispatcher) {
|
||||
fakeMonitorMode.value = MonitorMode.AUTOMATIC
|
||||
effectiveModeFlow.value = MonitorMode.AUTOMATIC
|
||||
connectedDevicesFlow.value = listOf(mockk(relaxed = true))
|
||||
val vm = createViewModel()
|
||||
|
||||
@@ -390,7 +396,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `AUTOMATIC mode - does NOT start monitor when no connected devices`() = runTest(testDispatcher) {
|
||||
fakeMonitorMode.value = MonitorMode.AUTOMATIC
|
||||
effectiveModeFlow.value = MonitorMode.AUTOMATIC
|
||||
connectedDevicesFlow.value = emptyList()
|
||||
val vm = createViewModel()
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import eu.darken.capod.common.bluetooth.NudgeAvailability
|
||||
import eu.darken.capod.common.bluetooth.NudgeCapabilityStore
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
class MonitorModeResolverTest : BaseTest() {
|
||||
|
||||
private val profilesFlow = MutableStateFlow<List<DeviceProfile>>(emptyList())
|
||||
private val nudgeFlow = MutableStateFlow(NudgeAvailability.UNKNOWN)
|
||||
|
||||
private val profilesRepo = mockk<DeviceProfilesRepo>().also {
|
||||
every { it.profiles } returns profilesFlow
|
||||
}
|
||||
private val nudgeStore = mockk<NudgeCapabilityStore>().also {
|
||||
every { it.availability } returns nudgeFlow
|
||||
}
|
||||
|
||||
private val resolver = MonitorModeResolver(profilesRepo, nudgeStore)
|
||||
|
||||
private fun profile(
|
||||
id: String = "p",
|
||||
address: String? = "AA:BB:CC:DD:EE:FF",
|
||||
autoConnect: Boolean = false,
|
||||
): AppleDeviceProfile = AppleDeviceProfile(
|
||||
id = id,
|
||||
label = id,
|
||||
address = address,
|
||||
autoConnect = autoConnect,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `case 1 - no profiles - MANUAL`() = runTest {
|
||||
profilesFlow.value = emptyList()
|
||||
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.MANUAL
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `case 2 - paired profile, autoConnect off - AUTOMATIC`() = runTest {
|
||||
profilesFlow.value = listOf(profile(autoConnect = false))
|
||||
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.AUTOMATIC
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `case 3 - paired profile, autoConnect on, nudge AVAILABLE - ALWAYS`() = runTest {
|
||||
profilesFlow.value = listOf(profile(autoConnect = true))
|
||||
nudgeFlow.value = NudgeAvailability.AVAILABLE
|
||||
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.ALWAYS
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `case 3 - paired profile, autoConnect on, nudge UNKNOWN - ALWAYS (assume available)`() = runTest {
|
||||
profilesFlow.value = listOf(profile(autoConnect = true))
|
||||
nudgeFlow.value = NudgeAvailability.UNKNOWN
|
||||
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.ALWAYS
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `case 3b - paired profile, autoConnect on, nudge BROKEN - AUTOMATIC`() = runTest {
|
||||
profilesFlow.value = listOf(profile(autoConnect = true))
|
||||
nudgeFlow.value = NudgeAvailability.BROKEN
|
||||
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.AUTOMATIC
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `case 4 - profile without paired address - MANUAL`() = runTest {
|
||||
profilesFlow.value = listOf(profile(address = null, autoConnect = true))
|
||||
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.MANUAL
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multi-profile primary-only - primary case 2 + secondary case 3 - AUTOMATIC`() = runTest {
|
||||
profilesFlow.value = listOf(
|
||||
profile(id = "primary", autoConnect = false),
|
||||
profile(id = "secondary", autoConnect = true),
|
||||
)
|
||||
nudgeFlow.value = NudgeAvailability.AVAILABLE
|
||||
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.AUTOMATIC
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multi-profile primary-only - primary case 4 + secondary case 2 - MANUAL`() = runTest {
|
||||
profilesFlow.value = listOf(
|
||||
profile(id = "primary", address = null),
|
||||
profile(id = "secondary", address = "AA:BB:CC:DD:EE:FF"),
|
||||
)
|
||||
|
||||
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)
|
||||
nudgeFlow.value = NudgeAvailability.AVAILABLE
|
||||
|
||||
profilesFlow.value = listOf(a, b)
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.ALWAYS
|
||||
|
||||
profilesFlow.value = listOf(b, a)
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.AUTOMATIC
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nudge availability flip from UNKNOWN to BROKEN drops case 3 to AUTOMATIC`() = runTest {
|
||||
profilesFlow.value = listOf(profile(autoConnect = true))
|
||||
nudgeFlow.value = NudgeAvailability.UNKNOWN
|
||||
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.ALWAYS
|
||||
|
||||
nudgeFlow.value = NudgeAvailability.BROKEN
|
||||
resolver.effectiveMode.first() shouldBe MonitorMode.AUTOMATIC
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class AutoConnectLogicTest : BaseTest() {
|
||||
autoConnect = AutoConnect(
|
||||
bluetoothManager = mockk(relaxed = true),
|
||||
deviceMonitor = mockk(relaxed = true),
|
||||
nudgeCapabilityStore = mockk(relaxed = true),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user