Handle Android scan abuse (#104)

* Handle Android scan abuse

* Improve null safety in start scanning

* Update android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt

Co-authored-by: Foti Dim <foti@navideck.com>

---------

Co-authored-by: Foti Dim <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2024-12-05 13:28:06 +05:30
committed by GitHub
parent 61e1448fe9
commit 19b8167954
2 changed files with 84 additions and 4 deletions
@@ -0,0 +1,79 @@
package com.navideck.universal_ble
import android.annotation.SuppressLint
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.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.util.LinkedList
private const val NUM_SCAN_DURATIONS_KEPT = 5
private const val EXCESSIVE_SCANNING_PERIOD_MS = 30 * 1000L
private const val TAG = "UniversalBlePlugin"
/**
* A safe wrapper for Bluetooth LE scanning operations that prevents excessive scanning.
*
* This class manages BLE scanning while adhering to Android's scanning frequency limits by:
* - Tracking scan start times over a 30-second window
* - Limiting to 5 scan operations within this window
* - Automatically scheduling delayed scans when frequency limits are exceeded
* - Providing safe start/stop scan operations with error handling
*
* The scanner will automatically delay new scan requests if the frequency limit is reached,
* and will retry once sufficient time has passed. This helps prevent scan failure errors
* and ensures compliance with Android's scanning restrictions.
*
* @property bluetoothManager The system's BluetoothManager used for scanning operations
*/
@SuppressLint("MissingPermission")
class SafeScanner(private val bluetoothManager: BluetoothManager) {
private val handler = Handler(Looper.myLooper()!!)
private val startTimes = LinkedList<Long>()
private var awaitingScan = false
fun startScan(filters: List<ScanFilter>, settings: ScanSettings, callback: ScanCallback) {
val now = System.currentTimeMillis()
startTimes.removeAll { now - it > EXCESSIVE_SCANNING_PERIOD_MS }
if (startTimes.size >= NUM_SCAN_DURATIONS_KEPT) {
if (awaitingScan) {
Log.e(TAG, "startScan: too frequent, awaiting scan..")
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
callback.onScanFailed(ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY)
}
awaitingScan = true
val delay = startTimes.first + EXCESSIVE_SCANNING_PERIOD_MS - now + 2_000
Log.e(TAG, "startScan: too frequent, schedule auto-start after $delay ms $startTimes")
handler.postDelayed({
Log.d(TAG, "Retrying scan after delay")
awaitingScan = false
startScan(filters, settings, callback)
}, delay)
} else {
awaitingScan = false
startTimes.addLast(now)
try {
bluetoothManager.adapter.bluetoothLeScanner?.startScan(filters, settings, callback)
} catch (e: Exception) {
Log.e(TAG, "Failed to start Scan : $e")
}
}
}
fun stopScan(callback: ScanCallback) {
awaitingScan = false
handler.removeCallbacksAndMessages(null)
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(callback)
}
}
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.bluetooth.*
import android.bluetooth.BluetoothDevice.BOND_BONDED
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
@@ -37,6 +38,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
private lateinit var context: Context
private var activity: Activity? = null
private lateinit var bluetoothManager: BluetoothManager
private lateinit var safeScanner: SafeScanner
private val cachedServicesMap = mutableMapOf<String, List<String>>()
private val devicesStateMap = mutableMapOf<String, Int>()
private val universalBleFilterUtil = UniversalBleFilterUtil()
@@ -50,13 +52,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
private val subscriptionResultFutureList = mutableListOf<SubscriptionResultFuture>()
private val pairResultFutures = mutableMapOf<String, (Result<Boolean>) -> Unit>()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this)
callbackChannel = UniversalBleCallbackChannel(flutterPluginBinding.binaryMessenger)
context = flutterPluginBinding.applicationContext
mainThreadHandler = Handler(Looper.getMainLooper())
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
safeScanner = SafeScanner(bluetoothManager)
val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
@@ -75,7 +77,6 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
mainThreadHandler = null
}
override fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit) {
callback(
Result.success(
@@ -132,7 +133,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
scanFilters = filter?.toScanFilters(filterServices) ?: emptyList<ScanFilter>()
}
bluetoothManager.adapter.bluetoothLeScanner?.startScan(
safeScanner.startScan(
scanFilters, settings, scanCallback
)
} catch (e: Exception) {
@@ -150,7 +151,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
"Bluetooth not enabled",
)
// check if already scanning
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback)
safeScanner.stopScan(scanCallback)
}
override fun connect(deviceId: String) {