From fc98db09a26a4985b47abeb03acca4d3538e7cc5 Mon Sep 17 00:00:00 2001 From: darken Date: Fri, 1 May 2026 19:59:01 +0200 Subject: [PATCH] fix: Correct PodModel feature flags for ear detection and call controls Sync flags with what the BLE classes actually report and what iOS exposes: - AirPods Gen 1/2/3: enable hasEarDetection (already parsed via DualApplePods) and hasEarDetectionToggle - AirPods Gen 3, Pro 1: enable hasEndCallMuteMic (force-sensor stems) - Powerbeats Pro, Beats Fit Pro: enable hasEarDetectionToggle (iOS exposes it) - Beats Solo Pro, Studio 3: drop hasEarDetection (over-ear, BLE class is bare SingleApplePods) - FAKE_AIRPODS_GEN1/2/3: enable hasEarDetection to match HasEarDetectionDual - Generalize microphone mode description from 'AirPod' to 'earbud' Tests rewritten as exhaustive set assertions plus implication invariants. --- .../darken/capod/pods/core/apple/PodModel.kt | 17 +- app/src/main/res/values/strings.xml | 2 +- .../pods/core/apple/ModelFeaturesTest.kt | 396 +++++++++++++++--- .../DefaultAapDeviceProfileNewSettingsTest.kt | 42 +- 4 files changed, 397 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt index 0d23321f..4317cfb6 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt @@ -22,7 +22,9 @@ enum class PodModel( Features( hasDualPods = true, hasCase = true, + hasEarDetection = true, hasMicrophoneMode = true, + hasEarDetectionToggle = true, ), modelNumbers = setOf("A1523", "A1722"), // L/R earphones leftPodIconRes = R.drawable.device_airpods_gen1_left, @@ -37,7 +39,9 @@ enum class PodModel( Features( hasDualPods = true, hasCase = true, + hasEarDetection = true, hasMicrophoneMode = true, + hasEarDetectionToggle = true, ), modelNumbers = setOf("A2031", "A2032"), // L/R earphones leftPodIconRes = R.drawable.device_airpods_gen1_left, @@ -52,10 +56,13 @@ enum class PodModel( Features( hasDualPods = true, hasCase = true, + hasEarDetection = true, hasPressSpeed = true, hasPressHoldDuration = true, hasToneVolume = true, + hasEndCallMuteMic = true, hasMicrophoneMode = true, + hasEarDetectionToggle = true, ), modelNumbers = setOf("A2564", "A2565"), // L/R earphones leftPodIconRes = R.drawable.device_airpods_gen3_left, @@ -74,6 +81,7 @@ enum class PodModel( hasPressSpeed = true, hasPressHoldDuration = true, hasToneVolume = true, + hasEndCallMuteMic = true, hasMicrophoneMode = true, hasEarDetectionToggle = true, hasSleepDetection = true, @@ -314,7 +322,6 @@ enum class PodModel( "Beats Solo Pro", R.drawable.device_beats_headphones, Features( - hasEarDetection = true, hasAncControl = true, ), modelNumbers = setOf("A1881"), // headphones @@ -343,7 +350,6 @@ enum class PodModel( "Beats Studio 3", R.drawable.device_beats_studio3, Features( - hasEarDetection = true, hasAncControl = true, ), modelNumbers = setOf("A1914"), // headphones @@ -410,6 +416,7 @@ enum class PodModel( hasDualPods = true, hasCase = true, hasEarDetection = true, + hasEarDetectionToggle = true, ), modelNumbers = setOf("A2047", "A2048", "A2453", "A2454"), // L/R earbuds, 2019 + 2020 revisions leftPodIconRes = R.drawable.device_powerbeats_pro_left, @@ -426,6 +433,7 @@ enum class PodModel( hasCase = true, hasEarDetection = true, hasAncControl = true, + hasMicrophoneMode = true, hasEarDetectionToggle = true, hasSleepDetection = true, ), @@ -444,6 +452,8 @@ enum class PodModel( hasCase = true, hasEarDetection = true, hasAncControl = true, + hasMicrophoneMode = true, + hasEarDetectionToggle = true, ), modelNumbers = setOf("A2576", "A2577", "A2578"), // L/R earbuds + case leftPodIconRes = R.drawable.device_beats_fitpro_left, @@ -458,6 +468,7 @@ enum class PodModel( Features( hasDualPods = true, hasCase = true, + hasEarDetection = true, ), leftPodIconRes = R.drawable.device_airpods_gen1_left, rightPodIconRes = R.drawable.device_airpods_gen1_right, @@ -471,6 +482,7 @@ enum class PodModel( Features( hasDualPods = true, hasCase = true, + hasEarDetection = true, ), leftPodIconRes = R.drawable.device_airpods_gen1_left, rightPodIconRes = R.drawable.device_airpods_gen1_right, @@ -484,6 +496,7 @@ enum class PodModel( Features( hasDualPods = true, hasCase = true, + hasEarDetection = true, ), leftPodIconRes = R.drawable.device_airpods_gen3_left, rightPodIconRes = R.drawable.device_airpods_gen3_right, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 04bbc366..ddcebaff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -515,7 +515,7 @@ General Microphone - Which AirPod is used as the microphone + Which earbud is used as the microphone Auto Right Left diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/ModelFeaturesTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/ModelFeaturesTest.kt index accd7772..2db48c97 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/ModelFeaturesTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/ModelFeaturesTest.kt @@ -1,5 +1,6 @@ package eu.darken.capod.pods.core.apple +import io.kotest.assertions.withClue import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -7,79 +8,364 @@ import testhelpers.BaseTest class ModelFeaturesTest : BaseTest() { @Test - fun `AirPods Pro 3 has all features`() { - val f = PodModel.AIRPODS_PRO3.features - f.hasDualPods shouldBe true - f.hasCase shouldBe true - f.hasEarDetection shouldBe true - f.hasAncControl shouldBe true + fun `feature flags match supported model matrix`() { + featureExpectations.forEach { expectation -> + withClue(expectation.name) { + modelsWith(expectation.predicate) shouldBe expectation.models + } + } } @Test - fun `AirPods Gen 1 has dual pods and case but no ear detection or ANC`() { - val f = PodModel.AIRPODS_GEN1.features - f.hasDualPods shouldBe true - f.hasCase shouldBe true - f.hasEarDetection shouldBe false - f.hasAncControl shouldBe false - } - - @Test - fun `AirPods Max is single device with ANC but no dual pods or case`() { - val f = PodModel.AIRPODS_MAX.features - f.hasDualPods shouldBe false - f.hasCase shouldBe false - f.hasEarDetection shouldBe true - f.hasAncControl shouldBe true - } - - @Test - fun `Beats Solo 3 has no features`() { - val f = PodModel.BEATS_SOLO_3.features - f.hasDualPods shouldBe false - f.hasCase shouldBe false - f.hasEarDetection shouldBe false - f.hasAncControl shouldBe false - } - - @Test - fun `PowerBeats Pro has dual pods, case, ear detection but no ANC`() { - val f = PodModel.POWERBEATS_PRO.features - f.hasDualPods shouldBe true - f.hasCase shouldBe true - f.hasEarDetection shouldBe true - f.hasAncControl shouldBe false + fun `models without ear detection match intentional matrix`() { + modelsWithout { it.hasEarDetection } shouldBe setOf( + PodModel.BEATS_FLEX, + PodModel.BEATS_SOLO_3, + PodModel.BEATS_SOLO_PRO, + PodModel.BEATS_SOLO_4, + PodModel.BEATS_SOLO_BUDS, + PodModel.BEATS_STUDIO_3, + PodModel.BEATS_STUDIO_BUDS, + PodModel.BEATS_STUDIO_BUDS_PLUS, + PodModel.BEATS_STUDIO_PRO, + PodModel.BEATS_X, + PodModel.POWERBEATS_3, + PodModel.POWERBEATS_4, + PodModel.UNKNOWN, + ) } @Test fun `UNKNOWN model has no features`() { - val f = PodModel.UNKNOWN.features - f.hasDualPods shouldBe false - f.hasCase shouldBe false - f.hasEarDetection shouldBe false - f.hasAncControl shouldBe false + PodModel.UNKNOWN.features shouldBe PodModel.Features() } @Test - fun `all ANC-capable dual-pod models have ear detection except Studio Buds`() { - val noEarDetectionAncModels = setOf( - PodModel.BEATS_STUDIO_BUDS, - PodModel.BEATS_STUDIO_BUDS_PLUS, - ) + fun `ear detection toggle implies ear detection`() { PodModel.entries - .filter { it.features.hasAncControl && it.features.hasDualPods } - .filter { it !in noEarDetectionAncModels } + .filter { it.features.hasEarDetectionToggle } .forEach { model -> - model.features.hasEarDetection shouldBe true + withClue(model.name) { + model.features.hasEarDetection shouldBe true + } } } @Test - fun `all models with case also have dual pods`() { + fun `sleep detection implies ear detection`() { + PodModel.entries + .filter { it.features.hasSleepDetection } + .forEach { model -> + withClue(model.name) { + model.features.hasEarDetection shouldBe true + } + } + } + + @Test + fun `case implies dual pods`() { PodModel.entries .filter { it.features.hasCase } .forEach { model -> - model.features.hasDualPods shouldBe true + withClue(model.name) { + model.features.hasDualPods shouldBe true + } } } -} \ No newline at end of file + + @Test + fun `adaptive ANC implies ANC control`() { + PodModel.entries + .filter { it.features.hasAdaptiveAnc } + .forEach { model -> + withClue(model.name) { + model.features.hasAncControl shouldBe true + } + } + } + + @Test + fun `adaptive audio noise implies adaptive ANC`() { + PodModel.entries + .filter { it.features.hasAdaptiveAudioNoise } + .forEach { model -> + withClue(model.name) { + model.features.hasAdaptiveAnc shouldBe true + } + } + } + + @Test + fun `listening mode cycle implies ANC control`() { + PodModel.entries + .filter { it.features.hasListeningModeCycle } + .forEach { model -> + withClue(model.name) { + model.features.hasAncControl shouldBe true + } + } + } + + @Test + fun `allow off option implies listening mode cycle`() { + PodModel.entries + .filter { it.features.hasAllowOffOption } + .forEach { model -> + withClue(model.name) { + model.features.hasListeningModeCycle shouldBe true + } + } + } + + private fun modelsWith(predicate: (PodModel.Features) -> Boolean): Set = PodModel.entries + .filter { predicate(it.features) } + .toSet() + + private fun modelsWithout(predicate: (PodModel.Features) -> Boolean): Set = PodModel.entries + .filterNot { predicate(it.features) } + .toSet() + + private data class FeatureExpectation( + val name: String, + val predicate: (PodModel.Features) -> Boolean, + val models: Set, + ) + + private fun feature( + name: String, + predicate: (PodModel.Features) -> Boolean, + models: Set, + ) = FeatureExpectation(name, predicate, models) + + private val dualPodModels = setOf( + PodModel.AIRPODS_GEN1, + PodModel.AIRPODS_GEN2, + PodModel.AIRPODS_GEN3, + PodModel.AIRPODS_GEN4, + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.BEATS_SOLO_BUDS, + PodModel.BEATS_STUDIO_BUDS, + PodModel.BEATS_STUDIO_BUDS_PLUS, + PodModel.POWERBEATS_PRO, + PodModel.POWERBEATS_PRO2, + PodModel.BEATS_FIT_PRO, + PodModel.FAKE_AIRPODS_GEN1, + PodModel.FAKE_AIRPODS_GEN2, + PodModel.FAKE_AIRPODS_GEN3, + PodModel.FAKE_AIRPODS_PRO, + PodModel.FAKE_AIRPODS_PRO2, + ) + + private val earDetectionModels = setOf( + PodModel.AIRPODS_GEN1, + PodModel.AIRPODS_GEN2, + PodModel.AIRPODS_GEN3, + PodModel.AIRPODS_GEN4, + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX, + PodModel.AIRPODS_MAX_USBC, + PodModel.AIRPODS_MAX2, + PodModel.POWERBEATS_PRO, + PodModel.POWERBEATS_PRO2, + PodModel.BEATS_FIT_PRO, + PodModel.FAKE_AIRPODS_GEN1, + PodModel.FAKE_AIRPODS_GEN2, + PodModel.FAKE_AIRPODS_GEN3, + PodModel.FAKE_AIRPODS_PRO, + PodModel.FAKE_AIRPODS_PRO2, + ) + + private val ancControlModels = setOf( + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX, + PodModel.AIRPODS_MAX_USBC, + PodModel.AIRPODS_MAX2, + PodModel.BEATS_SOLO_PRO, + PodModel.BEATS_STUDIO_3, + PodModel.BEATS_STUDIO_BUDS, + PodModel.BEATS_STUDIO_BUDS_PLUS, + PodModel.BEATS_STUDIO_PRO, + PodModel.POWERBEATS_PRO2, + PodModel.BEATS_FIT_PRO, + PodModel.FAKE_AIRPODS_PRO, + PodModel.FAKE_AIRPODS_PRO2, + ) + + private val adaptiveAncModels = setOf( + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX2, + ) + + private val conversationAwarenessModels = setOf( + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX2, + ) + + private val ncOneAirpodModels = setOf( + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + ) + + private val pressSpeedModels = setOf( + PodModel.AIRPODS_GEN3, + PodModel.AIRPODS_GEN4, + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX, + PodModel.AIRPODS_MAX_USBC, + PodModel.AIRPODS_MAX2, + ) + + private val volumeSwipeModels = setOf( + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + ) + + private val personalizedVolumeModels = setOf( + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX2, + ) + + private val toneVolumeModels = setOf( + PodModel.AIRPODS_GEN3, + PodModel.AIRPODS_GEN4, + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX, + PodModel.AIRPODS_MAX_USBC, + PodModel.AIRPODS_MAX2, + ) + + private val endCallMuteMicModels = setOf( + PodModel.AIRPODS_GEN3, + PodModel.AIRPODS_GEN4, + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + ) + + private val adaptiveAudioNoiseModels = setOf( + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX2, + ) + + private val microphoneModeModels = setOf( + PodModel.AIRPODS_GEN1, + PodModel.AIRPODS_GEN2, + PodModel.AIRPODS_GEN3, + PodModel.AIRPODS_GEN4, + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.POWERBEATS_PRO2, + PodModel.BEATS_FIT_PRO, + ) + + private val earDetectionToggleModels = setOf( + PodModel.AIRPODS_GEN1, + PodModel.AIRPODS_GEN2, + PodModel.AIRPODS_GEN3, + PodModel.AIRPODS_GEN4, + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX, + PodModel.AIRPODS_MAX_USBC, + PodModel.AIRPODS_MAX2, + PodModel.POWERBEATS_PRO, + PodModel.POWERBEATS_PRO2, + PodModel.BEATS_FIT_PRO, + ) + + private val listeningModeCycleModels = setOf( + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.AIRPODS_MAX, + PodModel.AIRPODS_MAX_USBC, + PodModel.AIRPODS_MAX2, + ) + + private val stemConfigModels = setOf( + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + ) + + private val sleepDetectionModels = setOf( + PodModel.AIRPODS_GEN4, + PodModel.AIRPODS_GEN4_ANC, + PodModel.AIRPODS_PRO2, + PodModel.AIRPODS_PRO2_USBC, + PodModel.AIRPODS_PRO3, + PodModel.POWERBEATS_PRO2, + ) + + private val featureExpectations = listOf( + feature("hasDualPods", { it.hasDualPods }, dualPodModels), + feature("hasCase", { it.hasCase }, dualPodModels), + feature("hasEarDetection", { it.hasEarDetection }, earDetectionModels), + feature("hasAncControl", { it.hasAncControl }, ancControlModels), + feature("hasAdaptiveAnc", { it.hasAdaptiveAnc }, adaptiveAncModels), + feature("hasConversationAwareness", { it.hasConversationAwareness }, conversationAwarenessModels), + feature("hasNcOneAirpod", { it.hasNcOneAirpod }, ncOneAirpodModels), + feature("hasPressSpeed", { it.hasPressSpeed }, pressSpeedModels), + feature("hasPressHoldDuration", { it.hasPressHoldDuration }, pressSpeedModels), + feature("hasVolumeSwipe", { it.hasVolumeSwipe }, volumeSwipeModels), + feature("hasVolumeSwipeLength", { it.hasVolumeSwipeLength }, volumeSwipeModels), + feature("hasPersonalizedVolume", { it.hasPersonalizedVolume }, personalizedVolumeModels), + feature("hasToneVolume", { it.hasToneVolume }, toneVolumeModels), + feature("hasEndCallMuteMic", { it.hasEndCallMuteMic }, endCallMuteMicModels), + feature("hasAdaptiveAudioNoise", { it.hasAdaptiveAudioNoise }, adaptiveAudioNoiseModels), + feature("hasMicrophoneMode", { it.hasMicrophoneMode }, microphoneModeModels), + feature("hasEarDetectionToggle", { it.hasEarDetectionToggle }, earDetectionToggleModels), + feature("hasListeningModeCycle", { it.hasListeningModeCycle }, listeningModeCycleModels), + feature("hasAllowOffOption", { it.hasAllowOffOption }, listeningModeCycleModels), + feature("hasStemConfig", { it.hasStemConfig }, stemConfigModels), + feature("hasSleepDetection", { it.hasSleepDetection }, sleepDetectionModels), + feature("hasDynamicEndOfCharge", { it.hasDynamicEndOfCharge }, setOf(PodModel.AIRPODS_PRO3)), + ) +} diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileNewSettingsTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileNewSettingsTest.kt index fb2596d9..df27cb5b 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileNewSettingsTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileNewSettingsTest.kt @@ -337,6 +337,7 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() { f.hasPressSpeed shouldBe true f.hasPressHoldDuration shouldBe true f.hasToneVolume shouldBe true + f.hasEndCallMuteMic shouldBe true f.hasListeningModeCycle shouldBe false f.hasStemConfig shouldBe false f.hasSleepDetection shouldBe true @@ -351,14 +352,31 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() { f.hasStemConfig shouldBe false } - @Test fun `Gen 1 has mic mode only`() { + @Test fun `Gen 1 has mic mode and ear detection toggle`() { val f = PodModel.AIRPODS_GEN1.features f.hasMicrophoneMode shouldBe true - f.hasEarDetectionToggle shouldBe false + f.hasEarDetection shouldBe true + f.hasEarDetectionToggle shouldBe true f.hasListeningModeCycle shouldBe false f.hasStemConfig shouldBe false } + @Test fun `Gen 2 and Gen 3 expose ear detection toggle`() { + val gen2 = PodModel.AIRPODS_GEN2.features + gen2.hasMicrophoneMode shouldBe true + gen2.hasEarDetection shouldBe true + gen2.hasEarDetectionToggle shouldBe true + + val gen3 = PodModel.AIRPODS_GEN3.features + gen3.hasMicrophoneMode shouldBe true + gen3.hasEarDetection shouldBe true + gen3.hasEarDetectionToggle shouldBe true + gen3.hasPressSpeed shouldBe true + gen3.hasPressHoldDuration shouldBe true + gen3.hasToneVolume shouldBe true + gen3.hasEndCallMuteMic shouldBe true + } + @Test fun `Max 2 has H2 features`() { val f = PodModel.AIRPODS_MAX2.features f.hasAdaptiveAnc shouldBe true @@ -382,6 +400,18 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() { f.hasEarDetectionToggle shouldBe true f.hasAncControl shouldBe true f.hasEarDetection shouldBe true + f.hasMicrophoneMode shouldBe true + } + + @Test fun `Powerbeats Pro and Beats Fit Pro expose ear detection toggle`() { + val powerbeatsPro = PodModel.POWERBEATS_PRO.features + powerbeatsPro.hasEarDetection shouldBe true + powerbeatsPro.hasEarDetectionToggle shouldBe true + + val beatsFitPro = PodModel.BEATS_FIT_PRO.features + beatsFitPro.hasEarDetection shouldBe true + beatsFitPro.hasEarDetectionToggle shouldBe true + beatsFitPro.hasMicrophoneMode shouldBe true } } @@ -413,6 +443,14 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() { } } + @Test fun `earDetectionToggle implies earDetection`() { + for (model in PodModel.entries) { + if (model.features.hasEarDetectionToggle) { + model.features.hasEarDetection shouldBe true + } + } + } + @Test fun `adaptiveAudioNoise implies adaptiveAnc`() { for (model in PodModel.entries) { if (model.features.hasAdaptiveAudioNoise) {