mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Attempt casual MAC based re-recognition and assign a semi persistent identifier.
This commit is contained in:
@@ -2,13 +2,31 @@ package eu.darken.capod.pods.core
|
||||
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
interface PodDevice {
|
||||
|
||||
val identifier: UUID
|
||||
|
||||
val scanResult: ScanResult
|
||||
|
||||
val lastSeenAt: Instant
|
||||
|
||||
val rssi: Int
|
||||
get() = scanResult.rssi
|
||||
|
||||
fun getSignalQuality(context: Context): String {
|
||||
val percentage = (100 - abs(rssi))
|
||||
return "~$percentage%"
|
||||
}
|
||||
|
||||
fun getLabel(context: Context): String
|
||||
|
||||
@get:DrawableRes
|
||||
val iconRes: Int
|
||||
get() = R.drawable.ic_baseline_earbuds_24
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package eu.darken.capod.pods.core.airpods
|
||||
|
||||
import android.bluetooth.le.ScanResult
|
||||
import dagger.Reusable
|
||||
import android.os.SystemClock
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
@@ -15,28 +14,42 @@ import eu.darken.capod.pods.core.airpods.models.AirPodsPro
|
||||
import eu.darken.capod.pods.core.airpods.models.UnknownAppleDevice
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Reusable
|
||||
@Singleton
|
||||
class AirPodsFactory @Inject constructor(
|
||||
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
||||
private val proximityPairingDecoder: ProximityPairing.Decoder
|
||||
) {
|
||||
|
||||
data class Message(
|
||||
// 0x07 ; AirPods message1 byte
|
||||
// 25; Length 1byte
|
||||
// 0x01
|
||||
val type: UByte,
|
||||
val deviceModel: UShort,
|
||||
val status: UByte,
|
||||
val batteryIndicator: UShort,
|
||||
val lidCount: UByte,
|
||||
val deviceColor: UByte,
|
||||
val encryptedData: UShort,
|
||||
)
|
||||
data class KnownDevice(
|
||||
val identifier: UUID,
|
||||
val scanResult: ScanResult
|
||||
) {
|
||||
val address: String
|
||||
get() = scanResult.device.address
|
||||
|
||||
suspend fun create(scanResult: ScanResult): PodDevice? {
|
||||
val rssi: Int
|
||||
get() = scanResult.rssi
|
||||
|
||||
val timestampNanos: Duration
|
||||
get() = Duration.ofNanos(scanResult.timestampNanos)
|
||||
|
||||
fun isOlderThan(age: Duration): Boolean {
|
||||
val now = Duration.ofNanos(SystemClock.elapsedRealtimeNanos())
|
||||
return now - timestampNanos > age
|
||||
}
|
||||
}
|
||||
|
||||
private val knownDevs = mutableMapOf<UUID, KnownDevice>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
private suspend fun getMessage(scanResult: ScanResult): ProximityPairing.Message? {
|
||||
val messages = try {
|
||||
continuityProtocolDecoder.decode(scanResult)
|
||||
} catch (e: Exception) {
|
||||
@@ -58,6 +71,58 @@ class AirPodsFactory @Inject constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
return proximityMessage
|
||||
}
|
||||
|
||||
private suspend fun recognizeDevice(scanResult: ScanResult): UUID {
|
||||
val address = scanResult.device.address
|
||||
|
||||
var identifier: UUID? = null
|
||||
|
||||
knownDevs.values.firstOrNull { it.address == address }?.let {
|
||||
log(TAG, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" }
|
||||
knownDevs[it.identifier] = it.copy(scanResult = scanResult)
|
||||
identifier = it.identifier
|
||||
}
|
||||
|
||||
if (identifier == null) {
|
||||
knownDevs.values
|
||||
.firstOrNull {
|
||||
it.rssi > -60 && !it.isOlderThan(Duration.ofSeconds(10))
|
||||
}
|
||||
?.let {
|
||||
log(TAG, VERBOSE) { "recognizeDevice: Close match based on RSSI and timestamp." }
|
||||
knownDevs[it.identifier] = it.copy(scanResult = scanResult)
|
||||
identifier = it.identifier
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (identifier == null) {
|
||||
log(TAG, VERBOSE) { "recognizeDevice: Mapping as new device" }
|
||||
identifier = UUID.randomUUID()
|
||||
}
|
||||
|
||||
knownDevs[identifier!!] = KnownDevice(
|
||||
identifier = identifier!!,
|
||||
scanResult = scanResult
|
||||
)
|
||||
|
||||
knownDevs.values.toList().forEach { knownDevice ->
|
||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
||||
log(TAG, VERBOSE) { "recognizeDevice: Removing stale known device: $knownDevice" }
|
||||
knownDevs.remove(knownDevice.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
return identifier!!
|
||||
}
|
||||
|
||||
suspend fun create(scanResult: ScanResult): PodDevice? = mutex.withLock {
|
||||
val pm = getMessage(scanResult) ?: return@withLock null
|
||||
|
||||
val identifier = recognizeDevice(scanResult)
|
||||
|
||||
log(TAG, INFO) {
|
||||
val data = scanResult.scanRecord!!.getManufacturerSpecificData(
|
||||
ContinuityProtocol.APPLE_COMPANY_IDENTIFIER
|
||||
@@ -69,14 +134,26 @@ class AirPodsFactory @Inject constructor(
|
||||
}
|
||||
|
||||
val dm = (
|
||||
((proximityMessage.data[1].toInt() and 255) shl 8) or (proximityMessage.data[2].toInt() and 255)
|
||||
((pm.data[1].toInt() and 255) shl 8) or (pm.data[2].toInt() and 255)
|
||||
).toUShort()
|
||||
val dmDirty = proximityMessage.data[1]
|
||||
val dmDirty = pm.data[1]
|
||||
|
||||
return when {
|
||||
dm == 0x0220.toUShort() || dmDirty == 2.toUByte() -> AirPodsGen1(scanResult, proximityMessage)
|
||||
dm == 0x0F20.toUShort() || dmDirty == 15.toUByte() -> AirPodsGen2(scanResult, proximityMessage)
|
||||
dm == 0x0e20.toUShort() || dmDirty == 14.toUByte() -> AirPodsPro(scanResult, proximityMessage)
|
||||
dm == 0x0220.toUShort() || dmDirty == 2.toUByte() -> AirPodsGen1(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dm == 0x0F20.toUShort() || dmDirty == 15.toUByte() -> AirPodsGen2(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
dm == 0x0e20.toUShort() || dmDirty == 14.toUByte() -> AirPodsPro(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
// dmDirty == 10.toUByte() -> {
|
||||
// TODO("Airpods Max")
|
||||
// }
|
||||
@@ -100,8 +177,12 @@ class AirPodsFactory @Inject constructor(
|
||||
// }
|
||||
else -> {
|
||||
log(TAG, WARN) { "Unknown proximity message type" }
|
||||
Bugs.report(IllegalArgumentException("Unknown ProximityMessage: $proximityMessage"))
|
||||
UnknownAppleDevice(scanResult, proximityMessage)
|
||||
Bugs.report(IllegalArgumentException("Unknown ProximityMessage: $pm"))
|
||||
UnknownAppleDevice(
|
||||
identifier = identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = pm
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user