mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Show unknown devices of all kinds.
This commit is contained in:
@@ -32,9 +32,9 @@ class OverviewAdapter @Inject constructor() :
|
||||
modules.add(TypedVHCreatorMod({ data[it] is DualApplePodsCardVH.Item }) { DualApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is SingleApplePodsCardVH.Item }) { SingleApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is BasicSingleApplePodsCardVH.Item }) { BasicSingleApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is MissingMainDeviceVH.Item }) { MissingMainDeviceVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is BluetoothDisabledVH.Item }) { BluetoothDisabledVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) })
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
+7
-2
@@ -6,6 +6,7 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
import java.time.Instant
|
||||
|
||||
@@ -28,10 +29,14 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
|
||||
}
|
||||
|
||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||
|
||||
reception.text = item.getReceptionText()
|
||||
|
||||
rawdata.text = device.rawDataHex
|
||||
details.text = when (item.device) {
|
||||
is ApplePods -> getString(R.string.pods_unknown_contact_dev)
|
||||
else -> getString(R.string.pods_unknown_label)
|
||||
}
|
||||
|
||||
rawdata.text = device.rawDataHex.joinToString("\n")
|
||||
}
|
||||
|
||||
data class Item(
|
||||
|
||||
@@ -40,10 +40,13 @@ interface PodDevice {
|
||||
val signalQuality: Float
|
||||
get() = ((100 - abs(rssi)) / 100f) * (max(BASE_CONFIDENCE, confidence))
|
||||
|
||||
val rawData: ByteArray
|
||||
val rawData: Map<Int, ByteArray>
|
||||
get() = scanResult.manufacturerSpecificData
|
||||
|
||||
val rawDataHex: String
|
||||
get() = rawData.joinToString(separator = " ") { String.format("%02X", it) }
|
||||
val rawDataHex: List<String>
|
||||
get() = rawData.entries.map { entry ->
|
||||
"${entry.key}: ${entry.value.joinToString(separator = " ") { String.format("%02X", it) }}"
|
||||
}
|
||||
|
||||
fun getLabel(context: Context): String = model.label
|
||||
|
||||
|
||||
@@ -2,32 +2,34 @@ package eu.darken.capod.pods.core
|
||||
|
||||
import dagger.Reusable
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.AppleFactory
|
||||
import eu.darken.capod.pods.core.unknown.UnknownDeviceFactory
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class PodFactory @Inject constructor(
|
||||
private val appleFactory: AppleFactory
|
||||
private val appleFactory: AppleFactory,
|
||||
private val unknownFactory: UnknownDeviceFactory,
|
||||
) {
|
||||
|
||||
suspend fun createPod(scanResult: BleScanResult): PodDevice? {
|
||||
log(TAG, VERBOSE) { "Trying to create Pod for $scanResult" }
|
||||
|
||||
val pod = appleFactory.create(scanResult)
|
||||
if (pod != null) {
|
||||
log(TAG) { "Pod created: $pod" }
|
||||
return pod
|
||||
} else {
|
||||
log(TAG, WARN) { "Not an AirPod : $scanResult" }
|
||||
log(TAG, INFO) { "Decoding $scanResult" }
|
||||
|
||||
var device = appleFactory.create(scanResult)
|
||||
|
||||
if (device == null) {
|
||||
log(TAG, VERBOSE) { "Using fallback factory" }
|
||||
device = unknownFactory.create(scanResult)
|
||||
}
|
||||
|
||||
|
||||
log(TAG, WARN) { "Failed to find matching PodFactory for $scanResult" }
|
||||
return null
|
||||
log(TAG, INFO) { "Pod created: $device" }
|
||||
return device
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -2,18 +2,15 @@ package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.Bugs
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.airpods.*
|
||||
import eu.darken.capod.pods.core.apple.beats.*
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -55,8 +52,6 @@ class AppleFactory @Inject constructor(
|
||||
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
|
||||
val pm = getMessage(scanResult) ?: return@withLock null
|
||||
|
||||
log(TAG, INFO) { "Decoding $scanResult" }
|
||||
|
||||
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
||||
|
||||
val device = (factory ?: unknownAppleFactory).create(
|
||||
|
||||
@@ -4,16 +4,12 @@ import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.toHex
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
|
||||
interface ApplePods : PodDevice {
|
||||
|
||||
val proximityMessage: ProximityPairing.Message
|
||||
|
||||
override val rawData: ByteArray
|
||||
get() = scanResult.getManufacturerSpecificData(ContinuityProtocol.APPLE_COMPANY_IDENTIFIER)!!
|
||||
|
||||
// We start counting at the airpods prefix byte
|
||||
val rawPrefix: UByte
|
||||
get() = proximityMessage.data[0]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.darken.capod.pods.core.unknown
|
||||
|
||||
import android.content.Context
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import java.time.Instant
|
||||
|
||||
data class UnknownDevice(
|
||||
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 confidence: Float = 0f,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : PodDevice {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
||||
|
||||
override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label)
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("PodDevice", "Unknown")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package eu.darken.capod.pods.core.unknown
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UnknownDeviceFactory @Inject constructor() {
|
||||
|
||||
private val lock = Mutex()
|
||||
|
||||
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
|
||||
var basic = UnknownDevice(
|
||||
identifier = PodDevice.Id(),
|
||||
scanResult = scanResult,
|
||||
)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
seenFirstAt = result.seenFirstAt,
|
||||
seenCounter = result.seenCounter,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
data class KnownDevice(
|
||||
val id: PodDevice.Id,
|
||||
val seenFirstAt: Instant,
|
||||
val seenCounter: Int,
|
||||
val history: List<PodDevice>
|
||||
) {
|
||||
|
||||
val lastAddress: String
|
||||
get() = history.last().address
|
||||
|
||||
val confidence: Float
|
||||
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
||||
|
||||
fun averageRssi(latest: Int): Int =
|
||||
history.map { it.rssi }.plus(latest).takeLast(10).median()
|
||||
|
||||
fun isOlderThan(age: Duration): Boolean {
|
||||
val now = Instant.now()
|
||||
return Duration.between(history.last().seenLastAt, now) > age
|
||||
}
|
||||
|
||||
private fun List<Int>.median(): Int = this.sorted().let {
|
||||
if (it.size % 2 == 0)
|
||||
(it[it.size / 2] + it[(it.size - 1) / 2]) / 2
|
||||
else
|
||||
it[it.size / 2]
|
||||
}
|
||||
|
||||
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
||||
|
||||
companion object {
|
||||
const val MAX_HISTORY = 10
|
||||
}
|
||||
}
|
||||
|
||||
private val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||
|
||||
private fun searchHistory(current: PodDevice): KnownDevice? {
|
||||
val scanResult = current.scanResult
|
||||
|
||||
knownDevices.values.toList().forEach { knownDevice ->
|
||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
||||
log(TAG, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
|
||||
knownDevices.remove(knownDevice.id)
|
||||
}
|
||||
}
|
||||
|
||||
knownDevices.values
|
||||
.filter { it.history.size > KnownDevice.MAX_HISTORY }
|
||||
.toList()
|
||||
.forEach {
|
||||
knownDevices[it.id] = it.copy(history = it.history.takeLast(KnownDevice.MAX_HISTORY))
|
||||
}
|
||||
|
||||
val recognizedDevice: KnownDevice? = knownDevices.values
|
||||
.firstOrNull { it.lastAddress == scanResult.address }
|
||||
?.also { log(TAG, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } }
|
||||
|
||||
if (recognizedDevice == null) {
|
||||
log(TAG) { "searchHistory1: Didn't recognize: $current" }
|
||||
}
|
||||
|
||||
return recognizedDevice
|
||||
}
|
||||
|
||||
private fun updateHistory(device: PodDevice) {
|
||||
val existing = knownDevices[device.identifier]
|
||||
|
||||
knownDevices[device.identifier] = when {
|
||||
existing != null -> {
|
||||
existing.copy(
|
||||
seenCounter = existing.seenCounter + 1,
|
||||
history = existing.history.plus(device)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
log(TAG) { "searchHistory1: Creating new history for $device" }
|
||||
KnownDevice(
|
||||
id = device.identifier,
|
||||
seenFirstAt = device.seenFirstAt,
|
||||
seenCounter = 1,
|
||||
history = listOf(device)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Pod", "Unknown", "Factory")
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/contact_dev_hint"
|
||||
android:id="@+id/details"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -97,7 +97,7 @@
|
||||
android:text="@string/pods_unknown_raw_data_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/contact_dev_hint" />
|
||||
app:layout_constraintTop_toBottomOf="@id/details" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/rawdata"
|
||||
|
||||
Reference in New Issue
Block a user