From ba29c233e2e9061fd5653030ffecdf44dd474f56 Mon Sep 17 00:00:00 2001 From: darken Date: Wed, 15 Apr 2026 07:59:31 +0200 Subject: [PATCH] refactor(aap): Send InitExt unconditionally to all devices Older devices silently ignore the 0x4D packet, so there is no need to gate it per model. Sending it unconditionally prevents silent feature degradation when new H2+ models are added without the flag. Remove needsInitExt from PodModel.Features. --- .../java/eu/darken/capod/pods/core/apple/PodModel.kt | 7 ------- .../capod/pods/core/apple/aap/AapConnection.kt | 10 ++++------ .../pods/core/apple/aap/protocol/AapDeviceProfile.kt | 7 ++++--- .../apple/aap/protocol/DefaultAapDeviceProfile.kt | 11 ++++------- .../DefaultAapDeviceProfileNewSettingsTest.kt | 1 - .../apple/aap/devices/DefaultAapDeviceProfileTest.kt | 12 +----------- 6 files changed, 13 insertions(+), 35 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 0877c079..af920971 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 @@ -99,7 +99,6 @@ enum class PodModel( hasAllowOffOption = true, hasStemConfig = true, hasSleepDetection = true, - needsInitExt = true, ), modelNumbers = setOf("A3055", "A3056", "A3057"), // earphones leftPodIconRes = R.drawable.device_airpods_gen4anc_left, @@ -158,7 +157,6 @@ enum class PodModel( hasAllowOffOption = true, hasStemConfig = true, hasSleepDetection = true, - needsInitExt = true, ), modelNumbers = setOf("A2698", "A2699", "A2931"), // earphones leftPodIconRes = R.drawable.device_airpods_pro2_left, @@ -192,7 +190,6 @@ enum class PodModel( hasAllowOffOption = true, hasStemConfig = true, hasSleepDetection = true, - needsInitExt = true, ), modelNumbers = setOf("A3047", "A3048", "A3049"), // earphones leftPodIconRes = R.drawable.device_airpods_pro2_left, @@ -226,7 +223,6 @@ enum class PodModel( hasAllowOffOption = true, hasStemConfig = true, hasSleepDetection = true, - needsInitExt = true, ), modelNumbers = setOf("A3063", "A3064", "A3065"), // earphones leftPodIconRes = R.drawable.device_airpods_pro2_left, @@ -281,7 +277,6 @@ enum class PodModel( hasEarDetectionToggle = true, hasListeningModeCycle = true, hasAllowOffOption = true, - needsInitExt = true, ), modelNumbers = setOf("A3454"), // headphones ), @@ -550,7 +545,5 @@ enum class PodModel( val hasAllowOffOption: Boolean = false, val hasStemConfig: Boolean = false, val hasSleepDetection: Boolean = false, - // Protocol - val needsInitExt: Boolean = false, ) } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/AapConnection.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/AapConnection.kt index 334e0696..9caaa34b 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/AapConnection.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/AapConnection.kt @@ -110,12 +110,10 @@ internal class AapConnection( } log(TAG) { "Notification enable sent" } - // Send InitExt for models that need it (Pro 2/3/USB-C, AP4 ANC) - profile.encodeInitExt()?.let { initExt -> - sock.outputStream.write(initExt) - sock.outputStream.flush() - log(TAG) { "InitExt sent" } - } + // Send InitExt — enables advanced features on H2+ devices, ignored by older models + sock.outputStream.write(profile.encodeInitExt()) + sock.outputStream.flush() + log(TAG) { "InitExt sent" } // Request private keys (IRK + ENC) for BLE encrypted battery profile.encodePrivateKeyRequest()?.let { keyReq -> diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/AapDeviceProfile.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/AapDeviceProfile.kt index 54ebc8f5..efdb9ccd 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/AapDeviceProfile.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/AapDeviceProfile.kt @@ -50,10 +50,11 @@ interface AapDeviceProfile { fun encodeNotificationEnable(): List /** - * Encode the extended init packet (0x4D) for models that require it - * (e.g., Pro 2/3/USB-C, AirPods 4 ANC). Returns null if not needed. + * Encode the extended init packet (0x4D). + * Enables advanced features (Adaptive Transparency, Conversational Awareness during playback) + * on H2+ devices; silently ignored by older models. */ - fun encodeInitExt(): ByteArray? + fun encodeInitExt(): ByteArray /** * Encode a private key request (command 0x30). diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/DefaultAapDeviceProfile.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/DefaultAapDeviceProfile.kt index 1505052d..dd3adf98 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/DefaultAapDeviceProfile.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/aap/protocol/DefaultAapDeviceProfile.kt @@ -83,13 +83,10 @@ class DefaultAapDeviceProfile( byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x0f, 0x00, 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte()), ) - override fun encodeInitExt(): ByteArray? { - if (!model.features.needsInitExt) return null - return byteArrayOf( - 0x04, 0x00, 0x04, 0x00, 0x4d, 0x00, 0xd7.toByte(), 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ) - } + override fun encodeInitExt(): ByteArray = byteArrayOf( + 0x04, 0x00, 0x04, 0x00, 0x4d, 0x00, 0xd7.toByte(), 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ) override fun encodeCommand(command: AapCommand): ByteArray = when (command) { is AapCommand.SetAncMode -> buildSettingsMessage(SETTING_ANC_MODE, encodeAncMode(command.mode)) 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 a6e4759d..d00e42fc 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 @@ -349,7 +349,6 @@ class DefaultAapDeviceProfileNewSettingsTest : BaseAapSessionTest() { 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 diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileTest.kt index cc3091c4..b67b1d3c 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/aap/devices/DefaultAapDeviceProfileTest.kt @@ -57,19 +57,9 @@ class DefaultAapDeviceProfileTest : BaseAapSessionTest() { @Nested inner class InitExtTests { - @Test - fun `returned for Pro 2`() { DefaultAapDeviceProfile(PodModel.AIRPODS_PRO2).encodeInitExt().shouldNotBeNull() } - @Test fun `returned for Pro 3`() { DefaultAapDeviceProfile(PodModel.AIRPODS_PRO3).encodeInitExt().shouldNotBeNull() } - @Test fun `returned for AP4 ANC`() { DefaultAapDeviceProfile(PodModel.AIRPODS_GEN4_ANC).encodeInitExt().shouldNotBeNull() } - @Test - 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`() { - profile.encodeInitExt()!![4] shouldBe 0x4d.toByte() + profile.encodeInitExt()[4] shouldBe 0x4d.toByte() } }