diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt index 9ff7ccd1..0a58c12b 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt @@ -311,12 +311,10 @@ class BluetoothManager2 @Inject constructor( address = device.address, name = device.name, internal = device, + // Read-only: a bonded device is not necessarily connected, and writing here would + // re-stamp entries that [connectedDevices] just pruned. seenFirstAt = seenDevicesLock.withLock { - seenDevicesCache[device.address] ?: run { - val now = timeSource.now() - seenDevicesCache[device.address] = now - now - } + seenDevicesCache[device.address] ?: timeSource.now() } ) diff --git a/app/src/test/java/eu/darken/capod/common/bluetooth/BluetoothManager2Test.kt b/app/src/test/java/eu/darken/capod/common/bluetooth/BluetoothManager2Test.kt new file mode 100644 index 00000000..6241cdfa --- /dev/null +++ b/app/src/test/java/eu/darken/capod/common/bluetooth/BluetoothManager2Test.kt @@ -0,0 +1,68 @@ +package eu.darken.capod.common.bluetooth + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.content.Context +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.TestTimeSource +import testhelpers.coroutine.TestDispatcherProvider +import java.time.Duration + +class BluetoothManager2Test : BaseTest() { + + private val deviceA = mockk().apply { + every { address } returns ADDRESS_A + every { name } returns "Pods A" + } + private val deviceB = mockk().apply { + every { address } returns ADDRESS_B + every { name } returns "Pods B" + } + private val btAdapter = mockk().apply { + every { bondedDevices } returns setOf(deviceA, deviceB) + } + private val btManager = mockk().apply { + every { adapter } returns btAdapter + } + private val timeSource = TestTimeSource() + + private fun TestScope.create() = BluetoothManager2( + appScope = backgroundScope, + dispatcherProvider = TestDispatcherProvider(), + context = mockk(), + manager = btManager, + timeSource = timeSource, + ) + + private suspend fun BluetoothManager2.bonded(address: BluetoothAddress) = + bondedDevices().first().single { it.address == address } + + /** + * A bonded device is not necessarily a connected one, so querying bonded devices must not put a + * timestamp into the cache that [BluetoothManager2.connectedDevices] later reads as a connect + * time. + */ + @Test + fun `querying bonded devices does not cache a timestamp`() = runTest { + val manager = create() + + manager.bonded(ADDRESS_A).seenFirstAt shouldBe timeSource.now() + + timeSource.advanceBy(Duration.ofSeconds(60)) + + manager.bonded(ADDRESS_A).seenFirstAt shouldBe timeSource.now() + } + + companion object { + private const val ADDRESS_A = "AA:BB:CC:DD:EE:F1" + private const val ADDRESS_B = "AA:BB:CC:DD:EE:F2" + } +}