refactor: Migrate all consumers from PodDevice to MonitoredDevice

Replace direct PodDevice/PodMonitor usage with MonitoredDevice/DeviceMonitor across ViewModels, UI screens, notifications, widgets, reactions, and service. All interface cast-based property access replaced with flat MonitoredDevice properties. Make L2capSocketFactory injectable.
This commit is contained in:
darken
2026-03-31 19:17:09 +02:00
committed by Matthias Urhahn
parent c19ee88ae6
commit ac21261d4d
30 changed files with 563 additions and 368 deletions
@@ -8,7 +8,8 @@ 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.main.core.PermissionTool
import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.monitor.core.DeviceMonitor
import eu.darken.capod.monitor.core.MonitoredDevice
import eu.darken.capod.monitor.core.worker.MonitorControl
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.profiles.core.AppleDeviceProfile
@@ -44,7 +45,7 @@ class OverviewViewModelTest : BaseTest() {
private val testDispatcher = UnconfinedTestDispatcher()
private lateinit var monitorControl: MonitorControl
private lateinit var podMonitor: PodMonitor
private lateinit var deviceMonitor: DeviceMonitor
private lateinit var permissionTool: PermissionTool
private lateinit var generalSettings: GeneralSettings
private lateinit var debugSettings: DebugSettings
@@ -53,7 +54,7 @@ class OverviewViewModelTest : BaseTest() {
private lateinit var profilesRepo: DeviceProfilesRepo
private lateinit var missingPermissionsFlow: MutableStateFlow<Set<Permission>>
private lateinit var devicesFlow: MutableStateFlow<List<PodDevice>>
private lateinit var devicesFlow: MutableStateFlow<List<MonitoredDevice>>
private lateinit var connectedDevicesFlow: MutableStateFlow<List<BluetoothDevice2>>
private lateinit var isBluetoothEnabledFlow: MutableStateFlow<Boolean>
private lateinit var profilesFlow: MutableStateFlow<List<DeviceProfile>>
@@ -76,7 +77,7 @@ class OverviewViewModelTest : BaseTest() {
monitorControl = mockk(relaxed = true)
podMonitor = mockk<PodMonitor>().also {
deviceMonitor = mockk<DeviceMonitor>().also {
every { it.devices } returns devicesFlow
}
@@ -117,7 +118,7 @@ class OverviewViewModelTest : BaseTest() {
private fun createViewModel() = OverviewViewModel(
dispatcherProvider = TestDispatcherProvider(testDispatcher),
monitorControl = monitorControl,
podMonitor = podMonitor,
deviceMonitor = deviceMonitor,
permissionTool = permissionTool,
generalSettings = generalSettings,
debugSettings = debugSettings,
@@ -153,7 +154,7 @@ class OverviewViewModelTest : BaseTest() {
@Test
fun `devices passed through when permissions granted`() = runTest(testDispatcher) {
val device = mockk<PodDevice>(relaxed = true)
val device = MonitoredDevice(ble = mockk(relaxed = true), aap = null)
devicesFlow.value = listOf(device)
val vm = createViewModel()
@@ -170,8 +171,14 @@ class OverviewViewModelTest : BaseTest() {
val withoutProfile = object : PodDevice.Meta {
override val profile: DeviceProfile? = null
}
val profiled = mockk<PodDevice>(relaxed = true) { every { meta } returns withProfile }
val unmatched = mockk<PodDevice>(relaxed = true) { every { meta } returns withoutProfile }
val profiled = MonitoredDevice(
ble = mockk(relaxed = true) { every { meta } returns withProfile },
aap = null,
)
val unmatched = MonitoredDevice(
ble = mockk(relaxed = true) { every { meta } returns withoutProfile },
aap = null,
)
val state = OverviewViewModel.State(
now = java.time.Instant.now(),
@@ -195,8 +202,14 @@ class OverviewViewModelTest : BaseTest() {
val withoutProfile = object : PodDevice.Meta {
override val profile: DeviceProfile? = null
}
val profiled = mockk<PodDevice>(relaxed = true) { every { meta } returns withProfile }
val unmatched = mockk<PodDevice>(relaxed = true) { every { meta } returns withoutProfile }
val profiled = MonitoredDevice(
ble = mockk(relaxed = true) { every { meta } returns withProfile },
aap = null,
)
val unmatched = MonitoredDevice(
ble = mockk(relaxed = true) { every { meta } returns withoutProfile },
aap = null,
)
val state = OverviewViewModel.State(
now = java.time.Instant.now(),
@@ -1,5 +1,10 @@
package eu.darken.capod.monitor.core
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasDualMicrophone
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.protocol.aap.AapConnectionState
@@ -13,6 +18,7 @@ import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import java.time.Instant
class MonitoredDeviceTest : BaseTest() {
@@ -88,4 +94,102 @@ class MonitoredDeviceTest : BaseTest() {
val device = MonitoredDevice(ble = null, aap = null)
device.model shouldBe PodDevice.Model.UNKNOWN
}
@Test
fun `identity properties delegate to BLE`() {
val id = PodDevice.Id()
val meta = mockk<PodDevice.Meta>(relaxed = true)
val device = MonitoredDevice(
ble = mockk(relaxed = true) {
every { identifier } returns id
every { this@mockk.meta } returns meta
},
aap = null,
)
device.identifier shouldBe id
device.meta shouldBe meta
}
@Test
fun `identity properties null when BLE null`() {
val device = MonitoredDevice(ble = null, aap = null)
device.identifier.shouldBeNull()
device.meta.shouldBeNull()
}
@Test
fun `signal timing properties delegate to BLE`() {
val now = Instant.now()
val earlier = now.minusSeconds(60)
val device = MonitoredDevice(
ble = mockk(relaxed = true) {
every { seenLastAt } returns now
every { seenFirstAt } returns earlier
every { signalQuality } returns 0.75f
every { rssi } returns -50
},
aap = null,
)
device.seenLastAt shouldBe now
device.seenFirstAt shouldBe earlier
device.signalQuality shouldBe 0.75f
device.rssi shouldBe -50
}
@Test
fun `signal timing defaults when BLE null`() {
val device = MonitoredDevice(ble = null, aap = null)
device.seenLastAt.shouldBeNull()
device.seenFirstAt.shouldBeNull()
device.signalQuality shouldBe 0f
device.rssi shouldBe 0
}
@Test
fun `charging properties delegate to BLE interfaces`() {
val mock = mockk<DualApplePods>(relaxed = true) {
every { model } returns PodDevice.Model.AIRPODS_PRO3
every { (this@mockk as HasChargeDetectionDual).isLeftPodCharging } returns true
every { (this@mockk as HasChargeDetectionDual).isRightPodCharging } returns false
every { (this@mockk as HasCase).isCaseCharging } returns true
}
val device = MonitoredDevice(ble = mock, aap = null)
device.isLeftPodCharging shouldBe true
device.isRightPodCharging shouldBe false
device.isCaseCharging shouldBe true
}
@Test
fun `ear detection properties delegate to BLE interfaces`() {
val mock = mockk<DualApplePods>(relaxed = true) {
every { model } returns PodDevice.Model.AIRPODS_PRO3
every { (this@mockk as HasEarDetectionDual).isLeftPodInEar } returns true
every { (this@mockk as HasEarDetectionDual).isRightPodInEar } returns false
every { (this@mockk as HasEarDetection).isBeingWorn } returns false
every { (this@mockk as HasEarDetectionDual).isEitherPodInEar } returns true
}
val device = MonitoredDevice(ble = mock, aap = null)
device.isLeftInEar shouldBe true
device.isRightInEar shouldBe false
device.isBeingWorn shouldBe false
device.isEitherPodInEar shouldBe true
}
@Test
fun `icon and label properties delegate to BLE`() {
val device = MonitoredDevice(
ble = mockk(relaxed = true) {
every { model } returns PodDevice.Model.AIRPODS_PRO3
every { iconRes } returns 42
},
aap = null,
)
device.iconRes shouldBe 42
}
@Test
fun `rawDataHex empty when BLE null`() {
val device = MonitoredDevice(ble = null, aap = null)
device.rawDataHex shouldBe emptyList()
}
}
@@ -16,7 +16,7 @@ class AutoConnectLogicTest : BaseTest() {
fun setup() {
autoConnect = AutoConnect(
bluetoothManager = mockk(relaxed = true),
podMonitor = mockk(relaxed = true),
deviceMonitor = mockk(relaxed = true),
generalSettings = mockk(relaxed = true),
reactionSettings = mockk(relaxed = true),
deviceProfilesRepo = mockk(relaxed = true),
@@ -16,7 +16,7 @@ class PlayPauseLogicTest : BaseTest() {
fun setup() {
// Create PlayPause instance with mocked dependencies (relaxed so we don't need to stub everything)
playPause = PlayPause(
podMonitor = mockk(relaxed = true),
deviceMonitor = mockk(relaxed = true),
bluetoothManager = mockk(relaxed = true),
reactionSettings = mockk(relaxed = true),
mediaControl = mockk(relaxed = true)
@@ -17,7 +17,7 @@ class PopUpReactionLogicTest : BaseTest() {
@BeforeEach
fun setup() {
popUpReaction = PopUpReaction(
podMonitor = mockk(relaxed = true),
deviceMonitor = mockk(relaxed = true),
reactionSettings = mockk(relaxed = true),
bluetoothManager = mockk(relaxed = true),
)