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 94c69e99..18a4fecd 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 @@ -83,6 +83,10 @@ interface PodDevice { "AirPods (Gen 3)", R.drawable.devic_airpods_gen2_both, ), + @Json(name = "airpods.gen4") AIRPODS_GEN4( + "AirPods (Gen 4)", + R.drawable.devic_airpods_gen2_both, + ), @Json(name = "airpods.pro") AIRPODS_PRO( "AirPods Pro", R.drawable.devic_airpods_pro2_both 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 bd42cfe4..83ecec0e 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 @@ -5,9 +5,27 @@ import dagger.Module import dagger.hilt.InstallIn 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.* +import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1 +import eu.darken.capod.pods.core.apple.airpods.AirPodsGen2 +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.AirPodsMax +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 +import eu.darken.capod.pods.core.apple.beats.BeatsFitPro +import eu.darken.capod.pods.core.apple.beats.BeatsFlex +import eu.darken.capod.pods.core.apple.beats.BeatsSolo3 +import eu.darken.capod.pods.core.apple.beats.BeatsStudio3 +import eu.darken.capod.pods.core.apple.beats.BeatsX +import eu.darken.capod.pods.core.apple.beats.PowerBeats3 +import eu.darken.capod.pods.core.apple.beats.PowerBeats4 +import eu.darken.capod.pods.core.apple.beats.PowerBeatsPro +import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen1 +import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen2 +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.FakeAirPodsPro2 @InstallIn(SingletonComponent::class) @Module @@ -16,6 +34,7 @@ abstract class AppleFactoryModule { @Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory @Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory @Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun airPodsGen4(factory: AirPodsGen4.Factory): ApplePodsFactory @Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory @Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen4.kt b/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen4.kt new file mode 100644 index 00000000..f793c673 --- /dev/null +++ b/app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen4.kt @@ -0,0 +1,70 @@ +package eu.darken.capod.pods.core.apple.airpods + +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.DualApplePods +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 + +data class AirPodsGen4( + 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 reliability: Float = PodDevice.BASE_CONFIDENCE, + private val rssiAverage: Int? = null, + private val cachedBatteryPercentage: Float? = null, + private val cachedCaseState: DualApplePods.LidState? = null +) : DualApplePods, HasStateDetectionAirPods { + + override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN4 + + override val batteryCasePercent: Float? + get() = super.batteryCasePercent ?: cachedBatteryPercentage + + override val caseLidState: DualApplePods.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 { + getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH + } + + override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods { + var basic = AirPodsGen4(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, + reliability = result.reliability, + cachedBatteryPercentage = result.getLatestCaseBattery(), + rssiAverage = result.rssiSmoothed(basic.rssi), + cachedCaseState = result.getLatestCaseLidState(basic) + ) + } + + } + + companion object { + private val DEVICE_CODE = 0x1920.toUShort() + private val TAG = logTag("PodDevice", "Apple", "AirPods", "Gen4") + } +} \ No newline at end of file diff --git a/app-common/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen4Test.kt b/app-common/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen4Test.kt new file mode 100644 index 00000000..f12cf37e --- /dev/null +++ b/app-common/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen4Test.kt @@ -0,0 +1,46 @@ +package eu.darken.capod.pods.core.apple.airpods + +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.BaseAirPodsTest +import eu.darken.capod.pods.core.apple.DualApplePods +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 AirPodsGen4Test : BaseAirPodsTest() { + + @Test + fun `AirPods Gen4 via log from #225`() = runTest { + create("07 19 01 19 20 2B 33 8F 11 00 04 59 D4 57 20 0F 1C 13 38 B2 00 74 E9 DD 70 D7 A5") { + rawPrefix shouldBe 0x01.toUByte() + rawDeviceModel shouldBe 0x1920.toUShort() + rawStatus shouldBe 0x2B.toUByte() + rawPodsBattery shouldBe 0x33.toUByte() + rawFlags shouldBe 0x8.toUShort() + rawCaseBattery shouldBe 0xF.toUShort() + rawCaseLidState shouldBe 0x11.toUByte() + rawDeviceColor shouldBe 0x00.toUByte() + rawSuffix shouldBe 0x04.toUByte() + + batteryLeftPodPercent shouldBe 0.3f + batteryRightPodPercent shouldBe 0.3f + + isCaseCharging shouldBe false + isLeftPodCharging shouldBe false + isRightPodCharging shouldBe false + + isLeftPodInEar shouldBe true + isRightPodInEar shouldBe true + batteryCasePercent shouldBe null + + caseLidState shouldBe DualApplePods.LidState.UNKNOWN + + state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE + + podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name + + model shouldBe PodDevice.Model.AIRPODS_GEN4 + } + } +} \ No newline at end of file