Remove deviceModel property from ApplePodsFactory interface

Removed the deviceModel property from ApplePodsFactory interface and all 25 implementing classes across AirPods, Beats, and misc device factories. This simplifies the factory pattern as the model information can be derived from the concrete implementation type itself.
This commit is contained in:
darken
2025-09-29 13:37:01 +02:00
parent 2cd209ee03
commit 89632d864a
31 changed files with 36 additions and 168 deletions
@@ -1,23 +0,0 @@
package eu.darken.capod.devices.core
import android.os.Parcelable
import com.squareup.moshi.JsonClass
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.protocol.IdentityResolvingKey
import eu.darken.capod.pods.core.apple.protocol.ProximityEncryptionKey
import kotlinx.parcelize.Parcelize
import java.util.UUID
@Parcelize
@JsonClass(generateAdapter = true)
data class DeviceProfile(
val id: String = UUID.randomUUID().toString(),
val name: String,
val address: BluetoothAddress? = null,
val model: PodDevice.Model = PodDevice.Model.UNKNOWN,
val identityKey: IdentityResolvingKey? = null,
val encryptionKey: ProximityEncryptionKey? = null,
val minimumSignalQuality: Float = 0.20f,
val isEnabled: Boolean = true
) : Parcelable
@@ -1,87 +0,0 @@
package eu.darken.capod.devices.core
import android.content.Context
import android.content.SharedPreferences
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DeviceProfilesRepo @Inject constructor(
@ApplicationContext private val context: Context,
private val moshi: Moshi,
) {
private val preferences: SharedPreferences = context.getSharedPreferences("device_profiles", Context.MODE_PRIVATE)
private val _profiles = MutableStateFlow<List<DeviceProfile>>(emptyList())
val profiles: Flow<List<DeviceProfile>> = _profiles.asStateFlow()
private val listType = Types.newParameterizedType(List::class.java, DeviceProfile::class.java)
private val adapter = moshi.adapter<List<DeviceProfile>>(listType)
init {
loadProfiles()
}
private fun loadProfiles() {
val json = preferences.getString(KEY_PROFILES, null)
val loadedProfiles = if (json != null) {
try {
adapter.fromJson(json) ?: emptyList()
} catch (e: Exception) {
log(VERBOSE) { "Failed to load device profiles: $e" }
emptyList()
}
} else {
emptyList()
}
_profiles.value = loadedProfiles
log(VERBOSE) { "Loaded ${loadedProfiles.size} device profiles" }
}
private fun saveProfiles() {
val json = adapter.toJson(_profiles.value)
preferences.edit().putString(KEY_PROFILES, json).apply()
log(VERBOSE) { "Saved ${_profiles.value.size} device profiles" }
}
fun addProfile(profile: DeviceProfile) {
val updatedProfiles = _profiles.value.toMutableList()
updatedProfiles.add(profile)
_profiles.value = updatedProfiles
saveProfiles()
}
fun updateProfile(profile: DeviceProfile) {
val updatedProfiles = _profiles.value.toMutableList()
val index = updatedProfiles.indexOfFirst { it.id == profile.id }
if (index != -1) {
updatedProfiles[index] = profile
_profiles.value = updatedProfiles
saveProfiles()
}
}
fun removeProfile(profileId: String) {
val updatedProfiles = _profiles.value.toMutableList()
updatedProfiles.removeAll { it.id == profileId }
_profiles.value = updatedProfiles
saveProfiles()
}
fun getProfile(profileId: String): DeviceProfile? {
return _profiles.value.find { it.id == profileId }
}
companion object {
private const val KEY_PROFILES = "profiles"
}
}
@@ -61,49 +61,53 @@ class AppleFactory @Inject constructor(
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
val proximityMessage = getMessage(scanResult) ?: return@withLock null
var isIRKMatch = false
val candidates = profilesRepo.profiles.first().filterIsInstance<AppleDeviceProfile>()
var profile = candidates.firstOrNull {
it.identityKey != null && rpaChecker.verify(scanResult.address, it.identityKey)
}
if (profile != null) {
isIRKMatch = true
log(TAG, VERBOSE) { "Got IRK match for from $profile for $scanResult" }
}
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) } ?: unknownAppleFactory
// TODO more checks heuristicbased
if (profile == null) {
profile = candidates.firstOrNull { it.model == factory.deviceModel }
log(TAG, VERBOSE) { "Got no IRK match for from $profile for $scanResult" }
val profiles = profilesRepo.profiles.first().filterIsInstance<AppleDeviceProfile>()
var profile = profiles.firstOrNull {
it.identityKey != null && rpaChecker.verify(scanResult.address, it.identityKey)
}
val payload = ProximityPayload(
val isIrkMatch = profile != null
if (isIrkMatch) log(TAG, VERBOSE) { "IRK match for $scanResult -> $profile" }
var payload = ProximityPayload(
public = ProximityPayload.Public(
proximityMessage.data.take(9).toUByteArray()
),
private = run {
if (!isIRKMatch || profile == null) return@run null
if (proximityMessage.data.size != ProximityPairing.PAIRING_MESSAGE_LENGTH) return@run null
val encKey = profile.encryptionKey?.takeIf { it.isNotEmpty() }
if (encKey == null) return@run null
val encrypted = proximityMessage.data.takeLast(16).toUByteArray().toByteArray()
proximityMessageDecrypter.decrypt(encrypted, encKey)?.let {
ProximityPayload.Private(data = it)
}
},
private = null,
)
if (profile != null && profile.encryptionKey != null) {
payload = payload.copy(
private = run {
if (proximityMessage.data.size != ProximityPairing.PAIRING_MESSAGE_LENGTH) return@run null
val encKey = profile.encryptionKey
val encrypted = proximityMessage.data.takeLast(16).toUByteArray().toByteArray()
proximityMessageDecrypter.decrypt(encrypted, encKey)?.let {
ProximityPayload.Private(data = it)
}
},
)
}
if (profile == null) {
val tempDevice = factory.create(
scanResult = scanResult,
payload = payload,
meta = ApplePods.AppleMeta(),
)
profile = profiles
.filter { it.model == tempDevice.model }
.filter { it.minimumSignalQuality <= tempDevice.signalQuality }
.firstOrNull()
}
factory.create(
scanResult = scanResult,
payload = payload,
meta = ApplePods.AppleMeta(
isIRKMatch = isIRKMatch,
isIRKMatch = profile != null,
profile = profile,
),
)
@@ -57,8 +57,8 @@ interface ApplePods : PodDevice {
}
data class AppleMeta(
val isIRKMatch: Boolean,
override val profile: AppleDeviceProfile?,
val isIRKMatch: Boolean = false,
override val profile: AppleDeviceProfile? = null,
) : PodDevice.Meta
override val meta: AppleMeta
@@ -7,7 +7,6 @@ import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
interface ApplePodsFactory {
val deviceModel: PodDevice.Model
fun isResponsible(message: ProximityMessage): Boolean
fun create(scanResult: BleScanResult, payload: ProximityPayload, meta: ApplePods.AppleMeta): ApplePods
@@ -42,7 +42,6 @@ data class AirPodsGen1(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -42,7 +42,6 @@ data class AirPodsGen2(
private val repo: PodHistoryRepo
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -42,7 +42,6 @@ data class AirPodsGen3(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -42,7 +42,6 @@ data class AirPodsGen4(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_GEN4
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -41,7 +41,6 @@ data class AirPodsGen4Anc(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_GEN4_ANC
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -48,7 +48,6 @@ data class AirPodsMax(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -48,7 +48,6 @@ data class AirPodsMaxUsbc(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_MAX_USBC
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -60,7 +60,6 @@ data class AirPodsPro(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -60,7 +60,6 @@ data class AirPodsPro2(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -60,7 +60,6 @@ data class AirPodsPro2Usbc(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2_USBC
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -41,7 +41,6 @@ data class BeatsFitPro(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.BEATS_FIT_PRO
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -33,7 +33,6 @@ data class BeatsFlex(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.BEATS_FLEX
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -32,7 +32,6 @@ data class BeatsSolo3(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -30,7 +30,6 @@ data class BeatsStudio3(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -33,7 +33,6 @@ data class BeatsX(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.BEATS_X
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -33,7 +33,6 @@ data class PowerBeats3(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.POWERBEATS_3
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -34,7 +34,6 @@ data class PowerBeats4(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.POWERBEATS_4
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -41,7 +41,6 @@ data class PowerBeatsPro(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -41,7 +41,6 @@ data class PowerBeatsPro2(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO2
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
}
@@ -44,7 +44,6 @@ data class FakeAirPodsGen1(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
@@ -42,7 +42,6 @@ data class FakeAirPodsGen2(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN2
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
@@ -44,7 +44,6 @@ data class FakeAirPodsGen3(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
@@ -62,7 +62,6 @@ data class FakeAirPodsPro(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
@@ -66,7 +66,6 @@ data class FakeAirPodsPro2(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
@@ -35,7 +35,6 @@ data class UnknownAppleDevice(
class Factory @Inject constructor(
private val repo: PodHistoryRepo,
) : ApplePodsFactory {
override val deviceModel: PodDevice.Model = PodDevice.Model.UNKNOWN
override fun isResponsible(message: ProximityMessage): Boolean = true
override fun create(
@@ -15,7 +15,7 @@ data class AppleDeviceProfile(
@Json(name = "label") override val label: String,
@Json(name = "priority") override val priority: Int = 0,
@Json(name = "model") override val model: PodDevice.Model = PodDevice.Model.UNKNOWN,
@Json(name = "minimumSignalQuality") override val minimumSignalQuality: Float? = DeviceProfile.DEFAULT_MINIMUM_SIGNAL_QUALITY,
@Json(name = "minimumSignalQuality") override val minimumSignalQuality: Float = DeviceProfile.DEFAULT_MINIMUM_SIGNAL_QUALITY,
@Json(name = "identityKey") val identityKey: IdentityResolvingKey? = null,
@Json(name = "encryptionKey") val encryptionKey: ProximityEncryptionKey? = null,
@Json(name = "address") override val address: String? = null,