mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d9316665 | ||
|
|
a234ac0dd2 | ||
|
|
3fc18bc40b | ||
|
|
0f924ecb82 | ||
|
|
6bd350ce42 | ||
|
|
e1b4ab14e4 | ||
|
|
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
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import android.bluetooth.le.ScanResult
|
|||||||
import android.bluetooth.le.ScanSettings
|
import android.bluetooth.le.ScanSettings
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.common.SystemClockWrap
|
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
@@ -27,7 +25,7 @@ import javax.inject.Singleton
|
|||||||
class BleScanner @Inject constructor(
|
class BleScanner @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val bluetoothManager: BluetoothManager2,
|
private val bluetoothManager: BluetoothManager2,
|
||||||
private val debugSettings: DebugSettings,
|
private val fakeBleData: FakeBleData,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun scan(
|
fun scan(
|
||||||
@@ -72,8 +70,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 +85,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,58 +106,7 @@ class BleScanner @Inject constructor(
|
|||||||
log(TAG, INFO) { "BleScanner stopped" }
|
log(TAG, INFO) { "BleScanner stopped" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { origs ->
|
.map { fakeBleData.maybeAddfakeData(it) }
|
||||||
val fakeDevices = mutableListOf<BleScanResult>()
|
|
||||||
if (debugSettings.showFakeData.value) {
|
|
||||||
// AirPods Pro
|
|
||||||
BleScanResult(
|
|
||||||
address = "78:73:AF:B4:85:5E",
|
|
||||||
rssi = -48,
|
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 0E 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
|
||||||
).run { fakeDevices.add(this) }
|
|
||||||
// AirPods Gen1
|
|
||||||
BleScanResult(
|
|
||||||
address = "4E:9E:D1:49:D2:6D",
|
|
||||||
rssi = -55,
|
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
|
|
||||||
).run { fakeDevices.add(this) }
|
|
||||||
// AirPods Max
|
|
||||||
BleScanResult(
|
|
||||||
address = "7E:E5:C7:65:D2:B5",
|
|
||||||
rssi = -30,
|
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
|
|
||||||
).run { fakeDevices.add(this) }
|
|
||||||
// BeatsFlex
|
|
||||||
BleScanResult(
|
|
||||||
address = "5E:9E:D1:49:D2:6D",
|
|
||||||
rssi = -59,
|
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
|
||||||
).run { fakeDevices.add(this) }
|
|
||||||
// Unknown Device
|
|
||||||
BleScanResult(
|
|
||||||
address = "6E:9E:D1:49:D2:6D",
|
|
||||||
rssi = -60,
|
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
|
||||||
).run { fakeDevices.add(this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
origs + fakeDevices
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun String.hexToByteArray(): ByteArray {
|
|
||||||
val trimmed = this
|
|
||||||
.replace(" ", "")
|
|
||||||
.replace(">", "")
|
|
||||||
.replace("<", "")
|
|
||||||
require(trimmed.length % 2 == 0) { "Not a HEX string" }
|
|
||||||
return trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Bluetooth", "BleScanner")
|
private val TAG = logTag("Bluetooth", "BleScanner")
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package eu.darken.capod.common.bluetooth
|
||||||
|
|
||||||
|
import dagger.Reusable
|
||||||
|
import eu.darken.capod.common.SystemClockWrap
|
||||||
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
class FakeBleData @Inject constructor(
|
||||||
|
private val debugSettings: DebugSettings,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun maybeAddfakeData(originals: List<BleScanResult>): List<BleScanResult> {
|
||||||
|
if (!debugSettings.showFakeData.value) return originals
|
||||||
|
return originals + getFakeData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFakeData(): Collection<BleScanResult> {
|
||||||
|
val fakeDevices = mutableListOf<BleScanResult>()
|
||||||
|
// AirPods Gen1
|
||||||
|
BleScanResult(
|
||||||
|
address = "78:73:AF:B4:85:22",
|
||||||
|
rssi = Random.nextInt(100) * -1,
|
||||||
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||||
|
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
||||||
|
).run {
|
||||||
|
if (Random.nextBoolean()) {
|
||||||
|
fakeDevices.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AirPods Gen2
|
||||||
|
BleScanResult(
|
||||||
|
address = "78:73:FF:B4:85:5E",
|
||||||
|
rssi = Random.nextInt(100) * -1,
|
||||||
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||||
|
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
||||||
|
).run {
|
||||||
|
if (Random.nextBoolean()) {
|
||||||
|
fakeDevices.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AirPods Gen3
|
||||||
|
BleScanResult(
|
||||||
|
address = "4E:9E:D1:49:D2:6D",
|
||||||
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
||||||
|
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
|
||||||
|
).run {
|
||||||
|
if (Random.nextBoolean()) {
|
||||||
|
fakeDevices.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// AirPods Max
|
||||||
|
BleScanResult(
|
||||||
|
address = "7E:E5:C7:65:D2:B5",
|
||||||
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
||||||
|
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
|
||||||
|
).run {
|
||||||
|
if (Random.nextBoolean()) {
|
||||||
|
fakeDevices.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// BeatsFlex
|
||||||
|
BleScanResult(
|
||||||
|
address = "5E:9E:D1:49:D2:6D",
|
||||||
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||||
|
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
||||||
|
).run {
|
||||||
|
if (Random.nextBoolean()) {
|
||||||
|
fakeDevices.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unknown Device
|
||||||
|
BleScanResult(
|
||||||
|
address = "6E:9E:D1:49:D2:6D",
|
||||||
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
||||||
|
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
||||||
|
).run {
|
||||||
|
if (Random.nextBoolean()) {
|
||||||
|
fakeDevices.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fakeDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun String.hexToByteArray(): ByteArray {
|
||||||
|
val trimmed = this
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace(">", "")
|
||||||
|
.replace("<", "")
|
||||||
|
require(trimmed.length % 2 == 0) { "Not a HEX string" }
|
||||||
|
return trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.listItems.observe2(ui) { adapter.update(it) }
|
vm.listItems.observe2(ui) {
|
||||||
|
adapter.update(it)
|
||||||
|
}
|
||||||
|
|
||||||
vm.workerAutolaunch.observe2 {
|
vm.workerAutolaunch.observe2 {
|
||||||
// While UI is active, subscribe to the autolaunch routine
|
// While UI is active, subscribe to the autolaunch routine
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.app.Activity
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import eu.darken.capod.common.WebpageTool
|
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
@@ -46,7 +45,6 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
debugSettings: DebugSettings,
|
debugSettings: DebugSettings,
|
||||||
private val upgradeRepo: UpgradeRepo,
|
private val upgradeRepo: UpgradeRepo,
|
||||||
private val bluetoothManager: BluetoothManager2,
|
private val bluetoothManager: BluetoothManager2,
|
||||||
private val webpageTool: WebpageTool,
|
|
||||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||||
|
|
||||||
val upgradeState = upgradeRepo.upgradeInfo.asLiveData2()
|
val upgradeState = upgradeRepo.upgradeInfo.asLiveData2()
|
||||||
@@ -55,7 +53,7 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
private val updateTicker = channelFlow<Unit> {
|
private val updateTicker = channelFlow<Unit> {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
trySend(Unit)
|
trySend(Unit)
|
||||||
delay(1000)
|
delay(3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +104,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(
|
||||||
|
|||||||
+1
-5
@@ -9,7 +9,6 @@ import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding
|
|||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||||
import eu.darken.capod.pods.core.getSignalQuality
|
|
||||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@@ -34,10 +33,7 @@ class BasicSingleApplePodsCardVH(parent: ViewGroup) :
|
|||||||
|
|
||||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||||
|
|
||||||
reception.text = device.getSignalQuality(context)
|
reception.text = item.getReceptionText()
|
||||||
if (item.isMainPod) {
|
|
||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
|
||||||
}
|
|
||||||
|
|
||||||
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||||
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||||
|
|||||||
+6
-5
@@ -2,6 +2,7 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
|||||||
|
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
@@ -10,6 +11,7 @@ import eu.darken.capod.pods.core.*
|
|||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor
|
import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class DualApplePodsCardVH(parent: ViewGroup) :
|
class DualApplePodsCardVH(parent: ViewGroup) :
|
||||||
@@ -36,12 +38,11 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
|||||||
}
|
}
|
||||||
deviceIcon.setImageResource(device.iconRes)
|
deviceIcon.setImageResource(device.iconRes)
|
||||||
|
|
||||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
lastSeen.text = context.getString(R.string.last_seen_x, device.lastSeenFormatted(item.now))
|
||||||
|
firstSeen.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now))
|
||||||
|
firstSeen.isGone = Duration.between(device.seenFirstAt, device.seenLastAt).toMinutes() < 1
|
||||||
|
|
||||||
reception.text = device.getSignalQuality(context)
|
reception.text = item.getReceptionText()
|
||||||
if (item.isMainPod) {
|
|
||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left Pod
|
// Left Pod
|
||||||
device.apply {
|
device.apply {
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.lists.BindableVH
|
import eu.darken.capod.common.lists.BindableVH
|
||||||
import eu.darken.capod.common.lists.differ.DifferItem
|
import eu.darken.capod.common.lists.differ.DifferItem
|
||||||
import eu.darken.capod.common.lists.modular.ModularAdapter
|
import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||||
import eu.darken.capod.main.ui.overview.OverviewAdapter
|
import eu.darken.capod.main.ui.overview.OverviewAdapter
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.getSignalQuality
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
|
abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
|
||||||
@@ -15,6 +17,10 @@ abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
|
|||||||
parent: ViewGroup
|
parent: ViewGroup
|
||||||
) : ModularAdapter.VH(layoutId, parent), BindableVH<D, B> {
|
) : ModularAdapter.VH(layoutId, parent), BindableVH<D, B> {
|
||||||
|
|
||||||
|
fun Item.getReceptionText(): String = device.getSignalQuality(context)
|
||||||
|
.let { if (showDebug) "$it ${device.seenCounter}" else it }
|
||||||
|
.let { if (isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it }
|
||||||
|
|
||||||
interface Item : OverviewAdapter.Item {
|
interface Item : OverviewAdapter.Item {
|
||||||
|
|
||||||
val now: Instant
|
val now: Instant
|
||||||
|
|||||||
+1
-5
@@ -10,7 +10,6 @@ import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding
|
|||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||||
import eu.darken.capod.pods.core.getSignalQuality
|
|
||||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@@ -35,10 +34,7 @@ class SingleApplePodsCardVH(parent: ViewGroup) :
|
|||||||
|
|
||||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||||
|
|
||||||
reception.text = device.getSignalQuality(context)
|
reception.text = item.getReceptionText()
|
||||||
if (item.isMainPod) {
|
|
||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
|
||||||
}
|
|
||||||
|
|
||||||
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||||
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||||
|
|||||||
+1
-5
@@ -6,7 +6,6 @@ import eu.darken.capod.R
|
|||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding
|
import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.getSignalQuality
|
|
||||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@@ -30,10 +29,7 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
|
|||||||
|
|
||||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||||
|
|
||||||
reception.text = device.getSignalQuality(context)
|
reception.text = item.getReceptionText()
|
||||||
if (item.isMainPod) {
|
|
||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
|
||||||
}
|
|
||||||
|
|
||||||
rawdata.text = device.rawDataHex
|
rawdata.text = device.rawDataHex
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,38 +35,47 @@ 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.seenLastAt, now) > Duration.ofSeconds(20)) {
|
||||||
log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
|
log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
|
||||||
deviceCache.remove(key)
|
deviceCache.remove(key)
|
||||||
}
|
}
|
||||||
@@ -77,41 +89,56 @@ class PodMonitor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val now = Instant.now()
|
val presorted = pods.values.sortPodsToInterest()
|
||||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
val main = presorted.determineMainDevice()
|
||||||
|
|
||||||
val aboveTresholdSorted = pods.values
|
presorted.sortedByDescending { it == main }
|
||||||
.filter { it.signalQuality > minimumSignalQuality }
|
|
||||||
.sortedWith(
|
|
||||||
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.rssi }
|
|
||||||
)
|
|
||||||
val belowThresholdSorted = pods.values
|
|
||||||
.filter { it.signalQuality <= minimumSignalQuality }
|
|
||||||
.sortedWith(
|
|
||||||
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.rssi }
|
|
||||||
)
|
|
||||||
|
|
||||||
val main = pods.values.determineMainDevice()
|
|
||||||
(aboveTresholdSorted + belowThresholdSorted).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 Collection<PodDevice>.sortPodsToInterest(): List<PodDevice> = this.let { devices ->
|
||||||
maxByOrNull { it.rssi }?.let filter@{ device ->
|
val now = Instant.now()
|
||||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
|
||||||
|
|
||||||
generalSettings.mainDeviceModel.value.let {
|
return@let devices.sortedWith(
|
||||||
if (device.model != it && it != PodDevice.Model.UNKNOWN) return@filter null
|
compareByDescending<PodDevice> { true }
|
||||||
|
.thenBy {
|
||||||
|
val age = Duration.between(it.seenLastAt, now).seconds
|
||||||
|
if (age < 5) 0L else (age / 3L).toLong()
|
||||||
|
}
|
||||||
|
.thenByDescending { it.signalQuality }
|
||||||
|
.thenByDescending { (it.seenCounter / 10) }
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<PodDevice>.determineMainDevice(): PodDevice? = this
|
||||||
|
.sortPodsToInterest()
|
||||||
|
.let { devices ->
|
||||||
|
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
||||||
|
|
||||||
|
val presorted = devices.sortedByDescending {
|
||||||
|
it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device.signalQuality > minimumSignalQuality) device else null
|
return@let presorted.firstOrNull()?.let { candidate ->
|
||||||
|
when {
|
||||||
|
candidate.model == PodDevice.Model.UNKNOWN -> null
|
||||||
|
mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null
|
||||||
|
candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null
|
||||||
|
else -> candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -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,28 @@ interface PodDevice {
|
|||||||
|
|
||||||
val model: Model
|
val model: Model
|
||||||
|
|
||||||
val lastSeenAt: Instant
|
val address: String
|
||||||
|
get() = scanResult.address
|
||||||
|
|
||||||
|
val seenLastAt: Instant
|
||||||
|
|
||||||
|
val seenFirstAt: Instant
|
||||||
|
|
||||||
|
val seenCounter: Int
|
||||||
|
|
||||||
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 +101,8 @@ interface PodDevice {
|
|||||||
"Unknown"
|
"Unknown"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_CONFIDENCE = 0.5f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -45,10 +45,19 @@ fun getBatteryDrawable(percent: Float?): Int = when {
|
|||||||
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
||||||
|
|
||||||
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||||
val duration = Duration.between(lastSeenAt, now)
|
val duration = Duration.between(seenLastAt, now)
|
||||||
return lastSeenFormatter.format(
|
return lastSeenFormatter.format(
|
||||||
duration.seconds.toDouble(),
|
duration.seconds.toDouble(),
|
||||||
RelativeDateTimeFormatter.Direction.LAST,
|
RelativeDateTimeFormatter.Direction.LAST,
|
||||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PodDevice.firstSeenFormatted(now: Instant): String {
|
||||||
|
val duration = Duration.between(seenFirstAt, now)
|
||||||
|
return lastSeenFormatter.format(
|
||||||
|
duration.toMinutes().toDouble(),
|
||||||
|
RelativeDateTimeFormatter.Direction.LAST,
|
||||||
|
RelativeDateTimeFormatter.RelativeUnit.MINUTES
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -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,147 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
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
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
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 seenFirstAt: Instant,
|
||||||
|
val seenCounter: Int,
|
||||||
|
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).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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -11,30 +11,43 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
data class UnknownAppleDevice(
|
data class UnknownAppleDevice(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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)
|
var basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return UnknownAppleDevice(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
rssiHistory = recognized.rssiHistory,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,22 @@ 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
|
||||||
|
|
||||||
data class AirPodsGen1 constructor(
|
data class AirPodsGen1 constructor(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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 +28,35 @@ 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)
|
var basic = AirPodsGen1(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen1(
|
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,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
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,17 +5,22 @@ 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
|
||||||
|
|
||||||
data class AirPodsGen2 constructor(
|
data class AirPodsGen2 constructor(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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 +28,35 @@ 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)
|
var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen2(
|
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,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
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,44 +5,57 @@ 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
|
||||||
|
|
||||||
data class AirPodsGen3 constructor(
|
data class AirPodsGen3 constructor(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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)
|
var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen3(
|
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,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
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,33 +5,47 @@ 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
|
||||||
|
|
||||||
data class AirPodsMax(
|
data class AirPodsMax(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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)
|
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return AirPodsMax(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
rssiHistory = recognized.rssiHistory,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,23 @@ 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
|
||||||
|
|
||||||
data class AirPodsPro(
|
data class AirPodsPro(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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 +29,40 @@ 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,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
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,33 +5,47 @@ 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
|
||||||
|
|
||||||
data class BeatsFlex(
|
data class BeatsFlex(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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)
|
var basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsFlex(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
rssiHistory = recognized.rssiHistory,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,33 +5,46 @@ 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
|
||||||
|
|
||||||
data class BeatsSolo3(
|
data class BeatsSolo3(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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)
|
var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsSolo3(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
rssiHistory = recognized.rssiHistory,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,33 +5,44 @@ 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
|
||||||
|
|
||||||
data class BeatsStudio3(
|
data class BeatsStudio3(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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)
|
var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsStudio3(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
rssiHistory = recognized.rssiHistory,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,33 +5,47 @@ 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
|
||||||
|
|
||||||
data class BeatsX(
|
data class BeatsX(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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)
|
var basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsX(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
rssiHistory = recognized.rssiHistory,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,32 +5,46 @@ 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
|
||||||
|
|
||||||
data class PowerBeats3(
|
data class PowerBeats3(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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)
|
var basic = PowerBeats3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return PowerBeats3(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = recognized.identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
rssiHistory = recognized.rssiHistory,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,22 @@ 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
|
||||||
|
|
||||||
data class PowerBeatsPro(
|
data class PowerBeatsPro(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
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 +28,35 @@ 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,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
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>
|
||||||
@@ -45,12 +45,24 @@
|
|||||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/first_seen"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/name"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/name"
|
||||||
|
app:layout_goneMarginBottom="8dp"
|
||||||
|
tools:text="Last seen: 3s ago" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/first_seen"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_top"
|
app:layout_constraintBottom_toTopOf="@id/barrier_top"
|
||||||
app:layout_constraintEnd_toEndOf="@id/name"
|
app:layout_constraintEnd_toEndOf="@id/name"
|
||||||
app:layout_constraintStart_toStartOf="@id/name"
|
app:layout_constraintStart_toStartOf="@id/name"
|
||||||
app:layout_constraintTop_toBottomOf="@id/name"
|
app:layout_constraintTop_toBottomOf="@id/last_seen"
|
||||||
tools:text="3s ago" />
|
tools:text="First seen: 3s ago" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/reception"
|
android:id="@+id/reception"
|
||||||
@@ -79,7 +91,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
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">CAPod</string>
|
||||||
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
|
<string name="app_name_foss">CAPod FOSS</string>
|
||||||
|
<string name="notification_channel_device_status_label">Geräte Status</string>
|
||||||
|
<string name="general_error_label">Fehler</string>
|
||||||
|
<string name="general_share_action">Teilen</string>
|
||||||
|
<string name="general_done_action">Fertig</string>
|
||||||
|
<string name="general_copy_action">Kopieren</string>
|
||||||
|
<string name="general_value_unknown_label">Unbekannt</string>
|
||||||
|
<string name="general_thank_you_label">Danke</string>
|
||||||
|
<string name="general_value_not_available_label">k.A.</string>
|
||||||
|
<string name="general_grant_permission_action">Berechtigung erteilen</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user