Compare commits

...
Author SHA1 Message Date
darken da5af8e4b1 Release: 2.19.0-rc1 2025-09-21 19:22:38 +02:00
Matthias Urhahn 6c848eaa55 Merge pull request #328 from d4rken-org/airpods-pro-3-merged
AirPods Pro 3 support
2025-09-21 19:13:57 +02:00
darken 15054d836d Merge branch 'airpods-pro-3' into airpods-pro-3-merged
# Conflicts:
#	app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt
#	app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro3.kt
2025-09-21 18:38:13 +02:00
darken 0ac9bba482 Merge branch 'airpods-pro-3' into airpods-pro-3-merged
# Conflicts:
#	app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt
#	app-common/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro3.kt
2025-09-21 18:33:10 +02:00
Tharen Abela 247bc13e0a Initial support for AirPods Pro 3
This commit introduces the AirPods Pro 3 model to the application, including its representation in the README, the PodDevice interface, and the AppleFactoryModule.

A new class for AirPods Pro 3 is created, along with corresponding unit tests to validate its functionality.

Based on the AirPods Pro 2 USBC.
2025-09-21 17:56:03 +02:00
li-fstz 33126be817 Add support for AirPods Pro 3 2025-09-21 17:55:56 +02:00
darken b02b31043b Release: 2.18.0-rc1 2025-09-03 19:41:29 +02:00
darken 6786b5aa8a Remove Norwegian (Bokmål) from supported languages. 2025-09-03 19:41:12 +02:00
10 changed files with 216 additions and 3 deletions
+1
View File
@@ -32,6 +32,7 @@ Currently supported models:
* AirPods Pro 1. Generation
* AirPods Pro 2. Generation
* AirPods Pro 2. Generation (USB-C)
* AirPods Pro 3. Generation
* AirPods Max
* Power Beats Pro
* Power Beats 3
+1 -1
View File
@@ -1 +1 @@
2.18.0-rc0 21800000
2.19.0-rc1 21900010
@@ -104,6 +104,10 @@ interface PodDevice {
"AirPods Pro 2 USB-C",
R.drawable.devic_airpods_pro2_both
),
@Json(name = "airpods.pro3") AIRPODS_PRO3(
"AirPods Pro 3",
R.drawable.devic_airpods_pro2_both
),
@Json(name = "airpods.max") AIRPODS_MAX(
"AirPods Max",
R.drawable.devic_headphones_generic
@@ -15,6 +15,7 @@ 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
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro3
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
@@ -43,6 +44,7 @@ abstract class AppleFactoryModule {
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsPro2Usbc(factory: AirPodsPro2Usbc.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsPro3(factory: AirPodsPro3.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun airPodsMax2(factory: AirPodsMaxUsbc.Factory): ApplePodsFactory
@@ -0,0 +1,101 @@
package eu.darken.capod.pods.core.apple.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.common.R
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.ApplePodsFactory
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject
data class AirPodsPro3(
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 payload: ProximityPayload,
override val flags: ApplePods.Flags,
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null
) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO3
@get:DrawableRes
override val iconRes: Int
get() = R.drawable.devic_airpods_pro2_both
@get:DrawableRes
override val caseIcon: Int
get() = R.drawable.devic_airpods_pro2_case
@get:DrawableRes
override val leftPodIcon: Int
get() = R.drawable.devic_airpods_pro2_left
@get:DrawableRes
override val rightPodIcon: Int
get() = R.drawable.devic_airpods_pro2_right
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: LidState
get() = cachedCaseState ?: super.caseLidState
override val rssi: Int
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
override fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
flags: ApplePods.Flags
): ApplePods {
var basic = AirPodsPro3(scanResult = scanResult, payload = payload, flags = flags)
val result = repo.search(basic)
if (result != null) basic = basic.copy(identifier = result.id)
repo.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 {
// Device code for AirPods Pro 3
// Following the pattern of other AirPods models (ends with 0x20 for Apple vendor)
private val DEVICE_CODE = 0x2720.toUShort()
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro3")
}
}
@@ -0,0 +1,94 @@
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.HasAppleColor
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
class AirPodsPro3Test : BaseAirPodsTest() {
/**
* Test case for AirPods Pro 3 - placeholder with correct device code
* Following the pattern of other AirPods models (0x2720)
*/
@Test
fun `AirPods Pro 3 - placeholder test`() = runTest {
// This test uses a placeholder hex string with the correct device code 0x2720
// The actual proximity pairing data will need to be captured from real AirPods Pro 3
create<AirPodsPro3>("07 19 01 27 20 0B 99 8F 11 00 04 BD A7 3B FF 2D 8A 3C AF 9B 1A 7C 74 B7 A9 D1 C3") {
pubPrefix shouldBe 0x01.toUByte()
pubDeviceModel shouldBe 0x2720.toUShort()
pubStatus shouldBe 0x0B.toUByte()
pubPodsBattery shouldBe 0x99.toUByte()
pubFlags shouldBe 0x8.toUShort()
pubCaseBattery shouldBe 0xF.toUShort()
pubCaseLidState shouldBe 0x11.toUByte()
pubDeviceColor shouldBe 0x0.toUByte()
pubSuffix shouldBe 0x04.toUByte()
isLeftPodMicrophone shouldBe false
isRightPodMicrophone shouldBe true
isLeftPodInEar shouldBe true
isRightPodInEar shouldBe true
batteryLeftPodPercent shouldBe 0.9f
batteryRightPodPercent shouldBe 0.9f
isCaseCharging shouldBe false
isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
model shouldBe PodDevice.Model.AIRPODS_PRO3
}
}
@Test
fun `AirPods Pro 3 - placeholder test - in case`() = runTest {
// This test uses a placeholder hex string with the correct device code 0x2720
// The actual proximity pairing data will need to be captured from real AirPods Pro 3
create<AirPodsPro3>("07 19 01 27 20 53 AA 98 32 00 05 49 0A B8 BF 8E 29 D8 70 12 0D A7 0C CE 77 56 00") {
isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false
isLeftPodInEar shouldBe true
isRightPodInEar shouldBe false
batteryLeftPodPercent shouldBe 1f
batteryRightPodPercent shouldBe 1f
isCaseCharging shouldBe false
isRightPodCharging shouldBe true
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe 0.8f
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
model shouldBe PodDevice.Model.AIRPODS_PRO3
}
}
@Test
fun `AirPods Pro 3`() = runTest {
create<AirPodsPro3>("07 19 01 27 20 0B 99 8F 11 00 05 43 A8 85 17 07 FF 41 62 2C FE 5E 20 08 1A 52 40") {
isLeftPodInEar shouldBe true
isRightPodInEar shouldBe true
isCaseCharging shouldBe false
isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
model shouldBe PodDevice.Model.AIRPODS_PRO3
}
}
}
@@ -0,0 +1,5 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
@@ -0,0 +1,5 @@
🐛 Bug fixes, 🚀 performance boosts, maybe even ✨ new features.
Changelog: https://capod.darken.eu/changelog
FYI: Its just me here — thanks for understanding if replies take a bit. ¯\_(ツ)_/¯
+1
View File
@@ -12,5 +12,6 @@ rm -rv ./metadata/android/kmr-TR
rm -rv ./metadata/android/ur-IN
rm -rv ./metadata/android/zu
rm -rv ./metadata/android/si-LK
rm -rv ./metadata/android/nb
find ./metadata/android -empty -type d -delete
exit 0
+2 -2
View File
@@ -1,6 +1,6 @@
### Updated by release.sh ###
project.versioning.major=2
project.versioning.minor=18
project.versioning.minor=19
project.versioning.patch=0
project.versioning.build=0
project.versioning.build=1
#############################