mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Add core device profile infrastructure
- Implement DeviceProfile interface with sealed interface pattern - Add AppleDeviceProfile for Apple devices with IRK support - Create DeviceProfilesRepo for profile persistence with Moshi - Add NameBasedPolyJsonAdapterFactory for polymorphic JSON serialization - Update PodDevice.Meta to include profile reference - Extend ApplePods with AppleMeta containing profile information - Add utility extensions for profile-based device filtering This establishes the foundation for device profile management system.
This commit is contained in:
@@ -1,2 +1,16 @@
|
||||
package eu.darken.capod.common.bluetooth
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
|
||||
suspend fun Collection<BleScanResult>.onlyNewAndUnique(): List<BleScanResult> = this
|
||||
.groupBy { it.address }
|
||||
.values
|
||||
.map { sameAdrDevs ->
|
||||
// For each address we only want the newest result, upstream may batch data
|
||||
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
||||
sameAdrDevs.minus(newest).let {
|
||||
if (it.isNotEmpty()) log( VERBOSE) { "Discarding stale results: $it" }
|
||||
}
|
||||
newest
|
||||
}
|
||||
+7
-1
@@ -1,11 +1,17 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package eu.darken.sdmse.common.serialization
|
||||
package eu.darken.capod.common.serialization
|
||||
|
||||
import com.squareup.moshi.*
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import javax.annotation.CheckReturnValue
|
||||
import kotlin.apply
|
||||
import kotlin.collections.isNotEmpty
|
||||
import kotlin.collections.plus
|
||||
import kotlin.collections.toTypedArray
|
||||
import kotlin.io.use
|
||||
import kotlin.jvm.javaClass
|
||||
|
||||
class NameBasedPolyJsonAdapterFactory<T> internal constructor(
|
||||
val baseType: Class<T>,
|
||||
|
||||
@@ -5,6 +5,7 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import eu.darken.capod.devices.core.DeviceProfile
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@@ -13,9 +14,10 @@ class SerializationModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun moshi(): Moshi = Moshi.Builder()
|
||||
.add(JavaInstantAdapter())
|
||||
.add(ByteArrayAdapter())
|
||||
.build()
|
||||
fun moshi(): Moshi = Moshi.Builder().apply {
|
||||
add(JavaInstantAdapter())
|
||||
add(ByteArrayAdapter())
|
||||
add(DeviceProfile.MOSHI_FACTORY)
|
||||
}.build()
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
package eu.darken.capod.devices.core
|
||||
|
||||
data class AppleDeviceProfile()
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
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 AppleDeviceProfile(
|
||||
@Json(name = "id") override val id: ProfileId = UUID.randomUUID().toString(),
|
||||
@Json(name = "name") override val name: String,
|
||||
@Json(name = "minimumSignalQuality") override val minimumSignalQuality: Float = 0.20f,
|
||||
@Json(name = "isEnabled") override val isEnabled: Boolean = true,
|
||||
@Json(name = "model") override val model: PodDevice.Model = PodDevice.Model.UNKNOWN,
|
||||
@Json(name = "priority") override val priority: Int = 0,
|
||||
@Json(name = "identityKey") val identityKey: IdentityResolvingKey? = null,
|
||||
@Json(name = "encryptionKey") val encryptionKey: ProximityEncryptionKey? = null,
|
||||
) : DeviceProfile
|
||||
@@ -1,23 +1,21 @@
|
||||
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.common.serialization.NameBasedPolyJsonAdapterFactory
|
||||
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
|
||||
import kotlin.jvm.java
|
||||
|
||||
@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
|
||||
sealed interface DeviceProfile : Parcelable {
|
||||
val id: ProfileId
|
||||
val name: String
|
||||
val minimumSignalQuality: Float
|
||||
val isEnabled: Boolean
|
||||
val model: PodDevice.Model
|
||||
val priority: Int
|
||||
|
||||
companion object {
|
||||
val MOSHI_FACTORY: NameBasedPolyJsonAdapterFactory<DeviceProfile> =
|
||||
NameBasedPolyJsonAdapterFactory.of(DeviceProfile::class.java)
|
||||
.withSubtype(AppleDeviceProfile::class.java, "identityKey")
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
package eu.darken.capod.devices.core
|
||||
|
||||
typealias ProfileId = String
|
||||
@@ -15,7 +15,7 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DeviceProfilesRepo @Inject constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val moshi: Moshi,
|
||||
) {
|
||||
|
||||
@@ -77,10 +77,6 @@ class DeviceProfilesRepo @Inject constructor(
|
||||
saveProfiles()
|
||||
}
|
||||
|
||||
fun getProfile(profileId: String): DeviceProfile? {
|
||||
return _profiles.value.find { it.id == profileId }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_PROFILES = "profiles"
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ class DeviceProfileVH(parent: ViewGroup) :
|
||||
|
||||
deviceName.text = profile.name
|
||||
deviceDetails.text = buildString {
|
||||
profile.address?.let {
|
||||
append(it.toString())
|
||||
append(" • ")
|
||||
}
|
||||
append(profile.model.name)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlin.collections.firstOrNull
|
||||
|
||||
fun PodMonitor.devicesWithProfiles(): Flow<List<PodDevice>> = devices
|
||||
.map { devices -> devices.filter { it.profile != null } }
|
||||
.map { devices -> devices.filter { it.meta.profile != null } }
|
||||
|
||||
fun PodMonitor.primaryDevice(): Flow<PodDevice?> = devicesWithProfiles().map { it.firstOrNull() }
|
||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.devices.core.DeviceProfile
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
@@ -58,6 +59,12 @@ interface PodDevice {
|
||||
"${entry.key}: ${entry.value.joinToString(separator = " ") { String.format("%02X", it) }}"
|
||||
}
|
||||
|
||||
interface Meta {
|
||||
val profile: DeviceProfile?
|
||||
}
|
||||
|
||||
val meta: Meta
|
||||
|
||||
fun getLabel(context: Context): String = model.label
|
||||
|
||||
@get:DrawableRes
|
||||
|
||||
@@ -2,13 +2,13 @@ package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.devices.core.AppleDeviceProfile
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
|
||||
interface ApplePods : PodDevice {
|
||||
|
||||
val payload: ProximityPayload
|
||||
val flags: Flags
|
||||
|
||||
// We start counting at the airpods prefix byte
|
||||
val pubPrefix: UByte
|
||||
@@ -56,7 +56,10 @@ interface ApplePods : PodDevice {
|
||||
)
|
||||
}
|
||||
|
||||
data class Flags(
|
||||
data class AppleMeta(
|
||||
val isIRKMatch: Boolean,
|
||||
)
|
||||
override val profile: AppleDeviceProfile?,
|
||||
) : PodDevice.Meta
|
||||
|
||||
override val meta: AppleMeta
|
||||
}
|
||||
Reference in New Issue
Block a user