fix(aap): Update feature flags for Max 2 and Powerbeats Pro 2, sync ear detection

Add H2-chip feature flags (adaptive ANC, conversation awareness, personalized volume, listening mode cycle, InitExt) to AirPods Max 2. Add sleep detection and ear detection toggle to Powerbeats Pro 2.

Auto-sync device ear detection setting when auto-play/auto-pause reactions are toggled. Add flag invariant tests to prevent future drift.
This commit is contained in:
darken
2026-04-15 06:47:05 +02:00
committed by Matthias Urhahn
parent 4b80ac7336
commit 397b32b425
5 changed files with 124 additions and 5 deletions
@@ -339,5 +339,76 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() {
f.hasListeningModeCycle shouldBe false
f.hasStemConfig shouldBe false
}
@Test fun `Max 2 has H2 features`() {
val f = PodModel.AIRPODS_MAX2.features
f.hasAdaptiveAnc shouldBe true
f.hasConversationAwareness shouldBe true
f.hasPersonalizedVolume shouldBe true
f.hasAdaptiveAudioNoise shouldBe true
f.hasListeningModeCycle shouldBe true
f.hasAllowOffOption shouldBe true
f.hasEarDetectionToggle shouldBe true
f.needsInitExt shouldBe true
// Headphone — no stem/swipe/dual-pod/case features
f.hasDualPods shouldBe false
f.hasCase shouldBe false
f.hasStemConfig shouldBe false
f.hasVolumeSwipe shouldBe false
f.hasSleepDetection shouldBe false
}
@Test fun `Powerbeats Pro 2 has sleep detection and ear detection toggle`() {
val f = PodModel.POWERBEATS_PRO2.features
f.hasSleepDetection shouldBe true
f.hasEarDetectionToggle shouldBe true
f.hasAncControl shouldBe true
f.hasEarDetection shouldBe true
}
}
// ── Feature Flag Invariants ─────────────────────────────
@Nested
inner class FeatureFlagInvariants {
@Test fun `adaptiveAnc implies ancControl`() {
for (model in PodModel.entries) {
if (model.features.hasAdaptiveAnc) {
model.features.hasAncControl shouldBe true
}
}
}
@Test fun `listeningModeCycle implies ancControl`() {
for (model in PodModel.entries) {
if (model.features.hasListeningModeCycle) {
model.features.hasAncControl shouldBe true
}
}
}
@Test fun `sleepDetection implies earDetection`() {
for (model in PodModel.entries) {
if (model.features.hasSleepDetection) {
model.features.hasEarDetection shouldBe true
}
}
}
@Test fun `adaptiveAudioNoise implies adaptiveAnc`() {
for (model in PodModel.entries) {
if (model.features.hasAdaptiveAudioNoise) {
model.features.hasAdaptiveAnc shouldBe true
}
}
}
@Test fun `allowOffOption implies listeningModeCycle`() {
for (model in PodModel.entries) {
if (model.features.hasAllowOffOption) {
model.features.hasListeningModeCycle shouldBe true
}
}
}
}
}
@@ -65,6 +65,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
fun `null for basic AirPods`() { DefaultAapDeviceProfile(PodModel.AIRPODS_GEN3).encodeInitExt().shouldBeNull() }
@Test fun `null for Pro 1`() { DefaultAapDeviceProfile(PodModel.AIRPODS_PRO).encodeInitExt().shouldBeNull() }
@Test fun `null for Max`() { DefaultAapDeviceProfile(PodModel.AIRPODS_MAX).encodeInitExt().shouldBeNull() }
@Test fun `returned for Max 2`() { DefaultAapDeviceProfile(PodModel.AIRPODS_MAX2).encodeInitExt().shouldNotBeNull() }
@Test
fun `has correct command byte`() {
@@ -109,6 +110,13 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
AapSetting.AncMode.Value.OFF, AapSetting.AncMode.Value.ON, AapSetting.AncMode.Value.TRANSPARENCY,
)
}
@Test
fun `Max 2 supports OFF, ON, TRANSPARENCY, ADAPTIVE`() {
ancModesFor(PodModel.AIRPODS_MAX2) shouldContainExactly listOf(
AapSetting.AncMode.Value.OFF, AapSetting.AncMode.Value.ON, AapSetting.AncMode.Value.TRANSPARENCY, AapSetting.AncMode.Value.ADAPTIVE,
)
}
}
// ── ANC Mode encode/decode ───────────────────────────────