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.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.monitor.core.MonitoredDevice
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
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.HasChargeDetection
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
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.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.SingleBlePodSnapshot
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
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.DeviceProfile
|
||||
import java.time.Instant
|
||||
@@ -39,7 +39,7 @@ object MockPodDataProvider {
|
||||
|
||||
// --- Dual pod scenarios ---
|
||||
|
||||
fun airPodsProFullCharge(): DualPodDevice = MockDualPodDevice(
|
||||
fun airPodsProFullCharge(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||
_model = PodModel.AIRPODS_PRO2,
|
||||
_label = "Work AirPods",
|
||||
batteryLeftPodPercent = 1.0f,
|
||||
@@ -50,7 +50,7 @@ object MockPodDataProvider {
|
||||
_caseIcon = R.drawable.device_airpods_pro2_case,
|
||||
)
|
||||
|
||||
fun airPodsProMixed(): DualPodDevice = MockDualPodDevice(
|
||||
fun airPodsProMixed(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||
_model = PodModel.AIRPODS_PRO2,
|
||||
_label = "My AirPods Pro",
|
||||
batteryLeftPodPercent = 0.80f,
|
||||
@@ -62,7 +62,7 @@ object MockPodDataProvider {
|
||||
_caseIcon = R.drawable.device_airpods_pro2_case,
|
||||
)
|
||||
|
||||
fun airPodsProLowBattery(): DualPodDevice = MockDualPodDevice(
|
||||
fun airPodsProLowBattery(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||
_model = PodModel.AIRPODS_PRO2,
|
||||
_label = "My AirPods Pro",
|
||||
batteryLeftPodPercent = 0.10f,
|
||||
@@ -75,7 +75,7 @@ object MockPodDataProvider {
|
||||
_caseIcon = R.drawable.device_airpods_pro2_case,
|
||||
)
|
||||
|
||||
fun airPodsProInCase(): DualPodDevice = MockDualPodDevice(
|
||||
fun airPodsProInCase(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||
_model = PodModel.AIRPODS_PRO2,
|
||||
_label = "My AirPods Pro",
|
||||
batteryLeftPodPercent = null,
|
||||
@@ -86,7 +86,7 @@ object MockPodDataProvider {
|
||||
_caseIcon = R.drawable.device_airpods_pro2_case,
|
||||
)
|
||||
|
||||
fun airPodsGen1Wearing(): DualPodDevice = MockDualPodDevice(
|
||||
fun airPodsGen1Wearing(): DualBlePodSnapshot = MockDualBlePodSnapshot(
|
||||
_model = PodModel.AIRPODS_GEN1,
|
||||
_label = "Old AirPods",
|
||||
batteryLeftPodPercent = 0.70f,
|
||||
@@ -100,7 +100,7 @@ object MockPodDataProvider {
|
||||
_caseIcon = R.drawable.device_airpods_gen1_case,
|
||||
)
|
||||
|
||||
fun powerBeatsPro(): DualPodDevice = MockDualPodDeviceNoCase(
|
||||
fun powerBeatsPro(): DualBlePodSnapshot = MockDualBlePodSnapshotNoCase(
|
||||
_model = PodModel.POWERBEATS_PRO,
|
||||
_label = "PowerBeats Pro",
|
||||
batteryLeftPodPercent = 0.55f,
|
||||
@@ -111,14 +111,14 @@ object MockPodDataProvider {
|
||||
|
||||
// --- Single pod scenarios ---
|
||||
|
||||
fun airPodsMax(): SinglePodDevice = MockSinglePodDevice(
|
||||
fun airPodsMax(): SingleBlePodSnapshot = MockSingleBlePodSnapshot(
|
||||
_model = PodModel.AIRPODS_MAX,
|
||||
_label = "AirPods Max",
|
||||
batteryHeadsetPercent = 0.85f,
|
||||
_isBeingWorn = true,
|
||||
)
|
||||
|
||||
fun airPodsMaxCharging(): SinglePodDevice = MockSingleApplePodDevice(
|
||||
fun airPodsMaxCharging(): SingleBlePodSnapshot = MockSingleAppleBlePodSnapshot(
|
||||
_model = PodModel.AIRPODS_MAX,
|
||||
_label = "AirPods Max",
|
||||
batteryHeadsetPercent = 0.40f,
|
||||
@@ -127,7 +127,7 @@ object MockPodDataProvider {
|
||||
_hasPrivatePayload = true,
|
||||
)
|
||||
|
||||
fun beatsSolo3(): SinglePodDevice = MockSinglePodDevice(
|
||||
fun beatsSolo3(): SingleBlePodSnapshot = MockSingleBlePodSnapshot(
|
||||
_model = PodModel.BEATS_SOLO_3,
|
||||
_label = "Beats Solo 3",
|
||||
batteryHeadsetPercent = 0.70f,
|
||||
@@ -135,7 +135,7 @@ object MockPodDataProvider {
|
||||
|
||||
// --- Unknown device ---
|
||||
|
||||
fun unknownDevice(): PodDevice = UnknownDevice(
|
||||
fun unknownDevice(): BlePodSnapshot = UnknownSnapshotBle(
|
||||
scanResult = dummyScanResult(rssi = -70),
|
||||
)
|
||||
|
||||
@@ -146,24 +146,24 @@ object MockPodDataProvider {
|
||||
model = model,
|
||||
)
|
||||
|
||||
// --- MonitoredDevice wrappers ---
|
||||
// --- PodDevice wrappers ---
|
||||
|
||||
fun dualPodMonitored(): MonitoredDevice = MonitoredDevice(
|
||||
fun dualPodMonitored(): PodDevice = PodDevice(
|
||||
ble = airPodsProFullCharge(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun dualPodMonitoredMixed(): MonitoredDevice = MonitoredDevice(
|
||||
fun dualPodMonitoredMixed(): PodDevice = PodDevice(
|
||||
ble = airPodsProMixed(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun singlePodMonitored(): MonitoredDevice = MonitoredDevice(
|
||||
fun singlePodMonitored(): PodDevice = PodDevice(
|
||||
ble = airPodsMax(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun unknownMonitored(): MonitoredDevice = MonitoredDevice(
|
||||
fun unknownMonitored(): PodDevice = PodDevice(
|
||||
ble = unknownDevice(),
|
||||
aap = null,
|
||||
)
|
||||
@@ -188,7 +188,7 @@ private data class MockUpgradeInfo(
|
||||
override val error: Throwable? = null,
|
||||
) : UpgradeRepo.Info
|
||||
|
||||
private class MockDualPodDevice(
|
||||
private class MockDualBlePodSnapshot(
|
||||
private val _model: PodModel,
|
||||
private val _label: String,
|
||||
override val batteryLeftPodPercent: Float?,
|
||||
@@ -205,8 +205,8 @@ private class MockDualPodDevice(
|
||||
override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right,
|
||||
private val _caseIcon: Int = R.drawable.device_airpods_gen1_case,
|
||||
rssi: Int = -50,
|
||||
) : DualPodDevice, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone {
|
||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
||||
) : DualBlePodSnapshot, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone {
|
||||
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id()
|
||||
override val model: PodModel = _model
|
||||
override val seenLastAt: Instant = MOCK_NOW
|
||||
override val seenFirstAt: Instant = MOCK_NOW
|
||||
@@ -215,7 +215,7 @@ private class MockDualPodDevice(
|
||||
override val reliability: Float = 1.0f
|
||||
override val signalQuality: Float = 0.75f
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ private class MockDualPodDevice(
|
||||
override val isRightPodMicrophone: Boolean = _isRightPodMicrophone
|
||||
}
|
||||
|
||||
private class MockDualPodDeviceNoCase(
|
||||
private class MockDualBlePodSnapshotNoCase(
|
||||
private val _model: PodModel,
|
||||
private val _label: String,
|
||||
override val batteryLeftPodPercent: Float?,
|
||||
@@ -247,8 +247,8 @@ private class MockDualPodDeviceNoCase(
|
||||
override val leftPodIcon: Int = R.drawable.device_airpods_gen1_left,
|
||||
override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right,
|
||||
rssi: Int = -50,
|
||||
) : DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual {
|
||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
||||
) : DualBlePodSnapshot, HasChargeDetectionDual, HasEarDetectionDual {
|
||||
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id()
|
||||
override val model: PodModel = _model
|
||||
override val seenLastAt: Instant = MOCK_NOW
|
||||
override val seenFirstAt: Instant = MOCK_NOW
|
||||
@@ -257,7 +257,7 @@ private class MockDualPodDeviceNoCase(
|
||||
override val reliability: Float = 1.0f
|
||||
override val signalQuality: Float = 0.70f
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -272,15 +272,15 @@ private class MockDualPodDeviceNoCase(
|
||||
override val isRightPodInEar: Boolean = false
|
||||
}
|
||||
|
||||
private class MockSinglePodDevice(
|
||||
private class MockSingleBlePodSnapshot(
|
||||
private val _model: PodModel,
|
||||
private val _label: String,
|
||||
override val batteryHeadsetPercent: Float?,
|
||||
private val _isHeadsetBeingCharged: Boolean = false,
|
||||
private val _isBeingWorn: Boolean = false,
|
||||
rssi: Int = -50,
|
||||
) : SinglePodDevice, HasChargeDetection, HasEarDetection {
|
||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
||||
) : SingleBlePodSnapshot, HasChargeDetection, HasEarDetection {
|
||||
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id()
|
||||
override val model: PodModel = _model
|
||||
override val seenLastAt: Instant = MOCK_NOW
|
||||
override val seenFirstAt: Instant = MOCK_NOW
|
||||
@@ -289,7 +289,7 @@ private class MockSinglePodDevice(
|
||||
override val reliability: Float = 1.0f
|
||||
override val signalQuality: Float = 0.80f
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ private class MockSinglePodDevice(
|
||||
}
|
||||
|
||||
@Suppress("PropertyName")
|
||||
private class MockSingleApplePodDevice(
|
||||
private class MockSingleAppleBlePodSnapshot(
|
||||
private val _model: PodModel,
|
||||
private val _label: String,
|
||||
override val batteryHeadsetPercent: Float?,
|
||||
@@ -312,8 +312,8 @@ private class MockSingleApplePodDevice(
|
||||
private val _isIRKMatch: Boolean = false,
|
||||
private val _hasPrivatePayload: Boolean = false,
|
||||
rssi: Int = -50,
|
||||
) : SinglePodDevice, ApplePods, HasChargeDetection, HasEarDetection {
|
||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
||||
) : SingleBlePodSnapshot, ApplePods, HasChargeDetection, HasEarDetection {
|
||||
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id()
|
||||
override val model: PodModel = _model
|
||||
override val seenLastAt: 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.UnknownPodDeviceCard
|
||||
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 java.time.Instant
|
||||
|
||||
@@ -257,7 +257,7 @@ fun OverviewScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PodDeviceCard(device: MonitoredDevice, showDebug: Boolean, now: Instant) {
|
||||
private fun PodDeviceCard(device: PodDevice, showDebug: Boolean, now: Instant) {
|
||||
when {
|
||||
device.hasDualPods -> DualPodsCard(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 eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
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.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.SingleEventFlow
|
||||
import eu.darken.capod.common.flow.combine
|
||||
|
||||
import eu.darken.capod.common.flow.throttleLatest
|
||||
import eu.darken.capod.common.navigation.Nav
|
||||
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.PermissionTool
|
||||
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.profiles.core.DeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import java.time.Instant
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
@@ -33,8 +32,8 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
|
||||
@HiltViewModel
|
||||
class OverviewViewModel @Inject constructor(
|
||||
@@ -71,6 +70,7 @@ class OverviewViewModel @Inject constructor(
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
MonitorMode.ALWAYS -> true
|
||||
}
|
||||
if (shouldStart) {
|
||||
@@ -122,7 +122,7 @@ class OverviewViewModel @Inject constructor(
|
||||
data class State(
|
||||
val now: Instant,
|
||||
val permissions: Set<Permission>,
|
||||
val devices: List<MonitoredDevice>,
|
||||
val devices: List<PodDevice>,
|
||||
val isDebugMode: Boolean,
|
||||
val isBluetoothEnabled: Boolean,
|
||||
val profiles: List<DeviceProfile>,
|
||||
@@ -130,8 +130,8 @@ class OverviewViewModel @Inject constructor(
|
||||
val showUnmatchedDevices: Boolean,
|
||||
) {
|
||||
val isScanBlocked: Boolean get() = permissions.any { it.isScanBlocking }
|
||||
val profiledDevices: List<MonitoredDevice> get() = devices.filter { it.meta?.profile != null }
|
||||
val unmatchedDevices: List<MonitoredDevice> get() = devices.filter { it.meta?.profile == null }
|
||||
val profiledDevices: List<PodDevice> 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) {
|
||||
|
||||
@@ -44,10 +44,10 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
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.lastSeenFormatted
|
||||
import eu.darken.capod.monitor.core.firstSeenFormatted
|
||||
import eu.darken.capod.pods.core.HasPodStyle
|
||||
import eu.darken.capod.pods.core.HasStateDetection
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
@@ -61,7 +61,7 @@ import java.time.Instant
|
||||
|
||||
@Composable
|
||||
fun DualPodsCard(
|
||||
device: MonitoredDevice,
|
||||
device: PodDevice,
|
||||
showDebug: Boolean,
|
||||
now: Instant,
|
||||
) {
|
||||
@@ -316,7 +316,7 @@ private fun PodGauge(
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun CaseRow(
|
||||
device: MonitoredDevice,
|
||||
device: PodDevice,
|
||||
) {
|
||||
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.Key
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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.PreviewWrapper
|
||||
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.lastSeenFormatted
|
||||
import eu.darken.capod.monitor.core.firstSeenFormatted
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import java.time.Duration
|
||||
@@ -56,7 +56,7 @@ import java.time.Instant
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun SinglePodsCard(
|
||||
device: MonitoredDevice,
|
||||
device: PodDevice,
|
||||
showDebug: Boolean,
|
||||
now: Instant,
|
||||
) {
|
||||
|
||||
@@ -21,18 +21,18 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.PreviewWrapper
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun UnknownPodDeviceCard(
|
||||
device: MonitoredDevice,
|
||||
device: PodDevice,
|
||||
showDebug: Boolean,
|
||||
now: Instant,
|
||||
) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.darken.capod.main.ui.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.Keep
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.glance.GlanceId
|
||||
@@ -10,7 +11,6 @@ import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.annotation.Keep
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
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.isPro
|
||||
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
|
||||
|
||||
class BatteryGlanceWidget : GlanceAppWidget() {
|
||||
@@ -45,7 +45,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
|
||||
val appWidgetId: Int
|
||||
val initialIsPro: Boolean
|
||||
val initialProfileId: String?
|
||||
val cachedDevice: MonitoredDevice?
|
||||
val cachedDevice: PodDevice?
|
||||
|
||||
try {
|
||||
ep = EntryPointAccessors.fromApplication(context, WidgetEntryPoint::class.java)
|
||||
@@ -139,7 +139,7 @@ class BatteryGlanceWidget : GlanceAppWidget() {
|
||||
var n = 2
|
||||
while (70 * n - 30 < size) {
|
||||
++n
|
||||
}
|
||||
}
|
||||
return n - 1
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.Context
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
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.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.toBatteryFloat
|
||||
@@ -13,7 +13,7 @@ object WidgetRenderStateMapper {
|
||||
|
||||
fun map(
|
||||
context: Context,
|
||||
device: MonitoredDevice?,
|
||||
device: PodDevice?,
|
||||
theme: WidgetTheme,
|
||||
isPro: Boolean,
|
||||
hasConfiguredProfile: Boolean,
|
||||
|
||||
@@ -1,12 +1,217 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
/**
|
||||
* Type alias for the BLE scan pipeline.
|
||||
*
|
||||
* The [PodMonitor] class handles BLE advertisement scanning and device detection.
|
||||
* New code should use [BlePodMonitor] to clarify that this is the BLE-only monitor,
|
||||
* not the unified merge point ([DeviceMonitor]).
|
||||
*
|
||||
* Full rename deferred to IDE refactoring pass.
|
||||
*/
|
||||
typealias BlePodMonitor = PodMonitor
|
||||
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.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
|
||||
* ([AapConnectionManager]) into unified [MonitoredDevice] objects.
|
||||
* ([AapConnectionManager]) into unified [PodDevice] objects.
|
||||
*
|
||||
* ViewModels should observe [devices] instead of accessing BlePodMonitor directly.
|
||||
*/
|
||||
@@ -20,21 +20,21 @@ class DeviceMonitor @Inject constructor(
|
||||
private val blePodMonitor: BlePodMonitor,
|
||||
private val aapManager: AapConnectionManager,
|
||||
) {
|
||||
val devices: Flow<List<MonitoredDevice>> = blePodMonitor.devices
|
||||
val devices: Flow<List<PodDevice>> = blePodMonitor.devices
|
||||
.combine(aapManager.allStates) { pods, aapStates ->
|
||||
pods.map { pod ->
|
||||
MonitoredDevice(
|
||||
PodDevice(
|
||||
ble = pod,
|
||||
aap = aapStates[pod.address],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getDeviceForProfile(profileId: String): MonitoredDevice? {
|
||||
suspend fun getDeviceForProfile(profileId: String): PodDevice? {
|
||||
log(TAG) { "getDeviceForProfile(profileId=$profileId)" }
|
||||
val bleDevice = blePodMonitor.getDeviceForProfile(profileId) ?: return null
|
||||
val aapState = aapManager.allStates.firstOrNull()?.get(bleDevice.address)
|
||||
return MonitoredDevice(ble = bleDevice, aap = aapState)
|
||||
return PodDevice(ble = bleDevice, aap = aapState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -8,17 +8,17 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
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 } }
|
||||
|
||||
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
|
||||
return "${multiplier.roundToInt()}%"
|
||||
}
|
||||
|
||||
fun MonitoredDevice.lastSeenFormatted(now: Instant): String {
|
||||
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||
val lastAt = seenLastAt ?: return ""
|
||||
val formatter = RelativeDateTimeFormatter.getInstance()
|
||||
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 formatter = RelativeDateTimeFormatter.getInstance()
|
||||
val duration = Duration.between(firstAt, now)
|
||||
|
||||
+10
-11
@@ -3,16 +3,15 @@ package eu.darken.capod.monitor.core
|
||||
import android.content.Context
|
||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||
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.HasChargeDetection
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
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.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.SingleBlePodSnapshot
|
||||
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.AapSetting
|
||||
@@ -23,15 +22,15 @@ import java.time.Instant
|
||||
* All properties are dynamically resolved from the best available source.
|
||||
* 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 aap: AapPodState?,
|
||||
) {
|
||||
// Identity
|
||||
val model: PodModel get() = ble?.model ?: PodModel.UNKNOWN
|
||||
val address: BluetoothAddress? get() = ble?.address
|
||||
val identifier: PodDevice.Id? get() = ble?.identifier
|
||||
val meta: PodDevice.Meta? get() = ble?.meta
|
||||
val identifier: BlePodSnapshot.Id? get() = ble?.identifier
|
||||
val meta: BlePodSnapshot.Meta? get() = ble?.meta
|
||||
|
||||
// Capabilities from Model.Features
|
||||
val hasCase: Boolean get() = model.features.hasCase
|
||||
@@ -48,16 +47,16 @@ data class MonitoredDevice(
|
||||
|
||||
// Battery — best available source
|
||||
val batteryLeft: Float?
|
||||
get() = (ble as? DualPodDevice)?.batteryLeftPodPercent
|
||||
get() = (ble as? DualBlePodSnapshot)?.batteryLeftPodPercent
|
||||
|
||||
val batteryRight: Float?
|
||||
get() = (ble as? DualPodDevice)?.batteryRightPodPercent
|
||||
get() = (ble as? DualBlePodSnapshot)?.batteryRightPodPercent
|
||||
|
||||
val batteryCase: Float?
|
||||
get() = (ble as? HasCase)?.batteryCasePercent
|
||||
|
||||
val batteryHeadset: Float?
|
||||
get() = (ble as? SinglePodDevice)?.batteryHeadsetPercent
|
||||
get() = (ble as? SingleBlePodSnapshot)?.batteryHeadsetPercent
|
||||
|
||||
// Charging
|
||||
val isLeftPodCharging: Boolean?
|
||||
@@ -99,10 +98,10 @@ data class MonitoredDevice(
|
||||
val iconRes: Int get() = ble?.iconRes ?: model.iconRes
|
||||
|
||||
val leftPodIcon: Int?
|
||||
get() = (ble as? DualPodDevice)?.leftPodIcon
|
||||
get() = (ble as? DualBlePodSnapshot)?.leftPodIcon
|
||||
|
||||
val rightPodIcon: Int?
|
||||
get() = (ble as? DualPodDevice)?.rightPodIcon
|
||||
get() = (ble as? DualBlePodSnapshot)?.rightPodIcon
|
||||
|
||||
val caseIcon: Int?
|
||||
get() = (ble as? HasCase)?.caseIcon
|
||||
@@ -78,7 +78,7 @@ class PodDeviceCache @Inject constructor(
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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 } }
|
||||
|
||||
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.BluetoothManager2
|
||||
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.logging.Logging.Priority.VERBOSE
|
||||
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.MonitorMode
|
||||
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.MonitorCoroutineScope
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.monitor.core.primaryDevice
|
||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
@@ -54,7 +55,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MonitorService : Service() {
|
||||
@@ -64,7 +64,7 @@ class MonitorService : Service() {
|
||||
@Inject lateinit var notificationManager: NotificationManager
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@Inject lateinit var permissionTool: PermissionTool
|
||||
@Inject lateinit var podMonitor: PodMonitor
|
||||
@Inject lateinit var blePodMonitor: BlePodMonitor
|
||||
@Inject lateinit var deviceMonitor: DeviceMonitor
|
||||
@Inject lateinit var bluetoothManager: BluetoothManager2
|
||||
@Inject lateinit var playPause: PlayPause
|
||||
@@ -179,7 +179,7 @@ class MonitorService : Service() {
|
||||
}
|
||||
|
||||
val monitorJob = deviceMonitor.primaryDevice()
|
||||
.setupCommonEventHandlers(TAG) { "PodMonitor" }
|
||||
.setupCommonEventHandlers(TAG) { "BlePodMonitor" }
|
||||
.distinctUntilChanged()
|
||||
.throttleLatest(1000)
|
||||
.onEach { currentDevice ->
|
||||
@@ -222,6 +222,7 @@ class MonitorService : Service() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val profiles = arguments[1] as List<DeviceProfile>
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val devices = arguments[2] as Collection<BluetoothDevice2>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
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.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
@@ -17,13 +17,13 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
fun createContentView(device: MonitoredDevice): RemoteViews = when {
|
||||
fun createContentView(device: PodDevice): RemoteViews = when {
|
||||
device.hasDualPods -> createDualPods(device)
|
||||
device.model != PodModel.UNKNOWN -> createSinglePod(device)
|
||||
else -> createUnknownDevice(device)
|
||||
}
|
||||
|
||||
private fun createDualPods(device: MonitoredDevice): RemoteViews = RemoteViews(
|
||||
private fun createDualPods(device: PodDevice): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_dual_pods_small
|
||||
).apply {
|
||||
@@ -55,7 +55,7 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
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,
|
||||
R.layout.monitor_notification_single_pods_small
|
||||
).apply {
|
||||
@@ -68,24 +68,27 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn == true) View.VISIBLE else View.GONE)
|
||||
}
|
||||
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,
|
||||
R.layout.monitor_notification_unknown_device_small
|
||||
).apply {
|
||||
setTextViewText(R.id.device, device.getLabel(context))
|
||||
}
|
||||
|
||||
fun createBigContentView(device: MonitoredDevice): RemoteViews = when {
|
||||
fun createBigContentView(device: PodDevice): RemoteViews = when {
|
||||
device.hasDualPods -> createDualPodsBig(device)
|
||||
device.model != PodModel.UNKNOWN -> createSinglePodBig(device)
|
||||
else -> createUnknownDeviceBig(device)
|
||||
}
|
||||
|
||||
private fun createDualPodsBig(device: MonitoredDevice): RemoteViews = RemoteViews(
|
||||
private fun createDualPodsBig(device: PodDevice): RemoteViews = RemoteViews(
|
||||
context.packageName,
|
||||
R.layout.monitor_notification_dual_pods_big
|
||||
).apply {
|
||||
@@ -120,7 +123,7 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
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,
|
||||
R.layout.monitor_notification_single_pods_big
|
||||
).apply {
|
||||
@@ -133,11 +136,14 @@ class MonitorNotificationViewFactory @Inject constructor(
|
||||
setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn == true) View.VISIBLE else View.GONE)
|
||||
}
|
||||
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,
|
||||
R.layout.monitor_notification_unknown_device_big
|
||||
).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.notifications.PendingIntentCompat
|
||||
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.formatBatteryPercent
|
||||
import javax.inject.Inject
|
||||
@@ -53,7 +53,11 @@ class MonitorNotifications @Inject constructor(
|
||||
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) {
|
||||
return baseBuilder(channelId).apply {
|
||||
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()
|
||||
|
||||
fun getNotificationConnected(podDevice: MonitoredDevice?): Notification =
|
||||
fun getNotificationConnected(podDevice: PodDevice?): Notification =
|
||||
getBuilder(podDevice, NOTIFICATION_CHANNEL_ID_CONNECTED).build()
|
||||
|
||||
fun getStartupNotification(): Notification =
|
||||
|
||||
@@ -1,15 +1,77 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
/**
|
||||
* Type alias for the BLE-sourced device snapshot.
|
||||
*
|
||||
* The [PodDevice] interface represents BLE scan data (battery, ear detection, case state)
|
||||
* extracted from Apple Continuity Protocol advertisements.
|
||||
*
|
||||
* New code should use [BlePodSnapshot] to clarify that this is the BLE data source,
|
||||
* not the unified facade ([eu.darken.capod.monitor.core.MonitoredDevice]).
|
||||
*
|
||||
* Full rename of [PodDevice] → [BlePodSnapshot] across the codebase is deferred to
|
||||
* an IDE refactoring pass (103 files, 470 occurrences).
|
||||
*/
|
||||
typealias BlePodSnapshot = PodDevice
|
||||
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 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 eu.darken.capod.R
|
||||
|
||||
interface DualPodDevice : PodDevice {
|
||||
interface DualBlePodSnapshot : BlePodSnapshot {
|
||||
|
||||
enum class Pod {
|
||||
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()}%" }
|
||||
?: 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
|
||||
return "${multiplier.roundToInt()}%"
|
||||
}
|
||||
@@ -36,7 +36,7 @@ fun getBatteryDrawable(percent: Float?): Int = when {
|
||||
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 duration = Duration.between(seenLastAt, now)
|
||||
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 duration = Duration.between(seenFirstAt, now)
|
||||
return formatter.format(
|
||||
|
||||
@@ -36,7 +36,7 @@ class PodFactory @Inject constructor(
|
||||
|
||||
data class Result(
|
||||
val scanResult: BleScanResult,
|
||||
val device: PodDevice
|
||||
val device: BlePodSnapshot
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface SinglePodDevice : PodDevice {
|
||||
interface SingleBlePodSnapshot : BlePodSnapshot {
|
||||
|
||||
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.log
|
||||
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.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.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
@@ -28,7 +28,7 @@ class AppleFactory @Inject constructor(
|
||||
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
||||
private val proximityMessageDecrypter: ProximityMessage.Decrypter,
|
||||
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory>,
|
||||
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
||||
private val unknownAppleFactory: UnknownAppleSnapshotBle.Factory,
|
||||
private val rpaChecker: RPAChecker,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
) {
|
||||
@@ -60,7 +60,7 @@ class AppleFactory @Inject constructor(
|
||||
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 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.upperNibble
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
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
|
||||
|
||||
@@ -59,7 +59,7 @@ interface ApplePods : PodDevice {
|
||||
data class AppleMeta(
|
||||
val isIRKMatch: Boolean = false,
|
||||
override val profile: AppleDeviceProfile? = null,
|
||||
) : PodDevice.Meta
|
||||
) : BlePodSnapshot.Meta
|
||||
|
||||
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.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
||||
import eu.darken.capod.pods.core.DualBlePodSnapshot
|
||||
import eu.darken.capod.pods.core.DualBlePodSnapshot.Pod
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
|
||||
interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
||||
interface DualApplePods : ApplePods, HasChargeDetectionDual, DualBlePodSnapshot, HasEarDetectionDual, HasCase,
|
||||
HasDualMicrophone, HasAppleColor {
|
||||
|
||||
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.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
|
||||
*/
|
||||
interface SingleApplePods : ApplePods, SinglePodDevice, HasAppleColor {
|
||||
interface SingleApplePods : ApplePods, SingleBlePodSnapshot, HasAppleColor {
|
||||
|
||||
override val batteryHeadsetPercent: Float?
|
||||
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.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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = 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.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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = 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.debug.logging.logTag
|
||||
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.HasEarDetection
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -19,14 +19,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : 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.debug.logging.logTag
|
||||
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.HasEarDetection
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -19,14 +19,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -18,14 +18,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -18,14 +18,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -18,14 +18,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -18,13 +18,13 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = 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.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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : 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.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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : 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.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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : 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.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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : 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.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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -15,14 +15,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : 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.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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -16,14 +16,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
override val meta: ApplePods.AppleMeta,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasStateDetectionAirPods {
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -17,14 +17,14 @@ import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = 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.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.protocol.ProximityPayload
|
||||
import java.time.Duration
|
||||
@@ -10,7 +10,7 @@ import java.time.Instant
|
||||
import kotlin.math.max
|
||||
|
||||
data class KnownDevice(
|
||||
val id: PodDevice.Id,
|
||||
val id: BlePodSnapshot.Id,
|
||||
val seenFirstAt: Instant,
|
||||
val seenCounter: Int,
|
||||
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.lowerNibble
|
||||
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.PodDevice
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
@@ -27,7 +27,7 @@ class PodHistoryRepo @Inject constructor(
|
||||
private val rpaChecker: RPAChecker,
|
||||
) {
|
||||
|
||||
private val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||
private val knownDevices = mutableMapOf<BlePodSnapshot.Id, KnownDevice>()
|
||||
|
||||
data class Identifier(
|
||||
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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -22,14 +22,14 @@ import javax.inject.Inject
|
||||
* Marketed as TWS i99999
|
||||
*/
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
) : 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.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
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.apple.ApplePods
|
||||
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
|
||||
*/
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
|
||||
@@ -4,10 +4,10 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -24,14 +24,14 @@ import javax.inject.Inject
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/1054404630118924308
|
||||
*/
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
|
||||
@@ -4,10 +4,10 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -24,14 +24,14 @@ import javax.inject.Inject
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/962068944196358234
|
||||
*/
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
) : DualApplePods, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||
|
||||
@@ -4,11 +4,11 @@ import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.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.HasChargeDetectionDual
|
||||
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.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||
@@ -25,18 +25,18 @@ import javax.inject.Inject
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
|
||||
*/
|
||||
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 seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val payload: ProximityPayload,
|
||||
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 cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null,
|
||||
) : DualApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
|
||||
) : DualApplePods, HasChargeDetectionDual, DualBlePodSnapshot, HasEarDetectionDual, HasCase {
|
||||
|
||||
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.common.bluetooth.BleScanResult
|
||||
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.apple.ApplePods
|
||||
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 javax.inject.Inject
|
||||
|
||||
data class UnknownAppleDevice(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
data class UnknownAppleSnapshotBle(
|
||||
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||
override val seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
@@ -43,7 +43,7 @@ data class UnknownAppleDevice(
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta,
|
||||
): ApplePods {
|
||||
var basic = UnknownAppleDevice(scanResult = scanResult, payload = payload, meta = meta)
|
||||
var basic = UnknownAppleSnapshotBle(scanResult = scanResult, payload = payload, meta = meta)
|
||||
val result = repo.search(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
@@ -47,6 +47,6 @@ class RPAChecker @Inject constructor() {
|
||||
}
|
||||
|
||||
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.log
|
||||
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.withLock
|
||||
import java.time.Duration
|
||||
@@ -17,9 +17,9 @@ class UnknownDeviceFactory @Inject constructor() {
|
||||
|
||||
private val lock = Mutex()
|
||||
|
||||
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
|
||||
var basic = UnknownDevice(
|
||||
identifier = PodDevice.Id(),
|
||||
suspend fun create(scanResult: BleScanResult): BlePodSnapshot = lock.withLock {
|
||||
var basic = UnknownSnapshotBle(
|
||||
identifier = BlePodSnapshot.Id(),
|
||||
scanResult = scanResult,
|
||||
)
|
||||
val result = searchHistory(basic)
|
||||
@@ -40,10 +40,10 @@ class UnknownDeviceFactory @Inject constructor() {
|
||||
}
|
||||
|
||||
data class KnownDevice(
|
||||
val id: PodDevice.Id,
|
||||
val id: BlePodSnapshot.Id,
|
||||
val seenFirstAt: Instant,
|
||||
val seenCounter: Int,
|
||||
val history: List<PodDevice>
|
||||
val history: List<BlePodSnapshot>
|
||||
) {
|
||||
|
||||
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
|
||||
|
||||
knownDevices.values.toList().forEach { knownDevice ->
|
||||
@@ -104,7 +104,7 @@ class UnknownDeviceFactory @Inject constructor() {
|
||||
return recognizedDevice
|
||||
}
|
||||
|
||||
private fun updateHistory(device: PodDevice) {
|
||||
private fun updateHistory(device: BlePodSnapshot) {
|
||||
val existing = knownDevices[device.identifier]
|
||||
|
||||
knownDevices[device.identifier] = when {
|
||||
@@ -114,6 +114,7 @@ class UnknownDeviceFactory @Inject constructor() {
|
||||
history = existing.history.plus(device)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
log(TAG) { "searchHistory1: Creating new history for $device" }
|
||||
KnownDevice(
|
||||
|
||||
+5
-5
@@ -4,13 +4,13 @@ import android.content.Context
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.PodModel
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
import java.time.Instant
|
||||
|
||||
data class UnknownDevice(
|
||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||
data class UnknownSnapshotBle(
|
||||
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
|
||||
override val seenLastAt: Instant = Instant.now(),
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
@@ -18,7 +18,7 @@ data class UnknownDevice(
|
||||
override val reliability: Float = 0f,
|
||||
override val meta: Meta = Meta(),
|
||||
private val rssiAverage: Int? = null,
|
||||
) : PodDevice {
|
||||
) : BlePodSnapshot {
|
||||
|
||||
override val model: PodModel = PodModel.UNKNOWN
|
||||
|
||||
@@ -29,7 +29,7 @@ data class UnknownDevice(
|
||||
|
||||
data class Meta(
|
||||
override val profile: DeviceProfile? = null
|
||||
) : PodDevice.Meta
|
||||
) : BlePodSnapshot.Meta
|
||||
|
||||
companion object {
|
||||
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.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.WARN
|
||||
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.withPrevious
|
||||
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.reaction.core.ReactionSettings
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
|
||||
@Singleton
|
||||
class PlayPause @Inject constructor(
|
||||
@@ -63,7 +62,7 @@ class PlayPause @Inject constructor(
|
||||
|
||||
when {
|
||||
previous!!.hasEarDetection && previous.hasDualPods &&
|
||||
current!!.hasEarDetection && current.hasDualPods -> {
|
||||
current!!.hasEarDetection && current.hasDualPods -> {
|
||||
// Dual pod devices (AirPods, AirPods Pro, etc.)
|
||||
log(TAG, VERBOSE) {
|
||||
"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.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.WARN
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.flow.withPrevious
|
||||
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.pods.core.apple.DualApplePods
|
||||
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}" }
|
||||
|
||||
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 isNewDeviceWithJustOpenedCase = !isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState
|
||||
|
||||
@@ -67,7 +65,7 @@ class PopUpReaction @Inject constructor(
|
||||
throttleCasePopUps(current)
|
||||
}
|
||||
|
||||
private fun throttleCasePopUps(current: MonitoredDevice): Event? {
|
||||
private fun throttleCasePopUps(current: PodDevice): Event? {
|
||||
val cooldownKey = current.meta?.profile?.id ?: current.identifier.toString()
|
||||
val now = Instant.now()
|
||||
val lastShown = caseCoolDowns[cooldownKey]
|
||||
@@ -89,12 +87,14 @@ class PopUpReaction @Inject constructor(
|
||||
caseCoolDowns[cooldownKey] = now
|
||||
Event.PopupShow(device = current)
|
||||
}
|
||||
|
||||
decision.shouldHide -> {
|
||||
if (!decision.shouldResetCooldown) {
|
||||
caseCoolDowns[cooldownKey] = now
|
||||
}
|
||||
Event.PopupHide()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class PopUpReaction @Inject constructor(
|
||||
sealed class Event {
|
||||
data class PopupShow(
|
||||
val eventAt: Instant = Instant.now(),
|
||||
val device: MonitoredDevice,
|
||||
val device: PodDevice,
|
||||
) : Event()
|
||||
|
||||
data class PopupHide(
|
||||
@@ -210,12 +210,14 @@ class PopUpReaction @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DualApplePods.LidState.CLOSED -> CasePopUpDecision(
|
||||
shouldShow = false,
|
||||
shouldHide = true,
|
||||
shouldResetCooldown = true,
|
||||
reason = "Lid CLOSED, resetting cooldown",
|
||||
)
|
||||
|
||||
else -> CasePopUpDecision(
|
||||
shouldShow = false,
|
||||
shouldHide = true,
|
||||
|
||||
@@ -34,7 +34,7 @@ import eu.darken.capod.R
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
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.pods.core.PodModel
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
@@ -44,7 +44,7 @@ import eu.darken.capod.pods.core.toBatteryOrNull
|
||||
|
||||
@Composable
|
||||
fun PopUpContent(
|
||||
device: MonitoredDevice,
|
||||
device: PodDevice,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -117,7 +117,7 @@ fun PopUpContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DualPodContent(device: MonitoredDevice) {
|
||||
private fun DualPodContent(device: PodDevice) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
@@ -148,7 +148,7 @@ private fun DualPodContent(device: MonitoredDevice) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SinglePodContent(device: MonitoredDevice) {
|
||||
private fun SinglePodContent(device: PodDevice) {
|
||||
BatteryColumn(
|
||||
iconRes = device.iconRes,
|
||||
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.main.core.GeneralSettings
|
||||
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.Singleton
|
||||
|
||||
@@ -50,9 +50,9 @@ class PopUpWindow @Inject constructor(
|
||||
|
||||
private var composeView: ComposeView? = 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 {
|
||||
log(TAG) { "open()" }
|
||||
|
||||
@@ -64,7 +64,7 @@ class PopUpWindow @Inject constructor(
|
||||
|
||||
teardown()
|
||||
|
||||
val state = mutableStateOf<MonitoredDevice?>(device)
|
||||
val state = mutableStateOf<PodDevice?>(device)
|
||||
deviceState = state
|
||||
|
||||
val owner = OverlayLifecycleOwner()
|
||||
|
||||
@@ -6,18 +6,18 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
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.logging.Logging.Priority.INFO
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
|
||||
import eu.darken.capod.common.uix.ViewModel4
|
||||
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.PodMonitor
|
||||
import eu.darken.capod.monitor.core.primaryDevice
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.unknown.UnknownDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.pods.core.unknown.UnknownSnapshotBle
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import javax.inject.Inject
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
|
||||
@HiltViewModel
|
||||
class TroubleShooterViewModel @Inject constructor(
|
||||
@@ -39,7 +38,7 @@ class TroubleShooterViewModel @Inject constructor(
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
private val podMonitor: PodMonitor,
|
||||
private val blePodMonitor: BlePodMonitor,
|
||||
private val deviceMonitor: DeviceMonitor,
|
||||
private val debugSettings: DebugSettings,
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
@@ -98,39 +97,40 @@ class TroubleShooterViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val doScan: suspend (Boolean, Boolean, Boolean, Boolean) -> Collection<PodDevice> = { hardwareFilteringDisabled,
|
||||
hardwareBatchingDisabled,
|
||||
indirectCallback,
|
||||
unfiltered ->
|
||||
val sb = StringBuilder("SCAN - Settings: ")
|
||||
sb.append("hardwareFilteringDisabled=$hardwareFilteringDisabled, ")
|
||||
sb.append("hardwareBatchingDisabled=$hardwareBatchingDisabled, ")
|
||||
sb.append("indirectCallback=$indirectCallback, ")
|
||||
sb.append("unfiltered=$unfiltered")
|
||||
progress(sb.toString())
|
||||
generalSettings.isOffloadedFilteringDisabled.valueBlocking = hardwareFilteringDisabled
|
||||
generalSettings.isOffloadedBatchingDisabled.valueBlocking = hardwareBatchingDisabled
|
||||
generalSettings.useIndirectScanResultCallback.valueBlocking = indirectCallback
|
||||
debugSettings.showUnfiltered.valueBlocking = unfiltered
|
||||
val doScan: suspend (Boolean, Boolean, Boolean, Boolean) -> Collection<BlePodSnapshot> =
|
||||
{ hardwareFilteringDisabled,
|
||||
hardwareBatchingDisabled,
|
||||
indirectCallback,
|
||||
unfiltered ->
|
||||
val sb = StringBuilder("SCAN - Settings: ")
|
||||
sb.append("hardwareFilteringDisabled=$hardwareFilteringDisabled, ")
|
||||
sb.append("hardwareBatchingDisabled=$hardwareBatchingDisabled, ")
|
||||
sb.append("indirectCallback=$indirectCallback, ")
|
||||
sb.append("unfiltered=$unfiltered")
|
||||
progress(sb.toString())
|
||||
generalSettings.isOffloadedFilteringDisabled.valueBlocking = hardwareFilteringDisabled
|
||||
generalSettings.isOffloadedBatchingDisabled.valueBlocking = hardwareBatchingDisabled
|
||||
generalSettings.useIndirectScanResultCallback.valueBlocking = indirectCallback
|
||||
debugSettings.showUnfiltered.valueBlocking = unfiltered
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
val devices = withTimeoutOrNull(STEP_TIME) {
|
||||
podMonitor.devices
|
||||
.take(10)
|
||||
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
||||
.toList()
|
||||
.flatten()
|
||||
.distinctBy { it.address }
|
||||
} ?: emptyList()
|
||||
log(TAG) { "SCAN: BLE Devices: $devices" }
|
||||
if (devices.isNotEmpty()) {
|
||||
progress("SCAN: Received data from ${devices.size} BLE devices")
|
||||
devices
|
||||
} else {
|
||||
progress("SCAN: No data received")
|
||||
devices
|
||||
val start = System.currentTimeMillis()
|
||||
val devices = withTimeoutOrNull(STEP_TIME) {
|
||||
blePodMonitor.devices
|
||||
.take(10)
|
||||
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
||||
.toList()
|
||||
.flatten()
|
||||
.distinctBy { it.address }
|
||||
} ?: emptyList()
|
||||
log(TAG) { "SCAN: BLE Devices: $devices" }
|
||||
if (devices.isNotEmpty()) {
|
||||
progress("SCAN: Received data from ${devices.size} BLE devices")
|
||||
devices
|
||||
} else {
|
||||
progress("SCAN: No data received")
|
||||
devices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
progress("Checking if we can receive BLE data at all.")
|
||||
@@ -158,14 +158,14 @@ class TroubleShooterViewModel @Inject constructor(
|
||||
run {
|
||||
progress("Checking for supported headphones.")
|
||||
|
||||
if (doScan(false, false, false, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(false, false, true, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(true, true, true, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(true, true, false, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(true, false, true, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(true, false, false, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(false, true, true, false).any { it !is UnknownDevice }) return@run
|
||||
if (doScan(false, true, 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 UnknownSnapshotBle }) return@run
|
||||
if (doScan(true, true, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(true, true, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(true, false, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(true, false, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(false, true, true, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
if (doScan(false, true, false, false).any { it !is UnknownSnapshotBle }) return@run
|
||||
|
||||
failure("No compatible headphones found", BleState.Result.Failure.Type.HEADPHONES)
|
||||
|
||||
@@ -196,7 +196,7 @@ class TroubleShooterViewModel @Inject constructor(
|
||||
|
||||
val otherDevices = withTimeoutOrNull(STEP_TIME) {
|
||||
val start = System.currentTimeMillis()
|
||||
podMonitor.devices
|
||||
blePodMonitor.devices
|
||||
.take(10)
|
||||
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
|
||||
.toList()
|
||||
@@ -215,7 +215,7 @@ class TroubleShooterViewModel @Inject constructor(
|
||||
progress("Creating profile for closest headphones.")
|
||||
|
||||
val candidate = otherDevices
|
||||
.filter { it !is UnknownDevice }
|
||||
.filter { it !is UnknownSnapshotBle }
|
||||
.maxBy { it.signalQuality }
|
||||
|
||||
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.PermissionTool
|
||||
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.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.BlePodSnapshot
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
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.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.coroutine.TestDispatcherProvider
|
||||
import testhelpers.datastore.FakeDataStoreValue
|
||||
import testhelpers.livedata.InstantExecutorExtension
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@ExtendWith(InstantExecutorExtension::class)
|
||||
@@ -54,7 +54,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
private lateinit var profilesRepo: DeviceProfilesRepo
|
||||
|
||||
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 isBluetoothEnabledFlow: MutableStateFlow<Boolean>
|
||||
private lateinit var profilesFlow: MutableStateFlow<List<DeviceProfile>>
|
||||
@@ -154,7 +154,7 @@ class OverviewViewModelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
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)
|
||||
|
||||
val vm = createViewModel()
|
||||
@@ -165,17 +165,17 @@ class OverviewViewModelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
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")
|
||||
}
|
||||
val withoutProfile = object : PodDevice.Meta {
|
||||
val withoutProfile = object : BlePodSnapshot.Meta {
|
||||
override val profile: DeviceProfile? = null
|
||||
}
|
||||
val profiled = MonitoredDevice(
|
||||
val profiled = PodDevice(
|
||||
ble = mockk(relaxed = true) { every { meta } returns withProfile },
|
||||
aap = null,
|
||||
)
|
||||
val unmatched = MonitoredDevice(
|
||||
val unmatched = PodDevice(
|
||||
ble = mockk(relaxed = true) { every { meta } returns withoutProfile },
|
||||
aap = null,
|
||||
)
|
||||
@@ -196,17 +196,17 @@ class OverviewViewModelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
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")
|
||||
}
|
||||
val withoutProfile = object : PodDevice.Meta {
|
||||
val withoutProfile = object : BlePodSnapshot.Meta {
|
||||
override val profile: DeviceProfile? = null
|
||||
}
|
||||
val profiled = MonitoredDevice(
|
||||
val profiled = PodDevice(
|
||||
ble = mockk(relaxed = true) { every { meta } returns withProfile },
|
||||
aap = null,
|
||||
)
|
||||
val unmatched = MonitoredDevice(
|
||||
val unmatched = PodDevice(
|
||||
ble = mockk(relaxed = true) { every { meta } returns withoutProfile },
|
||||
aap = null,
|
||||
)
|
||||
|
||||
+20
-20
@@ -1,10 +1,10 @@
|
||||
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.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
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.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.aap.AapConnectionState
|
||||
@@ -20,15 +20,15 @@ import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
import java.time.Instant
|
||||
|
||||
class MonitoredDeviceTest : BaseTest() {
|
||||
class PodDeviceTest : BaseTest() {
|
||||
|
||||
private fun mockDualPod(
|
||||
model: PodModel = PodModel.AIRPODS_PRO3,
|
||||
leftBattery: Float? = 0.8f,
|
||||
rightBattery: Float? = 0.9f,
|
||||
caseBattery: Float? = 0.5f,
|
||||
): PodDevice {
|
||||
// DualApplePods implements DualPodDevice + HasCase + other interfaces
|
||||
): BlePodSnapshot {
|
||||
// DualApplePods implements DualBlePodSnapshot + HasCase + other interfaces
|
||||
return mockk<DualApplePods>(relaxed = true) {
|
||||
every { this@mockk.model } returns model
|
||||
every { batteryLeftPodPercent } returns leftBattery
|
||||
@@ -39,14 +39,14 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
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.isAapConnected shouldBe false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capabilities come from model features`() {
|
||||
val device = MonitoredDevice(
|
||||
val device = PodDevice(
|
||||
ble = mockDualPod(model = PodModel.AIRPODS_PRO3),
|
||||
aap = null,
|
||||
)
|
||||
@@ -58,7 +58,7 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
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 },
|
||||
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.ancMode.shouldNotBeNull()
|
||||
device.ancMode!!.current shouldBe AncModeValue.TRANSPARENCY
|
||||
@@ -85,21 +85,21 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
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()
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `identity properties delegate to BLE`() {
|
||||
val id = PodDevice.Id()
|
||||
val meta = mockk<PodDevice.Meta>(relaxed = true)
|
||||
val device = MonitoredDevice(
|
||||
val id = BlePodSnapshot.Id()
|
||||
val meta = mockk<BlePodSnapshot.Meta>(relaxed = true)
|
||||
val device = PodDevice(
|
||||
ble = mockk(relaxed = true) {
|
||||
every { identifier } returns id
|
||||
every { this@mockk.meta } returns meta
|
||||
@@ -112,7 +112,7 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
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.meta.shouldBeNull()
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
fun `signal timing properties delegate to BLE`() {
|
||||
val now = Instant.now()
|
||||
val earlier = now.minusSeconds(60)
|
||||
val device = MonitoredDevice(
|
||||
val device = PodDevice(
|
||||
ble = mockk(relaxed = true) {
|
||||
every { seenLastAt } returns now
|
||||
every { seenFirstAt } returns earlier
|
||||
@@ -138,7 +138,7 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
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.seenFirstAt.shouldBeNull()
|
||||
device.signalQuality shouldBe 0f
|
||||
@@ -153,7 +153,7 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
every { (this@mockk as HasChargeDetectionDual).isRightPodCharging } returns false
|
||||
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.isRightPodCharging shouldBe false
|
||||
device.isCaseCharging shouldBe true
|
||||
@@ -168,7 +168,7 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
every { (this@mockk as HasEarDetection).isBeingWorn } returns false
|
||||
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.isRightInEar shouldBe false
|
||||
device.isBeingWorn shouldBe false
|
||||
@@ -177,7 +177,7 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `icon and label properties delegate to BLE`() {
|
||||
val device = MonitoredDevice(
|
||||
val device = PodDevice(
|
||||
ble = mockk(relaxed = true) {
|
||||
every { model } returns PodModel.AIRPODS_PRO3
|
||||
every { iconRes } returns 42
|
||||
@@ -189,7 +189,7 @@ class MonitoredDeviceTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `rawDataHex empty when BLE null`() {
|
||||
val device = MonitoredDevice(ble = null, aap = null)
|
||||
val device = PodDevice(ble = null, aap = null)
|
||||
device.rawDataHex shouldBe emptyList()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
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.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.types.instanceOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -28,13 +28,13 @@ class AirPodsFactoryTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
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") {
|
||||
this shouldBe instanceOf<UnknownAppleDevice>()
|
||||
this shouldBe instanceOf<UnknownAppleSnapshotBle>()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid data`() = runTest {
|
||||
create<PodDevice?>("abcd") {
|
||||
create<BlePodSnapshot?>("abcd") {
|
||||
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.fromHex
|
||||
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.profiles.core.DeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
@@ -60,7 +60,7 @@ abstract class BaseAirPodsTest : BaseTest() {
|
||||
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified T : PodDevice?> create(
|
||||
internal suspend inline fun <reified T : BlePodSnapshot?> create(
|
||||
hex: String,
|
||||
address: String = "77:49:4C:D8:25:0C",
|
||||
block: T.() -> Unit
|
||||
|
||||
Reference in New Issue
Block a user