Refactor time access behind TimeSource

This commit is contained in:
darken
2026-04-14 11:28:41 +02:00
committed by Matthias Urhahn
parent c1601c105c
commit 1cd8ae5ace
71 changed files with 362 additions and 212 deletions
@@ -1,7 +1,6 @@
package eu.darken.capod.common
import android.media.AudioManager
import android.os.SystemClock
import android.view.KeyEvent
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
import eu.darken.capod.common.debug.logging.log
@@ -13,6 +12,7 @@ import javax.inject.Singleton
@Singleton
class MediaControl @Inject constructor(
private val audioManager: AudioManager,
private val timeSource: TimeSource,
) {
private var capPauseExpiryElapsedRealtime: Long = 0L
@@ -20,7 +20,7 @@ class MediaControl @Inject constructor(
get() = audioManager.isMusicActive
val wasRecentlyPausedByCap: Boolean
get() = capPauseExpiryElapsedRealtime > SystemClock.elapsedRealtime()
get() = capPauseExpiryElapsedRealtime > timeSource.elapsedRealtime()
suspend fun sendPlay() {
log(TAG, INFO) { "sendPlay()" }
@@ -57,7 +57,7 @@ class MediaControl @Inject constructor(
internal suspend fun sendKey(keyCode: Int) {
log(TAG) { "Sending up+down KeyEvent: $keyCode" }
val eventTime = SystemClock.uptimeMillis()
val eventTime = timeSource.uptimeMillis()
audioManager.dispatchMediaKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyCode, 0))
delay(100)
audioManager.dispatchMediaKeyEvent(KeyEvent(eventTime + 200, eventTime + 200, KeyEvent.ACTION_UP, keyCode, 0))
@@ -82,7 +82,7 @@ class MediaControl @Inject constructor(
}
private fun markRecentCapPause() {
capPauseExpiryElapsedRealtime = SystemClock.elapsedRealtime() + RECENT_CAP_PAUSE_WINDOW_MS
capPauseExpiryElapsedRealtime = timeSource.elapsedRealtime() + RECENT_CAP_PAUSE_WINDOW_MS
}
private fun clearRecentCapPause() {
@@ -1,9 +0,0 @@
package eu.darken.capod.common
import android.os.SystemClock
object SystemClockWrap {
val elapsedRealtimeNanos: Long
get() = SystemClock.elapsedRealtimeNanos()
}
@@ -0,0 +1,14 @@
package eu.darken.capod.common
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@InstallIn(SingletonComponent::class)
@Module
abstract class TimeModule {
@Binds
abstract fun timeSource(defaultTimeSource: DefaultTimeSource): TimeSource
}
@@ -0,0 +1,29 @@
package eu.darken.capod.common
import android.os.SystemClock
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
interface TimeSource {
fun now(): Instant
fun currentTimeMillis(): Long
fun elapsedRealtime(): Long
fun elapsedRealtimeNanos(): Long
fun uptimeMillis(): Long
}
object SystemTimeSource : TimeSource {
override fun now(): Instant = Instant.now()
override fun currentTimeMillis(): Long = System.currentTimeMillis()
override fun elapsedRealtime(): Long = SystemClock.elapsedRealtime()
override fun elapsedRealtimeNanos(): Long = SystemClock.elapsedRealtimeNanos()
override fun uptimeMillis(): Long = SystemClock.uptimeMillis()
}
@Singleton
class DefaultTimeSource @Inject constructor() : TimeSource by SystemTimeSource
@@ -3,6 +3,8 @@ package eu.darken.capod.common.bluetooth
import android.bluetooth.le.ScanResult
import android.os.Parcelable
import androidx.core.util.forEach
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.serialization.InstantEpochMillisSerializer
import eu.darken.capod.common.serialization.MapIntByteArrayBase64Serializer
import kotlinx.parcelize.Parcelize
@@ -31,8 +33,11 @@ data class BleScanResult(
}
companion object {
fun fromScanResult(scanResult: ScanResult) = BleScanResult(
receivedAt = Instant.now(),
fun fromScanResult(
scanResult: ScanResult,
timeSource: TimeSource = SystemTimeSource,
) = BleScanResult(
receivedAt = timeSource.now(),
address = scanResult.device.address,
rssi = scanResult.rssi,
generatedAtNanos = scanResult.timestampNanos,
@@ -43,4 +48,4 @@ data class BleScanResult(
}
)
}
}
}
@@ -9,6 +9,7 @@ import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.Intent
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
@@ -33,6 +34,7 @@ class BleScanner @Inject constructor(
private val bluetoothManager: BluetoothManager2,
private val fakeBleData: FakeBleData,
private val scanResultForwarder: BleScanResultForwarder,
private val timeSource: TimeSource,
) {
@SuppressLint("MissingPermission") fun scan(
@@ -71,15 +73,15 @@ class BleScanner @Inject constructor(
if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" }
passed
}
.map { BleScanResult.fromScanResult(it) }
.map { BleScanResult.fromScanResult(it, timeSource) }
}
val callback = object : ScanCallback() {
var lastScanAt = System.currentTimeMillis()
var lastScanAt = timeSource.currentTimeMillis()
override fun onScanResult(callbackType: Int, result: ScanResult) {
log(TAG, VERBOSE) {
val delay = System.currentTimeMillis() - lastScanAt
lastScanAt = System.currentTimeMillis()
val delay = timeSource.currentTimeMillis() - lastScanAt
lastScanAt = timeSource.currentTimeMillis()
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
}
@@ -88,8 +90,8 @@ class BleScanner @Inject constructor(
override fun onBatchScanResults(results: MutableList<ScanResult>) {
log(TAG, VERBOSE) {
val delay = System.currentTimeMillis() - lastScanAt
lastScanAt = System.currentTimeMillis()
val delay = timeSource.currentTimeMillis() - lastScanAt
lastScanAt = timeSource.currentTimeMillis()
"onBatchScanResults(delay=${delay}ms, results=$results)"
}
@@ -210,4 +212,4 @@ class BleScanner @Inject constructor(
private const val CALLBACK_INTENT_REQUESTCODE = 270
private val TAG = logTag("Bluetooth", "BleScanner")
}
}
}
@@ -13,6 +13,7 @@ import android.content.IntentFilter
import android.os.Handler
import android.os.HandlerThread
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.Bugs
@@ -53,6 +54,7 @@ class BluetoothManager2 @Inject constructor(
private val dispatcherProvider: DispatcherProvider,
@ApplicationContext private val context: Context,
private val manager: BluetoothManager,
private val timeSource: TimeSource,
) {
val adapter: BluetoothAdapter?
@@ -255,8 +257,10 @@ class BluetoothManager2 @Inject constructor(
BluetoothDevice2(
internal = device,
seenFirstAt = seenDevicesLock.withLock {
seenDevicesCache[device.address] ?: Instant.now().also {
seenDevicesCache[device.address] = it
seenDevicesCache[device.address] ?: run {
val now = timeSource.now()
seenDevicesCache[device.address] = now
now
}
}
)
@@ -294,8 +298,10 @@ class BluetoothManager2 @Inject constructor(
BluetoothDevice2(
internal = device,
seenFirstAt = seenDevicesLock.withLock {
seenDevicesCache[device.address] ?: Instant.now().also {
seenDevicesCache[device.address] = it
seenDevicesCache[device.address] ?: run {
val now = timeSource.now()
seenDevicesCache[device.address] = now
now
}
}
)
@@ -364,4 +370,4 @@ class BluetoothManager2 @Inject constructor(
companion object {
private val TAG = logTag("Bluetooth", "Manager2")
}
}
}
@@ -1,10 +1,9 @@
package eu.darken.capod.common.bluetooth
import dagger.Reusable
import eu.darken.capod.common.SystemClockWrap
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.debug.DebugSettings
import eu.darken.capod.common.fromHex
import java.time.Instant
import javax.inject.Inject
import kotlin.random.Random
import eu.darken.capod.common.datastore.valueBlocking
@@ -12,6 +11,7 @@ import eu.darken.capod.common.datastore.valueBlocking
@Reusable
class FakeBleData @Inject constructor(
private val debugSettings: DebugSettings,
private val timeSource: TimeSource,
) {
fun maybeAddfakeData(originals: Collection<BleScanResult>): Collection<BleScanResult> {
@@ -21,12 +21,14 @@ class FakeBleData @Inject constructor(
fun getFakeData(): Collection<BleScanResult> {
val fakeDevices = mutableListOf<BleScanResult>()
val now = timeSource.now()
val generatedAt = timeSource.elapsedRealtimeNanos()
// AirPods Gen1
BleScanResult(
receivedAt = Instant.now(),
receivedAt = now,
address = "78:73:AF:B4:85:22",
rssi = Random.nextInt(100) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
generatedAtNanos = generatedAt + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".fromHex())
).run {
fakeDevices.add(this)
@@ -34,10 +36,10 @@ class FakeBleData @Inject constructor(
// AirPods Gen2
BleScanResult(
receivedAt = Instant.now(),
receivedAt = now,
address = "78:73:FF:B4:85:5E",
rssi = Random.nextInt(100) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
generatedAtNanos = generatedAt + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".fromHex())
).run {
fakeDevices.add(this)
@@ -45,30 +47,30 @@ class FakeBleData @Inject constructor(
// AirPods Gen3
BleScanResult(
receivedAt = Instant.now(),
receivedAt = now,
address = "4E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
generatedAtNanos = generatedAt + 200,
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".fromHex())
).run {
fakeDevices.add(this)
}
// AirPods Max
BleScanResult(
receivedAt = Instant.now(),
receivedAt = now,
address = "7E:E5:C7:65:D2:B5",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
generatedAtNanos = generatedAt + 300,
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".fromHex())
).run {
fakeDevices.add(this)
}
// BeatsFlex
BleScanResult(
receivedAt = Instant.now(),
receivedAt = now,
address = "5E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
generatedAtNanos = generatedAt + 400,
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".fromHex())
).run {
fakeDevices.add(this)
@@ -76,10 +78,10 @@ class FakeBleData @Inject constructor(
// Tws i99999
BleScanResult(
receivedAt = Instant.now(),
receivedAt = now,
address = "5E:9E:D1:29:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
generatedAtNanos = generatedAt + 400,
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".fromHex())
).run {
fakeDevices.add(this)
@@ -87,10 +89,10 @@ class FakeBleData @Inject constructor(
// Unknown Device
BleScanResult(
receivedAt = Instant.now(),
receivedAt = now,
address = "6E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
generatedAtNanos = generatedAt + 500,
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".fromHex())
).run {
fakeDevices.add(this)
@@ -98,4 +100,4 @@ class FakeBleData @Inject constructor(
return fakeDevices
}
}
}
@@ -2,6 +2,7 @@ package eu.darken.capod.common.debug.logging
import android.annotation.SuppressLint
import android.util.Log
import eu.darken.capod.common.TimeSource
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@@ -10,7 +11,10 @@ import java.time.Instant
@SuppressLint("LogNotTimber")
class FileLogger(private val logFile: File) : Logging.Logger {
class FileLogger(
private val logFile: File,
private val timeSource: TimeSource,
) : Logging.Logger {
private var logWriter: OutputStreamWriter? = null
@SuppressLint("SetWorldReadable")
@@ -57,7 +61,7 @@ class FileLogger(private val logFile: File) : Logging.Logger {
override fun log(priority: Logging.Priority, tag: String, message: String, metaData: Map<String, Any>?) {
logWriter?.let {
try {
it.write("${Instant.ofEpochMilli(System.currentTimeMillis())} ${priority.shortLabel}/$tag: $message\n")
it.write("${timeSource.now()} ${priority.shortLabel}/$tag: $message\n")
it.flush()
} catch (e: IOException) {
Log.e(TAG, "Failed to write log line.", e)
@@ -76,4 +80,3 @@ class FileLogger(private val logFile: File) : Logging.Logger {
private val TAG = logTag("Debug", "FileLogger")
}
}
@@ -1,5 +1,6 @@
package eu.darken.capod.common.debug.recording.core
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.debug.logging.FileLogger
import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
@@ -10,7 +11,9 @@ import kotlinx.coroutines.sync.withLock
import java.io.File
import javax.inject.Inject
class Recorder @Inject constructor() {
class Recorder @Inject constructor(
private val timeSource: TimeSource,
) {
private val mutex = Mutex()
private var fileLogger: FileLogger? = null
@@ -23,7 +26,7 @@ class Recorder @Inject constructor() {
suspend fun start(path: File) = mutex.withLock {
if (fileLogger != null) return@withLock
this.path = path
fileLogger = FileLogger(path)
fileLogger = FileLogger(path, timeSource)
fileLogger?.let {
it.start()
Logging.install(it)
@@ -45,4 +48,4 @@ class Recorder @Inject constructor() {
internal val TAG = logTag("Debug", "Log", "Recorder")
}
}
}
@@ -7,6 +7,8 @@ import androidx.annotation.VisibleForTesting
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.InstallId
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
@@ -32,6 +34,7 @@ class RecorderModule @Inject constructor(
@AppScope private val appScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
private val installId: InstallId,
private val timeSource: TimeSource,
) {
@Volatile
@@ -73,11 +76,11 @@ class RecorderModule @Inject constructor(
createSessionDir()
}
val logFile = File(sessionDir, "core.log")
val newRecorder = Recorder()
val newRecorder = Recorder(timeSource)
newRecorder.start(logFile)
if (!isResume) {
val startTime = System.currentTimeMillis()
val startTime = timeSource.currentTimeMillis()
writeTriggerFile(sessionDir, startTime)
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION}" }
@@ -99,7 +102,7 @@ class RecorderModule @Inject constructor(
copy(
recorder = newRecorder,
currentLogDir = sessionDir,
recordingStartedAt = if (recordingStartedAt > 0L) recordingStartedAt else System.currentTimeMillis(),
recordingStartedAt = if (recordingStartedAt > 0L) recordingStartedAt else timeSource.currentTimeMillis(),
persistedLogDir = null,
)
}
@@ -126,7 +129,7 @@ class RecorderModule @Inject constructor(
}
private fun createSessionDir(): File {
val timestamp = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC)
val timestamp = timeSource.now().atZone(java.time.ZoneOffset.UTC)
.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"))
val installIdPrefix = installId.id.take(8)
val dirName = "capod_${BuildConfigWrap.VERSION_NAME}_${timestamp}_$installIdPrefix"
@@ -179,7 +182,7 @@ class RecorderModule @Inject constructor(
if (!currentState.isRecording) return StopResult.NotRecording
val logDir = currentState.currentLogDir ?: return StopResult.NotRecording
val elapsed = System.currentTimeMillis() - currentState.recordingStartedAt
val elapsed = timeSource.currentTimeMillis() - currentState.recordingStartedAt
if (elapsed < MIN_RECORDING_MS) return StopResult.TooShort
stopRecorder()
@@ -235,7 +238,7 @@ class RecorderModule @Inject constructor(
@VisibleForTesting
internal fun parseTriggerContent(
content: String,
now: Long = System.currentTimeMillis(),
now: Long = SystemTimeSource.currentTimeMillis(),
): Pair<File, Long>? {
val trimmed = content.trim()
if (trimmed.isEmpty()) return null
@@ -8,6 +8,8 @@ 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.SingleEventFlow
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.main.core.MonitorMode
@@ -46,6 +48,7 @@ class DeviceSettingsViewModel @Inject constructor(
private val bluetoothManager: BluetoothManager2,
private val profilesRepo: DeviceProfilesRepo,
private val generalSettings: GeneralSettings,
private val timeSource: TimeSource,
) : ViewModel4(dispatcherProvider) {
private val targetProfileId = MutableStateFlow<ProfileId?>(null)
@@ -88,7 +91,7 @@ class DeviceSettingsViewModel @Inject constructor(
val connectedAddresses = connectedDevices.map { it.address }.toSet()
State(
device = device,
now = Instant.now(),
now = timeSource.now(),
isPro = upgrade.isPro,
isNudgeAvailable = bluetoothManager.isNudgeAvailable,
isForceConnecting = forcing,
@@ -110,7 +113,7 @@ class DeviceSettingsViewModel @Inject constructor(
data class State(
val device: PodDevice?,
val now: Instant = Instant.now(),
val now: Instant = SystemTimeSource.now(),
val isPro: Boolean = false,
val isNudgeAvailable: Boolean = true,
val isForceConnecting: Boolean = false,
@@ -38,6 +38,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider
@@ -327,7 +328,7 @@ private fun PodDeviceCard(
private fun OverviewScreenWithDevicesPreview() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = Instant.now(),
now = SystemTimeSource.now(),
permissions = emptySet(),
devices = listOf(
MockPodDataProvider.dualPodMonitoredMixed(),
@@ -356,7 +357,7 @@ private fun OverviewScreenWithDevicesPreview() = PreviewWrapper {
private fun OverviewScreenEmptyPreview() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = Instant.now(),
now = SystemTimeSource.now(),
permissions = emptySet(),
devices = emptyList(),
isDebugMode = false,
@@ -378,7 +379,7 @@ private fun OverviewScreenEmptyPreview() = PreviewWrapper {
private fun OverviewScreenNoProfilesPreview() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = Instant.now(),
now = SystemTimeSource.now(),
permissions = emptySet(),
devices = emptyList(),
isDebugMode = false,
@@ -400,7 +401,7 @@ private fun OverviewScreenNoProfilesPreview() = PreviewWrapper {
private fun OverviewScreenBluetoothOffPreview() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = Instant.now(),
now = SystemTimeSource.now(),
permissions = emptySet(),
devices = emptyList(),
isDebugMode = false,
@@ -12,6 +12,7 @@ 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
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.main.core.GeneralSettings
@@ -50,6 +51,7 @@ class OverviewViewModel @Inject constructor(
private val bluetoothManager: BluetoothManager2,
private val profilesRepo: DeviceProfilesRepo,
private val aapManager: AapConnectionManager,
private val timeSource: TimeSource,
) : ViewModel4(dispatcherProvider) {
val requestPermissionEvent = SingleEventFlow<Permission>()
@@ -112,7 +114,7 @@ class OverviewViewModel @Inject constructor(
showUnmatchedDevices,
) { _, permissions, devices, isDebugMode, isBluetoothEnabled, profiles, upgradeInfo, showUnmatched ->
State(
now = Instant.now(),
now = timeSource.now(),
permissions = permissions,
devices = devices,
isDebugMode = isDebugMode,
@@ -43,6 +43,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider
@@ -415,7 +416,7 @@ private fun DualPodsCardFullPreview() = PreviewWrapper {
DualPodsCard(
device = MockPodDataProvider.dualPodMonitoredWithAap(),
showDebug = false,
now = Instant.now(),
now = SystemTimeSource.now(),
isPro = false,
onDeviceSettings = {},
)
@@ -427,7 +428,7 @@ private fun DualPodsCardMinimalPreview() = PreviewWrapper {
DualPodsCard(
device = MockPodDataProvider.dualPodMonitored(),
showDebug = false,
now = Instant.now(),
now = SystemTimeSource.now(),
)
}
@@ -437,7 +438,7 @@ private fun DualPodsCardCachedPreview() = PreviewWrapper {
DualPodsCard(
device = MockPodDataProvider.dualPodCachedOnly(),
showDebug = false,
now = Instant.now(),
now = SystemTimeSource.now(),
)
}
@@ -447,7 +448,7 @@ private fun DualPodsCardMissingAddressPreview() = PreviewWrapper {
DualPodsCard(
device = MockPodDataProvider.dualPodMonitoredMixed(),
showDebug = false,
now = Instant.now(),
now = SystemTimeSource.now(),
onEditProfile = {},
)
}
@@ -43,6 +43,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider
@@ -286,7 +287,7 @@ private fun SinglePodsCardFullPreview() = PreviewWrapper {
SinglePodsCard(
device = MockPodDataProvider.singlePodMonitoredWithAap(),
showDebug = false,
now = Instant.now(),
now = SystemTimeSource.now(),
onDeviceSettings = {},
)
}
@@ -297,7 +298,7 @@ private fun SinglePodsCardMinimalPreview() = PreviewWrapper {
SinglePodsCard(
device = MockPodDataProvider.singlePodMonitored(),
showDebug = false,
now = Instant.now(),
now = SystemTimeSource.now(),
)
}
@@ -307,7 +308,7 @@ private fun SinglePodsCardCachedPreview() = PreviewWrapper {
SinglePodsCard(
device = MockPodDataProvider.singlePodCachedOnly(),
showDebug = false,
now = Instant.now(),
now = SystemTimeSource.now(),
)
}
@@ -317,7 +318,7 @@ private fun SinglePodsCardMissingAddressPreview() = PreviewWrapper {
SinglePodsCard(
device = MockPodDataProvider.singlePodMonitored(),
showDebug = false,
now = Instant.now(),
now = SystemTimeSource.now(),
onEditProfile = {},
)
}
@@ -53,6 +53,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.compose.ConfirmationDialog
import eu.darken.capod.common.compose.Preview2
@@ -452,7 +453,7 @@ private fun SessionRow(
)
val agoText = DateUtils.getRelativeTimeSpanString(
session.createdAt.toEpochMilli(),
System.currentTimeMillis(),
SystemTimeSource.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
)
@@ -58,6 +58,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.compose.ConfirmationDialog
@@ -338,7 +339,7 @@ fun ContactFormScreen(
val sizeText = Formatter.formatShortFileSize(context, session.diskSize)
val agoText = DateUtils.getRelativeTimeSpanString(
session.createdAt.toEpochMilli(),
System.currentTimeMillis(),
SystemTimeSource.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
)
@@ -1,6 +1,7 @@
package eu.darken.capod.monitor.core
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.coroutine.AppScope
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
@@ -42,6 +43,7 @@ class DeviceMonitor @Inject constructor(
private val deviceStateCache: DeviceStateCache,
private val profilesRepo: DeviceProfilesRepo,
private val aapLifecycleManager: AapLifecycleManager,
private val timeSource: TimeSource,
) {
init {
aapLifecycleManager.start()
@@ -155,7 +157,7 @@ class DeviceMonitor @Inject constructor(
for (device in devices) {
val profileId = device.profileId ?: continue
val existing = deviceStateCache.cachedStates.value[profileId]
val newState = device.toCachedState(existing) ?: continue
val newState = device.toCachedState(existing, timeSource.now()) ?: continue
log(TAG, VERBOSE) { "Persisting state for $profileId" }
deviceStateCache.save(profileId, newState)
@@ -3,6 +3,7 @@ package eu.darken.capod.monitor.core
import android.content.Context
import androidx.compose.runtime.Stable
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.monitor.core.cache.CachedDeviceState
import eu.darken.capod.pods.core.apple.PodModel
@@ -73,7 +74,7 @@ data class PodDevice(
val signalQuality: Float
get() {
val bleQuality = ble?.signalQuality ?: 0f
return (bleQuality + computeAapBoost(Instant.now())).coerceAtMost(1f)
return (bleQuality + computeAapBoost(SystemTimeSource.now())).coerceAtMost(1f)
}
/**
@@ -1,6 +1,7 @@
package eu.darken.capod.monitor.core.ble
import android.bluetooth.le.ScanFilter
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.BleScanner
import eu.darken.capod.common.bluetooth.BluetoothManager2
@@ -44,6 +45,7 @@ class BlePodMonitor @Inject constructor(
@AppScope private val appScope: CoroutineScope,
private val bleScanner: BleScanner,
private val podFactory: PodFactory,
private val timeSource: TimeSource,
private val generalSettings: GeneralSettings,
bluetoothManager: BluetoothManager2,
private val debugSettings: DebugSettings,
@@ -97,7 +99,7 @@ class BlePodMonitor @Inject constructor(
.replayingShare(appScope)
private suspend fun sortPodsToInterest(devices: Collection<BlePodSnapshot>): List<BlePodSnapshot> {
val now = Instant.now()
val now = timeSource.now()
val profiles = profilesRepo.currentProfiles() ?: emptyList()
return devices.sortedWith(
@@ -175,7 +177,7 @@ class BlePodMonitor @Inject constructor(
return emptyMap()
}
val now = Instant.now()
val now = timeSource.now()
deviceCache.toList().forEach { (key, value) ->
if (Duration.between(value.seenLastAt, now) > STALE_DEVICE_TIMEOUT) {
log(TAG, Logging.Priority.VERBOSE) { "Removing stale device from cache: $value" }
@@ -200,4 +202,4 @@ class BlePodMonitor @Inject constructor(
private val STALE_DEVICE_TIMEOUT = Duration.ofSeconds(20)
private val STALE_EVICTION_INTERVAL = Duration.ofSeconds(10)
}
}
}
@@ -1,5 +1,6 @@
package eu.darken.capod.monitor.core.cache
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.monitor.core.PodDevice
import eu.darken.capod.monitor.core.cache.CachedDeviceState.CachedBatterySlot
import eu.darken.capod.pods.core.apple.ble.DualBlePodSnapshot
@@ -18,7 +19,7 @@ import java.time.Instant
*/
fun PodDevice.toCachedState(
existing: CachedDeviceState?,
now: Instant = Instant.now(),
now: Instant = SystemTimeSource.now(),
): CachedDeviceState? {
if (!isLive) return null
val pid = profileId ?: return null
@@ -3,6 +3,7 @@ package eu.darken.capod.pods.core.apple.aap
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.l2cap.L2capSocketFactory
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
@@ -48,6 +49,7 @@ internal class AapConnection(
private val device: BluetoothDevice,
private val profile: AapDeviceProfile,
private val socketFactory: L2capSocketFactory,
private val timeSource: TimeSource,
private val psm: Int = 0x1001,
) {
@@ -168,7 +170,7 @@ internal class AapConnection(
if (currentAnc != null) {
_state.value = currentState
.withSetting(AapSetting.AncMode::class, currentAnc.copy(current = command.mode))
.copy(pendingAncMode = null, lastMessageAt = Instant.now())
.copy(pendingAncMode = null, lastMessageAt = timeSource.now())
} else {
_state.value = currentState.copy(pendingAncMode = null)
}
@@ -258,11 +260,11 @@ internal class AapConnection(
is AapCommand.SetDeviceName -> {
val currentInfo = baseState.deviceInfo ?: return
_state.value = baseState
.copy(deviceInfo = currentInfo.copy(name = command.name), lastMessageAt = Instant.now())
.copy(deviceInfo = currentInfo.copy(name = command.name), lastMessageAt = timeSource.now())
return
}
}
_state.value = baseState.withSetting(updated.first, updated.second).copy(lastMessageAt = Instant.now())
_state.value = baseState.withSetting(updated.first, updated.second).copy(lastMessageAt = timeSource.now())
}
private suspend fun sendRaw(command: AapCommand) {
@@ -272,7 +274,7 @@ internal class AapConnection(
val sock = socket ?: throw IOException("Socket is null")
sock.outputStream.write(bytes)
sock.outputStream.flush()
if (command is AapCommand.SetAncMode) lastAncCommandSentAt = System.currentTimeMillis()
if (command is AapCommand.SetAncMode) lastAncCommandSentAt = timeSource.currentTimeMillis()
log(TAG) { "Sent command: $command (${bytes.size} bytes)" }
}
}
@@ -360,7 +362,7 @@ internal class AapConnection(
val valid = batteries.filterValues { it.charging != AapPodState.ChargingState.DISCONNECTED }
_state.value = _state.value.copy(
batteries = _state.value.batteries + valid,
lastMessageAt = Instant.now(),
lastMessageAt = timeSource.now(),
)
log(TAG) { "Battery update: ${batteries.entries.map { "${it.key}=${(it.value.percent * 100).toInt()}% ${it.value.charging}" }}" }
return
@@ -369,14 +371,14 @@ internal class AapConnection(
// Try private key response
profile.decodePrivateKeyResponse(message)?.let { keys ->
log(TAG) { "Private keys received: IRK=${keys.irk != null}, ENC=${keys.encKey != null}" }
_state.value = _state.value.copy(lastMessageAt = Instant.now())
_state.value = _state.value.copy(lastMessageAt = timeSource.now())
_keysReceived.tryEmit(keys)
return
}
// Try device info
profile.decodeDeviceInfo(message)?.let { info ->
_state.value = _state.value.copy(deviceInfo = info, lastMessageAt = Instant.now())
_state.value = _state.value.copy(deviceInfo = info, lastMessageAt = timeSource.now())
log(TAG) { "Device info: ${info.name} (${info.modelNumber})" }
return
}
@@ -387,10 +389,10 @@ internal class AapConnection(
// Skip debounce for: first ANC mode (initial setup), echoes after our own command.
if (value is AapSetting.AncMode) {
val isFirstAncMode = _state.value.setting<AapSetting.AncMode>() == null
val sinceLastCommand = System.currentTimeMillis() - lastAncCommandSentAt
val sinceLastCommand = timeSource.currentTimeMillis() - lastAncCommandSentAt
if (isFirstAncMode || sinceLastCommand <= 3000L) {
ancDebounceJob?.cancel()
_state.value = _state.value.withSetting(key, value).copy(lastMessageAt = Instant.now())
_state.value = _state.value.withSetting(key, value).copy(lastMessageAt = timeSource.now())
log(TAG) { "Setting: ${key.simpleName} = $value" }
// After our command, firmware may cycle through modes before settling.
@@ -413,7 +415,7 @@ internal class AapConnection(
ancDebounceJob?.cancel()
ancDebounceJob = connectionScope?.launch {
delay(1500L)
_state.value = _state.value.withSetting(key, value).copy(lastMessageAt = Instant.now())
_state.value = _state.value.withSetting(key, value).copy(lastMessageAt = timeSource.now())
log(TAG) { "Setting (debounced): ${key.simpleName} = $value" }
}
}
@@ -426,7 +428,7 @@ internal class AapConnection(
prev != null && prev.primaryPod == value.secondaryPod && prev.secondaryPod == value.primaryPod
}
var newState = _state.value.withSetting(key, value).copy(lastMessageAt = Instant.now())
var newState = _state.value.withSetting(key, value).copy(lastMessageAt = timeSource.now())
if (clearPrimaryPod) {
newState = newState.copy(settings = newState.settings - AapSetting.PrimaryPod::class)
}
@@ -442,7 +444,7 @@ internal class AapConnection(
if (currentAnc != null) {
_state.value = _state.value
.withSetting(AapSetting.AncMode::class, currentAnc.copy(current = mode))
.copy(pendingAncMode = null, lastMessageAt = Instant.now())
.copy(pendingAncMode = null, lastMessageAt = timeSource.now())
} else {
_state.value = _state.value.copy(pendingAncMode = null)
}
@@ -1,6 +1,7 @@
package eu.darken.capod.pods.core.apple.aap
import android.bluetooth.BluetoothDevice
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
@@ -37,6 +38,7 @@ import javax.inject.Singleton
class AapConnectionManager @Inject constructor(
private val socketFactory: L2capSocketFactory,
@AppScope private val scope: CoroutineScope,
private val timeSource: TimeSource,
) {
companion object {
private val TAG = logTag("AapConnectionMgr")
@@ -77,7 +79,7 @@ class AapConnectionManager @Inject constructor(
intentionalDisconnects.remove(address)
val profile = AapDeviceProfile.Companion.forModel(model)
val connection = AapConnection(device, profile, socketFactory)
val connection = AapConnection(device, profile, socketFactory, timeSource = timeSource)
connections[address] = connection
try {
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble
import android.content.Context
import androidx.annotation.DrawableRes
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
@@ -53,7 +54,7 @@ interface BlePodSnapshot {
get() {
val sqRssi = ((rssi + 100) / 70f).coerceIn(0f, 1f)
val sqReliability = max(BASE_CONFIDENCE, reliability)
val sqAge = (Duration.between(seenFirstAt, Instant.now()).toMinutes().coerceAtMost(60) / 60f) * 0.25f
val sqAge = (Duration.between(seenFirstAt, SystemTimeSource.now()).toMinutes().coerceAtMost(60) / 60f) * 0.25f
log(VERBOSE) { "Signal Quality ($address): rssi=$sqRssi, reliability=$reliability, age=$sqAge" }
return (sqRssi + sqReliability + sqAge) / 2f
}
@@ -84,4 +85,4 @@ interface BlePodSnapshot {
companion object {
const val BASE_CONFIDENCE = 0.0f
}
}
}
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.airpods
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class AirPodsGen1(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.airpods
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class AirPodsGen2(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class AirPodsGen3(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class AirPodsGen4(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class AirPodsGen4Anc(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.airpods
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
@@ -20,8 +21,8 @@ import javax.inject.Inject
data class AirPodsMax(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.airpods
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
@@ -20,8 +21,8 @@ import javax.inject.Inject
data class AirPodsMax2(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.airpods
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
@@ -20,8 +21,8 @@ import javax.inject.Inject
data class AirPodsMaxUsbc(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -19,8 +20,8 @@ import javax.inject.Inject
data class AirPodsPro(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -19,8 +20,8 @@ import javax.inject.Inject
data class AirPodsPro2(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -19,8 +20,8 @@ import javax.inject.Inject
data class AirPodsPro2Usbc(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.airpods
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -19,8 +20,8 @@ import javax.inject.Inject
data class AirPodsPro3(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.beats
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class BeatsFitPro(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class BeatsFlex(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class BeatsSolo3(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class BeatsSolo4(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.beats
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class BeatsSoloBuds(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class BeatsSoloPro(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class BeatsStudio3(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.beats
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class BeatsStudioBuds(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.beats
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class BeatsStudioBudsPlus(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class BeatsStudioPro(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class BeatsX(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class PowerBeats3(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -17,8 +18,8 @@ import javax.inject.Inject
data class PowerBeats4(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.beats
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class PowerBeatsPro(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.beats
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -18,8 +19,8 @@ import javax.inject.Inject
data class PowerBeatsPro2(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.misc
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -23,8 +24,8 @@ import javax.inject.Inject
*/
data class FakeAirPodsGen1(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -1,5 +1,6 @@
package eu.darken.capod.pods.core.apple.ble.devices.misc
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -21,8 +22,8 @@ import javax.inject.Inject
*/
data class FakeAirPodsGen2(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.misc
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -25,8 +26,8 @@ import javax.inject.Inject
*/
data class FakeAirPodsGen3(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.misc
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -25,8 +26,8 @@ import javax.inject.Inject
*/
data class FakeAirPodsPro(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.misc
import androidx.annotation.DrawableRes
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -26,8 +27,8 @@ import javax.inject.Inject
*/
data class FakeAirPodsPro2(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.devices.misc
import android.content.Context
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -16,8 +17,8 @@ import javax.inject.Inject
data class UnknownAppleSnapshotBle(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.ble.history
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.collections.median
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
import eu.darken.capod.pods.core.apple.ble.devices.ApplePods
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityPayload
@@ -26,7 +27,7 @@ data class KnownDevice(
get() {
if (history.size < 2) return 0f
val now = Instant.now()
val now = SystemTimeSource.now()
val pingInterval = List(history.dropLast(1).size) { index ->
Duration.between(history[index].seenLastAt, history[index + 1].seenLastAt).toMillis()
@@ -45,7 +46,7 @@ data class KnownDevice(
}
fun rssiSmoothed(latest: Int): Int {
val now = Instant.now()
val now = SystemTimeSource.now()
return history
.filter { Duration.between(it.seenLastAt, now) < LOOKBACK }
.map { it.rssi }
@@ -54,7 +55,7 @@ data class KnownDevice(
}
fun isOlderThan(age: Duration): Boolean {
val now = Instant.now()
val now = SystemTimeSource.now()
return Duration.between(history.last().seenLastAt, now) > age
}
@@ -65,4 +66,4 @@ data class KnownDevice(
const val MAX_HISTORY = 60
val LOOKBACK = Duration.ofSeconds(30)
}
}
}
@@ -1,6 +1,7 @@
package eu.darken.capod.pods.core.unknown
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.SystemTimeSource
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
@@ -56,7 +57,7 @@ class UnknownDeviceFactory @Inject constructor() {
history.map { it.rssi }.plus(latest).takeLast(10).median()
fun isOlderThan(age: Duration): Boolean {
val now = Instant.now()
val now = SystemTimeSource.now()
return Duration.between(history.last().seenLastAt, now) > age
}
@@ -130,4 +131,4 @@ class UnknownDeviceFactory @Inject constructor() {
companion object {
private val TAG = logTag("Pod", "Unknown", "Factory")
}
}
}
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.unknown
import android.content.Context
import eu.darken.capod.R
import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
@@ -11,8 +12,8 @@ import java.time.Instant
data class UnknownSnapshotBle(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val reliability: Float = 0f,
@@ -34,4 +35,4 @@ data class UnknownSnapshotBle(
companion object {
private val TAG = logTag("PodDevice", "Unknown")
}
}
}
@@ -1,5 +1,6 @@
package eu.darken.capod.reaction.core.popup
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.bluetooth.BluetoothManager2
@@ -26,6 +27,7 @@ import javax.inject.Singleton
class PopUpReaction @Inject constructor(
private val deviceMonitor: DeviceMonitor,
private val bluetoothManager: BluetoothManager2,
private val timeSource: TimeSource,
) {
private val caseCoolDowns = java.util.concurrent.ConcurrentHashMap<String, Instant>()
@@ -45,7 +47,7 @@ class PopUpReaction @Inject constructor(
// with the toggle off). Dismiss any visible overlay immediately.
if (wasEligible && !isEligible) {
log(TAG) { "Case popup eligibility lost, emitting Hide" }
return@mapNotNull Event.PopupHide()
return@mapNotNull Event.PopupHide(timeSource.now())
}
if (!isEligible) return@mapNotNull null
@@ -71,7 +73,7 @@ class PopUpReaction @Inject constructor(
private fun throttleCasePopUps(current: PodDevice): Event? {
val cooldownKey = current.profileId ?: current.identifier?.toString() ?: return null
val now = Instant.now()
val now = timeSource.now()
val lastShown = caseCoolDowns[cooldownKey]
val decision = evaluateCasePopUp(
@@ -89,14 +91,14 @@ class PopUpReaction @Inject constructor(
return when {
decision.shouldShow -> {
caseCoolDowns[cooldownKey] = now
Event.PopupShow(device = current)
Event.PopupShow(eventAt = now, device = current)
}
decision.shouldHide -> {
if (!decision.shouldResetCooldown) {
caseCoolDowns[cooldownKey] = now
}
Event.PopupHide()
Event.PopupHide(now)
}
else -> null
@@ -142,7 +144,7 @@ class PopUpReaction @Inject constructor(
// Eligibility transition: dismiss any visible overlay immediately.
if (wasEligible && !isEligible) {
log(TAG) { "Connection popup eligibility lost, emitting Hide" }
return@mapNotNull Event.PopupHide()
return@mapNotNull Event.PopupHide(timeSource.now())
}
if (!isEligible) return@mapNotNull null
@@ -157,7 +159,7 @@ class PopUpReaction @Inject constructor(
log(TAG, VERBOSE) { "currentBroadcasted: $currentBroadcasted" }
if (previousConnected != null && previousBroadcasted != null && currentConnected == null) {
return@mapNotNull Event.PopupHide()
return@mapNotNull Event.PopupHide(timeSource.now())
}
if (currentConnected == null || currentBroadcasted == null) {
@@ -165,8 +167,9 @@ class PopUpReaction @Inject constructor(
}
val deviceSeenFirst = currentBroadcasted.seenFirstAt ?: return@mapNotNull null
val deviceAge = Duration.between(deviceSeenFirst, Instant.now())
val connectionAge = Duration.between(currentConnected.seenFirstAt, Instant.now())
val now = timeSource.now()
val deviceAge = Duration.between(deviceSeenFirst, now)
val connectionAge = Duration.between(currentConnected.seenFirstAt, now)
val decision = evaluateConnectionPopUp(
hasConnectedDevice = true,
@@ -179,8 +182,8 @@ class PopUpReaction @Inject constructor(
log(TAG) { "Connection popup decision: ${decision.reason}" }
if (decision.shouldShow) {
connectionCoolDowns[currentConnected.address] = Instant.now()
Event.PopupShow(device = currentBroadcasted)
connectionCoolDowns[currentConnected.address] = now
Event.PopupShow(eventAt = now, device = currentBroadcasted)
} else {
null
}
@@ -191,12 +194,12 @@ class PopUpReaction @Inject constructor(
sealed class Event {
data class PopupShow(
val eventAt: Instant = Instant.now(),
val eventAt: Instant,
val device: PodDevice,
) : Event()
data class PopupHide(
val eventAt: Instant = Instant.now(),
val eventAt: Instant,
) : Event()
}
@@ -4,6 +4,7 @@ import android.content.Context
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.datastore.valueBlocking
@@ -41,6 +42,7 @@ class TroubleShooterViewModel @Inject constructor(
private val blePodMonitor: BlePodMonitor,
private val deviceMonitor: DeviceMonitor,
private val debugSettings: DebugSettings,
private val timeSource: TimeSource,
) : ViewModel4(dispatcherProvider) {
private val _bleState = MutableStateFlow<BleState>(BleState.Intro())
@@ -113,11 +115,11 @@ class TroubleShooterViewModel @Inject constructor(
generalSettings.useIndirectScanResultCallback.valueBlocking = indirectCallback
debugSettings.showUnfiltered.valueBlocking = unfiltered
val start = System.currentTimeMillis()
val start = timeSource.elapsedRealtime()
val devices = withTimeoutOrNull(STEP_TIME) {
blePodMonitor.devices
.take(10)
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
.takeWhile { timeSource.elapsedRealtime() - start < STEP_TIME - 1000 }
.toList()
.flatten()
.distinctBy { it.address }
@@ -195,10 +197,10 @@ class TroubleShooterViewModel @Inject constructor(
progress("Checking all closeby headphones.")
val otherDevices = withTimeoutOrNull(STEP_TIME) {
val start = System.currentTimeMillis()
val start = timeSource.elapsedRealtime()
blePodMonitor.devices
.take(10)
.takeWhile { System.currentTimeMillis() - start < STEP_TIME - 1000 }
.takeWhile { timeSource.elapsedRealtime() - start < STEP_TIME - 1000 }
.toList()
.flatten()
.distinctBy { it.address }
@@ -1,41 +1,35 @@
package eu.darken.capod.common
import android.media.AudioManager
import android.os.SystemClock
import io.mockk.Runs
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import org.junit.jupiter.api.AfterEach
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.TestTimeSource
class MediaControlTest : BaseTest() {
private lateinit var audioManager: AudioManager
private lateinit var mediaControl: MediaControl
private lateinit var timeSource: TestTimeSource
@BeforeEach
fun setup() {
mockkStatic(SystemClock::class)
every { SystemClock.elapsedRealtime() } returns 1_000L
every { SystemClock.uptimeMillis() } returns 1_000L
timeSource = TestTimeSource(
elapsedRealtimeMs = 1_000L,
uptimeMillisValue = 1_000L,
)
audioManager = mockk(relaxed = true)
every { audioManager.dispatchMediaKeyEvent(any()) } just Runs
mediaControl = MediaControl(audioManager)
}
@AfterEach
fun teardown() {
unmockkStatic(SystemClock::class)
mediaControl = MediaControl(audioManager, timeSource)
}
@Test
@@ -1,5 +1,6 @@
package eu.darken.capod.main.ui.devicesettings
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.bluetooth.BluetoothManager2
@@ -36,6 +37,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import testhelpers.BaseTest
import testhelpers.TestTimeSource
import testhelpers.coroutine.TestDispatcherProvider
import testhelpers.livedata.InstantExecutorExtension
@@ -53,6 +55,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
private lateinit var bluetoothManager: BluetoothManager2
private lateinit var profilesRepo: DeviceProfilesRepo
private lateinit var generalSettings: GeneralSettings
private val timeSource: TimeSource = TestTimeSource()
private lateinit var devicesFlow: MutableStateFlow<List<PodDevice>>
private lateinit var upgradeInfoFlow: MutableStateFlow<UpgradeRepo.Info>
@@ -107,6 +110,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
bluetoothManager = bluetoothManager,
profilesRepo = profilesRepo,
generalSettings = generalSettings,
timeSource = timeSource,
)
@Test
@@ -1,5 +1,6 @@
package eu.darken.capod.main.ui.overview
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.debug.DebugSettings
@@ -34,6 +35,7 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import testhelpers.BaseTest
import testhelpers.TestTimeSource
import testhelpers.coroutine.TestDispatcherProvider
import testhelpers.datastore.FakeDataStoreValue
import testhelpers.livedata.InstantExecutorExtension
@@ -52,6 +54,7 @@ class OverviewViewModelTest : BaseTest() {
private lateinit var upgradeRepo: UpgradeRepo
private lateinit var bluetoothManager: BluetoothManager2
private lateinit var profilesRepo: DeviceProfilesRepo
private val timeSource: TimeSource = TestTimeSource()
private lateinit var missingPermissionsFlow: MutableStateFlow<Set<Permission>>
private lateinit var devicesFlow: MutableStateFlow<List<PodDevice>>
@@ -126,6 +129,7 @@ class OverviewViewModelTest : BaseTest() {
bluetoothManager = bluetoothManager,
profilesRepo = profilesRepo,
aapManager = mockk(relaxed = true),
timeSource = timeSource,
)
@Nested
@@ -1,5 +1,6 @@
package eu.darken.capod.monitor.core
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.monitor.core.aap.AapLifecycleManager
import eu.darken.capod.monitor.core.ble.BlePodMonitor
@@ -26,12 +27,14 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.TestTimeSource
import java.time.Instant
@OptIn(ExperimentalCoroutinesApi::class)
class DeviceMonitorTest : BaseTest() {
private val testDispatcher = UnconfinedTestDispatcher()
private val timeSource: TimeSource = TestTimeSource(wallNow = Instant.parse("2026-04-05T18:00:00Z"))
private val testAddress: BluetoothAddress = "AA:BB:CC:DD:EE:FF"
private val testProfile = AppleDeviceProfile(
@@ -135,6 +138,7 @@ class DeviceMonitorTest : BaseTest() {
deviceStateCache = deviceStateCache,
profilesRepo = profilesRepo,
aapLifecycleManager = aapLifecycleManager,
timeSource = timeSource,
)
}
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.aap
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import eu.darken.capod.common.TimeSource
import eu.darken.capod.common.bluetooth.l2cap.L2capSocketFactory
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
@@ -20,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.TestTimeSource
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
@@ -32,6 +34,7 @@ class AapConnectionManagerTest : BaseTest() {
private lateinit var socketFactory: L2capSocketFactory
private lateinit var manager: AapConnectionManager
private val timeSource: TimeSource = TestTimeSource()
private val testAddress = "AA:BB:CC:DD:EE:FF"
private val testDevice: BluetoothDevice = mockk(relaxed = true) {
@@ -44,6 +47,7 @@ class AapConnectionManagerTest : BaseTest() {
manager = AapConnectionManager(
socketFactory = socketFactory,
scope = testScope,
timeSource = timeSource,
)
}
@@ -2,7 +2,6 @@ package eu.darken.capod.pods.core.apple.ble.devices
import dagger.BindsInstance
import dagger.Component
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
@@ -15,9 +14,6 @@ import eu.darken.capod.profiles.core.DeviceProfilesRepo
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkObject
import org.junit.jupiter.api.AfterEach
import kotlinx.coroutines.flow.flowOf
import org.junit.jupiter.api.BeforeEach
import testhelpers.BaseTest
@@ -59,14 +55,6 @@ abstract class BaseBlePodsTest : BaseTest() {
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
mockkObject(SystemClockWrap)
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
}
@AfterEach
fun teardown() {
unmockkObject(SystemClockWrap)
}
internal suspend inline fun <reified T : BlePodSnapshot?> create(
@@ -75,7 +63,7 @@ abstract class BaseBlePodsTest : BaseTest() {
block: T.() -> Unit
) {
val result = BleScanResult(
receivedAt = Instant.now(),
receivedAt = Instant.parse("2026-01-01T00:00:00Z"),
address = address,
rssi = -66,
generatedAtNanos = 136136027721826,
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.TestTimeSource
import java.time.Duration
import java.time.Instant
@@ -19,6 +20,7 @@ class PopUpReactionLogicTest : BaseTest() {
popUpReaction = PopUpReaction(
deviceMonitor = mockk(relaxed = true),
bluetoothManager = mockk(relaxed = true),
timeSource = TestTimeSource(),
)
}
@@ -0,0 +1,30 @@
package testhelpers
import eu.darken.capod.common.TimeSource
import java.time.Duration
import java.time.Instant
class TestTimeSource(
var wallNow: Instant = Instant.parse("2026-01-01T00:00:00Z"),
var elapsedRealtimeMs: Long = 0L,
var elapsedRealtimeNanosValue: Long = 0L,
var uptimeMillisValue: Long = 0L,
) : TimeSource {
override fun now(): Instant = wallNow
override fun currentTimeMillis(): Long = wallNow.toEpochMilli()
override fun elapsedRealtime(): Long = elapsedRealtimeMs
override fun elapsedRealtimeNanos(): Long = elapsedRealtimeNanosValue
override fun uptimeMillis(): Long = uptimeMillisValue
fun advanceBy(duration: Duration) {
wallNow = wallNow.plus(duration)
elapsedRealtimeMs += duration.toMillis()
elapsedRealtimeNanosValue += duration.toNanos()
uptimeMillisValue += duration.toMillis()
}
}