From 6b922e88451ee44b52188cf7dd62066a6266d27a Mon Sep 17 00:00:00 2001 From: darken Date: Fri, 8 Nov 2024 09:10:34 +0100 Subject: [PATCH] Fix AirPods Max and AirPods Max (USB-C) support Closes #236 --- .../eu/darken/capod/pods/core/PodDevice.kt | 4 +-- .../pods/core/apple/AppleFactoryModule.kt | 4 +-- .../pods/core/apple/airpods/AirPodsMax.kt | 4 +-- .../{AirPodsMax2.kt => AirPodsMaxUsbc.kt} | 12 +++---- .../core/apple/airpods/AirPodsMaxUsbcTest.kt | 32 +++++++++++++++++++ 5 files changed, 44 insertions(+), 12 deletions(-) rename app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/{AirPodsMax2.kt => AirPodsMaxUsbc.kt} (89%) create mode 100644 app-common/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxUsbcTest.kt diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt b/app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt index d58e14a5..0de6d780 100644 --- a/app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt +++ b/app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt @@ -107,8 +107,8 @@ interface PodDevice { "AirPods Max", R.drawable.devic_headphones_generic ), - @Json(name = "airpods.max2") AIRPODS_MAX2( - "AirPods Max 2", + @Json(name = "airpods.max.usbc") AIRPODS_MAX_USBC( + "AirPods Max USB-C", R.drawable.devic_headphones_generic ), @Json(name = "beats.flex") BEATS_FLEX( diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt b/app-common/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt index da6de94f..c0807d06 100644 --- a/app-common/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt +++ b/app-common/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt @@ -11,7 +11,7 @@ import eu.darken.capod.pods.core.apple.airpods.AirPodsGen3 import eu.darken.capod.pods.core.apple.airpods.AirPodsGen4 import eu.darken.capod.pods.core.apple.airpods.AirPodsGen4Anc import eu.darken.capod.pods.core.apple.airpods.AirPodsMax -import eu.darken.capod.pods.core.apple.airpods.AirPodsMax2 +import eu.darken.capod.pods.core.apple.airpods.AirPodsMaxUsbc import eu.darken.capod.pods.core.apple.airpods.AirPodsPro import eu.darken.capod.pods.core.apple.airpods.AirPodsPro2 import eu.darken.capod.pods.core.apple.airpods.AirPodsPro2Usbc @@ -43,7 +43,7 @@ abstract class AppleFactoryModule { @Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory @Binds @IntoSet abstract fun airPodsPro2Usbc(factory: AirPodsPro2Usbc.Factory): ApplePodsFactory @Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory - @Binds @IntoSet abstract fun airPodsMax2(factory: AirPodsMax2.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun airPodsMax2(factory: AirPodsMaxUsbc.Factory): ApplePodsFactory @Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory @Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePodsFactory diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt b/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt index 2ccd586f..33c043e0 100644 --- a/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt +++ b/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt @@ -39,7 +39,7 @@ data class AirPodsMax( class Factory @Inject constructor() : SingleApplePodsFactory(TAG) { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { - getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH + getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH } override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods { @@ -63,7 +63,7 @@ data class AirPodsMax( } companion object { - private val DEVICE_CODE_DIRTY = 0x200A.toUByte() + private val DEVICE_CODE = 0x0A20.toUShort() private val TAG = logTag("PodDevice", "Apple", "AirPods", "Max") } } \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax2.kt b/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxUsbc.kt similarity index 89% rename from app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax2.kt rename to app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxUsbc.kt index afe2b61d..4b886a2f 100644 --- a/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax2.kt +++ b/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxUsbc.kt @@ -14,7 +14,7 @@ import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant import javax.inject.Inject -data class AirPodsMax2( +data class AirPodsMaxUsbc( override val identifier: PodDevice.Id = PodDevice.Id(), override val seenLastAt: Instant = Instant.now(), override val seenFirstAt: Instant = Instant.now(), @@ -25,7 +25,7 @@ data class AirPodsMax2( private val rssiAverage: Int? = null, ) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor { - override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX2 + override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX_USBC override val rssi: Int get() = rssiAverage ?: super.rssi @@ -39,11 +39,11 @@ data class AirPodsMax2( class Factory @Inject constructor() : SingleApplePodsFactory(TAG) { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { - getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH + getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH } override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods { - var basic = AirPodsMax2(scanResult = scanResult, proximityMessage = message) + var basic = AirPodsMaxUsbc(scanResult = scanResult, proximityMessage = message) val result = searchHistory(basic) if (result != null) basic = basic.copy(identifier = result.id) @@ -63,7 +63,7 @@ data class AirPodsMax2( } companion object { - private val DEVICE_CODE_DIRTY = 0x201F.toUByte() - private val TAG = logTag("PodDevice", "Apple", "AirPods", "Max2") + private val DEVICE_CODE = 0x1F20.toUShort() + private val TAG = logTag("PodDevice", "Apple", "AirPods", "Max", "USBC") } } diff --git a/app-common/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxUsbcTest.kt b/app-common/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxUsbcTest.kt new file mode 100644 index 00000000..6d93192d --- /dev/null +++ b/app-common/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMaxUsbcTest.kt @@ -0,0 +1,32 @@ +package eu.darken.capod.pods.core.apple.airpods + +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.BaseAirPodsTest +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +class AirPodsMaxUsbcTest : BaseAirPodsTest() { + + // Test data from https://github.com/d4rken-org/capod/issues/236 + @Test + fun `default AirPods Max`() = runTest { + create("07 19 01 1F 20 2B 05 80 03 12 C5 2E 8B F9 9A 7E 19 7B 63 0F 30 6E D7 3B E2 EC 32") { + rawPrefix shouldBe 0x01.toUByte() + rawDeviceModel shouldBe 0x1F20.toUShort() + rawStatus shouldBe 0x2B.toUByte() + rawPodsBattery shouldBe 0x05.toUByte() + rawFlags shouldBe 0x8.toUShort() + rawCaseBattery shouldBe 0x0.toUShort() + rawCaseLidState shouldBe 0x03.toUByte() + rawDeviceColor shouldBe 0x12.toUByte() + rawSuffix shouldBe 0xC5.toUByte() + + batteryHeadsetPercent shouldBe 0.5f + + isHeadsetBeingCharged shouldBe false + + model shouldBe PodDevice.Model.AIRPODS_MAX_USBC + } + } +} \ No newline at end of file