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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user