mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-11-08 08:01:09 +00:00
Fix BLE scan recovery after already-started failure
The error 1 recovery cleared the scanning flag before calling stopScanning, so the stack-level stop was always skipped. A new callback on every start also meant an earlier registration could never be stopped or detected. Reuse one ScanCallback and force a stack-level stop on every scan failure.
This commit is contained in:
parent
1f24973b06
commit
f2d04fe130
@ -214,7 +214,9 @@ class BluetoothGattClientManager(
|
||||
|
||||
val scanFilters = listOf(scanFilter)
|
||||
|
||||
scanCallback = object : ScanCallback() {
|
||||
// Reuse one callback: the stack tracks scans by callback identity, so a replaced callback
|
||||
// could never stop or recognize a scan registered under the previous one.
|
||||
scanCallback = scanCallback ?: object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
handleScanResult(result)
|
||||
}
|
||||
@ -228,12 +230,14 @@ class BluetoothGattClientManager(
|
||||
override fun onScanFailed(errorCode: Int) {
|
||||
isCurrentlyScanning = false
|
||||
lastScanStopTime = System.currentTimeMillis()
|
||||
// The stack keeps a registration that fails after startScan; release it regardless
|
||||
// of the flag so the next start doesn't report already-started.
|
||||
stopScanning(force = true)
|
||||
|
||||
when (errorCode) {
|
||||
1 -> {
|
||||
// Already started: the stack thinks a scan is running. Re-arm from a clean
|
||||
// state so we don't stay wedged (stop then restart with backoff).
|
||||
stopScanning()
|
||||
// Already started: the stack thought a scan was running. It was stopped
|
||||
// above, so re-arm with backoff from a clean state.
|
||||
scheduleScanRestart("already-started", SCAN_RETRY_BASE_MS)
|
||||
}
|
||||
2 -> {
|
||||
@ -277,10 +281,10 @@ class BluetoothGattClientManager(
|
||||
* Stop scanning
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private fun stopScanning() {
|
||||
private fun stopScanning(force: Boolean = false) {
|
||||
if (!permissionManager.hasBluetoothPermissions() || bleScanner == null) return
|
||||
|
||||
if (isCurrentlyScanning) {
|
||||
if (isCurrentlyScanning || force) {
|
||||
try {
|
||||
scanCallback?.let {
|
||||
bleScanner.stopScan(it)
|
||||
|
||||
@ -0,0 +1,137 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.le.BluetoothLeScanner
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import android.bluetooth.le.ScanSettings
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestCoroutineScheduler
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.argumentCaptor
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.inOrder
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class BluetoothGattClientManagerScanRecoveryRobolectricTest {
|
||||
private val scheduler = TestCoroutineScheduler()
|
||||
private val scope = CoroutineScope(StandardTestDispatcher(scheduler))
|
||||
private val scanner = mock<BluetoothLeScanner>()
|
||||
private lateinit var manager: BluetoothGattClientManager
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val adapter = mock<BluetoothAdapter> {
|
||||
on { isEnabled } doReturn true
|
||||
on { bluetoothLeScanner } doReturn scanner
|
||||
}
|
||||
val bluetoothManager = mock<BluetoothManager> {
|
||||
on { this.adapter } doReturn adapter
|
||||
}
|
||||
val context = mock<Context> {
|
||||
on { getSystemService(Context.BLUETOOTH_SERVICE) } doReturn bluetoothManager
|
||||
}
|
||||
val permissionManager = mock<BluetoothPermissionManager> {
|
||||
on { hasBluetoothPermissions() } doReturn true
|
||||
}
|
||||
val resolved = PowerProfileResolver.resolve(
|
||||
batteryLevel = 100,
|
||||
isCharging = true,
|
||||
isBackground = false,
|
||||
hasDirectPeers = false
|
||||
)
|
||||
val continuousProfile = resolved.copy(ble = resolved.ble.copy(continuousScan = true))
|
||||
val powerManager = mock<PowerManager> {
|
||||
on { profile } doReturn MutableStateFlow(continuousProfile)
|
||||
on { getScanSettings() } doReturn ScanSettings.Builder().build()
|
||||
}
|
||||
manager = BluetoothGattClientManager(
|
||||
context,
|
||||
scope,
|
||||
mock(),
|
||||
permissionManager,
|
||||
powerManager,
|
||||
null
|
||||
)
|
||||
|
||||
manager.start()
|
||||
scheduler.runCurrent()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `already started failure stops the registered scan before restarting it`() {
|
||||
val callback = startedCallback()
|
||||
|
||||
callback.onScanFailed(ScanCallback.SCAN_FAILED_ALREADY_STARTED)
|
||||
allowNextScanStart()
|
||||
scheduler.advanceTimeBy(ALREADY_STARTED_RETRY_MS + 1)
|
||||
|
||||
inOrder(scanner) {
|
||||
verify(scanner).startScan(any<List<ScanFilter>>(), any(), any<ScanCallback>())
|
||||
verify(scanner).stopScan(callback)
|
||||
verify(scanner).startScan(any<List<ScanFilter>>(), any(), any<ScanCallback>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restarted scan keeps the callback the stack has registered`() {
|
||||
val first = startedCallback()
|
||||
|
||||
manager.onScanStateChanged(false)
|
||||
allowNextScanStart()
|
||||
manager.onScanStateChanged(true)
|
||||
|
||||
val started = argumentCaptor<ScanCallback>()
|
||||
verify(scanner, times(2)).startScan(any<List<ScanFilter>>(), any(), started.capture())
|
||||
assertSame(first, started.secondValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure reported after registration releases the scan`() {
|
||||
val callback = startedCallback()
|
||||
|
||||
callback.onScanFailed(ScanCallback.SCAN_FAILED_INTERNAL_ERROR)
|
||||
|
||||
verify(scanner).stopScan(callback)
|
||||
}
|
||||
|
||||
private fun startedCallback(): ScanCallback {
|
||||
val started = argumentCaptor<ScanCallback>()
|
||||
verify(scanner).startScan(any<List<ScanFilter>>(), any(), started.capture())
|
||||
return started.firstValue
|
||||
}
|
||||
|
||||
/** The rate limit reads the wall clock, so clear the last start instead of sleeping past it. */
|
||||
private fun allowNextScanStart() {
|
||||
BluetoothGattClientManager::class.java
|
||||
.getDeclaredField("lastScanStartTime")
|
||||
.apply { isAccessible = true }
|
||||
.setLong(manager, 0L)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ALREADY_STARTED_RETRY_MS = 3_000L
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user