mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 19:26:12 -04:00
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:
@@ -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,
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+40
-2
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user