mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adef0b39e9 | ||
|
|
af12a0e5cb | ||
|
|
90fc2fac24 | ||
|
|
3be7d8b43f | ||
|
|
a35d8c57b4 | ||
|
|
f792431710 | ||
|
|
7ff20acfc0 | ||
|
|
8e20e0e108 | ||
|
|
a6fda04640 | ||
|
|
0bbcfb5e88 | ||
|
|
e79ba6bfe5 |
-3
@@ -109,9 +109,6 @@ class BillingClientConnectionProvider @Inject constructor(
|
|||||||
delay(3000 * attempt)
|
delay(3000 * attempt)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
.catch {
|
|
||||||
log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Client", "ConnectionProvider")
|
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Client", "ConnectionProvider")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class BillingDataRepo @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val connectionProvider = billingClientConnectionProvider.connection
|
private val connectionProvider = billingClientConnectionProvider.connection
|
||||||
|
.catch { log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" } }
|
||||||
.replayingShare(scope)
|
.replayingShare(scope)
|
||||||
|
|
||||||
val billingData: Flow<BillingData> = connectionProvider
|
val billingData: Flow<BillingData> = connectionProvider
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ class BleScanner @Inject constructor(
|
|||||||
)
|
)
|
||||||
if (adapter.isOffloadedScanBatchingSupported) {
|
if (adapter.isOffloadedScanBatchingSupported) {
|
||||||
when (scannerMode) {
|
when (scannerMode) {
|
||||||
ScannerMode.LOW_POWER -> setReportDelay(5000)
|
ScannerMode.LOW_POWER -> setReportDelay(2000)
|
||||||
ScannerMode.BALANCED -> setReportDelay(2500)
|
ScannerMode.BALANCED -> setReportDelay(1000)
|
||||||
ScannerMode.LOW_LATENCY -> setReportDelay(500)
|
ScannerMode.LOW_LATENCY -> setReportDelay(500)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -87,7 +87,7 @@ class BleScanner @Inject constructor(
|
|||||||
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
||||||
when (scannerMode) {
|
when (scannerMode) {
|
||||||
ScannerMode.LOW_POWER -> break
|
ScannerMode.LOW_POWER -> break
|
||||||
ScannerMode.BALANCED -> delay(2500)
|
ScannerMode.BALANCED -> delay(1000)
|
||||||
ScannerMode.LOW_LATENCY -> delay(500)
|
ScannerMode.LOW_LATENCY -> delay(500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun <T : Any> Flow<T>.shareLatest(
|
|||||||
)
|
)
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
|
|
||||||
fun <T : Any> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
|
fun <T : Any?> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
replay = 1,
|
replay = 1,
|
||||||
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)
|
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)
|
||||||
|
|||||||
@@ -106,11 +106,10 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
debugSettings.isDebugModeEnabled.flow,
|
debugSettings.isDebugModeEnabled.flow,
|
||||||
generalSettings.showAll.flow,
|
generalSettings.showAll.flow,
|
||||||
bluetoothManager.isBluetoothEnabled,
|
bluetoothManager.isBluetoothEnabled,
|
||||||
) { _, permissions, pods, isDebugMode, showAll, isBluetoothEnabled ->
|
podMonitor.mainDevice,
|
||||||
|
) { _, permissions, pods, isDebugMode, showAll, isBluetoothEnabled, mainPod ->
|
||||||
val items = mutableListOf<OverviewAdapter.Item>()
|
val items = mutableListOf<OverviewAdapter.Item>()
|
||||||
|
|
||||||
val mainPod = podMonitor.mainDevice.first()
|
|
||||||
|
|
||||||
permissions
|
permissions
|
||||||
.map { perm ->
|
.map { perm ->
|
||||||
PermissionCardVH.Item(
|
PermissionCardVH.Item(
|
||||||
|
|||||||
+2
-3
@@ -38,9 +38,8 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
|||||||
|
|
||||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||||
|
|
||||||
reception.text = device.getSignalQuality(context)
|
reception.text = device.getSignalQuality(context).let {
|
||||||
if (item.isMainPod) {
|
if (item.isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it
|
||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left Pod
|
// Left Pod
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.darken.capod.monitor.core
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.bluetooth.BleScanner
|
import eu.darken.capod.common.bluetooth.BleScanner
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
@@ -9,9 +10,11 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.replayingShare
|
import eu.darken.capod.common.flow.replayingShare
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.main.core.ScannerMode
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodFactory
|
import eu.darken.capod.pods.core.PodFactory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@@ -32,35 +35,44 @@ class PodMonitor @Inject constructor(
|
|||||||
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||||
private val cacheLock = Mutex()
|
private val cacheLock = Mutex()
|
||||||
|
|
||||||
|
private suspend fun List<BleScanResult>.preFilterAndMap(
|
||||||
|
scannerMode: ScannerMode
|
||||||
|
): List<PodDevice> = 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(TAG, VERBOSE) { "Discarding stale results: $it" }
|
||||||
|
}
|
||||||
|
newest
|
||||||
|
}
|
||||||
|
.mapNotNull { podFactory.createPod(it) }
|
||||||
|
|
||||||
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
||||||
.flatMapLatest { isBluetoothEnabled ->
|
.flatMapLatest { isBluetoothEnabled ->
|
||||||
if (isBluetoothEnabled) {
|
if (isBluetoothEnabled) {
|
||||||
|
log(TAG) { "Bluetooth is enabled" }
|
||||||
generalSettings.scannerMode.flow
|
generalSettings.scannerMode.flow
|
||||||
|
.flatMapLatest { mode ->
|
||||||
|
bleScanner.scan(scannerMode = mode).map { it.preFilterAndMap(mode) }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
||||||
emptyFlow()
|
flowOf(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flatMapLatest { bleScanner.scan(scannerMode = it) }
|
.map { newPods ->
|
||||||
.map { result ->
|
|
||||||
// For each address we only want the newest result, upstream may batch data
|
|
||||||
result.groupBy { it.address }
|
|
||||||
.values
|
|
||||||
.map { sameAdrDevs ->
|
|
||||||
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
|
||||||
sameAdrDevs.minus(newest).let {
|
|
||||||
if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" }
|
|
||||||
}
|
|
||||||
newest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { scanResults ->
|
|
||||||
val newPods = scanResults
|
|
||||||
.mapNotNull { podFactory.createPod(it) }
|
|
||||||
|
|
||||||
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||||
|
|
||||||
cacheLock.withLock {
|
cacheLock.withLock {
|
||||||
|
if (newPods == null) {
|
||||||
|
log(TAG) { "Null result, Bluetooth is disabled." }
|
||||||
|
deviceCache.clear()
|
||||||
|
return@map emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
deviceCache.toList().forEach { (key, value) ->
|
deviceCache.toList().forEach { (key, value) ->
|
||||||
if (Duration.between(value.lastSeenAt, now) > Duration.ofSeconds(20)) {
|
if (Duration.between(value.lastSeenAt, now) > Duration.ofSeconds(20)) {
|
||||||
@@ -79,40 +91,50 @@ class PodMonitor @Inject constructor(
|
|||||||
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
||||||
|
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
||||||
|
|
||||||
val aboveTresholdSorted = pods.values
|
val aboveTresholdSorted = pods.values
|
||||||
.filter { it.signalQuality > minimumSignalQuality }
|
.filter { it.signalQuality > minimumSignalQuality }
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.rssi }
|
compareByDescending<PodDevice> { it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN }
|
||||||
|
.thenBy {
|
||||||
|
val age = Duration.between(it.lastSeenAt, now).seconds
|
||||||
|
if (age < 3) 0L else age
|
||||||
|
}
|
||||||
|
.thenByDescending { it.signalQuality }
|
||||||
)
|
)
|
||||||
val belowThresholdSorted = pods.values
|
val belowThresholdSorted = pods.values
|
||||||
.filter { it.signalQuality <= minimumSignalQuality }
|
.filter { it.signalQuality <= minimumSignalQuality }
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.rssi }
|
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.signalQuality }
|
||||||
)
|
)
|
||||||
|
|
||||||
val main = pods.values.determineMainDevice()
|
val presorted = (aboveTresholdSorted + belowThresholdSorted)
|
||||||
(aboveTresholdSorted + belowThresholdSorted).sortedByDescending { it == main }
|
val main = presorted.determineMainDevice()
|
||||||
|
presorted.sortedByDescending { it == main }
|
||||||
}
|
}
|
||||||
.onStart { emit(emptyList()) }
|
.onStart { emit(emptyList()) }
|
||||||
.catch {
|
.retryWhen { cause, attempt ->
|
||||||
log(TAG, ERROR) { "PodMonitor failed:\n${it.asLog()}" }
|
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
|
||||||
|
delay(3000)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
.replayingShare(appScope)
|
.replayingShare(appScope)
|
||||||
|
|
||||||
val mainDevice: Flow<PodDevice?>
|
val mainDevice: Flow<PodDevice?>
|
||||||
get() = devices.map { it.determineMainDevice() }
|
get() = devices
|
||||||
|
.map { it.determineMainDevice() }
|
||||||
|
.replayingShare(appScope)
|
||||||
|
|
||||||
private fun Collection<PodDevice>.determineMainDevice(): PodDevice? =
|
private fun List<PodDevice>.determineMainDevice(): PodDevice? = this.firstOrNull()?.let {
|
||||||
maxByOrNull { it.rssi }?.let filter@{ device ->
|
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
||||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
when {
|
||||||
|
it.model == PodDevice.Model.UNKNOWN -> null
|
||||||
generalSettings.mainDeviceModel.value.let {
|
mainDeviceModel != PodDevice.Model.UNKNOWN && it.model != mainDeviceModel -> null
|
||||||
if (device.model != it && it != PodDevice.Model.UNKNOWN) return@filter null
|
it.signalQuality <= generalSettings.minimumSignalQuality.value -> null
|
||||||
}
|
else -> it
|
||||||
|
|
||||||
if (device.signalQuality > minimumSignalQuality) device else null
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Monitor", "PodMonitor")
|
private val TAG = logTag("Monitor", "PodMonitor")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import eu.darken.capod.main.core.PermissionTool
|
|||||||
import eu.darken.capod.monitor.core.*
|
import eu.darken.capod.monitor.core.*
|
||||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||||
import eu.darken.capod.reaction.ReactionHub
|
import eu.darken.capod.reaction.ReactionHub
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -68,9 +69,13 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
Result.success(inputData)
|
Result.success(inputData)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Bugs.report(tag = TAG, "Execution failed", exception = e)
|
if (e !is CancellationException) {
|
||||||
finishedWithError = true
|
Bugs.report(tag = TAG, "Execution failed", exception = e)
|
||||||
Result.failure(inputData)
|
finishedWithError = true
|
||||||
|
Result.failure(inputData)
|
||||||
|
} else {
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.workerScope.cancel("Worker finished (withError?=$finishedWithError).")
|
this.workerScope.cancel("Worker finished (withError?=$finishedWithError).")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
interface PodDevice {
|
interface PodDevice {
|
||||||
|
|
||||||
@@ -16,21 +17,24 @@ interface PodDevice {
|
|||||||
|
|
||||||
val model: Model
|
val model: Model
|
||||||
|
|
||||||
|
val address: String
|
||||||
|
get() = scanResult.address
|
||||||
|
|
||||||
val lastSeenAt: Instant
|
val lastSeenAt: Instant
|
||||||
|
|
||||||
val scanResult: BleScanResult
|
val scanResult: BleScanResult
|
||||||
|
|
||||||
val rssiHistory: List<Int>
|
|
||||||
|
|
||||||
val rssi: Int
|
val rssi: Int
|
||||||
get() = rssiHistory.sum() / rssiHistory.size
|
get() = scanResult.rssi
|
||||||
|
|
||||||
|
val confidence: Float
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is not correct but it works ¯\_(ツ)_/¯
|
* This is not correct but it works ¯\_(ツ)_/¯
|
||||||
* The range of the RSSI is device specific (ROMs).
|
* The range of the RSSI is device specific (ROMs).
|
||||||
*/
|
*/
|
||||||
val signalQuality: Float
|
val signalQuality: Float
|
||||||
get() = (100 - abs(rssi)) / 100f
|
get() = ((100 - abs(rssi)) / 100f) * (max(BASE_CONFIDENCE, confidence))
|
||||||
|
|
||||||
val rawData: ByteArray
|
val rawData: ByteArray
|
||||||
|
|
||||||
@@ -93,4 +97,8 @@ interface PodDevice {
|
|||||||
"Unknown"
|
"Unknown"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_CONFIDENCE = 0.5f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ import javax.inject.Singleton
|
|||||||
class AppleFactory @Inject constructor(
|
class AppleFactory @Inject constructor(
|
||||||
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
||||||
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
||||||
private val podFactories: @JvmSuppressWildcards Set<ApplePods.Factory>,
|
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory<out ApplePods>>,
|
||||||
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
@@ -12,17 +12,17 @@ import eu.darken.capod.pods.core.apple.beats.*
|
|||||||
@Module
|
@Module
|
||||||
abstract class AppleFactoryModule {
|
abstract class AppleFactoryModule {
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.common.SystemClockWrap
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.toHex
|
import eu.darken.capod.common.toHex
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
interface ApplePods : PodDevice {
|
interface ApplePods : PodDevice {
|
||||||
|
|
||||||
@@ -55,103 +49,4 @@ interface ApplePods : PodDevice {
|
|||||||
get() = proximityMessage.data[8]
|
get() = proximityMessage.data[8]
|
||||||
|
|
||||||
|
|
||||||
abstract class Factory(private val tag: String) {
|
|
||||||
|
|
||||||
internal data class ValueCache(
|
|
||||||
val caseBatteryPercentage: Float?
|
|
||||||
)
|
|
||||||
|
|
||||||
internal val cachedValues = mutableMapOf<PodDevice.Id, ValueCache>()
|
|
||||||
|
|
||||||
internal data class KnownDevice(
|
|
||||||
val identifier: PodDevice.Id,
|
|
||||||
val scanResult: BleScanResult,
|
|
||||||
val message: ProximityPairing.Message,
|
|
||||||
val rssiHistory: List<Int> = emptyList()
|
|
||||||
) {
|
|
||||||
val address: String
|
|
||||||
get() = scanResult.address
|
|
||||||
|
|
||||||
val rssi: Int
|
|
||||||
get() = scanResult.rssi
|
|
||||||
|
|
||||||
val timestampNanos: Duration
|
|
||||||
get() = Duration.ofNanos(scanResult.generatedAtNanos)
|
|
||||||
|
|
||||||
fun isOlderThan(age: Duration): Boolean {
|
|
||||||
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos)
|
|
||||||
return now - timestampNanos > age
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val knownDevs = mutableMapOf<PodDevice.Id, KnownDevice>()
|
|
||||||
|
|
||||||
internal fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): KnownDevice {
|
|
||||||
val address = scanResult.address
|
|
||||||
|
|
||||||
var recognizedDevice: KnownDevice? = knownDevs.values
|
|
||||||
.firstOrNull { it.address == address }
|
|
||||||
?.also { log(tag, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" } }
|
|
||||||
|
|
||||||
if (recognizedDevice == null) {
|
|
||||||
val currentMarkers = message.getRecogMarkers()
|
|
||||||
recognizedDevice = knownDevs.values
|
|
||||||
.firstOrNull { it.message.getRecogMarkers() == currentMarkers }
|
|
||||||
?.also {
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: Close match based similarity markers." }
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: Old marker: ${it.message.getRecogMarkers()}" }
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: New marker: $currentMarkers" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recognizedDevice = if (recognizedDevice == null) {
|
|
||||||
log(tag, VERBOSE) { "recognizeDevice: Mapping as new device" }
|
|
||||||
KnownDevice(
|
|
||||||
identifier = PodDevice.Id(),
|
|
||||||
scanResult = scanResult,
|
|
||||||
message = message,
|
|
||||||
rssiHistory = listOf(scanResult.rssi)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
recognizedDevice.copy(
|
|
||||||
scanResult = scanResult,
|
|
||||||
message = message,
|
|
||||||
rssiHistory = recognizedDevice.rssiHistory
|
|
||||||
.toMutableList()
|
|
||||||
.let {
|
|
||||||
if (it.size > 2) it.removeAt(0)
|
|
||||||
it.plus(scanResult.rssi)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
knownDevs[recognizedDevice.identifier] = recognizedDevice
|
|
||||||
|
|
||||||
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 recognizedDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ModelInfo(
|
|
||||||
val full: UShort,
|
|
||||||
val dirty: UByte,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
|
||||||
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
|
||||||
dirty = data[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
|
||||||
|
|
||||||
abstract fun create(
|
|
||||||
scanResult: BleScanResult,
|
|
||||||
proximityMessage: ProximityPairing.Message,
|
|
||||||
): ApplePods
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
import eu.darken.capod.common.SystemClockWrap
|
||||||
|
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.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||||
|
|
||||||
|
data class Markings(
|
||||||
|
val vendor: UByte,
|
||||||
|
val length: UByte,
|
||||||
|
val device: UShort,
|
||||||
|
val podBatteryData: Set<UShort>,
|
||||||
|
val caseBatteryData: UShort,
|
||||||
|
val deviceColor: UByte,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
||||||
|
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
||||||
|
length = ProximityPairing.PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||||
|
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||||
|
// Make comparison order independent
|
||||||
|
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
||||||
|
caseBatteryData = data[5].lowerNibble,
|
||||||
|
deviceColor = data[7]
|
||||||
|
)
|
||||||
|
|
||||||
|
data class KnownDevice(
|
||||||
|
val id: PodDevice.Id,
|
||||||
|
val history: List<ApplePods>
|
||||||
|
) {
|
||||||
|
val lastMessage: ProximityPairing.Message
|
||||||
|
get() = history.last().proximityMessage
|
||||||
|
|
||||||
|
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).average().toInt()
|
||||||
|
|
||||||
|
val timestampNanos: Duration
|
||||||
|
get() = Duration.ofNanos(history.last().scanResult.generatedAtNanos)
|
||||||
|
|
||||||
|
fun isOlderThan(age: Duration): Boolean {
|
||||||
|
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos)
|
||||||
|
return now - timestampNanos > age
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_HISTORY = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||||
|
|
||||||
|
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||||
|
val scanResult = current.scanResult
|
||||||
|
val message = current.proximityMessage
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
var recognizedDevice: KnownDevice? = knownDevices.values
|
||||||
|
.firstOrNull { it.lastAddress == scanResult.address }
|
||||||
|
?.also { log(tag, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } }
|
||||||
|
|
||||||
|
if (recognizedDevice == null) {
|
||||||
|
val currentMarkers = message.getApplePodsMarkings()
|
||||||
|
recognizedDevice = knownDevices.values
|
||||||
|
.firstOrNull { it.lastMessage.getApplePodsMarkings() == currentMarkers }
|
||||||
|
?.also { log(tag) { "searchHistory1: Close match based on similarity: $currentMarkers" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recognizedDevice == null) {
|
||||||
|
log(tag) { "searchHistory1: Didn't recognize: $message" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return recognizedDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHistory(device: PodType) {
|
||||||
|
val existing = knownDevices[device.identifier]
|
||||||
|
|
||||||
|
knownDevices[device.identifier] = when {
|
||||||
|
existing != null -> {
|
||||||
|
existing.copy(history = existing.history.plus(device))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
log(tag) { "searchHistory1: Creating new history for $device" }
|
||||||
|
KnownDevice(
|
||||||
|
id = device.identifier,
|
||||||
|
history = listOf(device)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ModelInfo(
|
||||||
|
val full: UShort,
|
||||||
|
val dirty: UByte,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
||||||
|
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||||
|
dirty = data[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
||||||
|
|
||||||
|
abstract fun create(
|
||||||
|
scanResult: BleScanResult,
|
||||||
|
proximityMessage: ProximityPairing.Message,
|
||||||
|
): ApplePods
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
abstract class BasicSingleApplePodsFactory(private val tag: String) : ApplePodsFactory<BasicSingleApplePods>(tag)
|
||||||
@@ -182,4 +182,5 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
|||||||
|
|
||||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||||
|
|
||||||
|
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
|
||||||
|
|
||||||
|
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
|
||||||
|
leftPodBattery = batteryLeftPodPercent,
|
||||||
|
rightPodBattery = batteryRightPodPercent,
|
||||||
|
microPhoneLeft = isLeftPodMicrophone,
|
||||||
|
microPhoneRight = isRightPodMicrophone,
|
||||||
|
chargingLeft = isLeftPodCharging,
|
||||||
|
chargingRight = isRightPodCharging,
|
||||||
|
color = deviceColor,
|
||||||
|
model = model
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split pods, one in case, one not.
|
||||||
|
*/
|
||||||
|
data class SplitPodsMarkings(
|
||||||
|
val leftPodBattery: Float?,
|
||||||
|
val rightPodBattery: Float?,
|
||||||
|
val microPhoneLeft: Boolean,
|
||||||
|
val microPhoneRight: Boolean,
|
||||||
|
val chargingLeft: Boolean,
|
||||||
|
val chargingRight: Boolean,
|
||||||
|
val color: DualApplePods.DeviceColor,
|
||||||
|
val model: PodDevice.Model,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
|
||||||
|
val target = device.getCaseMatchMarkings()
|
||||||
|
|
||||||
|
return filter { known ->
|
||||||
|
known.history
|
||||||
|
.filterIsInstance<DualApplePods>()
|
||||||
|
.any { it.getCaseMatchMarkings() == target }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
||||||
|
.filterIsInstance<DualApplePods>()
|
||||||
|
.mapNotNull { it.batteryCasePercent }
|
||||||
|
.lastOrNull()
|
||||||
|
|
||||||
|
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||||
|
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||||
|
if (definitive.contains(basic.caseLidState)) return null
|
||||||
|
|
||||||
|
return history
|
||||||
|
.filterIsInstance<AirPodsPro>()
|
||||||
|
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
|
||||||
|
?.caseLidState
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchHistory(current: DualApplePods): KnownDevice? {
|
||||||
|
val basicResult = super.searchHistory(current)
|
||||||
|
|
||||||
|
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
||||||
|
|
||||||
|
log(tag, DEBUG) { "searchHistory2: Case ignored matches(${caseIgnored.size}): $caseIgnored" }
|
||||||
|
|
||||||
|
return when (caseIgnored.size) {
|
||||||
|
0 -> basicResult
|
||||||
|
1 -> caseIgnored.single()
|
||||||
|
else -> {
|
||||||
|
log(tag) { "searchHistory2: More than one result when ignoring case markers." }
|
||||||
|
val oldest = caseIgnored.maxByOrNull { it.history.size } ?: return null
|
||||||
|
|
||||||
|
caseIgnored.minus(oldest).forEach {
|
||||||
|
log(tag) { "searchHistory2: Removing outlier: $it" }
|
||||||
|
knownDevices.remove(it.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
abstract class SingleApplePodsFactory(private val tag: String) : ApplePodsFactory<SingleApplePods>(tag)
|
||||||
@@ -14,27 +14,35 @@ data class UnknownAppleDevice(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val rssiHistory: List<Int>
|
override val confidence: Float = 0f,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : ApplePods {
|
) : ApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
||||||
|
|
||||||
override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label)
|
override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label)
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<ApplePods>(TAG) {
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
scanResult: BleScanResult,
|
scanResult: BleScanResult,
|
||||||
proximityMessage: ProximityPairing.Message,
|
proximityMessage: ProximityPairing.Message,
|
||||||
): ApplePods {
|
): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return UnknownAppleDevice(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,8 +15,10 @@ data class AirPodsGen1 constructor(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
override val rssiHistory: List<Int>
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||||
@@ -23,27 +26,32 @@ data class AirPodsGen1 constructor(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = AirPodsGen1(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen1(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
return basic.copy(
|
||||||
rssiHistory = recognized.rssiHistory,
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[recognized.identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,8 +15,10 @@ data class AirPodsGen2 constructor(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
override val rssiHistory: List<Int>,
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||||
@@ -23,27 +26,32 @@ data class AirPodsGen2 constructor(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen2(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
return basic.copy(
|
||||||
rssiHistory = recognized.rssiHistory,
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[recognized.identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,35 +15,42 @@ data class AirPodsGen3 constructor(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
override val rssiHistory: List<Int>,
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen3(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
return basic.copy(
|
||||||
rssiHistory = recognized.rssiHistory,
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[recognized.identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,24 +15,32 @@ data class AirPodsMax(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val rssiHistory: List<Int>,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return AirPodsMax(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,8 +16,10 @@ data class AirPodsPro(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
override val rssiHistory: List<Int>,
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||||
@@ -23,33 +27,38 @@ data class AirPodsPro(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsPro(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[recognized.identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x0e20.toUShort()
|
||||||
companion object {
|
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro", "Factory")
|
||||||
private val DEVICE_CODE = 0x0e20.toUShort()
|
}
|
||||||
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,24 +15,32 @@ data class BeatsFlex(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val rssiHistory: List<Int>,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsFlex(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,24 +15,31 @@ data class BeatsSolo3(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val rssiHistory: List<Int>,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsSolo3(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,24 +15,29 @@ data class BeatsStudio3(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val rssiHistory: List<Int>,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsStudio3(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,24 +15,32 @@ data class BeatsX(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val rssiHistory: List<Int>,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsX(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,23 +15,31 @@ data class PowerBeats3(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val rssiHistory: List<Int>,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
val basic = PowerBeats3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return PowerBeats3(
|
updateHistory(basic)
|
||||||
identifier = recognized.identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,8 +15,10 @@ data class PowerBeatsPro(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
override val rssiHistory: List<Int>,
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||||
@@ -23,27 +26,33 @@ data class PowerBeatsPro(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
var basic = PowerBeatsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = PowerBeatsPro(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
|
||||||
rssiHistory = recognized.rssiHistory,
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[recognized.identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package eu.darken.capod.pods.core.apple.protocol
|
|||||||
import android.bluetooth.le.ScanFilter
|
import android.bluetooth.le.ScanFilter
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.lowerNibble
|
|
||||||
import eu.darken.capod.common.upperNibble
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@@ -15,24 +13,6 @@ object ProximityPairing {
|
|||||||
val length: Int,
|
val length: Int,
|
||||||
val data: UByteArray
|
val data: UByteArray
|
||||||
) {
|
) {
|
||||||
data class Markers(
|
|
||||||
val vendor: UByte,
|
|
||||||
val length: UByte,
|
|
||||||
val device: UShort,
|
|
||||||
val podBatteryData: Set<UShort>,
|
|
||||||
val caseBatteryData: UShort,
|
|
||||||
val deviceColor: UByte,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getRecogMarkers(): Markers = Markers(
|
|
||||||
vendor = CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
|
||||||
length = PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
|
||||||
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
|
||||||
// Make comparison order independent
|
|
||||||
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
|
||||||
caseBatteryData = data[5].lowerNibble,
|
|
||||||
deviceColor = data[7]
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
|
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
|
||||||
@@ -81,6 +61,6 @@ object ProximityPairing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
||||||
private val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
internal val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
||||||
private const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
internal const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,5c-3.87,0 -7,3.13 -7,7h2c0,-2.76 2.24,-5 5,-5s5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM13,14.29c0.88,-0.39 1.5,-1.26 1.5,-2.29 0,-1.38 -1.12,-2.5 -2.5,-2.5S9.5,10.62 9.5,12c0,1.02 0.62,1.9 1.5,2.29v3.3L7.59,21 9,22.41l3,-3 3,3L16.41,21 13,17.59v-3.3zM12,1C5.93,1 1,5.93 1,12h2c0,-4.97 4.03,-9 9,-9s9,4.03 9,9h2c0,-6.07 -4.93,-11 -11,-11z" />
|
||||||
|
</vector>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
|
|
||||||
'version' : [
|
'version' : [
|
||||||
'major': 1,
|
'major': 1,
|
||||||
'minor': 2,
|
'minor': 3,
|
||||||
'patch': 0,
|
'patch': 0,
|
||||||
'build': 0,
|
'build': 0,
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user