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