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 8a825916..28aefcd6 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,11 @@ interface PodDevice { @Json(name = "fakes.tws.i99999") TWS_I99999( "TWS i99999" ), - @Json(name = "fakes.varunr.airpodspro") VARUNR_AIRPODS_PRO( - "Fake AirPods Pro" + @Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO( + "AirPods Pro?" + ), + @Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3( + "AirPods (Gen 3)?" ), @Json(name = "unknown") UNKNOWN( "Unknown" 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 8a1e1289..e4b49314 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 @@ -7,8 +7,9 @@ import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet import eu.darken.capod.pods.core.apple.airpods.* import eu.darken.capod.pods.core.apple.beats.* +import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen3 +import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro import eu.darken.capod.pods.core.apple.misc.Twsi99999 -import eu.darken.capod.pods.core.apple.misc.VarunrAirPodsPro @InstallIn(SingletonComponent::class) @Module @@ -30,6 +31,6 @@ abstract class AppleFactoryModule { @Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory @Binds @IntoSet abstract fun fakesTwsi999999(factory: Twsi99999.Factory): ApplePodsFactory - @Binds @IntoSet - abstract fun fakesVarunrAirPodsPro(factory: VarunrAirPodsPro.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory } \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsGen3.kt b/app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsGen3.kt new file mode 100644 index 00000000..2ee3b7b6 --- /dev/null +++ b/app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsGen3.kt @@ -0,0 +1,75 @@ +package eu.darken.capod.pods.core.apple.misc + +import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods +import eu.darken.capod.pods.core.apple.DualAirPods +import eu.darken.capod.pods.core.apple.DualApplePodsFactory +import eu.darken.capod.pods.core.apple.protocol.ProximityPairing +import java.time.Instant +import javax.inject.Inject + +/** + * AirPods Gen3 clone + * Shorter data structure. + * https://discord.com/channels/548521543039189022/927235844127993866/1054404630118924308 + */ +data class FakeAirPodsGen3 constructor( + override val identifier: PodDevice.Id = PodDevice.Id(), + override val seenLastAt: Instant = Instant.now(), + override val seenFirstAt: Instant = Instant.now(), + override val seenCounter: Int = 1, + override val scanResult: BleScanResult, + override val proximityMessage: ProximityPairing.Message, + override val confidence: Float = PodDevice.BASE_CONFIDENCE, + private val rssiAverage: Int? = null, + private val cachedBatteryPercentage: Float? = null, + private val cachedCaseState: DualAirPods.LidState? = null +) : DualAirPods { + override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3 + + override val batteryCasePercent: Float? + get() = super.batteryCasePercent ?: cachedBatteryPercentage + + override val caseLidState: DualAirPods.LidState + get() = cachedCaseState ?: super.caseLidState + + override val rssi: Int + get() = rssiAverage ?: super.rssi + + class Factory @Inject constructor() : DualApplePodsFactory(TAG) { + + override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { + // Official message length is 19HEX, i.e. binary 25, did they copy this wrong? + getModelInfo().full == DEVICE_CODE && length == 19 + } + + override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods { + var basic = FakeAirPodsGen3(scanResult = scanResult, proximityMessage = message) + val result = searchHistory(basic) + + if (result != null) basic = basic.copy(identifier = result.id) + updateHistory(basic) + + if (result == null) return basic + + return basic.copy( + identifier = result.id, + seenFirstAt = result.seenFirstAt, + seenLastAt = scanResult.receivedAt, + seenCounter = result.seenCounter, + confidence = result.confidence, + cachedBatteryPercentage = result.getLatestCaseBattery(), + rssiAverage = result.averageRssi(basic.rssi), + cachedCaseState = result.getLatestCaseLidState(basic) + ) + } + + } + + companion object { + private val DEVICE_CODE = 0x1320.toUShort() + private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Gen3") + } +} \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/VarunrAirPodsPro.kt b/app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsPro.kt similarity index 92% rename from app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/VarunrAirPodsPro.kt rename to app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsPro.kt index 00a05e59..f28cece1 100644 --- a/app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/VarunrAirPodsPro.kt +++ b/app-common/src/main/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsPro.kt @@ -20,7 +20,7 @@ import javax.inject.Inject * AirPods Pro clone similar to Twsi999999. * Shorter data structure. */ -data class VarunrAirPodsPro constructor( +data class FakeAirPodsPro constructor( override val identifier: PodDevice.Id = PodDevice.Id(), override val seenLastAt: Instant = Instant.now(), override val seenFirstAt: Instant = Instant.now(), @@ -32,7 +32,7 @@ data class VarunrAirPodsPro constructor( private val cachedBatteryPercentage: Float? = null, ) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase { - override val model: PodDevice.Model = PodDevice.Model.VARUNR_AIRPODS_PRO + override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO override val rssi: Int get() = rssiAverage ?: super.rssi @@ -109,7 +109,7 @@ data class VarunrAirPodsPro constructor( override val isCaseCharging: Boolean get() = rawFlags.isBitSet(2) - class Factory @Inject constructor() : ApplePodsFactory(TAG) { + class Factory @Inject constructor() : ApplePodsFactory(TAG) { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { // Official message length is 19HEX, i.e. binary 25, did they copy this wrong? @@ -117,7 +117,7 @@ data class VarunrAirPodsPro constructor( } override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods { - var basic = VarunrAirPodsPro(scanResult = scanResult, proximityMessage = message) + var basic = FakeAirPodsPro(scanResult = scanResult, proximityMessage = message) val result = searchHistory(basic) if (result != null) basic = basic.copy(identifier = result.id) @@ -139,6 +139,6 @@ data class VarunrAirPodsPro constructor( companion object { private val DEVICE_CODE = 0x0E20.toUShort() - private val TAG = logTag("PodDevice", "Apple", "Varunr", "AirPodsPro") + private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Pro") } } \ No newline at end of file diff --git a/app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsGen3Test.kt b/app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsGen3Test.kt new file mode 100644 index 00000000..7994b48b --- /dev/null +++ b/app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsGen3Test.kt @@ -0,0 +1,34 @@ +package eu.darken.capod.pods.core.apple.misc + +import eu.darken.capod.pods.core.apple.BaseAirPodsTest +import eu.darken.capod.pods.core.apple.DualAirPods +import eu.darken.capod.pods.core.apple.HasAppleColor +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +class FakeAirPodsGen3Test : BaseAirPodsTest() { + + @Test + fun `charging in case`() = runTest { + create("07 13 01 13 20 75 AA 37 34 00 10 00 E4 E4 64 00 00 00 00 00 00") { + batteryLeftPodPercent shouldBe 1f + batteryRightPodPercent shouldBe 1f + + isCaseCharging shouldBe false + isLeftPodCharging shouldBe true + isRightPodCharging shouldBe true + + isLeftPodInEar shouldBe false + isRightPodInEar shouldBe false + batteryCasePercent shouldBe 0.7f + + caseLidState shouldBe DualAirPods.LidState.OPEN + + state shouldBe DualAirPods.ConnectionState.UNKNOWN + + podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name + + } + } +} \ No newline at end of file diff --git a/app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/VarunrAirPodsProTest.kt b/app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsProTest.kt similarity index 85% rename from app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/VarunrAirPodsProTest.kt rename to app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsProTest.kt index 52f857ec..4bc8837b 100644 --- a/app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/VarunrAirPodsProTest.kt +++ b/app-common/src/test/java/eu/darken/capod/pods/core/apple/misc/FakeAirPodsProTest.kt @@ -5,11 +5,11 @@ import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test -class VarunrAirPodsProTest : BaseAirPodsTest() { +class FakeAirPodsProTest : BaseAirPodsTest() { @Test fun `guessed data`() = runTest { - create("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") { + create("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") { rawPrefix shouldBe 0x01.toUByte() rawDeviceModel shouldBe 0x0E20.toUShort() rawStatus shouldBe 0x71.toUByte()