mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
refactor: Rename PodDevice to BlePodSnapshot, MonitoredDevice to PodDevice, extract PodModel
Complete type rename chain: PodDevice (interface) becomes BlePodSnapshot, MonitoredDevice (facade) claims PodDevice name, Model enum extracted to top-level PodModel. Delete redundant type alias files. Fix stale comments and log tags.
This commit is contained in:
@@ -4,20 +4,20 @@ import android.content.Context
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
|
import eu.darken.capod.pods.core.DualBlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetection
|
import eu.darken.capod.pods.core.HasChargeDetection
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.SinglePodDevice
|
import eu.darken.capod.pods.core.SingleBlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||||
import eu.darken.capod.pods.core.unknown.UnknownDevice
|
import eu.darken.capod.pods.core.unknown.UnknownSnapshotBle
|
||||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -39,7 +39,7 @@ object MockPodDataProvider {
|
|||||||
|
|
||||||
// --- Dual pod scenarios ---
|
// --- Dual pod scenarios ---
|
||||||
|
|
||||||
fun airPodsProFullCharge(): DualPodDevice = MockDualPodDevice(
|
fun airPodsProFullCharge(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||||
_model = PodModel.AIRPODS_PRO2,
|
_model = PodModel.AIRPODS_PRO2,
|
||||||
_label = "Work AirPods",
|
_label = "Work AirPods",
|
||||||
batteryLeftPodPercent = 1.0f,
|
batteryLeftPodPercent = 1.0f,
|
||||||
@@ -50,7 +50,7 @@ object MockPodDataProvider {
|
|||||||
_caseIcon = R.drawable.device_airpods_pro2_case,
|
_caseIcon = R.drawable.device_airpods_pro2_case,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun airPodsProMixed(): DualPodDevice = MockDualPodDevice(
|
fun airPodsProMixed(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||||
_model = PodModel.AIRPODS_PRO2,
|
_model = PodModel.AIRPODS_PRO2,
|
||||||
_label = "My AirPods Pro",
|
_label = "My AirPods Pro",
|
||||||
batteryLeftPodPercent = 0.80f,
|
batteryLeftPodPercent = 0.80f,
|
||||||
@@ -62,7 +62,7 @@ object MockPodDataProvider {
|
|||||||
_caseIcon = R.drawable.device_airpods_pro2_case,
|
_caseIcon = R.drawable.device_airpods_pro2_case,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun airPodsProLowBattery(): DualPodDevice = MockDualPodDevice(
|
fun airPodsProLowBattery(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||||
_model = PodModel.AIRPODS_PRO2,
|
_model = PodModel.AIRPODS_PRO2,
|
||||||
_label = "My AirPods Pro",
|
_label = "My AirPods Pro",
|
||||||
batteryLeftPodPercent = 0.10f,
|
batteryLeftPodPercent = 0.10f,
|
||||||
@@ -75,7 +75,7 @@ object MockPodDataProvider {
|
|||||||
_caseIcon = R.drawable.device_airpods_pro2_case,
|
_caseIcon = R.drawable.device_airpods_pro2_case,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun airPodsProInCase(): DualPodDevice = MockDualPodDevice(
|
fun airPodsProInCase(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||||
_model = PodModel.AIRPODS_PRO2,
|
_model = PodModel.AIRPODS_PRO2,
|
||||||
_label = "My AirPods Pro",
|
_label = "My AirPods Pro",
|
||||||
batteryLeftPodPercent = null,
|
batteryLeftPodPercent = null,
|
||||||
@@ -86,7 +86,7 @@ object MockPodDataProvider {
|
|||||||
_caseIcon = R.drawable.device_airpods_pro2_case,
|
_caseIcon = R.drawable.device_airpods_pro2_case,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun airPodsGen1Wearing(): DualPodDevice = MockDualPodDevice(
|
fun airPodsGen1Wearing(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||||
_model = PodModel.AIRPODS_GEN1,
|
_model = PodModel.AIRPODS_GEN1,
|
||||||
_label = "Old AirPods",
|
_label = "Old AirPods",
|
||||||
batteryLeftPodPercent = 0.70f,
|
batteryLeftPodPercent = 0.70f,
|
||||||
@@ -100,7 +100,7 @@ object MockPodDataProvider {
|
|||||||
_caseIcon = R.drawable.device_airpods_gen1_case,
|
_caseIcon = R.drawable.device_airpods_gen1_case,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun powerBeatsPro(): DualPodDevice = MockDualPodDeviceNoCase(
|
fun powerBeatsPro(): DualBlePodSnapshot = MockDualBlePodSnapshotNoCase(
|
||||||
_model = PodModel.POWERBEATS_PRO,
|
_model = PodModel.POWERBEATS_PRO,
|
||||||
_label = "PowerBeats Pro",
|
_label = "PowerBeats Pro",
|
||||||
batteryLeftPodPercent = 0.55f,
|
batteryLeftPodPercent = 0.55f,
|
||||||
@@ -111,14 +111,14 @@ object MockPodDataProvider {
|
|||||||
|
|
||||||
// --- Single pod scenarios ---
|
// --- Single pod scenarios ---
|
||||||
|
|
||||||
fun airPodsMax(): SinglePodDevice = MockSinglePodDevice(
|
fun airPodsMax(): SingleBlePodSnapshot = MockSingleBlePodSnapshot(
|
||||||
_model = PodModel.AIRPODS_MAX,
|
_model = PodModel.AIRPODS_MAX,
|
||||||
_label = "AirPods Max",
|
_label = "AirPods Max",
|
||||||
batteryHeadsetPercent = 0.85f,
|
batteryHeadsetPercent = 0.85f,
|
||||||
_isBeingWorn = true,
|
_isBeingWorn = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun airPodsMaxCharging(): SinglePodDevice = MockSingleApplePodDevice(
|
fun airPodsMaxCharging(): SingleBlePodSnapshot = MockSingleAppleBlePodSnapshot(
|
||||||
_model = PodModel.AIRPODS_MAX,
|
_model = PodModel.AIRPODS_MAX,
|
||||||
_label = "AirPods Max",
|
_label = "AirPods Max",
|
||||||
batteryHeadsetPercent = 0.40f,
|
batteryHeadsetPercent = 0.40f,
|
||||||
@@ -127,7 +127,7 @@ object MockPodDataProvider {
|
|||||||
_hasPrivatePayload = true,
|
_hasPrivatePayload = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun beatsSolo3(): SinglePodDevice = MockSinglePodDevice(
|
fun beatsSolo3(): SingleBlePodSnapshot = MockSingleBlePodSnapshot(
|
||||||
_model = PodModel.BEATS_SOLO_3,
|
_model = PodModel.BEATS_SOLO_3,
|
||||||
_label = "Beats Solo 3",
|
_label = "Beats Solo 3",
|
||||||
batteryHeadsetPercent = 0.70f,
|
batteryHeadsetPercent = 0.70f,
|
||||||
@@ -135,7 +135,7 @@ object MockPodDataProvider {
|
|||||||
|
|
||||||
// --- Unknown device ---
|
// --- Unknown device ---
|
||||||
|
|
||||||
fun unknownDevice(): PodDevice = UnknownDevice(
|
fun unknownDevice(): BlePodSnapshot = UnknownSnapshotBle(
|
||||||
scanResult = dummyScanResult(rssi = -70),
|
scanResult = dummyScanResult(rssi = -70),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -146,24 +146,24 @@ object MockPodDataProvider {
|
|||||||
model = model,
|
model = model,
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- MonitoredDevice wrappers ---
|
// --- PodDevice wrappers ---
|
||||||
|
|
||||||
fun dualPodMonitored(): MonitoredDevice = MonitoredDevice(
|
fun dualPodMonitored(): PodDevice = PodDevice(
|
||||||
ble = airPodsProFullCharge(),
|
ble = airPodsProFullCharge(),
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun dualPodMonitoredMixed(): MonitoredDevice = MonitoredDevice(
|
fun dualPodMonitoredMixed(): PodDevice = PodDevice(
|
||||||
ble = airPodsProMixed(),
|
ble = airPodsProMixed(),
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun singlePodMonitored(): MonitoredDevice = MonitoredDevice(
|
fun singlePodMonitored(): PodDevice = PodDevice(
|
||||||
ble = airPodsMax(),
|
ble = airPodsMax(),
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun unknownMonitored(): MonitoredDevice = MonitoredDevice(
|
fun unknownMonitored(): PodDevice = PodDevice(
|
||||||
ble = unknownDevice(),
|
ble = unknownDevice(),
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
@@ -188,7 +188,7 @@ private data class MockUpgradeInfo(
|
|||||||
override val error: Throwable? = null,
|
override val error: Throwable? = null,
|
||||||
) : UpgradeRepo.Info
|
) : UpgradeRepo.Info
|
||||||
|
|
||||||
private class MockDualPodDevice(
|
private class MockDualBlePodSnapshot(
|
||||||
private val _model: PodModel,
|
private val _model: PodModel,
|
||||||
private val _label: String,
|
private val _label: String,
|
||||||
override val batteryLeftPodPercent: Float?,
|
override val batteryLeftPodPercent: Float?,
|
||||||
@@ -205,8 +205,8 @@ private class MockDualPodDevice(
|
|||||||
override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right,
|
override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right,
|
||||||
private val _caseIcon: Int = R.drawable.device_airpods_gen1_case,
|
private val _caseIcon: Int = R.drawable.device_airpods_gen1_case,
|
||||||
rssi: Int = -50,
|
rssi: Int = -50,
|
||||||
) : DualPodDevice, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone {
|
) : DualBlePodSnapshot, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone {
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id()
|
||||||
override val model: PodModel = _model
|
override val model: PodModel = _model
|
||||||
override val seenLastAt: Instant = MOCK_NOW
|
override val seenLastAt: Instant = MOCK_NOW
|
||||||
override val seenFirstAt: Instant = MOCK_NOW
|
override val seenFirstAt: Instant = MOCK_NOW
|
||||||
@@ -215,7 +215,7 @@ private class MockDualPodDevice(
|
|||||||
override val reliability: Float = 1.0f
|
override val reliability: Float = 1.0f
|
||||||
override val signalQuality: Float = 0.75f
|
override val signalQuality: Float = 0.75f
|
||||||
override val iconRes: Int = _model.iconRes
|
override val iconRes: Int = _model.iconRes
|
||||||
override val meta: PodDevice.Meta = object : PodDevice.Meta {
|
override val meta: BlePodSnapshot.Meta = object : BlePodSnapshot.Meta {
|
||||||
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
|
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ private class MockDualPodDevice(
|
|||||||
override val isRightPodMicrophone: Boolean = _isRightPodMicrophone
|
override val isRightPodMicrophone: Boolean = _isRightPodMicrophone
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MockDualPodDeviceNoCase(
|
private class MockDualBlePodSnapshotNoCase(
|
||||||
private val _model: PodModel,
|
private val _model: PodModel,
|
||||||
private val _label: String,
|
private val _label: String,
|
||||||
override val batteryLeftPodPercent: Float?,
|
override val batteryLeftPodPercent: Float?,
|
||||||
@@ -247,8 +247,8 @@ private class MockDualPodDeviceNoCase(
|
|||||||
override val leftPodIcon: Int = R.drawable.device_airpods_gen1_left,
|
override val leftPodIcon: Int = R.drawable.device_airpods_gen1_left,
|
||||||
override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right,
|
override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right,
|
||||||
rssi: Int = -50,
|
rssi: Int = -50,
|
||||||
) : DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual {
|
) : DualBlePodSnapshot, HasChargeDetectionDual, HasEarDetectionDual {
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id()
|
||||||
override val model: PodModel = _model
|
override val model: PodModel = _model
|
||||||
override val seenLastAt: Instant = MOCK_NOW
|
override val seenLastAt: Instant = MOCK_NOW
|
||||||
override val seenFirstAt: Instant = MOCK_NOW
|
override val seenFirstAt: Instant = MOCK_NOW
|
||||||
@@ -257,7 +257,7 @@ private class MockDualPodDeviceNoCase(
|
|||||||
override val reliability: Float = 1.0f
|
override val reliability: Float = 1.0f
|
||||||
override val signalQuality: Float = 0.70f
|
override val signalQuality: Float = 0.70f
|
||||||
override val iconRes: Int = _model.iconRes
|
override val iconRes: Int = _model.iconRes
|
||||||
override val meta: PodDevice.Meta = object : PodDevice.Meta {
|
override val meta: BlePodSnapshot.Meta = object : BlePodSnapshot.Meta {
|
||||||
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
|
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,15 +272,15 @@ private class MockDualPodDeviceNoCase(
|
|||||||
override val isRightPodInEar: Boolean = false
|
override val isRightPodInEar: Boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MockSinglePodDevice(
|
private class MockSingleBlePodSnapshot(
|
||||||
private val _model: PodModel,
|
private val _model: PodModel,
|
||||||
private val _label: String,
|
private val _label: String,
|
||||||
override val batteryHeadsetPercent: Float?,
|
override val batteryHeadsetPercent: Float?,
|
||||||
private val _isHeadsetBeingCharged: Boolean = false,
|
private val _isHeadsetBeingCharged: Boolean = false,
|
||||||
private val _isBeingWorn: Boolean = false,
|
private val _isBeingWorn: Boolean = false,
|
||||||
rssi: Int = -50,
|
rssi: Int = -50,
|
||||||
) : SinglePodDevice, HasChargeDetection, HasEarDetection {
|
) : SingleBlePodSnapshot, HasChargeDetection, HasEarDetection {
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id()
|
||||||
override val model: PodModel = _model
|
override val model: PodModel = _model
|
||||||
override val seenLastAt: Instant = MOCK_NOW
|
override val seenLastAt: Instant = MOCK_NOW
|
||||||
override val seenFirstAt: Instant = MOCK_NOW
|
override val seenFirstAt: Instant = MOCK_NOW
|
||||||
@@ -289,7 +289,7 @@ private class MockSinglePodDevice(
|
|||||||
override val reliability: Float = 1.0f
|
override val reliability: Float = 1.0f
|
||||||
override val signalQuality: Float = 0.80f
|
override val signalQuality: Float = 0.80f
|
||||||
override val iconRes: Int = _model.iconRes
|
override val iconRes: Int = _model.iconRes
|
||||||
override val meta: PodDevice.Meta = object : PodDevice.Meta {
|
override val meta: BlePodSnapshot.Meta = object : BlePodSnapshot.Meta {
|
||||||
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
|
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ private class MockSinglePodDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("PropertyName")
|
@Suppress("PropertyName")
|
||||||
private class MockSingleApplePodDevice(
|
private class MockSingleAppleBlePodSnapshot(
|
||||||
private val _model: PodModel,
|
private val _model: PodModel,
|
||||||
private val _label: String,
|
private val _label: String,
|
||||||
override val batteryHeadsetPercent: Float?,
|
override val batteryHeadsetPercent: Float?,
|
||||||
@@ -312,8 +312,8 @@ private class MockSingleApplePodDevice(
|
|||||||
private val _isIRKMatch: Boolean = false,
|
private val _isIRKMatch: Boolean = false,
|
||||||
private val _hasPrivatePayload: Boolean = false,
|
private val _hasPrivatePayload: Boolean = false,
|
||||||
rssi: Int = -50,
|
rssi: Int = -50,
|
||||||
) : SinglePodDevice, ApplePods, HasChargeDetection, HasEarDetection {
|
) : SingleBlePodSnapshot, ApplePods, HasChargeDetection, HasEarDetection {
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id()
|
||||||
override val model: PodModel = _model
|
override val model: PodModel = _model
|
||||||
override val seenLastAt: Instant = MOCK_NOW
|
override val seenLastAt: Instant = MOCK_NOW
|
||||||
override val seenFirstAt: Instant = MOCK_NOW
|
override val seenFirstAt: Instant = MOCK_NOW
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import eu.darken.capod.main.ui.overview.cards.PermissionCard
|
|||||||
import eu.darken.capod.main.ui.overview.cards.SinglePodsCard
|
import eu.darken.capod.main.ui.overview.cards.SinglePodsCard
|
||||||
import eu.darken.capod.main.ui.overview.cards.UnknownPodDeviceCard
|
import eu.darken.capod.main.ui.overview.cards.UnknownPodDeviceCard
|
||||||
import eu.darken.capod.main.ui.overview.cards.UnmatchedDevicesCard
|
import eu.darken.capod.main.ui.overview.cards.UnmatchedDevicesCard
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ fun OverviewScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PodDeviceCard(device: MonitoredDevice, showDebug: Boolean, now: Instant) {
|
private fun PodDeviceCard(device: PodDevice, showDebug: Boolean, now: Instant) {
|
||||||
when {
|
when {
|
||||||
device.hasDualPods -> DualPodsCard(device = device, showDebug = showDebug, now = now)
|
device.hasDualPods -> DualPodsCard(device = device, showDebug = showDebug, now = now)
|
||||||
device.model != PodModel.UNKNOWN -> SinglePodsCard(device = device, showDebug = showDebug, now = now)
|
device.model != PodModel.UNKNOWN -> SinglePodsCard(device = device, showDebug = showDebug, now = now)
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package eu.darken.capod.main.ui.overview
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
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.datastore.valueBlocking
|
||||||
import eu.darken.capod.common.debug.DebugSettings
|
import eu.darken.capod.common.debug.DebugSettings
|
||||||
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
|
||||||
import eu.darken.capod.common.flow.SingleEventFlow
|
import eu.darken.capod.common.flow.SingleEventFlow
|
||||||
import eu.darken.capod.common.flow.combine
|
import eu.darken.capod.common.flow.combine
|
||||||
|
|
||||||
import eu.darken.capod.common.flow.throttleLatest
|
import eu.darken.capod.common.flow.throttleLatest
|
||||||
import eu.darken.capod.common.navigation.Nav
|
import eu.darken.capod.common.navigation.Nav
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
@@ -18,11 +18,10 @@ import eu.darken.capod.main.core.GeneralSettings
|
|||||||
import eu.darken.capod.main.core.MonitorMode
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
import java.time.Instant
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
@@ -33,8 +32,8 @@ import kotlinx.coroutines.flow.flowOf
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import eu.darken.capod.common.datastore.valueBlocking
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class OverviewViewModel @Inject constructor(
|
class OverviewViewModel @Inject constructor(
|
||||||
@@ -71,6 +70,7 @@ class OverviewViewModel @Inject constructor(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MonitorMode.ALWAYS -> true
|
MonitorMode.ALWAYS -> true
|
||||||
}
|
}
|
||||||
if (shouldStart) {
|
if (shouldStart) {
|
||||||
@@ -122,7 +122,7 @@ class OverviewViewModel @Inject constructor(
|
|||||||
data class State(
|
data class State(
|
||||||
val now: Instant,
|
val now: Instant,
|
||||||
val permissions: Set<Permission>,
|
val permissions: Set<Permission>,
|
||||||
val devices: List<MonitoredDevice>,
|
val devices: List<PodDevice>,
|
||||||
val isDebugMode: Boolean,
|
val isDebugMode: Boolean,
|
||||||
val isBluetoothEnabled: Boolean,
|
val isBluetoothEnabled: Boolean,
|
||||||
val profiles: List<DeviceProfile>,
|
val profiles: List<DeviceProfile>,
|
||||||
@@ -130,8 +130,8 @@ class OverviewViewModel @Inject constructor(
|
|||||||
val showUnmatchedDevices: Boolean,
|
val showUnmatchedDevices: Boolean,
|
||||||
) {
|
) {
|
||||||
val isScanBlocked: Boolean get() = permissions.any { it.isScanBlocking }
|
val isScanBlocked: Boolean get() = permissions.any { it.isScanBlocking }
|
||||||
val profiledDevices: List<MonitoredDevice> get() = devices.filter { it.meta?.profile != null }
|
val profiledDevices: List<PodDevice> get() = devices.filter { it.meta?.profile != null }
|
||||||
val unmatchedDevices: List<MonitoredDevice> get() = devices.filter { it.meta?.profile == null }
|
val unmatchedDevices: List<PodDevice> get() = devices.filter { it.meta?.profile == null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPermissionResult(@Suppress("UNUSED_PARAMETER") granted: Boolean) {
|
fun onPermissionResult(@Suppress("UNUSED_PARAMETER") granted: Boolean) {
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ import eu.darken.capod.R
|
|||||||
import eu.darken.capod.common.compose.Preview2
|
import eu.darken.capod.common.compose.Preview2
|
||||||
import eu.darken.capod.common.compose.PreviewWrapper
|
import eu.darken.capod.common.compose.PreviewWrapper
|
||||||
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
|
import eu.darken.capod.monitor.core.firstSeenFormatted
|
||||||
import eu.darken.capod.monitor.core.getSignalQuality
|
import eu.darken.capod.monitor.core.getSignalQuality
|
||||||
import eu.darken.capod.monitor.core.lastSeenFormatted
|
import eu.darken.capod.monitor.core.lastSeenFormatted
|
||||||
import eu.darken.capod.monitor.core.firstSeenFormatted
|
|
||||||
import eu.darken.capod.pods.core.HasPodStyle
|
import eu.darken.capod.pods.core.HasPodStyle
|
||||||
import eu.darken.capod.pods.core.HasStateDetection
|
import eu.darken.capod.pods.core.HasStateDetection
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
@@ -61,7 +61,7 @@ import java.time.Instant
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DualPodsCard(
|
fun DualPodsCard(
|
||||||
device: MonitoredDevice,
|
device: PodDevice,
|
||||||
showDebug: Boolean,
|
showDebug: Boolean,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
) {
|
) {
|
||||||
@@ -316,7 +316,7 @@ private fun PodGauge(
|
|||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun CaseRow(
|
private fun CaseRow(
|
||||||
device: MonitoredDevice,
|
device: PodDevice,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import androidx.compose.material.icons.twotone.BatteryChargingFull
|
|||||||
import androidx.compose.material.icons.twotone.Hearing
|
import androidx.compose.material.icons.twotone.Hearing
|
||||||
import androidx.compose.material.icons.twotone.Key
|
import androidx.compose.material.icons.twotone.Key
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -44,10 +44,10 @@ import eu.darken.capod.R
|
|||||||
import eu.darken.capod.common.compose.Preview2
|
import eu.darken.capod.common.compose.Preview2
|
||||||
import eu.darken.capod.common.compose.PreviewWrapper
|
import eu.darken.capod.common.compose.PreviewWrapper
|
||||||
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
|
import eu.darken.capod.monitor.core.firstSeenFormatted
|
||||||
import eu.darken.capod.monitor.core.getSignalQuality
|
import eu.darken.capod.monitor.core.getSignalQuality
|
||||||
import eu.darken.capod.monitor.core.lastSeenFormatted
|
import eu.darken.capod.monitor.core.lastSeenFormatted
|
||||||
import eu.darken.capod.monitor.core.firstSeenFormatted
|
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -56,7 +56,7 @@ import java.time.Instant
|
|||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SinglePodsCard(
|
fun SinglePodsCard(
|
||||||
device: MonitoredDevice,
|
device: PodDevice,
|
||||||
showDebug: Boolean,
|
showDebug: Boolean,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
|
||||||
import eu.darken.capod.monitor.core.getSignalQuality
|
|
||||||
import eu.darken.capod.monitor.core.lastSeenFormatted
|
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
|
||||||
import eu.darken.capod.common.compose.Preview2
|
import eu.darken.capod.common.compose.Preview2
|
||||||
import eu.darken.capod.common.compose.PreviewWrapper
|
import eu.darken.capod.common.compose.PreviewWrapper
|
||||||
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||||
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
|
import eu.darken.capod.monitor.core.getSignalQuality
|
||||||
|
import eu.darken.capod.monitor.core.lastSeenFormatted
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UnknownPodDeviceCard(
|
fun UnknownPodDeviceCard(
|
||||||
device: MonitoredDevice,
|
device: PodDevice,
|
||||||
showDebug: Boolean,
|
showDebug: Boolean,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.darken.capod.main.ui.widget
|
|||||||
|
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.Keep
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.glance.GlanceId
|
import androidx.glance.GlanceId
|
||||||
@@ -10,7 +11,6 @@ import androidx.glance.appwidget.GlanceAppWidget
|
|||||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
import androidx.glance.appwidget.SizeMode
|
import androidx.glance.appwidget.SizeMode
|
||||||
import androidx.glance.appwidget.provideContent
|
import androidx.glance.appwidget.provideContent
|
||||||
import androidx.annotation.Keep
|
|
||||||
import dagger.hilt.EntryPoint
|
import dagger.hilt.EntryPoint
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
@@ -23,7 +23,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
import eu.darken.capod.common.upgrade.isPro
|
import eu.darken.capod.common.upgrade.isPro
|
||||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
|
|
||||||
class BatteryGlanceWidget : GlanceAppWidget() {
|
class BatteryGlanceWidget : GlanceAppWidget() {
|
||||||
@@ -45,7 +45,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
|
|||||||
val appWidgetId: Int
|
val appWidgetId: Int
|
||||||
val initialIsPro: Boolean
|
val initialIsPro: Boolean
|
||||||
val initialProfileId: String?
|
val initialProfileId: String?
|
||||||
val cachedDevice: MonitoredDevice?
|
val cachedDevice: PodDevice?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ep = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java)
|
ep = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java)
|
||||||
@@ -139,7 +139,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
|
|||||||
var n = 2
|
var n = 2
|
||||||
while (70 * n - 30 < size) {
|
while (70 * n - 30 < size) {
|
||||||
++n
|
++n
|
||||||
}
|
}
|
||||||
return n - 1
|
return n - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||||
import eu.darken.capod.pods.core.toBatteryFloat
|
import eu.darken.capod.pods.core.toBatteryFloat
|
||||||
@@ -13,7 +13,7 @@ object WidgetRenderStateMapper {
|
|||||||
|
|
||||||
fun map(
|
fun map(
|
||||||
context: Context,
|
context: Context,
|
||||||
device: MonitoredDevice?,
|
device: PodDevice?,
|
||||||
theme: WidgetTheme,
|
theme: WidgetTheme,
|
||||||
isPro: Boolean,
|
isPro: Boolean,
|
||||||
hasConfiguredProfile: Boolean,
|
hasConfiguredProfile: Boolean,
|
||||||
|
|||||||
@@ -1,12 +1,217 @@
|
|||||||
package eu.darken.capod.monitor.core
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
/**
|
import android.bluetooth.le.ScanFilter
|
||||||
* Type alias for the BLE scan pipeline.
|
import eu.darken.capod.common.bluetooth.BleScanner
|
||||||
*
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
* The [PodMonitor] class handles BLE advertisement scanning and device detection.
|
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||||
* New code should use [BlePodMonitor] to clarify that this is the BLE-only monitor,
|
import eu.darken.capod.common.bluetooth.onlyNewAndUnique
|
||||||
* not the unified merge point ([DeviceMonitor]).
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
*
|
import eu.darken.capod.common.debug.DebugSettings
|
||||||
* Full rename deferred to IDE refactoring pass.
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
*/
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
typealias BlePodMonitor = PodMonitor
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.flow.replayingShare
|
||||||
|
import eu.darken.capod.common.flow.throttleLatest
|
||||||
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
|
import eu.darken.capod.pods.core.PodFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
|
import eu.darken.capod.profiles.core.currentProfiles
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.retryWhen
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BlePodMonitor @Inject constructor(
|
||||||
|
@AppScope private val appScope: CoroutineScope,
|
||||||
|
private val bleScanner: BleScanner,
|
||||||
|
private val podFactory: PodFactory,
|
||||||
|
private val generalSettings: GeneralSettings,
|
||||||
|
bluetoothManager: BluetoothManager2,
|
||||||
|
private val debugSettings: DebugSettings,
|
||||||
|
private val podDeviceCache: PodDeviceCache,
|
||||||
|
permissionTool: PermissionTool,
|
||||||
|
private val profilesRepo: DeviceProfilesRepo,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val deviceCache = mutableMapOf<BlePodSnapshot.Id, BlePodSnapshot>()
|
||||||
|
private val cacheLock = Mutex()
|
||||||
|
|
||||||
|
val devices: Flow<List<BlePodSnapshot>> = combine(
|
||||||
|
permissionTool.missingScanPermissions,
|
||||||
|
bluetoothManager.isBluetoothEnabled
|
||||||
|
) { missingScanPermissions, isBluetoothEnabled ->
|
||||||
|
log(TAG) { "devices: missingScanPermissions=$missingScanPermissions, isBluetoothEnabled=$isBluetoothEnabled" }
|
||||||
|
missingScanPermissions.isEmpty() && isBluetoothEnabled
|
||||||
|
}
|
||||||
|
.flatMapLatest { isReady ->
|
||||||
|
if (!isReady) {
|
||||||
|
log(TAG, WARN) { "Bluetooth is not ready" }
|
||||||
|
flowOf(null)
|
||||||
|
} else {
|
||||||
|
createBleScanner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { results -> results?.mapNotNull { podFactory.createPod(it) } }
|
||||||
|
.map { processWithCache(it).values }
|
||||||
|
.flatMapLatest { devices ->
|
||||||
|
flowOf(sortPodsToInterest(devices))
|
||||||
|
}
|
||||||
|
.retryWhen { cause, attempt ->
|
||||||
|
if (cause is SecurityException) {
|
||||||
|
log(TAG, WARN) { "PodMonitor failed due to missing permission, not retrying: ${cause.asLog()}" }
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
|
||||||
|
delay(3000)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onStart { emit(emptyList()) }
|
||||||
|
.replayingShare(appScope)
|
||||||
|
|
||||||
|
private suspend fun sortPodsToInterest(devices: Collection<BlePodSnapshot>): List<BlePodSnapshot> {
|
||||||
|
val now = Instant.now()
|
||||||
|
val profiles = profilesRepo.currentProfiles() ?: emptyList()
|
||||||
|
|
||||||
|
return devices.sortedWith(
|
||||||
|
compareBy<BlePodSnapshot> { device ->
|
||||||
|
// Use profile position in list as priority (0 = highest priority)
|
||||||
|
device.meta.profile?.let { profile ->
|
||||||
|
profiles.indexOfFirst { it.id == profile.id }.takeIf { it >= 0 } ?: Int.MAX_VALUE
|
||||||
|
} ?: Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
.thenBy {
|
||||||
|
val age = Duration.between(it.seenLastAt, now).seconds
|
||||||
|
if (age < 5) 0L else (age / 3L)
|
||||||
|
}
|
||||||
|
.thenByDescending { it.signalQuality }
|
||||||
|
.thenByDescending { (it.seenCounter / 10) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ScannerOptions(
|
||||||
|
val scannerMode: ScannerMode,
|
||||||
|
val showUnfiltered: Boolean,
|
||||||
|
val offloadedFilteringDisabled: Boolean,
|
||||||
|
val offloadedBatchingDisabled: Boolean,
|
||||||
|
val disableDirectCallback: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createBleScanner() = combine(
|
||||||
|
generalSettings.scannerMode.flow,
|
||||||
|
debugSettings.showUnfiltered.flow,
|
||||||
|
generalSettings.isOffloadedBatchingDisabled.flow,
|
||||||
|
generalSettings.isOffloadedFilteringDisabled.flow,
|
||||||
|
generalSettings.useIndirectScanResultCallback.flow,
|
||||||
|
) {
|
||||||
|
scannermode,
|
||||||
|
showUnfiltered,
|
||||||
|
isOffloadedBatchingDisabled,
|
||||||
|
isOffloadedFilteringDisabled,
|
||||||
|
useIndirectScanResultCallback,
|
||||||
|
->
|
||||||
|
ScannerOptions(
|
||||||
|
scannerMode = scannermode,
|
||||||
|
showUnfiltered = showUnfiltered,
|
||||||
|
offloadedFilteringDisabled = isOffloadedFilteringDisabled,
|
||||||
|
offloadedBatchingDisabled = isOffloadedBatchingDisabled,
|
||||||
|
disableDirectCallback = useIndirectScanResultCallback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.throttleLatest(1000)
|
||||||
|
.flatMapLatest { options ->
|
||||||
|
val filters = when {
|
||||||
|
options.showUnfiltered -> {
|
||||||
|
log(TAG, WARN) { "Using unfiltered scan mode" }
|
||||||
|
setOf(ScanFilter.Builder().build())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> ProximityPairing.getBleScanFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
bleScanner.scan(
|
||||||
|
filters = filters,
|
||||||
|
scannerMode = options.scannerMode,
|
||||||
|
disableOffloadFiltering = options.offloadedFilteringDisabled,
|
||||||
|
disableOffloadBatching = options.offloadedBatchingDisabled,
|
||||||
|
disableDirectScanCallback = options.disableDirectCallback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map { it.onlyNewAndUnique() }
|
||||||
|
|
||||||
|
private suspend fun processWithCache(
|
||||||
|
newPods: List<PodFactory.Result>?
|
||||||
|
): Map<BlePodSnapshot.Id, BlePodSnapshot> = cacheLock.withLock {
|
||||||
|
if (newPods == null) {
|
||||||
|
log(TAG) { "Null result, Bluetooth is disabled." }
|
||||||
|
deviceCache.clear()
|
||||||
|
return emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Instant.now()
|
||||||
|
deviceCache.toList().forEach { (key, value) ->
|
||||||
|
if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) {
|
||||||
|
log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
|
||||||
|
deviceCache.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pods = mutableMapOf<BlePodSnapshot.Id, BlePodSnapshot>()
|
||||||
|
|
||||||
|
pods.putAll(deviceCache)
|
||||||
|
|
||||||
|
newPods.map { it.device }.forEach {
|
||||||
|
deviceCache[it.identifier] = it
|
||||||
|
pods[it.identifier] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
newPods
|
||||||
|
.mapNotNull {
|
||||||
|
val profileId = it.device.meta.profile?.id ?: return@mapNotNull null
|
||||||
|
profileId to it.device.scanResult
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
.run { podDeviceCache.saveAll(this) }
|
||||||
|
return pods
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getDeviceForProfile(profileId: String): BlePodSnapshot? {
|
||||||
|
log(TAG) { "getDeviceForProfile(profileId=$profileId)" }
|
||||||
|
|
||||||
|
val liveDevice = devices.firstOrNull()?.firstOrNull { device ->
|
||||||
|
device.meta.profile?.id == profileId
|
||||||
|
}
|
||||||
|
if (liveDevice != null) {
|
||||||
|
log(TAG) { "Found live device for profile $profileId: $liveDevice" }
|
||||||
|
return liveDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
val cachedDevice = podDeviceCache.load(profileId)?.let {
|
||||||
|
podFactory.createPod(it)?.device
|
||||||
|
}
|
||||||
|
log(TAG) { "Cached device for profile $profileId: $cachedDevice" }
|
||||||
|
return cachedDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = logTag("Monitor", "PodMonitor")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import javax.inject.Singleton
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Single merge point: combines BLE scan data ([BlePodMonitor]) with AAP connection data
|
* Single merge point: combines BLE scan data ([BlePodMonitor]) with AAP connection data
|
||||||
* ([AapConnectionManager]) into unified [MonitoredDevice] objects.
|
* ([AapConnectionManager]) into unified [PodDevice] objects.
|
||||||
*
|
*
|
||||||
* ViewModels should observe [devices] instead of accessing BlePodMonitor directly.
|
* ViewModels should observe [devices] instead of accessing BlePodMonitor directly.
|
||||||
*/
|
*/
|
||||||
@@ -20,21 +20,21 @@ class DeviceMonitor @Inject constructor(
|
|||||||
private val blePodMonitor: BlePodMonitor,
|
private val blePodMonitor: BlePodMonitor,
|
||||||
private val aapManager: AapConnectionManager,
|
private val aapManager: AapConnectionManager,
|
||||||
) {
|
) {
|
||||||
val devices: Flow<List<MonitoredDevice>> = blePodMonitor.devices
|
val devices: Flow<List<PodDevice>> = blePodMonitor.devices
|
||||||
.combine(aapManager.allStates) { pods, aapStates ->
|
.combine(aapManager.allStates) { pods, aapStates ->
|
||||||
pods.map { pod ->
|
pods.map { pod ->
|
||||||
MonitoredDevice(
|
PodDevice(
|
||||||
ble = pod,
|
ble = pod,
|
||||||
aap = aapStates[pod.address],
|
aap = aapStates[pod.address],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDeviceForProfile(profileId: String): MonitoredDevice? {
|
suspend fun getDeviceForProfile(profileId: String): PodDevice? {
|
||||||
log(TAG) { "getDeviceForProfile(profileId=$profileId)" }
|
log(TAG) { "getDeviceForProfile(profileId=$profileId)" }
|
||||||
val bleDevice = blePodMonitor.getDeviceForProfile(profileId) ?: return null
|
val bleDevice = blePodMonitor.getDeviceForProfile(profileId) ?: return null
|
||||||
val aapState = aapManager.allStates.firstOrNull()?.get(bleDevice.address)
|
val aapState = aapManager.allStates.firstOrNull()?.get(bleDevice.address)
|
||||||
return MonitoredDevice(ble = bleDevice, aap = aapState)
|
return PodDevice(ble = bleDevice, aap = aapState)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ import java.time.Duration
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun DeviceMonitor.devicesWithProfiles(): Flow<List<MonitoredDevice>> = devices
|
fun DeviceMonitor.devicesWithProfiles(): Flow<List<PodDevice>> = devices
|
||||||
.map { devices -> devices.filter { it.meta?.profile != null } }
|
.map { devices -> devices.filter { it.meta?.profile != null } }
|
||||||
|
|
||||||
fun DeviceMonitor.primaryDevice(): Flow<MonitoredDevice?> = devicesWithProfiles().map { it.firstOrNull() }
|
fun DeviceMonitor.primaryDevice(): Flow<PodDevice?> = devicesWithProfiles().map { it.firstOrNull() }
|
||||||
|
|
||||||
fun MonitoredDevice.getSignalQuality(context: Context): String {
|
fun PodDevice.getSignalQuality(context: Context): String {
|
||||||
val multiplier = 100 * signalQuality
|
val multiplier = 100 * signalQuality
|
||||||
return "${multiplier.roundToInt()}%"
|
return "${multiplier.roundToInt()}%"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MonitoredDevice.lastSeenFormatted(now: Instant): String {
|
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||||
val lastAt = seenLastAt ?: return ""
|
val lastAt = seenLastAt ?: return ""
|
||||||
val formatter = RelativeDateTimeFormatter.getInstance()
|
val formatter = RelativeDateTimeFormatter.getInstance()
|
||||||
val duration = Duration.between(lastAt, now)
|
val duration = Duration.between(lastAt, now)
|
||||||
@@ -37,7 +37,7 @@ fun MonitoredDevice.lastSeenFormatted(now: Instant): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MonitoredDevice.firstSeenFormatted(now: Instant): String {
|
fun PodDevice.firstSeenFormatted(now: Instant): String {
|
||||||
val firstAt = seenFirstAt ?: return ""
|
val firstAt = seenFirstAt ?: return ""
|
||||||
val formatter = RelativeDateTimeFormatter.getInstance()
|
val formatter = RelativeDateTimeFormatter.getInstance()
|
||||||
val duration = Duration.between(firstAt, now)
|
val duration = Duration.between(firstAt, now)
|
||||||
|
|||||||
+10
-11
@@ -3,16 +3,15 @@ package eu.darken.capod.monitor.core
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.DualBlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetection
|
import eu.darken.capod.pods.core.HasChargeDetection
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.SinglePodDevice
|
import eu.darken.capod.pods.core.SingleBlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.protocol.aap.AapPodState
|
import eu.darken.capod.pods.core.apple.protocol.aap.AapPodState
|
||||||
import eu.darken.capod.pods.core.apple.protocol.aap.AapSetting
|
import eu.darken.capod.pods.core.apple.protocol.aap.AapSetting
|
||||||
@@ -23,15 +22,15 @@ import java.time.Instant
|
|||||||
* All properties are dynamically resolved from the best available source.
|
* All properties are dynamically resolved from the best available source.
|
||||||
* Consumers don't need to know whether data came from BLE or AAP.
|
* Consumers don't need to know whether data came from BLE or AAP.
|
||||||
*/
|
*/
|
||||||
data class MonitoredDevice(
|
data class PodDevice(
|
||||||
internal val ble: BlePodSnapshot?,
|
internal val ble: BlePodSnapshot?,
|
||||||
internal val aap: AapPodState?,
|
internal val aap: AapPodState?,
|
||||||
) {
|
) {
|
||||||
// Identity
|
// Identity
|
||||||
val model: PodModel get() = ble?.model ?: PodModel.UNKNOWN
|
val model: PodModel get() = ble?.model ?: PodModel.UNKNOWN
|
||||||
val address: BluetoothAddress? get() = ble?.address
|
val address: BluetoothAddress? get() = ble?.address
|
||||||
val identifier: PodDevice.Id? get() = ble?.identifier
|
val identifier: BlePodSnapshot.Id? get() = ble?.identifier
|
||||||
val meta: PodDevice.Meta? get() = ble?.meta
|
val meta: BlePodSnapshot.Meta? get() = ble?.meta
|
||||||
|
|
||||||
// Capabilities from Model.Features
|
// Capabilities from Model.Features
|
||||||
val hasCase: Boolean get() = model.features.hasCase
|
val hasCase: Boolean get() = model.features.hasCase
|
||||||
@@ -48,16 +47,16 @@ data class MonitoredDevice(
|
|||||||
|
|
||||||
// Battery — best available source
|
// Battery — best available source
|
||||||
val batteryLeft: Float?
|
val batteryLeft: Float?
|
||||||
get() = (ble as? DualPodDevice)?.batteryLeftPodPercent
|
get() = (ble as? DualBlePodSnapshot)?.batteryLeftPodPercent
|
||||||
|
|
||||||
val batteryRight: Float?
|
val batteryRight: Float?
|
||||||
get() = (ble as? DualPodDevice)?.batteryRightPodPercent
|
get() = (ble as? DualBlePodSnapshot)?.batteryRightPodPercent
|
||||||
|
|
||||||
val batteryCase: Float?
|
val batteryCase: Float?
|
||||||
get() = (ble as? HasCase)?.batteryCasePercent
|
get() = (ble as? HasCase)?.batteryCasePercent
|
||||||
|
|
||||||
val batteryHeadset: Float?
|
val batteryHeadset: Float?
|
||||||
get() = (ble as? SinglePodDevice)?.batteryHeadsetPercent
|
get() = (ble as? SingleBlePodSnapshot)?.batteryHeadsetPercent
|
||||||
|
|
||||||
// Charging
|
// Charging
|
||||||
val isLeftPodCharging: Boolean?
|
val isLeftPodCharging: Boolean?
|
||||||
@@ -99,10 +98,10 @@ data class MonitoredDevice(
|
|||||||
val iconRes: Int get() = ble?.iconRes ?: model.iconRes
|
val iconRes: Int get() = ble?.iconRes ?: model.iconRes
|
||||||
|
|
||||||
val leftPodIcon: Int?
|
val leftPodIcon: Int?
|
||||||
get() = (ble as? DualPodDevice)?.leftPodIcon
|
get() = (ble as? DualBlePodSnapshot)?.leftPodIcon
|
||||||
|
|
||||||
val rightPodIcon: Int?
|
val rightPodIcon: Int?
|
||||||
get() = (ble as? DualPodDevice)?.rightPodIcon
|
get() = (ble as? DualBlePodSnapshot)?.rightPodIcon
|
||||||
|
|
||||||
val caseIcon: Int?
|
val caseIcon: Int?
|
||||||
get() = (ble as? HasCase)?.caseIcon
|
get() = (ble as? HasCase)?.caseIcon
|
||||||
@@ -78,7 +78,7 @@ class PodDeviceCache @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Monitor", "PodMonitor", "Cache")
|
private val TAG = logTag("Monitor", "BlePodMonitor", "Cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core
|
|
||||||
|
|
||||||
import android.bluetooth.le.ScanFilter
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanner
|
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
|
||||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
|
||||||
import eu.darken.capod.common.bluetooth.onlyNewAndUnique
|
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
|
||||||
import eu.darken.capod.common.debug.DebugSettings
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
|
||||||
import eu.darken.capod.common.debug.logging.asLog
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
|
||||||
import eu.darken.capod.common.flow.replayingShare
|
|
||||||
import eu.darken.capod.common.flow.throttleLatest
|
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodFactory
|
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
|
||||||
import eu.darken.capod.profiles.core.currentProfiles
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.flow.retryWhen
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class PodMonitor @Inject constructor(
|
|
||||||
@AppScope private val appScope: CoroutineScope,
|
|
||||||
private val bleScanner: BleScanner,
|
|
||||||
private val podFactory: PodFactory,
|
|
||||||
private val generalSettings: GeneralSettings,
|
|
||||||
bluetoothManager: BluetoothManager2,
|
|
||||||
private val debugSettings: DebugSettings,
|
|
||||||
private val podDeviceCache: PodDeviceCache,
|
|
||||||
permissionTool: PermissionTool,
|
|
||||||
private val profilesRepo: DeviceProfilesRepo,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
|
||||||
private val cacheLock = Mutex()
|
|
||||||
|
|
||||||
val devices: Flow<List<PodDevice>> = combine(
|
|
||||||
permissionTool.missingScanPermissions,
|
|
||||||
bluetoothManager.isBluetoothEnabled
|
|
||||||
) { missingScanPermissions, isBluetoothEnabled ->
|
|
||||||
log(TAG) { "devices: missingScanPermissions=$missingScanPermissions, isBluetoothEnabled=$isBluetoothEnabled" }
|
|
||||||
missingScanPermissions.isEmpty() && isBluetoothEnabled
|
|
||||||
}
|
|
||||||
.flatMapLatest { isReady ->
|
|
||||||
if (!isReady) {
|
|
||||||
log(TAG, WARN) { "Bluetooth is not ready" }
|
|
||||||
flowOf(null)
|
|
||||||
} else {
|
|
||||||
createBleScanner()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { results -> results?.mapNotNull { podFactory.createPod(it) } }
|
|
||||||
.map { processWithCache(it).values }
|
|
||||||
.flatMapLatest { devices ->
|
|
||||||
flowOf(sortPodsToInterest(devices))
|
|
||||||
}
|
|
||||||
.retryWhen { cause, attempt ->
|
|
||||||
if (cause is SecurityException) {
|
|
||||||
log(TAG, WARN) { "PodMonitor failed due to missing permission, not retrying: ${cause.asLog()}" }
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
|
|
||||||
delay(3000)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onStart { emit(emptyList()) }
|
|
||||||
.replayingShare(appScope)
|
|
||||||
|
|
||||||
private suspend fun sortPodsToInterest(devices: Collection<PodDevice>): List<PodDevice> {
|
|
||||||
val now = Instant.now()
|
|
||||||
val profiles = profilesRepo.currentProfiles() ?: emptyList()
|
|
||||||
|
|
||||||
return devices.sortedWith(
|
|
||||||
compareBy<PodDevice> { device ->
|
|
||||||
// Use profile position in list as priority (0 = highest priority)
|
|
||||||
device.meta.profile?.let { profile ->
|
|
||||||
profiles.indexOfFirst { it.id == profile.id }.takeIf { it >= 0 } ?: Int.MAX_VALUE
|
|
||||||
} ?: Int.MAX_VALUE
|
|
||||||
}
|
|
||||||
.thenBy {
|
|
||||||
val age = Duration.between(it.seenLastAt, now).seconds
|
|
||||||
if (age < 5) 0L else (age / 3L)
|
|
||||||
}
|
|
||||||
.thenByDescending { it.signalQuality }
|
|
||||||
.thenByDescending { (it.seenCounter / 10) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class ScannerOptions(
|
|
||||||
val scannerMode: ScannerMode,
|
|
||||||
val showUnfiltered: Boolean,
|
|
||||||
val offloadedFilteringDisabled: Boolean,
|
|
||||||
val offloadedBatchingDisabled: Boolean,
|
|
||||||
val disableDirectCallback: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun createBleScanner() = combine(
|
|
||||||
generalSettings.scannerMode.flow,
|
|
||||||
debugSettings.showUnfiltered.flow,
|
|
||||||
generalSettings.isOffloadedBatchingDisabled.flow,
|
|
||||||
generalSettings.isOffloadedFilteringDisabled.flow,
|
|
||||||
generalSettings.useIndirectScanResultCallback.flow,
|
|
||||||
) {
|
|
||||||
scannermode,
|
|
||||||
showUnfiltered,
|
|
||||||
isOffloadedBatchingDisabled,
|
|
||||||
isOffloadedFilteringDisabled,
|
|
||||||
useIndirectScanResultCallback,
|
|
||||||
->
|
|
||||||
ScannerOptions(
|
|
||||||
scannerMode = scannermode,
|
|
||||||
showUnfiltered = showUnfiltered,
|
|
||||||
offloadedFilteringDisabled = isOffloadedFilteringDisabled,
|
|
||||||
offloadedBatchingDisabled = isOffloadedBatchingDisabled,
|
|
||||||
disableDirectCallback = useIndirectScanResultCallback,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.throttleLatest(1000)
|
|
||||||
.flatMapLatest { options ->
|
|
||||||
val filters = when {
|
|
||||||
options.showUnfiltered -> {
|
|
||||||
log(TAG, WARN) { "Using unfiltered scan mode" }
|
|
||||||
setOf(ScanFilter.Builder().build())
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> ProximityPairing.getBleScanFilter()
|
|
||||||
}
|
|
||||||
|
|
||||||
bleScanner.scan(
|
|
||||||
filters = filters,
|
|
||||||
scannerMode = options.scannerMode,
|
|
||||||
disableOffloadFiltering = options.offloadedFilteringDisabled,
|
|
||||||
disableOffloadBatching = options.offloadedBatchingDisabled,
|
|
||||||
disableDirectScanCallback = options.disableDirectCallback,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.map { it.onlyNewAndUnique() }
|
|
||||||
|
|
||||||
private suspend fun processWithCache(
|
|
||||||
newPods: List<PodFactory.Result>?
|
|
||||||
): Map<PodDevice.Id, PodDevice> = cacheLock.withLock {
|
|
||||||
if (newPods == null) {
|
|
||||||
log(TAG) { "Null result, Bluetooth is disabled." }
|
|
||||||
deviceCache.clear()
|
|
||||||
return emptyMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Instant.now()
|
|
||||||
deviceCache.toList().forEach { (key, value) ->
|
|
||||||
if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) {
|
|
||||||
log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
|
|
||||||
deviceCache.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
|
||||||
|
|
||||||
pods.putAll(deviceCache)
|
|
||||||
|
|
||||||
newPods.map { it.device }.forEach {
|
|
||||||
deviceCache[it.identifier] = it
|
|
||||||
pods[it.identifier] = it
|
|
||||||
}
|
|
||||||
|
|
||||||
newPods
|
|
||||||
.mapNotNull {
|
|
||||||
val profileId = it.device.meta.profile?.id ?: return@mapNotNull null
|
|
||||||
profileId to it.device.scanResult
|
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
.run { podDeviceCache.saveAll(this) }
|
|
||||||
return pods
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getDeviceForProfile(profileId: String): PodDevice? {
|
|
||||||
log(TAG) { "getDeviceForProfile(profileId=$profileId)" }
|
|
||||||
|
|
||||||
val liveDevice = devices.firstOrNull()?.firstOrNull { device ->
|
|
||||||
device.meta.profile?.id == profileId
|
|
||||||
}
|
|
||||||
if (liveDevice != null) {
|
|
||||||
log(TAG) { "Found live device for profile $profileId: $liveDevice" }
|
|
||||||
return liveDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
val cachedDevice = podDeviceCache.load(profileId)?.let {
|
|
||||||
podFactory.createPod(it)?.device
|
|
||||||
}
|
|
||||||
log(TAG) { "Cached device for profile $profileId: $cachedDevice" }
|
|
||||||
return cachedDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG = logTag("Monitor", "PodMonitor")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package eu.darken.capod.monitor.core
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlin.collections.firstOrNull
|
|
||||||
|
|
||||||
fun PodMonitor.devicesWithProfiles(): Flow<List<PodDevice>> = devices
|
fun BlePodMonitor.devicesWithProfiles(): Flow<List<BlePodSnapshot>> = devices
|
||||||
.map { devices -> devices.filter { it.meta.profile != null } }
|
.map { devices -> devices.filter { it.meta.profile != null } }
|
||||||
|
|
||||||
fun PodMonitor.primaryDevice(): Flow<PodDevice?> = devicesWithProfiles().map { it.firstOrNull() }
|
fun BlePodMonitor.primaryDevice(): Flow<BlePodSnapshot?> = devicesWithProfiles().map { it.firstOrNull() }
|
||||||
@@ -14,6 +14,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||||
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.datastore.valueBlocking
|
||||||
import eu.darken.capod.common.debug.Bugs
|
import eu.darken.capod.common.debug.Bugs
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
@@ -26,9 +27,9 @@ import eu.darken.capod.common.hasApiLevel
|
|||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.main.core.MonitorMode
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
|
import eu.darken.capod.monitor.core.BlePodMonitor
|
||||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
|
||||||
import eu.darken.capod.monitor.core.primaryDevice
|
import eu.darken.capod.monitor.core.primaryDevice
|
||||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
@@ -54,7 +55,6 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import eu.darken.capod.common.datastore.valueBlocking
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MonitorService : Service() {
|
class MonitorService : Service() {
|
||||||
@@ -64,7 +64,7 @@ class MonitorService : Service() {
|
|||||||
@Inject lateinit var notificationManager: NotificationManager
|
@Inject lateinit var notificationManager: NotificationManager
|
||||||
@Inject lateinit var generalSettings: GeneralSettings
|
@Inject lateinit var generalSettings: GeneralSettings
|
||||||
@Inject lateinit var permissionTool: PermissionTool
|
@Inject lateinit var permissionTool: PermissionTool
|
||||||
@Inject lateinit var podMonitor: PodMonitor
|
@Inject lateinit var blePodMonitor: BlePodMonitor
|
||||||
@Inject lateinit var deviceMonitor: DeviceMonitor
|
@Inject lateinit var deviceMonitor: DeviceMonitor
|
||||||
@Inject lateinit var bluetoothManager: BluetoothManager2
|
@Inject lateinit var bluetoothManager: BluetoothManager2
|
||||||
@Inject lateinit var playPause: PlayPause
|
@Inject lateinit var playPause: PlayPause
|
||||||
@@ -179,7 +179,7 @@ class MonitorService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val monitorJob = deviceMonitor.primaryDevice()
|
val monitorJob = deviceMonitor.primaryDevice()
|
||||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
.setupCommonEventHandlers(TAG) { "BlePodMonitor" }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.throttleLatest(1000)
|
.throttleLatest(1000)
|
||||||
.onEach { currentDevice ->
|
.onEach { currentDevice ->
|
||||||
@@ -222,6 +222,7 @@ class MonitorService : Service() {
|
|||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val profiles = arguments[1] as List<DeviceProfile>
|
val profiles = arguments[1] as List<DeviceProfile>
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val devices = arguments[2] as Collection<BluetoothDevice2>
|
val devices = arguments[2] as Collection<BluetoothDevice2>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import android.view.View
|
|||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||||
@@ -17,13 +17,13 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
@ApplicationContext private val context: Context
|
@ApplicationContext private val context: Context
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createContentView(device: MonitoredDevice): RemoteViews = when {
|
fun createContentView(device: PodDevice): RemoteViews = when {
|
||||||
device.hasDualPods -> createDualPods(device)
|
device.hasDualPods -> createDualPods(device)
|
||||||
device.model != PodModel.UNKNOWN -> createSinglePod(device)
|
device.model != PodModel.UNKNOWN -> createSinglePod(device)
|
||||||
else -> createUnknownDevice(device)
|
else -> createUnknownDevice(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDualPods(device: MonitoredDevice): RemoteViews = RemoteViews(
|
private fun createDualPods(device: PodDevice): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_dual_pods_small
|
R.layout.monitor_notification_dual_pods_small
|
||||||
).apply {
|
).apply {
|
||||||
@@ -55,7 +55,7 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSinglePod(device: MonitoredDevice): RemoteViews = RemoteViews(
|
private fun createSinglePod(device: PodDevice): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_single_pods_small
|
R.layout.monitor_notification_single_pods_small
|
||||||
).apply {
|
).apply {
|
||||||
@@ -68,24 +68,27 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn == true) View.VISIBLE else View.GONE)
|
setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn == true) View.VISIBLE else View.GONE)
|
||||||
}
|
}
|
||||||
if (device.isHeadsetBeingCharged != null) {
|
if (device.isHeadsetBeingCharged != null) {
|
||||||
setViewVisibility(R.id.headphones_charging, if (device.isHeadsetBeingCharged == true) View.VISIBLE else View.GONE)
|
setViewVisibility(
|
||||||
|
R.id.headphones_charging,
|
||||||
|
if (device.isHeadsetBeingCharged == true) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUnknownDevice(device: MonitoredDevice): RemoteViews = RemoteViews(
|
private fun createUnknownDevice(device: PodDevice): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_unknown_device_small
|
R.layout.monitor_notification_unknown_device_small
|
||||||
).apply {
|
).apply {
|
||||||
setTextViewText(R.id.device, device.getLabel(context))
|
setTextViewText(R.id.device, device.getLabel(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createBigContentView(device: MonitoredDevice): RemoteViews = when {
|
fun createBigContentView(device: PodDevice): RemoteViews = when {
|
||||||
device.hasDualPods -> createDualPodsBig(device)
|
device.hasDualPods -> createDualPodsBig(device)
|
||||||
device.model != PodModel.UNKNOWN -> createSinglePodBig(device)
|
device.model != PodModel.UNKNOWN -> createSinglePodBig(device)
|
||||||
else -> createUnknownDeviceBig(device)
|
else -> createUnknownDeviceBig(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDualPodsBig(device: MonitoredDevice): RemoteViews = RemoteViews(
|
private fun createDualPodsBig(device: PodDevice): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_dual_pods_big
|
R.layout.monitor_notification_dual_pods_big
|
||||||
).apply {
|
).apply {
|
||||||
@@ -120,7 +123,7 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSinglePodBig(device: MonitoredDevice): RemoteViews = RemoteViews(
|
private fun createSinglePodBig(device: PodDevice): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_single_pods_big
|
R.layout.monitor_notification_single_pods_big
|
||||||
).apply {
|
).apply {
|
||||||
@@ -133,11 +136,14 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn == true) View.VISIBLE else View.GONE)
|
setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn == true) View.VISIBLE else View.GONE)
|
||||||
}
|
}
|
||||||
if (device.isHeadsetBeingCharged != null) {
|
if (device.isHeadsetBeingCharged != null) {
|
||||||
setViewVisibility(R.id.headphones_charging, if (device.isHeadsetBeingCharged == true) View.VISIBLE else View.GONE)
|
setViewVisibility(
|
||||||
|
R.id.headphones_charging,
|
||||||
|
if (device.isHeadsetBeingCharged == true) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUnknownDeviceBig(device: MonitoredDevice): RemoteViews = RemoteViews(
|
private fun createUnknownDeviceBig(device: PodDevice): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_unknown_device_big
|
R.layout.monitor_notification_unknown_device_big
|
||||||
).apply {
|
).apply {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ 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.notifications.PendingIntentCompat
|
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||||
import eu.darken.capod.main.ui.MainActivity
|
import eu.darken.capod.main.ui.MainActivity
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -53,7 +53,11 @@ class MonitorNotifications @Inject constructor(
|
|||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBuilder(device: MonitoredDevice?, channelId: String, showHint: Boolean = false): NotificationCompat.Builder {
|
private fun getBuilder(
|
||||||
|
device: PodDevice?,
|
||||||
|
channelId: String,
|
||||||
|
showHint: Boolean = false
|
||||||
|
): NotificationCompat.Builder {
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
return baseBuilder(channelId).apply {
|
return baseBuilder(channelId).apply {
|
||||||
if (showHint) {
|
if (showHint) {
|
||||||
@@ -122,10 +126,10 @@ class MonitorNotifications @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNotification(podDevice: MonitoredDevice?, showHint: Boolean = false): Notification =
|
fun getNotification(podDevice: PodDevice?, showHint: Boolean = false): Notification =
|
||||||
getBuilder(podDevice, NOTIFICATION_CHANNEL_ID, showHint).build()
|
getBuilder(podDevice, NOTIFICATION_CHANNEL_ID, showHint).build()
|
||||||
|
|
||||||
fun getNotificationConnected(podDevice: MonitoredDevice?): Notification =
|
fun getNotificationConnected(podDevice: PodDevice?): Notification =
|
||||||
getBuilder(podDevice, NOTIFICATION_CHANNEL_ID_CONNECTED).build()
|
getBuilder(podDevice, NOTIFICATION_CHANNEL_ID_CONNECTED).build()
|
||||||
|
|
||||||
fun getStartupNotification(): Notification =
|
fun getStartupNotification(): Notification =
|
||||||
|
|||||||
@@ -1,15 +1,77 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
/**
|
import android.content.Context
|
||||||
* Type alias for the BLE-sourced device snapshot.
|
import androidx.annotation.DrawableRes
|
||||||
*
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
* The [PodDevice] interface represents BLE scan data (battery, ear detection, case state)
|
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||||
* extracted from Apple Continuity Protocol advertisements.
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
*
|
import eu.darken.capod.common.debug.logging.log
|
||||||
* New code should use [BlePodSnapshot] to clarify that this is the BLE data source,
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
* not the unified facade ([eu.darken.capod.monitor.core.MonitoredDevice]).
|
import java.time.Duration
|
||||||
*
|
import java.time.Instant
|
||||||
* Full rename of [PodDevice] → [BlePodSnapshot] across the codebase is deferred to
|
import java.util.UUID
|
||||||
* an IDE refactoring pass (103 files, 470 occurrences).
|
import kotlin.math.abs
|
||||||
*/
|
import kotlin.math.max
|
||||||
typealias BlePodSnapshot = PodDevice
|
|
||||||
|
interface BlePodSnapshot {
|
||||||
|
|
||||||
|
val identifier: Id
|
||||||
|
|
||||||
|
val model: PodModel
|
||||||
|
|
||||||
|
val address: BluetoothAddress
|
||||||
|
get() = scanResult.address
|
||||||
|
|
||||||
|
val seenLastAt: Instant
|
||||||
|
|
||||||
|
val seenFirstAt: Instant
|
||||||
|
|
||||||
|
val seenCounter: Int
|
||||||
|
|
||||||
|
val scanResult: BleScanResult
|
||||||
|
|
||||||
|
val rssi: Int
|
||||||
|
get() = scanResult.rssi
|
||||||
|
|
||||||
|
val reliability: Float
|
||||||
|
|
||||||
|
val signalQuality: Float
|
||||||
|
get() {
|
||||||
|
/**
|
||||||
|
* This is not correct but it works ¯\_(ツ)_/¯
|
||||||
|
* The range of the RSSI is device specific (ROMs).
|
||||||
|
*/
|
||||||
|
val sqRssi = ((100 - abs(rssi)) / 100f)
|
||||||
|
val sqReliability = max(BASE_CONFIDENCE, reliability)
|
||||||
|
val sqAge = (Duration.between(seenFirstAt, Instant.now()).toMinutes().coerceAtMost(60) / 60f) * 0.25f
|
||||||
|
log(VERBOSE) { "Signal Quality ($address): rssi=$sqRssi, reliability=$reliability, age=$sqAge" }
|
||||||
|
return (sqRssi + sqReliability + sqAge) / 2f
|
||||||
|
}
|
||||||
|
|
||||||
|
val rawData: Map<Int, ByteArray>
|
||||||
|
get() = scanResult.manufacturerSpecificData
|
||||||
|
|
||||||
|
val rawDataHex: List<String>
|
||||||
|
get() = rawData.entries.map { entry ->
|
||||||
|
"${entry.key}: ${entry.value.joinToString(separator = " ") { String.format("%02X", it) }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Meta {
|
||||||
|
val profile: DeviceProfile?
|
||||||
|
}
|
||||||
|
|
||||||
|
val meta: Meta
|
||||||
|
|
||||||
|
fun getLabel(context: Context): String = model.label
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val iconRes: Int
|
||||||
|
get() = model.iconRes
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class Id(private val id: UUID = UUID.randomUUID())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_CONFIDENCE = 0.0f
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@ package eu.darken.capod.pods.core
|
|||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
|
|
||||||
interface DualPodDevice : PodDevice {
|
interface DualBlePodSnapshot : BlePodSnapshot {
|
||||||
|
|
||||||
enum class Pod {
|
enum class Pod {
|
||||||
LEFT,
|
LEFT,
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package eu.darken.capod.pods.core
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
interface PodDevice {
|
|
||||||
|
|
||||||
val identifier: Id
|
|
||||||
|
|
||||||
val model: PodModel
|
|
||||||
|
|
||||||
val address: BluetoothAddress
|
|
||||||
get() = scanResult.address
|
|
||||||
|
|
||||||
val seenLastAt: Instant
|
|
||||||
|
|
||||||
val seenFirstAt: Instant
|
|
||||||
|
|
||||||
val seenCounter: Int
|
|
||||||
|
|
||||||
val scanResult: BleScanResult
|
|
||||||
|
|
||||||
val rssi: Int
|
|
||||||
get() = scanResult.rssi
|
|
||||||
|
|
||||||
val reliability: Float
|
|
||||||
|
|
||||||
val signalQuality: Float
|
|
||||||
get() {
|
|
||||||
/**
|
|
||||||
* This is not correct but it works ¯\_(ツ)_/¯
|
|
||||||
* The range of the RSSI is device specific (ROMs).
|
|
||||||
*/
|
|
||||||
val sqRssi = ((100 - abs(rssi)) / 100f)
|
|
||||||
val sqReliability = max(BASE_CONFIDENCE, reliability)
|
|
||||||
val sqAge = (Duration.between(seenFirstAt, Instant.now()).toMinutes().coerceAtMost(60) / 60f) * 0.25f
|
|
||||||
log(VERBOSE) { "Signal Quality ($address): rssi=$sqRssi, reliability=$reliability, age=$sqAge" }
|
|
||||||
return (sqRssi + sqReliability + sqAge) / 2f
|
|
||||||
}
|
|
||||||
|
|
||||||
val rawData: Map<Int, ByteArray>
|
|
||||||
get() = scanResult.manufacturerSpecificData
|
|
||||||
|
|
||||||
val rawDataHex: List<String>
|
|
||||||
get() = rawData.entries.map { entry ->
|
|
||||||
"${entry.key}: ${entry.value.joinToString(separator = " ") { String.format("%02X", it) }}"
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Meta {
|
|
||||||
val profile: DeviceProfile?
|
|
||||||
}
|
|
||||||
|
|
||||||
val meta: Meta
|
|
||||||
|
|
||||||
fun getLabel(context: Context): String = model.label
|
|
||||||
|
|
||||||
@get:DrawableRes
|
|
||||||
val iconRes: Int
|
|
||||||
get() = model.iconRes
|
|
||||||
|
|
||||||
@JvmInline
|
|
||||||
value class Id(private val id: UUID = UUID.randomUUID())
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val BASE_CONFIDENCE = 0.0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ fun formatBatteryPercent(context: Context, percent: Float?): String =
|
|||||||
percent?.let { "${(it * 100).roundToInt()}%" }
|
percent?.let { "${(it * 100).roundToInt()}%" }
|
||||||
?: context.getString(R.string.general_value_not_available_label)
|
?: context.getString(R.string.general_value_not_available_label)
|
||||||
|
|
||||||
fun PodDevice.getSignalQuality(context: Context): String {
|
fun BlePodSnapshot.getSignalQuality(context: Context): String {
|
||||||
val multiplier = 100 * signalQuality
|
val multiplier = 100 * signalQuality
|
||||||
return "${multiplier.roundToInt()}%"
|
return "${multiplier.roundToInt()}%"
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ fun getBatteryDrawable(percent: Float?): Int = when {
|
|||||||
else -> R.drawable.ic_baseline_battery_0_bar_24
|
else -> R.drawable.ic_baseline_battery_0_bar_24
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
fun BlePodSnapshot.lastSeenFormatted(now: Instant): String {
|
||||||
val formatter = RelativeDateTimeFormatter.getInstance()
|
val formatter = RelativeDateTimeFormatter.getInstance()
|
||||||
val duration = Duration.between(seenLastAt, now)
|
val duration = Duration.between(seenLastAt, now)
|
||||||
return if (duration > Duration.ofMinutes(1)) {
|
return if (duration > Duration.ofMinutes(1)) {
|
||||||
@@ -54,7 +54,7 @@ fun PodDevice.lastSeenFormatted(now: Instant): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PodDevice.firstSeenFormatted(now: Instant): String {
|
fun BlePodSnapshot.firstSeenFormatted(now: Instant): String {
|
||||||
val formatter = RelativeDateTimeFormatter.getInstance()
|
val formatter = RelativeDateTimeFormatter.getInstance()
|
||||||
val duration = Duration.between(seenFirstAt, now)
|
val duration = Duration.between(seenFirstAt, now)
|
||||||
return formatter.format(
|
return formatter.format(
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class PodFactory @Inject constructor(
|
|||||||
|
|
||||||
data class Result(
|
data class Result(
|
||||||
val scanResult: BleScanResult,
|
val scanResult: BleScanResult,
|
||||||
val device: PodDevice
|
val device: BlePodSnapshot
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
interface SinglePodDevice : PodDevice {
|
interface SingleBlePodSnapshot : BlePodSnapshot {
|
||||||
|
|
||||||
val batteryHeadsetPercent: Float?
|
val batteryHeadsetPercent: Float?
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,9 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
|||||||
import eu.darken.capod.common.debug.logging.asLog
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
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
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
|
import eu.darken.capod.pods.core.apple.misc.UnknownAppleSnapshotBle
|
||||||
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.ProximityMessage
|
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
@@ -28,7 +28,7 @@ class AppleFactory @Inject constructor(
|
|||||||
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
||||||
private val proximityMessageDecrypter: ProximityMessage.Decrypter,
|
private val proximityMessageDecrypter: ProximityMessage.Decrypter,
|
||||||
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory>,
|
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory>,
|
||||||
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
private val unknownAppleFactory: UnknownAppleSnapshotBle.Factory,
|
||||||
private val rpaChecker: RPAChecker,
|
private val rpaChecker: RPAChecker,
|
||||||
private val profilesRepo: DeviceProfilesRepo,
|
private val profilesRepo: DeviceProfilesRepo,
|
||||||
) {
|
) {
|
||||||
@@ -60,7 +60,7 @@ class AppleFactory @Inject constructor(
|
|||||||
return proximityMessage
|
return proximityMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
|
suspend fun create(scanResult: BleScanResult): BlePodSnapshot? = lock.withLock {
|
||||||
val proximityMessage = getMessage(scanResult) ?: return@withLock null
|
val proximityMessage = getMessage(scanResult) ?: return@withLock null
|
||||||
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) } ?: unknownAppleFactory
|
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) } ?: unknownAppleFactory
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package eu.darken.capod.pods.core.apple
|
|||||||
|
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||||
|
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||||
|
|
||||||
interface ApplePods : PodDevice {
|
interface ApplePods : BlePodSnapshot {
|
||||||
|
|
||||||
val payload: ProximityPayload
|
val payload: ProximityPayload
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ interface ApplePods : PodDevice {
|
|||||||
data class AppleMeta(
|
data class AppleMeta(
|
||||||
val isIRKMatch: Boolean = false,
|
val isIRKMatch: Boolean = false,
|
||||||
override val profile: AppleDeviceProfile? = null,
|
override val profile: AppleDeviceProfile? = null,
|
||||||
) : PodDevice.Meta
|
) : BlePodSnapshot.Meta
|
||||||
|
|
||||||
override val meta: AppleMeta
|
override val meta: AppleMeta
|
||||||
}
|
}
|
||||||
@@ -4,14 +4,14 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.DualBlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
import eu.darken.capod.pods.core.DualBlePodSnapshot.Pod
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
|
|
||||||
interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
interface DualApplePods : ApplePods, HasChargeDetectionDual, DualBlePodSnapshot, HasEarDetectionDual, HasCase,
|
||||||
HasDualMicrophone, HasAppleColor {
|
HasDualMicrophone, HasAppleColor {
|
||||||
|
|
||||||
val primaryPod: Pod
|
val primaryPod: Pod
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package eu.darken.capod.pods.core.apple
|
|||||||
|
|
||||||
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.lowerNibble
|
||||||
import eu.darken.capod.pods.core.SinglePodDevice
|
import eu.darken.capod.pods.core.SingleBlePodSnapshot
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Devices that only present a single charge level, e.g. most Beats devices
|
* Devices that only present a single charge level, e.g. most Beats devices
|
||||||
*/
|
*/
|
||||||
interface SingleApplePods : ApplePods, SinglePodDevice, HasAppleColor {
|
interface SingleApplePods : ApplePods, SingleBlePodSnapshot, HasAppleColor {
|
||||||
|
|
||||||
override val batteryHeadsetPercent: Float?
|
override val batteryHeadsetPercent: Float?
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsGen1(
|
data class AirPodsGen1(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsGen2(
|
data class AirPodsGen2(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsGen3(
|
data class AirPodsGen3(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsGen4(
|
data class AirPodsGen4(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsGen4Anc(
|
data class AirPodsGen4Anc(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasChargeDetection
|
import eu.darken.capod.pods.core.HasChargeDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -19,14 +19,14 @@ 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: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
|
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasChargeDetection
|
import eu.darken.capod.pods.core.HasChargeDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -19,14 +19,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsMaxUsbc(
|
data class AirPodsMaxUsbc(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
|
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -18,14 +18,14 @@ 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: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -18,14 +18,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsPro2(
|
data class AirPodsPro2(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -18,14 +18,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsPro2Usbc(
|
data class AirPodsPro2Usbc(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -18,13 +18,13 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class AirPodsPro3(
|
data class AirPodsPro3(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class BeatsFitPro(
|
data class BeatsFitPro(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.beats
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -15,14 +15,14 @@ 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: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.beats
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -15,14 +15,14 @@ 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: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.beats
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -15,14 +15,14 @@ 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: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.beats
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -15,14 +15,14 @@ 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: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.beats
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -15,14 +15,14 @@ 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: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.beats
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -16,14 +16,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class PowerBeats4(
|
data class PowerBeats4(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods, HasStateDetectionAirPods {
|
) : SingleApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -17,14 +17,14 @@ 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: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class PowerBeatsPro2(
|
data class PowerBeatsPro2(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualApplePods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.history
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||||
import eu.darken.capod.common.collections.median
|
import eu.darken.capod.common.collections.median
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -10,7 +10,7 @@ import java.time.Instant
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
data class KnownDevice(
|
data class KnownDevice(
|
||||||
val id: PodDevice.Id,
|
val id: BlePodSnapshot.Id,
|
||||||
val seenFirstAt: Instant,
|
val seenFirstAt: Instant,
|
||||||
val seenCounter: Int,
|
val seenCounter: Int,
|
||||||
val history: List<ApplePods>,
|
val history: List<ApplePods>,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ 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.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
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
|
||||||
@@ -27,7 +27,7 @@ class PodHistoryRepo @Inject constructor(
|
|||||||
private val rpaChecker: RPAChecker,
|
private val rpaChecker: RPAChecker,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
private val knownDevices = mutableMapOf<BlePodSnapshot.Id, KnownDevice>()
|
||||||
|
|
||||||
data class Identifier(
|
data class Identifier(
|
||||||
val device: UShort,
|
val device: UShort,
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package eu.darken.capod.pods.core.apple.misc
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -22,14 +22,14 @@ import javax.inject.Inject
|
|||||||
* Marketed as TWS i99999
|
* Marketed as TWS i99999
|
||||||
*/
|
*/
|
||||||
data class FakeAirPodsGen1(
|
data class FakeAirPodsGen1(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package eu.darken.capod.pods.core.apple.misc
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -20,14 +20,14 @@ import javax.inject.Inject
|
|||||||
* Similar data structure but a lot of placeholder values or hardcoded values
|
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||||
*/
|
*/
|
||||||
data class FakeAirPodsGen2(
|
data class FakeAirPodsGen2(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -24,14 +24,14 @@ import javax.inject.Inject
|
|||||||
* https://discord.com/channels/548521543039189022/927235844127993866/1054404630118924308
|
* https://discord.com/channels/548521543039189022/927235844127993866/1054404630118924308
|
||||||
*/
|
*/
|
||||||
data class FakeAirPodsGen3(
|
data class FakeAirPodsGen3(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -24,14 +24,14 @@ import javax.inject.Inject
|
|||||||
* https://discord.com/channels/548521543039189022/927235844127993866/962068944196358234
|
* https://discord.com/channels/548521543039189022/927235844127993866/962068944196358234
|
||||||
*/
|
*/
|
||||||
data class FakeAirPodsPro(
|
data class FakeAirPodsPro(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : DualApplePods, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
) : DualApplePods, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import androidx.annotation.DrawableRes
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
|
import eu.darken.capod.pods.core.DualBlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -25,18 +25,18 @@ import javax.inject.Inject
|
|||||||
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
|
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
|
||||||
*/
|
*/
|
||||||
data class FakeAirPodsPro2(
|
data class FakeAirPodsPro2(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val payload: ProximityPayload,
|
override val payload: ProximityPayload,
|
||||||
override val meta: ApplePods.AppleMeta,
|
override val meta: ApplePods.AppleMeta,
|
||||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null,
|
private val cachedCaseState: LidState? = null,
|
||||||
) : DualApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
|
) : DualApplePods, HasChargeDetectionDual, DualBlePodSnapshot, HasEarDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodModel = PodModel.FAKE_AIRPODS_PRO2
|
override val model: PodModel = PodModel.FAKE_AIRPODS_PRO2
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
@@ -14,8 +14,8 @@ import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class UnknownAppleDevice(
|
data class UnknownAppleSnapshotBle(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
@@ -43,7 +43,7 @@ data class UnknownAppleDevice(
|
|||||||
payload: ProximityPayload,
|
payload: ProximityPayload,
|
||||||
meta: ApplePods.AppleMeta,
|
meta: ApplePods.AppleMeta,
|
||||||
): ApplePods {
|
): ApplePods {
|
||||||
var basic = UnknownAppleDevice(scanResult = scanResult, payload = payload, meta = meta)
|
var basic = UnknownAppleSnapshotBle(scanResult = scanResult, payload = payload, meta = meta)
|
||||||
val result = repo.search(basic)
|
val result = repo.search(basic)
|
||||||
|
|
||||||
if (result != null) basic = basic.copy(identifier = result.id)
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
@@ -47,6 +47,6 @@ class RPAChecker @Inject constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Monitor", "PodMonitor", "RPAChecker")
|
private val TAG = logTag("Monitor", "BlePodMonitor", "RPAChecker")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -17,9 +17,9 @@ class UnknownDeviceFactory @Inject constructor() {
|
|||||||
|
|
||||||
private val lock = Mutex()
|
private val lock = Mutex()
|
||||||
|
|
||||||
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
|
suspend fun create(scanResult: BleScanResult): BlePodSnapshot = lock.withLock {
|
||||||
var basic = UnknownDevice(
|
var basic = UnknownSnapshotBle(
|
||||||
identifier = PodDevice.Id(),
|
identifier = BlePodSnapshot.Id(),
|
||||||
scanResult = scanResult,
|
scanResult = scanResult,
|
||||||
)
|
)
|
||||||
val result = searchHistory(basic)
|
val result = searchHistory(basic)
|
||||||
@@ -40,10 +40,10 @@ class UnknownDeviceFactory @Inject constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class KnownDevice(
|
data class KnownDevice(
|
||||||
val id: PodDevice.Id,
|
val id: BlePodSnapshot.Id,
|
||||||
val seenFirstAt: Instant,
|
val seenFirstAt: Instant,
|
||||||
val seenCounter: Int,
|
val seenCounter: Int,
|
||||||
val history: List<PodDevice>
|
val history: List<BlePodSnapshot>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val lastAddress: String
|
val lastAddress: String
|
||||||
@@ -74,9 +74,9 @@ class UnknownDeviceFactory @Inject constructor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
private val knownDevices = mutableMapOf<BlePodSnapshot.Id, KnownDevice>()
|
||||||
|
|
||||||
private fun searchHistory(current: PodDevice): KnownDevice? {
|
private fun searchHistory(current: BlePodSnapshot): KnownDevice? {
|
||||||
val scanResult = current.scanResult
|
val scanResult = current.scanResult
|
||||||
|
|
||||||
knownDevices.values.toList().forEach { knownDevice ->
|
knownDevices.values.toList().forEach { knownDevice ->
|
||||||
@@ -104,7 +104,7 @@ class UnknownDeviceFactory @Inject constructor() {
|
|||||||
return recognizedDevice
|
return recognizedDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateHistory(device: PodDevice) {
|
private fun updateHistory(device: BlePodSnapshot) {
|
||||||
val existing = knownDevices[device.identifier]
|
val existing = knownDevices[device.identifier]
|
||||||
|
|
||||||
knownDevices[device.identifier] = when {
|
knownDevices[device.identifier] = when {
|
||||||
@@ -114,6 +114,7 @@ class UnknownDeviceFactory @Inject constructor() {
|
|||||||
history = existing.history.plus(device)
|
history = existing.history.plus(device)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
log(TAG) { "searchHistory1: Creating new history for $device" }
|
log(TAG) { "searchHistory1: Creating new history for $device" }
|
||||||
KnownDevice(
|
KnownDevice(
|
||||||
|
|||||||
+5
-5
@@ -4,13 +4,13 @@ import android.content.Context
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
data class UnknownDevice(
|
data class UnknownSnapshotBle(
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||||
override val seenLastAt: Instant = Instant.now(),
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
@@ -18,7 +18,7 @@ data class UnknownDevice(
|
|||||||
override val reliability: Float = 0f,
|
override val reliability: Float = 0f,
|
||||||
override val meta: Meta = Meta(),
|
override val meta: Meta = Meta(),
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : PodDevice {
|
) : BlePodSnapshot {
|
||||||
|
|
||||||
override val model: PodModel = PodModel.UNKNOWN
|
override val model: PodModel = PodModel.UNKNOWN
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ data class UnknownDevice(
|
|||||||
|
|
||||||
data class Meta(
|
data class Meta(
|
||||||
override val profile: DeviceProfile? = null
|
override val profile: DeviceProfile? = null
|
||||||
) : PodDevice.Meta
|
) : BlePodSnapshot.Meta
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("PodDevice", "Unknown")
|
private val TAG = logTag("PodDevice", "Unknown")
|
||||||
@@ -2,6 +2,7 @@ package eu.darken.capod.reaction.core.playpause
|
|||||||
|
|
||||||
import eu.darken.capod.common.MediaControl
|
import eu.darken.capod.common.MediaControl
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
|
import eu.darken.capod.common.datastore.valueBlocking
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
@@ -9,7 +10,6 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
|
||||||
import eu.darken.capod.monitor.core.primaryDevice
|
import eu.darken.capod.monitor.core.primaryDevice
|
||||||
import eu.darken.capod.reaction.core.ReactionSettings
|
import eu.darken.capod.reaction.core.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import eu.darken.capod.common.datastore.valueBlocking
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class PlayPause @Inject constructor(
|
class PlayPause @Inject constructor(
|
||||||
@@ -63,7 +62,7 @@ class PlayPause @Inject constructor(
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
previous!!.hasEarDetection && previous.hasDualPods &&
|
previous!!.hasEarDetection && previous.hasDualPods &&
|
||||||
current!!.hasEarDetection && current.hasDualPods -> {
|
current!!.hasEarDetection && current.hasDualPods -> {
|
||||||
// Dual pod devices (AirPods, AirPods Pro, etc.)
|
// Dual pod devices (AirPods, AirPods Pro, etc.)
|
||||||
log(TAG, VERBOSE) {
|
log(TAG, VERBOSE) {
|
||||||
"Dual-pod device: left=${current.isLeftInEar}, right=${current.isRightInEar}"
|
"Dual-pod device: left=${current.isLeftInEar}, right=${current.isRightInEar}"
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ package eu.darken.capod.reaction.core.popup
|
|||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.monitor.core.primaryDevice
|
import eu.darken.capod.monitor.core.primaryDevice
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.core.ReactionSettings
|
import eu.darken.capod.reaction.core.ReactionSettings
|
||||||
@@ -55,7 +53,7 @@ class PopUpReaction @Inject constructor(
|
|||||||
log(TAG, VERBOSE) { "previous-id=${previous?.identifier}, current-id=${current.identifier}" }
|
log(TAG, VERBOSE) { "previous-id=${previous?.identifier}, current-id=${current.identifier}" }
|
||||||
|
|
||||||
val isSameDeviceOrProfile = previous?.identifier == current.identifier ||
|
val isSameDeviceOrProfile = previous?.identifier == current.identifier ||
|
||||||
(previous?.meta?.profile?.id != null && previous.meta?.profile?.id == current.meta?.profile?.id)
|
(previous?.meta?.profile?.id != null && previous.meta?.profile?.id == current.meta?.profile?.id)
|
||||||
val isSameDeviceWithCaseNowOpen = isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState
|
val isSameDeviceWithCaseNowOpen = isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState
|
||||||
val isNewDeviceWithJustOpenedCase = !isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState
|
val isNewDeviceWithJustOpenedCase = !isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState
|
||||||
|
|
||||||
@@ -67,7 +65,7 @@ class PopUpReaction @Inject constructor(
|
|||||||
throttleCasePopUps(current)
|
throttleCasePopUps(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun throttleCasePopUps(current: MonitoredDevice): Event? {
|
private fun throttleCasePopUps(current: PodDevice): Event? {
|
||||||
val cooldownKey = current.meta?.profile?.id ?: current.identifier.toString()
|
val cooldownKey = current.meta?.profile?.id ?: current.identifier.toString()
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
val lastShown = caseCoolDowns[cooldownKey]
|
val lastShown = caseCoolDowns[cooldownKey]
|
||||||
@@ -89,12 +87,14 @@ class PopUpReaction @Inject constructor(
|
|||||||
caseCoolDowns[cooldownKey] = now
|
caseCoolDowns[cooldownKey] = now
|
||||||
Event.PopupShow(device = current)
|
Event.PopupShow(device = current)
|
||||||
}
|
}
|
||||||
|
|
||||||
decision.shouldHide -> {
|
decision.shouldHide -> {
|
||||||
if (!decision.shouldResetCooldown) {
|
if (!decision.shouldResetCooldown) {
|
||||||
caseCoolDowns[cooldownKey] = now
|
caseCoolDowns[cooldownKey] = now
|
||||||
}
|
}
|
||||||
Event.PopupHide()
|
Event.PopupHide()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ class PopUpReaction @Inject constructor(
|
|||||||
sealed class Event {
|
sealed class Event {
|
||||||
data class PopupShow(
|
data class PopupShow(
|
||||||
val eventAt: Instant = Instant.now(),
|
val eventAt: Instant = Instant.now(),
|
||||||
val device: MonitoredDevice,
|
val device: PodDevice,
|
||||||
) : Event()
|
) : Event()
|
||||||
|
|
||||||
data class PopupHide(
|
data class PopupHide(
|
||||||
@@ -210,12 +210,14 @@ class PopUpReaction @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DualApplePods.LidState.CLOSED -> CasePopUpDecision(
|
DualApplePods.LidState.CLOSED -> CasePopUpDecision(
|
||||||
shouldShow = false,
|
shouldShow = false,
|
||||||
shouldHide = true,
|
shouldHide = true,
|
||||||
shouldResetCooldown = true,
|
shouldResetCooldown = true,
|
||||||
reason = "Lid CLOSED, resetting cooldown",
|
reason = "Lid CLOSED, resetting cooldown",
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> CasePopUpDecision(
|
else -> CasePopUpDecision(
|
||||||
shouldShow = false,
|
shouldShow = false,
|
||||||
shouldHide = true,
|
shouldHide = true,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import eu.darken.capod.R
|
|||||||
import eu.darken.capod.common.compose.Preview2
|
import eu.darken.capod.common.compose.Preview2
|
||||||
import eu.darken.capod.common.compose.PreviewWrapper
|
import eu.darken.capod.common.compose.PreviewWrapper
|
||||||
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.monitor.core.getSignalQuality
|
import eu.darken.capod.monitor.core.getSignalQuality
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||||
@@ -44,7 +44,7 @@ import eu.darken.capod.pods.core.toBatteryOrNull
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PopUpContent(
|
fun PopUpContent(
|
||||||
device: MonitoredDevice,
|
device: PodDevice,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -117,7 +117,7 @@ fun PopUpContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DualPodContent(device: MonitoredDevice) {
|
private fun DualPodContent(device: PodDevice) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
@@ -148,7 +148,7 @@ private fun DualPodContent(device: MonitoredDevice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SinglePodContent(device: MonitoredDevice) {
|
private fun SinglePodContent(device: PodDevice) {
|
||||||
BatteryColumn(
|
BatteryColumn(
|
||||||
iconRes = device.iconRes,
|
iconRes = device.iconRes,
|
||||||
batteryPercent = device.batteryHeadset.toBatteryFloat(),
|
batteryPercent = device.batteryHeadset.toBatteryFloat(),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.theming.CapodTheme
|
import eu.darken.capod.common.theming.CapodTheme
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.main.core.currentThemeState
|
import eu.darken.capod.main.core.currentThemeState
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -50,9 +50,9 @@ class PopUpWindow @Inject constructor(
|
|||||||
|
|
||||||
private var composeView: ComposeView? = null
|
private var composeView: ComposeView? = null
|
||||||
private var lifecycleOwner: OverlayLifecycleOwner? = null
|
private var lifecycleOwner: OverlayLifecycleOwner? = null
|
||||||
private var deviceState: MutableState<MonitoredDevice?>? = null
|
private var deviceState: MutableState<PodDevice?>? = null
|
||||||
|
|
||||||
fun show(device: MonitoredDevice) {
|
fun show(device: PodDevice) {
|
||||||
try {
|
try {
|
||||||
log(TAG) { "open()" }
|
log(TAG) { "open()" }
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class PopUpWindow @Inject constructor(
|
|||||||
|
|
||||||
teardown()
|
teardown()
|
||||||
|
|
||||||
val state = mutableStateOf<MonitoredDevice?>(device)
|
val state = mutableStateOf<PodDevice?>(device)
|
||||||
deviceState = state
|
deviceState = state
|
||||||
|
|
||||||
val owner = OverlayLifecycleOwner()
|
val owner = OverlayLifecycleOwner()
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
|
import eu.darken.capod.common.datastore.valueBlocking
|
||||||
import eu.darken.capod.common.debug.DebugSettings
|
import eu.darken.capod.common.debug.DebugSettings
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
||||||
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
|
||||||
|
|
||||||
import eu.darken.capod.common.uix.ViewModel4
|
import eu.darken.capod.common.uix.ViewModel4
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.monitor.core.BlePodMonitor
|
||||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
|
||||||
import eu.darken.capod.monitor.core.primaryDevice
|
import eu.darken.capod.monitor.core.primaryDevice
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.unknown.UnknownDevice
|
import eu.darken.capod.pods.core.unknown.UnknownSnapshotBle
|
||||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.takeWhile
|
|||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import eu.darken.capod.common.datastore.valueBlocking
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TroubleShooterViewModel @Inject constructor(
|
class TroubleShooterViewModel @Inject constructor(
|
||||||
@@ -39,7 +38,7 @@ class TroubleShooterViewModel @Inject constructor(
|
|||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
private val generalSettings: GeneralSettings,
|
private val generalSettings: GeneralSettings,
|
||||||
private val profilesRepo: DeviceProfilesRepo,
|
private val profilesRepo: DeviceProfilesRepo,
|
||||||
private val podMonitor: PodMonitor,
|
private val blePodMonitor: BlePodMonitor,
|
||||||
private val deviceMonitor: DeviceMonitor,
|
private val deviceMonitor: DeviceMonitor,
|
||||||
private val debugSettings: DebugSettings,
|
private val debugSettings: DebugSettings,
|
||||||
) : ViewModel4(dispatcherProvider) {
|
) : ViewModel4(dispatcherProvider) {
|
||||||
@@ -98,39 +97,40 @@ class TroubleShooterViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val doScan: suspend (Boolean, Boolean, Boolean, Boolean) -> Collection<PodDevice> = { hardwareFilteringDisabled,
|
val doScan: suspend (Boolean, Boolean, Boolean, Boolean) -> Collection<BlePodSnapshot> =
|
||||||
hardwareBatchingDisabled,
|
{ hardwareFilteringDisabled,
|
||||||
indirectCallback,
|
hardwareBatchingDisabled,
|
||||||
unfiltered ->
|
indirectCallback,
|
||||||
val sb = StringBuilder("SCAN - Settings: ")
|
unfiltered ->
|
||||||
sb.append("hardwareFilteringDisabled=$hardwareFilteringDisabled, ")
|
val sb = StringBuilder("SCAN - Settings: ")
|
||||||
sb.append("hardwareBatchingDisabled=$hardwareBatchingDisabled, ")
|
sb.append("hardwareFilteringDisabled=$hardwareFilteringDisabled, ")
|
||||||
sb.append("indirectCallback=$indirectCallback, ")
|
sb.append("hardwareBatchingDisabled=$hardwareBatchingDisabled, ")
|
||||||
sb.append("unfiltered=$unfiltered")
|
sb.append("indirectCallback=$indirectCallback, ")
|
||||||
progress(sb.toString())
|
sb.append("unfiltered=$unfiltered")
|
||||||
generalSettings.isOffloadedFilteringDisabled.valueBlocking = hardwareFilteringDisabled
|
progress(sb.toString())
|
||||||
generalSettings.isOffloadedBatchingDisabled.valueBlocking = hardwareBatchingDisabled
|
generalSettings.isOffloadedFilteringDisabled.valueBlocking = hardwareFilteringDisabled
|
||||||
generalSettings.useIndirectScanResultCallback.valueBlocking = indirectCallback
|
generalSettings.isOffloadedBatchingDisabled.valueBlocking = hardwareBatchingDisabled
|
||||||
debugSettings.showUnfiltered.valueBlocking = unfiltered
|
generalSettings.useIndirectScanResultCallback.valueBlocking = indirectCallback
|
||||||
|
debugSettings.showUnfiltered.valueBlocking = unfiltered
|
||||||
|
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val devices = withTimeoutOrNull(STEP_TIME) {
|
val devices = withTimeoutOrNull(STEP_TIME) {
|
||||||
podMonitor.devices
|
blePodMonitor.devices
|
||||||
.take(10)
|
.take(10)
|
||||||
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
||||||
.toList()
|
.toList()
|
||||||
.flatten()
|
.flatten()
|
||||||
.distinctBy { it.address }
|
.distinctBy { it.address }
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
log(TAG) { "SCAN: BLE Devices: $devices" }
|
log(TAG) { "SCAN: BLE Devices: $devices" }
|
||||||
if (devices.isNotEmpty()) {
|
if (devices.isNotEmpty()) {
|
||||||
progress("SCAN: Received data from ${devices.size} BLE devices")
|
progress("SCAN: Received data from ${devices.size} BLE devices")
|
||||||
devices
|
devices
|
||||||
} else {
|
} else {
|
||||||
progress("SCAN: No data received")
|
progress("SCAN: No data received")
|
||||||
devices
|
devices
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
run {
|
run {
|
||||||
progress("Checking if we can receive BLE data at all.")
|
progress("Checking if we can receive BLE data at all.")
|
||||||
@@ -158,14 +158,14 @@ class TroubleShooterViewModel @Inject constructor(
|
|||||||
run {
|
run {
|
||||||
progress("Checking for supported headphones.")
|
progress("Checking for supported headphones.")
|
||||||
|
|
||||||
if (doScan(false, false, false, false).any { it !is UnknownDevice }) return@run
|
if (doScan(false, false, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||||
if (doScan(false, false, true, false).any { it !is UnknownDevice }) return@run
|
if (doScan(false, false, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||||
if (doScan(true, true, true, false).any { it !is UnknownDevice }) return@run
|
if (doScan(true, true, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||||
if (doScan(true, true, false, false).any { it !is UnknownDevice }) return@run
|
if (doScan(true, true, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||||
if (doScan(true, false, true, false).any { it !is UnknownDevice }) return@run
|
if (doScan(true, false, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||||
if (doScan(true, false, false, false).any { it !is UnknownDevice }) return@run
|
if (doScan(true, false, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||||
if (doScan(false, true, true, false).any { it !is UnknownDevice }) return@run
|
if (doScan(false, true, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||||
if (doScan(false, true, false, false).any { it !is UnknownDevice }) return@run
|
if (doScan(false, true, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||||
|
|
||||||
failure("No compatible headphones found", BleState.Result.Failure.Type.HEADPHONES)
|
failure("No compatible headphones found", BleState.Result.Failure.Type.HEADPHONES)
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ class TroubleShooterViewModel @Inject constructor(
|
|||||||
|
|
||||||
val otherDevices = withTimeoutOrNull(STEP_TIME) {
|
val otherDevices = withTimeoutOrNull(STEP_TIME) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
podMonitor.devices
|
blePodMonitor.devices
|
||||||
.take(10)
|
.take(10)
|
||||||
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
||||||
.toList()
|
.toList()
|
||||||
@@ -215,7 +215,7 @@ class TroubleShooterViewModel @Inject constructor(
|
|||||||
progress("Creating profile for closest headphones.")
|
progress("Creating profile for closest headphones.")
|
||||||
|
|
||||||
val candidate = otherDevices
|
val candidate = otherDevices
|
||||||
.filter { it !is UnknownDevice }
|
.filter { it !is UnknownSnapshotBle }
|
||||||
.maxBy { it.signalQuality }
|
.maxBy { it.signalQuality }
|
||||||
|
|
||||||
log(TAG, INFO) { "Candidate is $candidate" }
|
log(TAG, INFO) { "Candidate is $candidate" }
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import eu.darken.capod.main.core.GeneralSettings
|
|||||||
import eu.darken.capod.main.core.MonitorMode
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
import eu.darken.capod.monitor.core.DeviceMonitor
|
import eu.darken.capod.monitor.core.DeviceMonitor
|
||||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
import eu.darken.capod.monitor.core.PodDevice
|
||||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
@@ -32,11 +32,11 @@ import org.junit.jupiter.api.AfterEach
|
|||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import testhelpers.BaseTest
|
import testhelpers.BaseTest
|
||||||
import testhelpers.coroutine.TestDispatcherProvider
|
import testhelpers.coroutine.TestDispatcherProvider
|
||||||
import testhelpers.datastore.FakeDataStoreValue
|
import testhelpers.datastore.FakeDataStoreValue
|
||||||
import testhelpers.livedata.InstantExecutorExtension
|
import testhelpers.livedata.InstantExecutorExtension
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@ExtendWith(InstantExecutorExtension::class)
|
@ExtendWith(InstantExecutorExtension::class)
|
||||||
@@ -54,7 +54,7 @@ class OverviewViewModelTest : BaseTest() {
|
|||||||
private lateinit var profilesRepo: DeviceProfilesRepo
|
private lateinit var profilesRepo: DeviceProfilesRepo
|
||||||
|
|
||||||
private lateinit var missingPermissionsFlow: MutableStateFlow<Set<Permission>>
|
private lateinit var missingPermissionsFlow: MutableStateFlow<Set<Permission>>
|
||||||
private lateinit var devicesFlow: MutableStateFlow<List<MonitoredDevice>>
|
private lateinit var devicesFlow: MutableStateFlow<List<PodDevice>>
|
||||||
private lateinit var connectedDevicesFlow: MutableStateFlow<List<BluetoothDevice2>>
|
private lateinit var connectedDevicesFlow: MutableStateFlow<List<BluetoothDevice2>>
|
||||||
private lateinit var isBluetoothEnabledFlow: MutableStateFlow<Boolean>
|
private lateinit var isBluetoothEnabledFlow: MutableStateFlow<Boolean>
|
||||||
private lateinit var profilesFlow: MutableStateFlow<List<DeviceProfile>>
|
private lateinit var profilesFlow: MutableStateFlow<List<DeviceProfile>>
|
||||||
@@ -154,7 +154,7 @@ class OverviewViewModelTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `devices passed through when permissions granted`() = runTest(testDispatcher) {
|
fun `devices passed through when permissions granted`() = runTest(testDispatcher) {
|
||||||
val device = MonitoredDevice(ble = mockk(relaxed = true), aap = null)
|
val device = PodDevice(ble = mockk(relaxed = true), aap = null)
|
||||||
devicesFlow.value = listOf(device)
|
devicesFlow.value = listOf(device)
|
||||||
|
|
||||||
val vm = createViewModel()
|
val vm = createViewModel()
|
||||||
@@ -165,17 +165,17 @@ class OverviewViewModelTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `profiledDevices returns only devices with non-null profile`() {
|
fun `profiledDevices returns only devices with non-null profile`() {
|
||||||
val withProfile = object : PodDevice.Meta {
|
val withProfile = object : BlePodSnapshot.Meta {
|
||||||
override val profile: DeviceProfile = AppleDeviceProfile(label = "Test")
|
override val profile: DeviceProfile = AppleDeviceProfile(label = "Test")
|
||||||
}
|
}
|
||||||
val withoutProfile = object : PodDevice.Meta {
|
val withoutProfile = object : BlePodSnapshot.Meta {
|
||||||
override val profile: DeviceProfile? = null
|
override val profile: DeviceProfile? = null
|
||||||
}
|
}
|
||||||
val profiled = MonitoredDevice(
|
val profiled = PodDevice(
|
||||||
ble = mockk(relaxed = true) { every { meta } returns withProfile },
|
ble = mockk(relaxed = true) { every { meta } returns withProfile },
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
val unmatched = MonitoredDevice(
|
val unmatched = PodDevice(
|
||||||
ble = mockk(relaxed = true) { every { meta } returns withoutProfile },
|
ble = mockk(relaxed = true) { every { meta } returns withoutProfile },
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
@@ -196,17 +196,17 @@ class OverviewViewModelTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `unmatchedDevices returns only devices with null profile`() {
|
fun `unmatchedDevices returns only devices with null profile`() {
|
||||||
val withProfile = object : PodDevice.Meta {
|
val withProfile = object : BlePodSnapshot.Meta {
|
||||||
override val profile: DeviceProfile = AppleDeviceProfile(label = "Test")
|
override val profile: DeviceProfile = AppleDeviceProfile(label = "Test")
|
||||||
}
|
}
|
||||||
val withoutProfile = object : PodDevice.Meta {
|
val withoutProfile = object : BlePodSnapshot.Meta {
|
||||||
override val profile: DeviceProfile? = null
|
override val profile: DeviceProfile? = null
|
||||||
}
|
}
|
||||||
val profiled = MonitoredDevice(
|
val profiled = PodDevice(
|
||||||
ble = mockk(relaxed = true) { every { meta } returns withProfile },
|
ble = mockk(relaxed = true) { every { meta } returns withProfile },
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
val unmatched = MonitoredDevice(
|
val unmatched = PodDevice(
|
||||||
ble = mockk(relaxed = true) { every { meta } returns withoutProfile },
|
ble = mockk(relaxed = true) { every { meta } returns withoutProfile },
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
|
|||||||
+20
-20
@@ -1,10 +1,10 @@
|
|||||||
package eu.darken.capod.monitor.core
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.PodModel
|
import eu.darken.capod.pods.core.PodModel
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.protocol.aap.AapConnectionState
|
import eu.darken.capod.pods.core.apple.protocol.aap.AapConnectionState
|
||||||
@@ -20,15 +20,15 @@ import org.junit.jupiter.api.Test
|
|||||||
import testhelpers.BaseTest
|
import testhelpers.BaseTest
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class MonitoredDeviceTest : BaseTest() {
|
class PodDeviceTest : BaseTest() {
|
||||||
|
|
||||||
private fun mockDualPod(
|
private fun mockDualPod(
|
||||||
model: PodModel = PodModel.AIRPODS_PRO3,
|
model: PodModel = PodModel.AIRPODS_PRO3,
|
||||||
leftBattery: Float? = 0.8f,
|
leftBattery: Float? = 0.8f,
|
||||||
rightBattery: Float? = 0.9f,
|
rightBattery: Float? = 0.9f,
|
||||||
caseBattery: Float? = 0.5f,
|
caseBattery: Float? = 0.5f,
|
||||||
): PodDevice {
|
): BlePodSnapshot {
|
||||||
// DualApplePods implements DualPodDevice + HasCase + other interfaces
|
// DualApplePods implements DualBlePodSnapshot + HasCase + other interfaces
|
||||||
return mockk<DualApplePods>(relaxed = true) {
|
return mockk<DualApplePods>(relaxed = true) {
|
||||||
every { this@mockk.model } returns model
|
every { this@mockk.model } returns model
|
||||||
every { batteryLeftPodPercent } returns leftBattery
|
every { batteryLeftPodPercent } returns leftBattery
|
||||||
@@ -39,14 +39,14 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `BLE-only device exposes battery from BLE`() {
|
fun `BLE-only device exposes battery from BLE`() {
|
||||||
val device = MonitoredDevice(ble = mockDualPod(leftBattery = 0.8f), aap = null)
|
val device = PodDevice(ble = mockDualPod(leftBattery = 0.8f), aap = null)
|
||||||
device.batteryLeft shouldBe 0.8f
|
device.batteryLeft shouldBe 0.8f
|
||||||
device.isAapConnected shouldBe false
|
device.isAapConnected shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `capabilities come from model features`() {
|
fun `capabilities come from model features`() {
|
||||||
val device = MonitoredDevice(
|
val device = PodDevice(
|
||||||
ble = mockDualPod(model = PodModel.AIRPODS_PRO3),
|
ble = mockDualPod(model = PodModel.AIRPODS_PRO3),
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
@@ -58,7 +58,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Beats Solo 3 has no dual pods or case`() {
|
fun `Beats Solo 3 has no dual pods or case`() {
|
||||||
val device = MonitoredDevice(
|
val device = PodDevice(
|
||||||
ble = mockk(relaxed = true) { every { model } returns PodModel.BEATS_SOLO_3 },
|
ble = mockk(relaxed = true) { every { model } returns PodModel.BEATS_SOLO_3 },
|
||||||
aap = null,
|
aap = null,
|
||||||
)
|
)
|
||||||
@@ -77,7 +77,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val device = MonitoredDevice(ble = mockDualPod(), aap = aap)
|
val device = PodDevice(ble = mockDualPod(), aap = aap)
|
||||||
device.isAapConnected shouldBe true
|
device.isAapConnected shouldBe true
|
||||||
device.ancMode.shouldNotBeNull()
|
device.ancMode.shouldNotBeNull()
|
||||||
device.ancMode!!.current shouldBe AncModeValue.TRANSPARENCY
|
device.ancMode!!.current shouldBe AncModeValue.TRANSPARENCY
|
||||||
@@ -85,21 +85,21 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ANC mode is null when not AAP connected`() {
|
fun `ANC mode is null when not AAP connected`() {
|
||||||
val device = MonitoredDevice(ble = mockDualPod(), aap = null)
|
val device = PodDevice(ble = mockDualPod(), aap = null)
|
||||||
device.ancMode.shouldBeNull()
|
device.ancMode.shouldBeNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `null BLE gives UNKNOWN model`() {
|
fun `null BLE gives UNKNOWN model`() {
|
||||||
val device = MonitoredDevice(ble = null, aap = null)
|
val device = PodDevice(ble = null, aap = null)
|
||||||
device.model shouldBe PodModel.UNKNOWN
|
device.model shouldBe PodModel.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `identity properties delegate to BLE`() {
|
fun `identity properties delegate to BLE`() {
|
||||||
val id = PodDevice.Id()
|
val id = BlePodSnapshot.Id()
|
||||||
val meta = mockk<PodDevice.Meta>(relaxed = true)
|
val meta = mockk<BlePodSnapshot.Meta>(relaxed = true)
|
||||||
val device = MonitoredDevice(
|
val device = PodDevice(
|
||||||
ble = mockk(relaxed = true) {
|
ble = mockk(relaxed = true) {
|
||||||
every { identifier } returns id
|
every { identifier } returns id
|
||||||
every { this@mockk.meta } returns meta
|
every { this@mockk.meta } returns meta
|
||||||
@@ -112,7 +112,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `identity properties null when BLE null`() {
|
fun `identity properties null when BLE null`() {
|
||||||
val device = MonitoredDevice(ble = null, aap = null)
|
val device = PodDevice(ble = null, aap = null)
|
||||||
device.identifier.shouldBeNull()
|
device.identifier.shouldBeNull()
|
||||||
device.meta.shouldBeNull()
|
device.meta.shouldBeNull()
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
fun `signal timing properties delegate to BLE`() {
|
fun `signal timing properties delegate to BLE`() {
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
val earlier = now.minusSeconds(60)
|
val earlier = now.minusSeconds(60)
|
||||||
val device = MonitoredDevice(
|
val device = PodDevice(
|
||||||
ble = mockk(relaxed = true) {
|
ble = mockk(relaxed = true) {
|
||||||
every { seenLastAt } returns now
|
every { seenLastAt } returns now
|
||||||
every { seenFirstAt } returns earlier
|
every { seenFirstAt } returns earlier
|
||||||
@@ -138,7 +138,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `signal timing defaults when BLE null`() {
|
fun `signal timing defaults when BLE null`() {
|
||||||
val device = MonitoredDevice(ble = null, aap = null)
|
val device = PodDevice(ble = null, aap = null)
|
||||||
device.seenLastAt.shouldBeNull()
|
device.seenLastAt.shouldBeNull()
|
||||||
device.seenFirstAt.shouldBeNull()
|
device.seenFirstAt.shouldBeNull()
|
||||||
device.signalQuality shouldBe 0f
|
device.signalQuality shouldBe 0f
|
||||||
@@ -153,7 +153,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
every { (this@mockk as HasChargeDetectionDual).isRightPodCharging } returns false
|
every { (this@mockk as HasChargeDetectionDual).isRightPodCharging } returns false
|
||||||
every { (this@mockk as HasCase).isCaseCharging } returns true
|
every { (this@mockk as HasCase).isCaseCharging } returns true
|
||||||
}
|
}
|
||||||
val device = MonitoredDevice(ble = mock, aap = null)
|
val device = PodDevice(ble = mock, aap = null)
|
||||||
device.isLeftPodCharging shouldBe true
|
device.isLeftPodCharging shouldBe true
|
||||||
device.isRightPodCharging shouldBe false
|
device.isRightPodCharging shouldBe false
|
||||||
device.isCaseCharging shouldBe true
|
device.isCaseCharging shouldBe true
|
||||||
@@ -168,7 +168,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
every { (this@mockk as HasEarDetection).isBeingWorn } returns false
|
every { (this@mockk as HasEarDetection).isBeingWorn } returns false
|
||||||
every { (this@mockk as HasEarDetectionDual).isEitherPodInEar } returns true
|
every { (this@mockk as HasEarDetectionDual).isEitherPodInEar } returns true
|
||||||
}
|
}
|
||||||
val device = MonitoredDevice(ble = mock, aap = null)
|
val device = PodDevice(ble = mock, aap = null)
|
||||||
device.isLeftInEar shouldBe true
|
device.isLeftInEar shouldBe true
|
||||||
device.isRightInEar shouldBe false
|
device.isRightInEar shouldBe false
|
||||||
device.isBeingWorn shouldBe false
|
device.isBeingWorn shouldBe false
|
||||||
@@ -177,7 +177,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `icon and label properties delegate to BLE`() {
|
fun `icon and label properties delegate to BLE`() {
|
||||||
val device = MonitoredDevice(
|
val device = PodDevice(
|
||||||
ble = mockk(relaxed = true) {
|
ble = mockk(relaxed = true) {
|
||||||
every { model } returns PodModel.AIRPODS_PRO3
|
every { model } returns PodModel.AIRPODS_PRO3
|
||||||
every { iconRes } returns 42
|
every { iconRes } returns 42
|
||||||
@@ -189,7 +189,7 @@ class MonitoredDeviceTest : BaseTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `rawDataHex empty when BLE null`() {
|
fun `rawDataHex empty when BLE null`() {
|
||||||
val device = MonitoredDevice(ble = null, aap = null)
|
val device = PodDevice(ble = null, aap = null)
|
||||||
device.rawDataHex shouldBe emptyList()
|
device.rawDataHex shouldBe emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
|
||||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||||
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
|
import eu.darken.capod.pods.core.apple.misc.UnknownAppleSnapshotBle
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.kotest.matchers.types.instanceOf
|
import io.kotest.matchers.types.instanceOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -28,13 +28,13 @@ class AirPodsFactoryTest : BaseAirPodsTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `unknown AppleDevice`() = runTest {
|
fun `unknown AppleDevice`() = runTest {
|
||||||
create<ApplePods>("07 19 01 FF FF 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
create<ApplePods>("07 19 01 FF FF 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
||||||
this shouldBe instanceOf<UnknownAppleDevice>()
|
this shouldBe instanceOf<UnknownAppleSnapshotBle>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `invalid data`() = runTest {
|
fun `invalid data`() = runTest {
|
||||||
create<PodDevice?>("abcd") {
|
create<BlePodSnapshot?>("abcd") {
|
||||||
this shouldBe null
|
this shouldBe null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import eu.darken.capod.common.SystemClockWrap
|
|||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.fromHex
|
import eu.darken.capod.common.fromHex
|
||||||
import eu.darken.capod.common.serialization.SerializationModule
|
import eu.darken.capod.common.serialization.SerializationModule
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
@@ -60,7 +60,7 @@ abstract class BaseAirPodsTest : BaseTest() {
|
|||||||
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
|
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend inline fun <reified T : PodDevice?> create(
|
internal suspend inline fun <reified T : BlePodSnapshot?> create(
|
||||||
hex: String,
|
hex: String,
|
||||||
address: String = "77:49:4C:D8:25:0C",
|
address: String = "77:49:4C:D8:25:0C",
|
||||||
block: T.() -> Unit
|
block: T.() -> Unit
|
||||||
|
|||||||
Reference in New Issue
Block a user