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.app.Activity
|
||||||
import android.bluetooth.*
|
import android.bluetooth.*
|
||||||
import android.bluetooth.BluetoothDevice.BOND_BONDED
|
import android.bluetooth.BluetoothDevice.BOND_BONDED
|
||||||
|
import android.bluetooth.le.BluetoothLeScanner
|
||||||
import android.bluetooth.le.ScanCallback
|
import android.bluetooth.le.ScanCallback
|
||||||
import android.bluetooth.le.ScanFilter
|
import android.bluetooth.le.ScanFilter
|
||||||
import android.bluetooth.le.ScanResult
|
import android.bluetooth.le.ScanResult
|
||||||
@@ -37,6 +38,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
|||||||
private lateinit var context: Context
|
private lateinit var context: Context
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private lateinit var bluetoothManager: BluetoothManager
|
private lateinit var bluetoothManager: BluetoothManager
|
||||||
|
private lateinit var safeScanner: SafeScanner
|
||||||
private val cachedServicesMap = mutableMapOf<String, List<String>>()
|
private val cachedServicesMap = mutableMapOf<String, List<String>>()
|
||||||
private val devicesStateMap = mutableMapOf<String, Int>()
|
private val devicesStateMap = mutableMapOf<String, Int>()
|
||||||
private val universalBleFilterUtil = UniversalBleFilterUtil()
|
private val universalBleFilterUtil = UniversalBleFilterUtil()
|
||||||
@@ -50,13 +52,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
|||||||
private val subscriptionResultFutureList = mutableListOf<SubscriptionResultFuture>()
|
private val subscriptionResultFutureList = mutableListOf<SubscriptionResultFuture>()
|
||||||
private val pairResultFutures = mutableMapOf<String, (Result<Boolean>) -> Unit>()
|
private val pairResultFutures = mutableMapOf<String, (Result<Boolean>) -> Unit>()
|
||||||
|
|
||||||
|
|
||||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this)
|
UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this)
|
||||||
callbackChannel = UniversalBleCallbackChannel(flutterPluginBinding.binaryMessenger)
|
callbackChannel = UniversalBleCallbackChannel(flutterPluginBinding.binaryMessenger)
|
||||||
context = flutterPluginBinding.applicationContext
|
context = flutterPluginBinding.applicationContext
|
||||||
mainThreadHandler = Handler(Looper.getMainLooper())
|
mainThreadHandler = Handler(Looper.getMainLooper())
|
||||||
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
safeScanner = SafeScanner(bluetoothManager)
|
||||||
|
|
||||||
val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||||
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
|
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
|
||||||
@@ -75,7 +77,6 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
|||||||
mainThreadHandler = null
|
mainThreadHandler = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit) {
|
override fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit) {
|
||||||
callback(
|
callback(
|
||||||
Result.success(
|
Result.success(
|
||||||
@@ -132,7 +133,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
|||||||
scanFilters = filter?.toScanFilters(filterServices) ?: emptyList<ScanFilter>()
|
scanFilters = filter?.toScanFilters(filterServices) ?: emptyList<ScanFilter>()
|
||||||
}
|
}
|
||||||
|
|
||||||
bluetoothManager.adapter.bluetoothLeScanner?.startScan(
|
safeScanner.startScan(
|
||||||
scanFilters, settings, scanCallback
|
scanFilters, settings, scanCallback
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -150,7 +151,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
|||||||
"Bluetooth not enabled",
|
"Bluetooth not enabled",
|
||||||
)
|
)
|
||||||
// check if already scanning
|
// check if already scanning
|
||||||
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback)
|
safeScanner.stopScan(scanCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun connect(deviceId: String) {
|
override fun connect(deviceId: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user