Skip to content

Android BLE Scanning — Complete Guide

DodaTech Updated 2026-06-24 2 min read

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_CONNECT on Android 12+.
  • Use ScanFilter to filter by service UUID.
  • Use ScanSettings.SCAN_MODE_LOW_POWER for background scanning.
  • Always call stopScan() to save battery.
  • Handle ScanFailed errors (especially SCAN_FAILED_APPLICATION_REGISTRATION_FAILED).

Common Mistakes with bluetooth scan

  1. Forgetting that lazy evaluation defers computation until the value is forced, causing space leaks with unevaluated thunks
  2. Using return to exit a function early instead of wrapping a pure value in the monad
  3. 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

### What BLE permissions are needed on Android 12+?

BLUETOOTH_SCAN (scan), BLUETOOTH_CONNECT (connect), and ACCESS_FINE_LOCATION (for BLE-based location). On Android 11-, only ACCESS_FINE_LOCATION.

### Why does my scan return no devices?

Check: (1) Bluetooth is enabled, (2) Location is enabled (required for BLE scan), (3) Permissions granted, (4) The device is advertising, (5) No scan filter mismatch.

### What is the difference between SCAN_MODE_LOW_POWER and SCAN_MODE_LOW_LATENCY?

LOW_POWER scans less frequently (battery efficient, slower discovery). LOW_LATENCY scans continuously (faster discovery, more battery). Use LOW_LATENCY during active device search.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro