mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
fix(reaction): Fix autoplay/autopause for AAP-based ear detection
PlayPause coerced null per-side ear values to false, making all ear states invisible when resolvedPrimaryPod was unknown. Fall back to AAP aggregate state (isBeingWorn/isEitherPodInEar) when per-side mapping is unavailable, keeping PodDevice.isLeftInEar/isRightInEar truthful for UI consumers. Add isEitherPodInEar to PlayPauseMonitorKey for proper dedup. Add diagnostic logging at the distinctUntilChangedBy boundary.
This commit is contained in:
@@ -810,6 +810,79 @@ class PodDeviceTest : BaseTest() {
|
||||
device.rssiQuality shouldBe 1.0f
|
||||
}
|
||||
|
||||
// --- AAP aggregate ear detection (null per-side) tests ---
|
||||
|
||||
@Test
|
||||
fun `isLeftInEar null when AAP ear detection present but no primaryPod and no BLE`() {
|
||||
val aap = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
settings = mapOf(
|
||||
AapSetting.EarDetection::class to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
),
|
||||
),
|
||||
)
|
||||
val device = PodDevice(profileId = "p1", ble = null, aap = aap, profileModel = PodModel.AIRPODS_PRO3)
|
||||
// Per-side is null because resolvedPrimaryPod is null (no AAP PrimaryPod, no BLE)
|
||||
device.isLeftInEar.shouldBeNull()
|
||||
device.isRightInEar.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isBeingWorn works without resolvedPrimaryPod`() {
|
||||
val aapBothIn = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
settings = mapOf(
|
||||
AapSetting.EarDetection::class to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
),
|
||||
),
|
||||
)
|
||||
val deviceBothIn = PodDevice(profileId = "p1", ble = null, aap = aapBothIn, profileModel = PodModel.AIRPODS_PRO3)
|
||||
deviceBothIn.isBeingWorn shouldBe true
|
||||
|
||||
val aapOneOut = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
settings = mapOf(
|
||||
AapSetting.EarDetection::class to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
),
|
||||
),
|
||||
)
|
||||
val deviceOneOut = PodDevice(profileId = "p1", ble = null, aap = aapOneOut, profileModel = PodModel.AIRPODS_PRO3)
|
||||
deviceOneOut.isBeingWorn shouldBe false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isEitherPodInEar works without resolvedPrimaryPod`() {
|
||||
val aapOneIn = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
settings = mapOf(
|
||||
AapSetting.EarDetection::class to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
),
|
||||
),
|
||||
)
|
||||
val device = PodDevice(profileId = "p1", ble = null, aap = aapOneIn, profileModel = PodModel.AIRPODS_PRO3)
|
||||
device.isEitherPodInEar shouldBe true
|
||||
|
||||
val aapNoneIn = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
settings = mapOf(
|
||||
AapSetting.EarDetection::class to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
),
|
||||
),
|
||||
)
|
||||
val deviceNone = PodDevice(profileId = "p1", ble = null, aap = aapNoneIn, profileModel = PodModel.AIRPODS_PRO3)
|
||||
deviceNone.isEitherPodInEar shouldBe false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rssiQuality - BLE value wins over AAP READY`() {
|
||||
val ble = mockk<DualApplePods>(relaxed = true) {
|
||||
|
||||
@@ -610,6 +610,118 @@ class PlayPauseLogicTest : BaseTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class AapAggregateTests {
|
||||
|
||||
@Test
|
||||
fun `fromAapAggregate - both in ear`() {
|
||||
val state = EarDetectionState.fromAapAggregate(isBeingWorn = true, isEitherPodInEar = true)
|
||||
state.bothInEar shouldBe true
|
||||
state.eitherInEar shouldBe true
|
||||
state.podCount shouldBe 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromAapAggregate - one in ear`() {
|
||||
val state = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = true)
|
||||
state.bothInEar shouldBe false
|
||||
state.eitherInEar shouldBe true
|
||||
state.podCount shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromAapAggregate - none in ear`() {
|
||||
val state = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = false)
|
||||
state.bothInEar shouldBe false
|
||||
state.eitherInEar shouldBe false
|
||||
state.podCount shouldBe 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregate normal mode - both to none - should pause`() {
|
||||
val previous = EarDetectionState.fromAapAggregate(isBeingWorn = true, isEitherPodInEar = true)
|
||||
val current = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = false)
|
||||
|
||||
val decision = playPause.evaluatePlayPauseAction(
|
||||
previous = previous, current = current,
|
||||
onePodMode = false, isCurrentlyPlaying = true,
|
||||
)
|
||||
decision.shouldPause shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregate normal mode - none to both - should play`() {
|
||||
val previous = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = false)
|
||||
val current = EarDetectionState.fromAapAggregate(isBeingWorn = true, isEitherPodInEar = true)
|
||||
|
||||
val decision = playPause.evaluatePlayPauseAction(
|
||||
previous = previous, current = current,
|
||||
onePodMode = false, isCurrentlyPlaying = false,
|
||||
)
|
||||
decision.shouldPlay shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregate normal mode - both to one - should pause`() {
|
||||
val previous = EarDetectionState.fromAapAggregate(isBeingWorn = true, isEitherPodInEar = true)
|
||||
val current = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = true)
|
||||
|
||||
val decision = playPause.evaluatePlayPauseAction(
|
||||
previous = previous, current = current,
|
||||
onePodMode = false, isCurrentlyPlaying = true,
|
||||
)
|
||||
decision.shouldPause shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregate one-pod mode - none to one - should play`() {
|
||||
val previous = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = false)
|
||||
val current = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = true)
|
||||
|
||||
val decision = playPause.evaluatePlayPauseAction(
|
||||
previous = previous, current = current,
|
||||
onePodMode = true, isCurrentlyPlaying = false,
|
||||
)
|
||||
decision.shouldPlay shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregate one-pod mode - one to none - should pause`() {
|
||||
val previous = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = true)
|
||||
val current = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = false)
|
||||
|
||||
val decision = playPause.evaluatePlayPauseAction(
|
||||
previous = previous, current = current,
|
||||
onePodMode = true, isCurrentlyPlaying = true,
|
||||
)
|
||||
decision.shouldPause shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregate one-pod mode - one to both - should play`() {
|
||||
val previous = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = true)
|
||||
val current = EarDetectionState.fromAapAggregate(isBeingWorn = true, isEitherPodInEar = true)
|
||||
|
||||
val decision = playPause.evaluatePlayPauseAction(
|
||||
previous = previous, current = current,
|
||||
onePodMode = true, isCurrentlyPlaying = false,
|
||||
)
|
||||
decision.shouldPlay shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregate one-pod mode - both to one - should pause`() {
|
||||
val previous = EarDetectionState.fromAapAggregate(isBeingWorn = true, isEitherPodInEar = true)
|
||||
val current = EarDetectionState.fromAapAggregate(isBeingWorn = false, isEitherPodInEar = true)
|
||||
|
||||
val decision = playPause.evaluatePlayPauseAction(
|
||||
previous = previous, current = current,
|
||||
onePodMode = true, isCurrentlyPlaying = true,
|
||||
)
|
||||
decision.shouldPause shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class EarDetectionStateTests {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user