Attempt casual MAC based re-recognition and assign a semi persistent identifier.

This commit is contained in:
darken
2022-01-03 18:09:22 +01:00
parent 2d97a7300c
commit 768ef95a75
2 changed files with 123 additions and 24 deletions
@@ -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
)
}
}
}