mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
fix(reaction): Stop bonded-device queries from poisoning connect times
seenFirstAt on a connected device is meant to be the connect time, and the connected-devices flow maintains that by pruning its cache to the currently connected addresses on every emission. bondedDevices() wrote into the same cache for every bonded device, connected or not, and never pruned. AapAutoConnect queries bonded devices on every connected-devices emission, the disconnect one included, so an entry pruned at disconnect was re-stamped milliseconds later at disconnect time. The next reconnect then inherited the previous disconnect as its connect time, and the popup reaction rejects a connection older than 30 seconds — so any reconnect more than half a minute after the previous disconnect silently lost its popup. bondedDevices() is now a cache reader: a connected bonded device still reports its true connect time, a non-connected one gets the current time, which no caller reads. The connected-devices path becomes the cache's only writer, which is the invariant its prune-and-stamp logic already assumed.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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<BluetoothDevice>().apply {
|
||||
every { address } returns ADDRESS_A
|
||||
every { name } returns "Pods A"
|
||||
}
|
||||
private val deviceB = mockk<BluetoothDevice>().apply {
|
||||
every { address } returns ADDRESS_B
|
||||
every { name } returns "Pods B"
|
||||
}
|
||||
private val btAdapter = mockk<BluetoothAdapter>().apply {
|
||||
every { bondedDevices } returns setOf(deviceA, deviceB)
|
||||
}
|
||||
private val btManager = mockk<BluetoothManager>().apply {
|
||||
every { adapter } returns btAdapter
|
||||
}
|
||||
private val timeSource = TestTimeSource()
|
||||
|
||||
private fun TestScope.create() = BluetoothManager2(
|
||||
appScope = backgroundScope,
|
||||
dispatcherProvider = TestDispatcherProvider(),
|
||||
context = mockk<Context>(),
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user