fix(aap): Correct EndCallMuteMic mapping based on real-device behavior

Swap the logical mapping for `EndCallMuteMic` settings to align with hardware states observed on AirPods Pro devices. Updated session tests for Pro 1, 2, and 3 to reflect the corrected protocol behavior.
This commit is contained in:
darken
2026-04-16 10:32:21 +02:00
committed by Matthias Urhahn
parent c5d9bed5f0
commit fd6a9667e2
5 changed files with 31 additions and 30 deletions
@@ -10,7 +10,6 @@ import eu.darken.capod.pods.core.apple.aap.protocol.DefaultAapDeviceProfile
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
@@ -132,7 +131,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
fun `round-trip all modes`() {
for (mode in AapSetting.AncMode.Value.entries) {
val encoded = profile.encodeCommand(AapCommand.SetAncMode(mode))
val decoded = decodeSetting<AapSetting.AncMode>(AapMessage.Companion.parse(encoded)!!)
val decoded = decodeSetting<AapSetting.AncMode>(AapMessage.parse(encoded)!!)
decoded.current shouldBe mode
}
}
@@ -166,7 +165,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
fun `round-trip all values`() {
for (v in AapSetting.PressSpeed.Value.entries) {
val encoded = profile.encodeCommand(AapCommand.SetPressSpeed(v))
decodeSetting<AapSetting.PressSpeed>(AapMessage.Companion.parse(encoded)!!).value shouldBe v
decodeSetting<AapSetting.PressSpeed>(AapMessage.parse(encoded)!!).value shouldBe v
}
}
}
@@ -184,7 +183,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
fun `round-trip all values`() {
for (v in AapSetting.PressHoldDuration.Value.entries) {
val encoded = profile.encodeCommand(AapCommand.SetPressHoldDuration(v))
decodeSetting<AapSetting.PressHoldDuration>(AapMessage.Companion.parse(encoded)!!).value shouldBe v
decodeSetting<AapSetting.PressHoldDuration>(AapMessage.parse(encoded)!!).value shouldBe v
}
}
}
@@ -224,7 +223,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
fun `round-trip all values`() {
for (v in AapSetting.VolumeSwipeLength.Value.entries) {
val encoded = profile.encodeCommand(AapCommand.SetVolumeSwipeLength(v))
decodeSetting<AapSetting.VolumeSwipeLength>(AapMessage.Companion.parse(encoded)!!).value shouldBe v
decodeSetting<AapSetting.VolumeSwipeLength>(AapMessage.parse(encoded)!!).value shouldBe v
}
}
}
@@ -259,7 +258,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
@Test fun `encode decode round-trip`() {
for (ui in listOf(0, 25, 50, 75, 100)) {
val encoded = profile.encodeCommand(AapCommand.SetAdaptiveAudioNoise(ui))
decodeSetting<AapSetting.AdaptiveAudioNoise>(AapMessage.Companion.parse(encoded)!!).level shouldBe ui
decodeSetting<AapSetting.AdaptiveAudioNoise>(AapMessage.parse(encoded)!!).level shouldBe ui
}
}
}
@@ -280,7 +279,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
0x04, 0x00, 0x04, 0x00,
0x09, 0x00,
0x24, 0x20,
0x02,
0x03,
0x00, 0x00,
)
}
@@ -297,7 +296,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
0x04, 0x00, 0x04, 0x00,
0x09, 0x00,
0x24, 0x20,
0x03,
0x02,
0x00, 0x00,
)
}
@@ -342,22 +341,22 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
@Test
fun `decode compact format 0x20 combined 0x02`() {
val ecm = decodeSetting<AapSetting.EndCallMuteMic>(aapMessage("04 00 04 00 09 00 24 20 02 00 00"))
ecm.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.SINGLE_PRESS
ecm.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.DOUBLE_PRESS
ecm.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS
ecm.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS
}
@Test
fun `decode compact format 0x20 combined 0x03`() {
val ecm = decodeSetting<AapSetting.EndCallMuteMic>(aapMessage("04 00 04 00 09 00 24 20 03 00 00"))
ecm.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS
ecm.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS
ecm.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.SINGLE_PRESS
ecm.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.DOUBLE_PRESS
}
@Test
fun `decode compact format subtype 0x00 from real Pro 3`() {
val ecm = decodeSetting<AapSetting.EndCallMuteMic>(aapMessage("04 00 04 00 09 00 24 00 03 00 00"))
ecm.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS
ecm.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS
ecm.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.SINGLE_PRESS
ecm.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.DOUBLE_PRESS
}
@Test
@@ -462,7 +461,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
val enc = ByteArray(16) { 0x22.toByte() }
val payload = byteArrayOf(0x02, 0x01, 0x00, 16, 0x00, *irk, 0x04, 0x00, 16, 0x00, *enc)
val raw = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x31, 0x00) + payload
val result = profile.decodePrivateKeyResponse(AapMessage.Companion.parse(raw)!!)!!
val result = profile.decodePrivateKeyResponse(AapMessage.parse(raw)!!)!!
result.irk shouldBe irk
result.encKey shouldBe enc
}
@@ -471,7 +470,7 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
fun `decode response with only IRK`() {
val irk = ByteArray(16) { 0xAA.toByte() }
val raw = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x31, 0x00, 0x01, 0x01, 0x00, 16, 0x00, *irk)
val result = profile.decodePrivateKeyResponse(AapMessage.Companion.parse(raw)!!)!!
val result = profile.decodePrivateKeyResponse(AapMessage.parse(raw)!!)!!
result.irk shouldBe irk
result.encKey.shouldBeNull()
}
@@ -573,4 +572,4 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() {
@Test fun `unknown setting ID returns null`() { profile.decodeSetting(settingsMessage(0x7F, 0x01)).shouldBeNull() }
@Test fun `non-settings command returns null`() { profile.decodeSetting(aapMessage("04 00 04 00 1D 00 01 02 03 04")).shouldBeNull() }
@Test fun `payload too short returns null`() { profile.decodeSetting(aapMessage("04 00 04 00 09 00 0D")).shouldBeNull() }
}
}
@@ -142,8 +142,8 @@ class AirPodsPro2UsbcAapSessionTest : BaseAapSessionTest() {
@Test fun `end call mute mic - subtype 0x00`() {
decodeSetting<AapSetting.EndCallMuteMic>("04 00 04 00 09 00 24 00 03 00 00").let {
it.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS
it.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS
it.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.SINGLE_PRESS
it.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.DOUBLE_PRESS
}
}
@@ -243,7 +243,7 @@ class AirPodsPro2UsbcAapSessionTest : BaseAapSessionTest() {
@Test
fun `unknown settings IDs return null`() {
val unknownIds = listOf(0x29, 0x2C, 0x2F, 0x33, 0x3E)
val unknownIds = listOf(0x29, 0x2C, 0x2F, 0x33)
for (id in unknownIds) {
profile.decodeSetting(settingsMessage(id, 0x02)).shouldBeNull()
}
@@ -134,7 +134,7 @@ class AirPodsPro3AapSessionTest : BaseAapSessionTest() {
}
@Test fun `end call mute mic - subtype 0x00`() {
decodeSetting<AapSetting.EndCallMuteMic>("04 00 04 00 09 00 24 00 03 00 00").let { it.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS; it.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS }
decodeSetting<AapSetting.EndCallMuteMic>("04 00 04 00 09 00 24 00 03 00 00").let { it.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.SINGLE_PRESS; it.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.DOUBLE_PRESS }
}
@Test fun `conversational awareness OFF`() {
@@ -237,10 +237,10 @@ class AirPodsPro3AapSessionTest : BaseAapSessionTest() {
@Test
fun `unknown settings IDs return null`() {
val unknownIds = listOf(0x29, 0x2C, 0x2F, 0x33, 0x30, 0x3E, 0x37, 0x38, 0x3B)
val unknownIds = listOf(0x29, 0x2C, 0x2F, 0x33, 0x30, 0x37, 0x38, 0x3B)
for (id in unknownIds) {
profile.decodeSetting(settingsMessage(id, 0x01)).shouldBeNull()
}
}
}
}
}
@@ -137,8 +137,8 @@ class AirPodsProAapSessionTest : BaseAapSessionTest() {
@Test fun `end call mute mic - subtype 0x20`() {
decodeSetting<AapSetting.EndCallMuteMic>("04 00 04 00 09 00 24 20 03 00 00").let {
it.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS
it.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS
it.muteMic shouldBe AapSetting.EndCallMuteMic.MuteMicMode.SINGLE_PRESS
it.endCall shouldBe AapSetting.EndCallMuteMic.EndCallMode.DOUBLE_PRESS
}
}
}