Android BLE Scanning — Complete Guide
In this tutorial, you'll learn about Android BLE Scanning. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
The Problem
Your BLE scan finds no devices, or scanning crashes on Android 12+ with BLUETOOTH_SCAN permission errors.
Wrong Approach ❌
// No scan filter — returns ALL BLE devices in range
val scanner = BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner
scanner.startScan(callback) // Thousands of scan results!
Output: Overwhelming scan results. Can't find target device. Battery drain.
Right Approach ✅
class BleScanner(private val context: Context) {
private val bluetoothLeScanner: BluetoothLeScanner?
private val scanResults = mutableMapOf<String, ScanResult>()
// Scan callback
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
scanResults[result.device.address] = result
onDeviceFound?.invoke(result)
}
override fun onBatchScanResults(results: List<ScanResult>) {
results.forEach { scanResults[it.device.address] = it }
}
override fun onScanFailed(errorCode: Int) {
Log.e("BleScan", "Scan failed with error: $errorCode")
onScanError?.invoke(errorCode)
}
}
var onDeviceFound: ((ScanResult) -> Unit)? = null
var onScanError: ((Int) -> Unit)? = null
init {
val adapter = BluetoothAdapter.getDefaultAdapter()
bluetoothLeScanner = adapter?.bluetoothLeScanner
}
// Filtered scan for specific service
fun startScan(serviceUuid: String? = null) {
if (!hasPermissions()) {
requestPermissions()
return
}
val filters = if (serviceUuid != null) {
listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(UUID.fromString(serviceUuid)))
.build()
)
} else emptyList()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // Low power for background
.setReportDelay(0) // Immediate results
.build()
bluetoothLeScanner?.startScan(filters, settings, scanCallback)
}
fun stopScan() {
bluetoothLeScanner?.stopScan(scanCallback)
}
// Parse advertisement data
fun getDeviceName(result: ScanResult): String {
return result.device.name ?: "Unknown"
}
fun getRssi(result: ScanResult): Int {
return result.rssi
}
fun hasServiceData(result: ScanResult, serviceUuid: String): Boolean {
return result.scanRecord?.serviceData?.containsKey(ParcelUuid(UUID.fromString(serviceUuid))) ?: false
}
private fun hasPermissions(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PERMISSION_GRANTED
} else {
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PERMISSION_GRANTED
}
}
}
Output: Efficient BLE scanning with proper filtering and permissions.
Prevention
- Request
BLUETOOTH_SCAN+BLUETOOTH_CONNECTon Android 12+. - Use
ScanFilterto filter by service UUID. - Use
ScanSettings.SCAN_MODE_LOW_POWERfor background scanning. - Always call
stopScan()to save battery. - Handle
ScanFailederrors (especiallySCAN_FAILED_APPLICATION_REGISTRATION_FAILED).
Common Mistakes with bluetooth scan
- Forgetting that lazy evaluation defers computation until the value is forced, causing space leaks with unevaluated thunks
- Using
returnto exit a function early instead of wrapping a pure value in the monad - Mixing let bindings with <- bindings in do notation, producing type errors
These mistakes appear frequently in real-world Android code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.
Practice Exercise
Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.
This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro