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.
This commit is contained in:
darken
2026-05-01 20:08:29 +02:00
committed by Matthias Urhahn
parent e1c0a702dd
commit fc98db09a2
4 changed files with 397 additions and 60 deletions
@@ -22,7 +22,9 @@ enum class PodModel(
Features( Features(
hasDualPods = true, hasDualPods = true,
hasCase = true, hasCase = true,
hasEarDetection = true,
hasMicrophoneMode = true, hasMicrophoneMode = true,
hasEarDetectionToggle = true,
), ),
modelNumbers = setOf("A1523", "A1722"), // L/R earphones modelNumbers = setOf("A1523", "A1722"), // L/R earphones
leftPodIconRes = R.drawable.device_airpods_gen1_left, leftPodIconRes = R.drawable.device_airpods_gen1_left,
@@ -37,7 +39,9 @@ enum class PodModel(
Features( Features(
hasDualPods = true, hasDualPods = true,
hasCase = true, hasCase = true,
hasEarDetection = true,
hasMicrophoneMode = true, hasMicrophoneMode = true,
hasEarDetectionToggle = true,
), ),
modelNumbers = setOf("A2031", "A2032"), // L/R earphones modelNumbers = setOf("A2031", "A2032"), // L/R earphones
leftPodIconRes = R.drawable.device_airpods_gen1_left, leftPodIconRes = R.drawable.device_airpods_gen1_left,
@@ -52,10 +56,13 @@ enum class PodModel(
Features( Features(
hasDualPods = true, hasDualPods = true,
hasCase = true, hasCase = true,
hasEarDetection = true,
hasPressSpeed = true, hasPressSpeed = true,
hasPressHoldDuration = true, hasPressHoldDuration = true,
hasToneVolume = true, hasToneVolume = true,
hasEndCallMuteMic = true,
hasMicrophoneMode = true, hasMicrophoneMode = true,
hasEarDetectionToggle = true,
), ),
modelNumbers = setOf("A2564", "A2565"), // L/R earphones modelNumbers = setOf("A2564", "A2565"), // L/R earphones
leftPodIconRes = R.drawable.device_airpods_gen3_left, leftPodIconRes = R.drawable.device_airpods_gen3_left,
@@ -74,6 +81,7 @@ enum class PodModel(
hasPressSpeed = true, hasPressSpeed = true,
hasPressHoldDuration = true, hasPressHoldDuration = true,
hasToneVolume = true, hasToneVolume = true,
hasEndCallMuteMic = true,
hasMicrophoneMode = true, hasMicrophoneMode = true,
hasEarDetectionToggle = true, hasEarDetectionToggle = true,
hasSleepDetection = true, hasSleepDetection = true,
@@ -314,7 +322,6 @@ enum class PodModel(
"Beats Solo Pro", "Beats Solo Pro",
R.drawable.device_beats_headphones, R.drawable.device_beats_headphones,
Features( Features(
hasEarDetection = true,
hasAncControl = true, hasAncControl = true,
), ),
modelNumbers = setOf("A1881"), // headphones modelNumbers = setOf("A1881"), // headphones
@@ -343,7 +350,6 @@ enum class PodModel(
"Beats Studio 3", "Beats Studio 3",
R.drawable.device_beats_studio3, R.drawable.device_beats_studio3,
Features( Features(
hasEarDetection = true,
hasAncControl = true, hasAncControl = true,
), ),
modelNumbers = setOf("A1914"), // headphones modelNumbers = setOf("A1914"), // headphones
@@ -410,6 +416,7 @@ enum class PodModel(
hasDualPods = true, hasDualPods = true,
hasCase = true, hasCase = true,
hasEarDetection = true, hasEarDetection = true,
hasEarDetectionToggle = true,
), ),
modelNumbers = setOf("A2047", "A2048", "A2453", "A2454"), // L/R earbuds, 2019 + 2020 revisions modelNumbers = setOf("A2047", "A2048", "A2453", "A2454"), // L/R earbuds, 2019 + 2020 revisions
leftPodIconRes = R.drawable.device_powerbeats_pro_left, leftPodIconRes = R.drawable.device_powerbeats_pro_left,
@@ -426,6 +433,7 @@ enum class PodModel(
hasCase = true, hasCase = true,
hasEarDetection = true, hasEarDetection = true,
hasAncControl = true, hasAncControl = true,
hasMicrophoneMode = true,
hasEarDetectionToggle = true, hasEarDetectionToggle = true,
hasSleepDetection = true, hasSleepDetection = true,
), ),
@@ -444,6 +452,8 @@ enum class PodModel(
hasCase = true, hasCase = true,
hasEarDetection = true, hasEarDetection = true,
hasAncControl = true, hasAncControl = true,
hasMicrophoneMode = true,
hasEarDetectionToggle = true,
), ),
modelNumbers = setOf("A2576", "A2577", "A2578"), // L/R earbuds + case modelNumbers = setOf("A2576", "A2577", "A2578"), // L/R earbuds + case
leftPodIconRes = R.drawable.device_beats_fitpro_left, leftPodIconRes = R.drawable.device_beats_fitpro_left,
@@ -458,6 +468,7 @@ enum class PodModel(
Features( Features(
hasDualPods = true, hasDualPods = true,
hasCase = true, hasCase = true,
hasEarDetection = true,
), ),
leftPodIconRes = R.drawable.device_airpods_gen1_left, leftPodIconRes = R.drawable.device_airpods_gen1_left,
rightPodIconRes = R.drawable.device_airpods_gen1_right, rightPodIconRes = R.drawable.device_airpods_gen1_right,
@@ -471,6 +482,7 @@ enum class PodModel(
Features( Features(
hasDualPods = true, hasDualPods = true,
hasCase = true, hasCase = true,
hasEarDetection = true,
), ),
leftPodIconRes = R.drawable.device_airpods_gen1_left, leftPodIconRes = R.drawable.device_airpods_gen1_left,
rightPodIconRes = R.drawable.device_airpods_gen1_right, rightPodIconRes = R.drawable.device_airpods_gen1_right,
@@ -484,6 +496,7 @@ enum class PodModel(
Features( Features(
hasDualPods = true, hasDualPods = true,
hasCase = true, hasCase = true,
hasEarDetection = true,
), ),
leftPodIconRes = R.drawable.device_airpods_gen3_left, leftPodIconRes = R.drawable.device_airpods_gen3_left,
rightPodIconRes = R.drawable.device_airpods_gen3_right, rightPodIconRes = R.drawable.device_airpods_gen3_right,
+1 -1
View File
@@ -515,7 +515,7 @@
<!-- New device settings --> <!-- New device settings -->
<string name="device_settings_category_general_label">General</string> <string name="device_settings_category_general_label">General</string>
<string name="device_settings_microphone_mode_label">Microphone</string> <string name="device_settings_microphone_mode_label">Microphone</string>
<string name="device_settings_microphone_mode_description">Which AirPod is used as the microphone</string> <string name="device_settings_microphone_mode_description">Which earbud is used as the microphone</string>
<string name="device_settings_microphone_mode_auto">Auto</string> <string name="device_settings_microphone_mode_auto">Auto</string>
<string name="device_settings_microphone_mode_right">Right</string> <string name="device_settings_microphone_mode_right">Right</string>
<string name="device_settings_microphone_mode_left">Left</string> <string name="device_settings_microphone_mode_left">Left</string>
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple
import io.kotest.assertions.withClue
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import testhelpers.BaseTest import testhelpers.BaseTest
@@ -7,79 +8,364 @@ import testhelpers.BaseTest
class ModelFeaturesTest : BaseTest() { class ModelFeaturesTest : BaseTest() {
@Test @Test
fun `AirPods Pro 3 has all features`() { fun `feature flags match supported model matrix`() {
val f = PodModel.AIRPODS_PRO3.features featureExpectations.forEach { expectation ->
f.hasDualPods shouldBe true withClue(expectation.name) {
f.hasCase shouldBe true modelsWith(expectation.predicate) shouldBe expectation.models
f.hasEarDetection shouldBe true }
f.hasAncControl shouldBe true }
} }
@Test @Test
fun `AirPods Gen 1 has dual pods and case but no ear detection or ANC`() { fun `models without ear detection match intentional matrix`() {
val f = PodModel.AIRPODS_GEN1.features modelsWithout { it.hasEarDetection } shouldBe setOf(
f.hasDualPods shouldBe true PodModel.BEATS_FLEX,
f.hasCase shouldBe true PodModel.BEATS_SOLO_3,
f.hasEarDetection shouldBe false PodModel.BEATS_SOLO_PRO,
f.hasAncControl shouldBe false PodModel.BEATS_SOLO_4,
} PodModel.BEATS_SOLO_BUDS,
PodModel.BEATS_STUDIO_3,
@Test PodModel.BEATS_STUDIO_BUDS,
fun `AirPods Max is single device with ANC but no dual pods or case`() { PodModel.BEATS_STUDIO_BUDS_PLUS,
val f = PodModel.AIRPODS_MAX.features PodModel.BEATS_STUDIO_PRO,
f.hasDualPods shouldBe false PodModel.BEATS_X,
f.hasCase shouldBe false PodModel.POWERBEATS_3,
f.hasEarDetection shouldBe true PodModel.POWERBEATS_4,
f.hasAncControl shouldBe true PodModel.UNKNOWN,
} )
@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
} }
@Test @Test
fun `UNKNOWN model has no features`() { fun `UNKNOWN model has no features`() {
val f = PodModel.UNKNOWN.features PodModel.UNKNOWN.features shouldBe PodModel.Features()
f.hasDualPods shouldBe false
f.hasCase shouldBe false
f.hasEarDetection shouldBe false
f.hasAncControl shouldBe false
} }
@Test @Test
fun `all ANC-capable dual-pod models have ear detection except Studio Buds`() { fun `ear detection toggle implies ear detection`() {
val noEarDetectionAncModels = setOf(
PodModel.BEATS_STUDIO_BUDS,
PodModel.BEATS_STUDIO_BUDS_PLUS,
)
PodModel.entries PodModel.entries
.filter { it.features.hasAncControl && it.features.hasDualPods } .filter { it.features.hasEarDetectionToggle }
.filter { it !in noEarDetectionAncModels }
.forEach { model -> .forEach { model ->
withClue(model.name) {
model.features.hasEarDetection shouldBe true model.features.hasEarDetection shouldBe true
} }
} }
}
@Test @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 PodModel.entries
.filter { it.features.hasCase } .filter { it.features.hasCase }
.forEach { model -> .forEach { model ->
withClue(model.name) {
model.features.hasDualPods shouldBe true model.features.hasDualPods shouldBe true
} }
} }
} }
@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> = PodModel.entries
.filter { predicate(it.features) }
.toSet()
private fun modelsWithout(predicate: (PodModel.Features) -> Boolean): Set<PodModel> = PodModel.entries
.filterNot { predicate(it.features) }
.toSet()
private data class FeatureExpectation(
val name: String,
val predicate: (PodModel.Features) -> Boolean,
val models: Set<PodModel>,
)
private fun feature(
name: String,
predicate: (PodModel.Features) -> Boolean,
models: Set<PodModel>,
) = 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)),
)
}
@@ -337,6 +337,7 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() {
f.hasPressSpeed shouldBe true f.hasPressSpeed shouldBe true
f.hasPressHoldDuration shouldBe true f.hasPressHoldDuration shouldBe true
f.hasToneVolume shouldBe true f.hasToneVolume shouldBe true
f.hasEndCallMuteMic shouldBe true
f.hasListeningModeCycle shouldBe false f.hasListeningModeCycle shouldBe false
f.hasStemConfig shouldBe false f.hasStemConfig shouldBe false
f.hasSleepDetection shouldBe true f.hasSleepDetection shouldBe true
@@ -351,14 +352,31 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() {
f.hasStemConfig shouldBe false 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 val f = PodModel.AIRPODS_GEN1.features
f.hasMicrophoneMode shouldBe true f.hasMicrophoneMode shouldBe true
f.hasEarDetectionToggle shouldBe false f.hasEarDetection shouldBe true
f.hasEarDetectionToggle shouldBe true
f.hasListeningModeCycle shouldBe false f.hasListeningModeCycle shouldBe false
f.hasStemConfig 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`() { @Test fun `Max 2 has H2 features`() {
val f = PodModel.AIRPODS_MAX2.features val f = PodModel.AIRPODS_MAX2.features
f.hasAdaptiveAnc shouldBe true f.hasAdaptiveAnc shouldBe true
@@ -382,6 +400,18 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() {
f.hasEarDetectionToggle shouldBe true f.hasEarDetectionToggle shouldBe true
f.hasAncControl shouldBe true f.hasAncControl shouldBe true
f.hasEarDetection 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`() { @Test fun `adaptiveAudioNoise implies adaptiveAnc`() {
for (model in PodModel.entries) { for (model in PodModel.entries) {
if (model.features.hasAdaptiveAudioNoise) { if (model.features.hasAdaptiveAudioNoise) {