BluetoothLeScanner è una classe Android per scansionare dispositivi Bluetooth Low Energy, disponibile dall'API 21 (Android 5.0). BluetoothLeScanner ha sostituito il metodo obsoleto startLeScan su BluetoothAdapter, fornendo un'API flessibile con configurazione di scansione (ScanSettings), filtraggio (ScanFilter) e supporto per la modalità background (PendingIntent). L'istanza si ottiene tramite BluetoothAdapter.getBluetoothLeScanner(). Secondo Android Developers, 2026, BluetoothLeScanner supporta tre modalità di alimentazione e consente di scansionare pacchetti pubblicitari BLE con filtraggio per UUID del servizio, nome del dispositivo o indirizzo MAC.
Punti Chiave
BluetoothLeScanner è una classe di sistema per gestire la scansione BLE su Android. A differenza di BluetoothAdapter.startLeScan(), che accetta un semplice LeScanCallback, BluetoothLeScanner fornisce un'API orientata agli oggetti con impostazioni, filtri e gestione avanzata degli errori. Questa classe è stata introdotta nell'API 21 (Android 5.0) insieme al supporto BLE 4.2 e rimane il metodo principale di scansione BLE su tutte le versioni moderne di Android.
L'ottenimento di un'istanza BluetoothLeScanner avviene tramite BluetoothAdapter.getBluetoothLeScanner(). Il metodo restituisce null se l'adattatore Bluetooth non è disponibile (Bluetooth disattivato o dispositivo non supporta BLE). Prima di ottenerlo, verificare BluetoothAdapter.isEnabled() e la presenza di FEATURE_BLUETOOTH_LE tramite PackageManager. Una volta ottenuto lo scanner, la scansione può essere avviata su qualsiasi thread — Android pianifica le operazioni BLE su un thread interno dello stack Bluetooth.
// Ottenere BluetoothLeScanner
class BLEScannerManager(context: Context) {
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val adapter: BluetoothAdapter? = bluetoothManager.adapter
private var scanner: BluetoothLeScanner? = null
fun initScanner(): Boolean {
// Verificare disponibilità BLE
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
return false
}
// Verificare Bluetooth attivato
if (adapter?.isEnabled != true) {
return false
}
// Ottenere scanner
scanner = adapter?.bluetoothLeScanner
return scanner != null
}
// Verificare disponibilità scanner
val isAvailable: Boolean
get() = scanner != null
// Avviare scansione base senza filtri
fun startBasicScan() {
scanner?.startScan(object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
handleResult(result)
}
})
}
private fun handleResult(result: ScanResult) {
val device = result.device
print("Device: ${device.name ?: "Unnamed"}, RSSI: ${result.rssi}, address: ${device.address}")
}
}
La classe BLEScannerManager dimostra l'ottenimento e l'inizializzazione sicuri di BluetoothLeScanner. initScanner verifica la disponibilità BLE tramite hasSystemFeature, Bluetooth attivato e ottenimento riuscito dello scanner. startBasicScan avvia la scansione senza impostazioni o filtri — rileva tutti i dispositivi BLE nel raggio d'azione. handleResult analizza ScanResult: BluetoothDevice (nome, indirizzo), RSSI (intensità del segnale), scanRecord (dati pubblicitari).
ScanSettings è una classe per la configurazione della scansione BLE. Il parametro principale è la modalità di scansione (scanMode), che determina il compromesso tra consumo energetico e latenza di rilevamento. ScanSettings.Builder consente di configurare: scanMode, callbackType (CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH, CALLBACK_TYPE_MATCH_LOST), matchMode (MATCH_MODE_AGGRESSIVE, MATCH_MODE_STICKY), reportDelay (ritardo di consegna batch) e phy (PHY_LE_1M, PHY_LE_2M, PHY_LE_CODED).
Tre modalità di scansione: SCAN_MODE_LOW_POWER (0) — scansione in background con basso consumo, ritardo di rilevamento di diversi secondi. SCAN_MODE_BALANCED (1) — modalità bilanciata per la maggior parte degli scenari. SCAN_MODE_LOW_LATENCY (2) — latenza di rilevamento minima (circa 100 ms), consumo massimo. Per la ricerca attiva di dispositivi utilizzare LOW_LATENCY, per il monitoraggio in background utilizzare LOW_POWER.
reportDelay — ritardo in millisecondi prima della consegna batch dei risultati. Se reportDelay = 0, i risultati vengono inviati immediatamente al rilevamento. Se > 0, Android accumula i risultati e invia un batch tramite onBatchScanResults. La consegna batch riduce le invocazioni di callback e il consumo energetico, adatta per scansione in background a bassa priorità.
// Configurazione ScanSettings per diversi scenari
class ScanSettingsProvider {
// 1. Scansione rapida (ricerca attiva)
fun lowLatencyScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setReportDelay(0)
.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED)
.build()
}
// 2. Scansione efficiente (monitoraggio background)
fun lowPowerScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setReportDelay(2000) // batch ogni 2 secondi
.build()
}
// 3. Scansione BLE Long Range (Coded PHY)
fun longRangeScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setPhy(ScanSettings.PHY_LE_CODED)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
}
// 4. Scansiona solo su 2M PHY (BLE 5.0+)
fun highSpeedScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setPhy(ScanSettings.PHY_LE_2M)
.build()
}
}
La classe ScanSettingsProvider contiene configurazioni tipiche. lowLatencyScan — per scansione UI (ricerca “qui e ora”). lowPowerScan — per monitoraggio in background con batch ogni 2 secondi e callbackType FIRST_MATCH (si attiva solo al primo rilevamento). longRangeScan utilizza PHY_LE_CODED (BLE Long Range, fino a 1 km). highSpeedScan — PHY_LE_2M (2 Mbit/s, solo dispositivi BLE 5.0+).
ScanFilter è una classe per filtrare i risultati della scansione BLE. Senza filtro, BluetoothLeScanner restituisce tutti i dispositivi BLE nel raggio — in un ambiente BLE denso possono essere centinaia di pacchetti al minuto. ScanFilter restringe i risultati ai dispositivi desiderati, riducendo il consumo energetico e il carico dell'app. I filtri vengono applicati a livello dello stack Bluetooth — i pacchetti non adatti vengono scartati prima di raggiungere l'app.
Tipi di filtri: setServiceUuid — UUID del servizio (formato completo a 128 bit richiesto). setDeviceName — sottostringa del nome del dispositivo (sensibile a maiuscole/minuscole, corrispondenza esatta di sottostringa). setDeviceAddress — indirizzo MAC esatto. setManufacturerData — dati del produttore (ID azienda + maschera). È possibile impostare più filtri per una singola scansione — il dispositivo deve corrispondere a tutti (logica AND). Per logica OR, avviare più scansioni.
// Creazione ScanFilter per diversi scenari
class ScanFilterFactory {
// 1. Filtra per UUID servizio (Monitor frequenza cardiaca)
fun byHeartRateService(): ScanFilter {
return ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
)
.build()
}
// 2. Filtra per nome dispositivo ( "iBeacon*")
fun byDeviceName(): ScanFilter {
return ScanFilter.Builder()
.setDeviceName("Sensor")
.build()
}
// 3. MAC- ( dispositivi)
fun byMacAddress(mac: String): ScanFilter {
return ScanFilter.Builder()
.setDeviceAddress(mac)
.build()
}
// 4. Filtro combinato (UUID + )
fun combinedFilter(): List<ScanFilter> {
return listOf(
ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
)
.setDeviceName("MyDevice")
.build()
)
}
// 5. dati produttore
fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
return ScanFilter.Builder()
.setManufacturerData(companyId, data, mask)
.build()
}
}
La classe ScanFilterFactory mostra tutti i tipi di filtro. byHeartRateService filtra i dispositivi con il servizio frequenza cardiaca 0x180D. byDeviceName trova dispositivi contenenti “Sensor” nel nome (Apple consiglia nomi univoci per il filtraggio). byMacAddress — ricerca esatta di un dispositivo specifico. combinedFilter — filtro AND per UUID e nome. byManufacturer — filtro per dati del produttore (ad esempio, per iBeacon viene utilizzato l'ID azienda Apple 0x004C).
ScanCallback è una classe astratta per ricevere i risultati della scansione BLE. Contiene tre metodi: onScanResult — risultato singolo (tipo di callback, ScanResult), onBatchScanResults — risultati batch per reportDelay > 0, onScanFailed — codice di errore. Tutti i metodi vengono chiamati sul thread principale di Android. Per elaborazioni lunghe in onScanResult, utilizzare coroutine o HandlerThread.
ScanResult contiene: BluetoothDevice device, int rssi (livello del segnale in dBm), ScanRecord scanRecord (dati pubblicitari), long timestampNanos (tempo di rilevamento dall'avvio del sistema). ScanRecord fornisce: getServiceData() — UUID + dati personalizzati, getManufacturerSpecificData() — dati del produttore, getAdvertiseFlags() — flag BLE. Il tipo di callback indica: CALLBACK_TYPE_ALL_MATCHES — corrispondenza con il filtro, CALLBACK_TYPE_FIRST_MATCH — primo rilevamento, CALLBACK_TYPE_MATCH_LOST — dispositivo perso.
Codici di errore onScanFailed: SCAN_FAILED_ALREADY_STARTED (1) — scansione già avviata, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) — registrazione dell'app nello stack Bluetooth fallita, SCAN_FAILED_INTERNAL_ERROR (3) — errore interno dello stack, SCAN_FAILED_FEATURE_UNSUPPORTED (4) — scansione BLE non supportata sul dispositivo.
// Risultati scansione completi e gestione errori
class ScanResultHandler {
private val results = mutableListOf<ScanResult>()
val scanCallback = object : ScanCallback() {
// 1. Risultato singolo
override fun onScanResult(callbackType: Int, result: ScanResult) {
// callbackType: 1 = ALL_MATCHES, 2 = FIRST_MATCH, 4 = MATCH_LOST
if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
onDeviceLost(result)
return
}
// Aggiungi a elenco (dedup per indirizzo)
val existingIndex = results.indexOfFirst {
it.device.address == result.device.address
}
if (existingIndex >= 0) {
results[existingIndex] = result // aggiorna RSSI
} else {
results.add(result)
}
// Estrai dati dal pacchetto pubblicitario
val record = result.scanRecord
val serviceData = record?.serviceData
val manufacturerData = record?.manufacturerSpecificData
print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
}
// 2. Risultati batch (reportDelay > 0)
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.let { batch ->
print("Batch: ${batch.size} devices")
}
}
// 3. Errore scansione
override fun onScanFailed(errorCode: Int) {
val error = when (errorCode) {
ScanCallback.SCAN_FAILED_ALREADY_STARTED -> "Already scanning"
ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "Registration failed"
ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> "Internal error"
ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> "BLE not supported"
else -> "Unknown error: $errorCode"
}
print("Error: $error")
}
}
private fun onDeviceLost(result: ScanResult) {
results.removeAll { it.device.address == result.device.address }
print("Device lost: ${result.device.address}")
}
}
La classe ScanResultHandler gestisce tutti i tipi di callback di BluetoothLeScanner. onScanResult aggiorna l'elenco dei dispositivi con deduplicazione per indirizzo MAC — RSSI viene aggiornato per i dispositivi già trovati. CALLBACK_TYPE_MATCH_LOST segnala la perdita del dispositivo (rimozione dall'elenco). onBatchScanResults elabora i risultati batch per reportDelay > 0. onScanFailed mappa i codici di errore in messaggi leggibili — critico per il debug della scansione BLE.
Scansione PendingIntent è un meccanismo di BluetoothLeScanner per la scansione BLE che funziona anche quando l'app è in background (con restrizioni Android 8+). Invece di ScanCallback, viene utilizzato un PendingIntent che invia un Broadcast al BroadcastReceiver di sistema al rilevamento di un dispositivo BLE. Ciò consente all'app di ricevere notifiche di dispositivi BLE senza rimanere in memoria (il sistema crea il processo alla ricezione del broadcast).
Limitazioni della scansione in background: Su Android 8+ (API 26), i servizi in background sono limitati — la scansione PendingIntent aggira questa limitazione tramite BroadcastReceiver, che il sistema può avviare alla ricezione di un evento BLE. Su Android 10+ (API 29), la scansione BLE in background è ulteriormente limitata dalle politiche di risparmio energetico dei produttori (Xiaomi, Huawei, Samsung bloccano le operazioni BLE in background). Per scenari BLE critici, è richiesta una notifica di foreground service.
// Scansione BLE in background tramite PendingIntent
class BackgroundBLEScanner(private val context: Context) {
private val scanner: BluetoothLeScanner? by lazy {
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner
}
fun startBackgroundScan() {
// Crea PendingIntent per BroadcastReceiver
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Impostazioni scansione background
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.build()
// Avvia scansione background
scanner?.startScan(
null, // filtri
settings,
pendingIntent
)
}
fun stopBackgroundScan() {
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
scanner?.stopScan(pendingIntent)
}
}
// BroadcastReceiver BLE-
class BLEBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Ottieni risultati scansione
val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
results?.let { scanResults ->
for (result in scanResults) {
// Invia notifica all'utente
showNotification(context, result.device.name ?: " ")
}
}
}
private fun showNotification(context: Context, name: String) {
val notification = Notification.Builder(context, "ble_channel")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("BLE devices")
.setContentText("Found: $name")
.setAutoCancel(true)
.build()
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
manager.notify(System.currentTimeMillis().toInt(), notification)
}
}
La classe BackgroundBLEScanner avvia la scansione BLE in background tramite PendingIntent. startBackgroundScan crea un PendingIntent che, al rilevamento di un dispositivo BLE, invia un Broadcast a BLEBroadcastReceiver. Il BroadcastReceiver estrae ScanResult tramite getPendingIntentScanResults() e può mostrare una notifica o inviare dati al server. Questo approccio funziona anche se l'app è stata terminata dal sistema — Android riavvia il BroadcastReceiver alla ricezione del broadcast.
Esempio completo di uno scanner BLE in Kotlin che utilizza BluetoothLeScanner con ScanSettings, ScanFilter e ScanCallback per trovare dispositivi Heart Rate Monitor. Lo scanner mostra un elenco dei dispositivi trovati con RSSI e UUID dei servizi, con possibilità di connettersi tramite BluetoothGatt.
// Scanner BLE completo con coroutine in Kotlin
class DeviceScanner(private val context: Context) {
private val adapter: BluetoothAdapter? by lazy {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE)
as BluetoothManager
manager.adapter
}
private val scanner: BluetoothLeScanner? by lazy {
adapter?.bluetoothLeScanner
}
fun startScan(duration: Long = 10000): Flow<ScanResult> = callbackFlow {
// Controlla stato Bluetooth
if (adapter?.isEnabled != true) {
close(IllegalStateException("Bluetooth disabled"))
return@callbackFlow
}
// Configurazione scansione
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val filters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
.build()
)
val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
trySend(result)
}
override fun onScanFailed(errorCode: Int) {
close(BLEException("Scan failed: $errorCode"))
}
}
// Avvia scansione
scanner?.startScan(filters, settings, callback)
// Arresto automatico dopo durata
delay(duration)
scanner?.stopScan(callback)
close()
}.flowOn(Dispatchers.IO)
fun stop() {
scanner?.stopScan(object : ScanCallback() {})
}
}
class BLEException(message: String) : Exception(message)
La classe DeviceScanner utilizza Kotlin Flow (callbackFlow) per la scansione BLE reattiva. La scansione viene avviata con impostazioni LOW_LATENCY e un filtro UUID Heart Rate Service. I risultati vengono emessi tramite onScanResult in un Flow. Arresto automatico dopo una durata specificata (10 secondi predefiniti). FlowOn(Dispatchers.IO) scarica le operazioni BLE su un thread in background. Questo approccio consente la scansione BLE nell'architettura MVVM tramite viewModelScope.launch e collect.
Domande Frequenti
BluetoothLeScanner è una classe Android (API 21+) per la scansione BLE. Si ottiene tramite BluetoothAdapter.getBluetoothLeScanner(). Supporta tre modalità di scansione (LOW_POWER, BALANCED, LOW_LATENCY), filtraggio per UUID, nome e indirizzo MAC, risultati batch e PendingIntent per scansione in background. Sostituisce il metodo obsoleto BluetoothAdapter.startLeScan().
SCAN_MODE_LOW_POWER — modalità background con ritardo di rilevamento di 5–10 secondi, consumo minimo. SCAN_MODE_LOW_LATENCY — modalità attiva con ritardo di circa 100 ms, consumo massimo. SCAN_MODE_BALANCED — compromesso (~2 secondi di ritardo). Utilizzare LOW_LATENCY per scansione UI, LOW_POWER con PendingIntent per monitoraggio in background.
Cause: Bluetooth disattivato (verificare adapter.isEnabled), permessi mancanti (BLUETOOTH_SCAN su API 31+, ACCESS_FINE_LOCATION su API 23–30), scanner = null (adattatore non disponibile), dispositivo fuori portata o filtro errato. Verificare anche onScanFailed — il codice di errore indica la causa: SCAN_FAILED_ALREADY_STARTED (1) o SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2).
Utilizzare la versione PendingIntent di startScan() — passare un PendingIntent invece di ScanCallback. Quando viene rilevato un dispositivo BLE, Android invia un Broadcast al BroadcastReceiver, che può essere avviato dal sistema anche se l'app è in background. Per Android 8+, aggiungere il BroadcastReceiver al manifest. Su Android 10+, considerare le restrizioni di risparmio energetico dei produttori.
BluetoothLeScanner non ha limiti sul numero di dispositivi rilevabili — il limite dipende dalla saturazione BLE dell'ambiente. Un ufficio può avere 20–50 dispositivi BLE attivi, un centro commerciale centinaia. Utilizzare ScanFilter (per UUID, nome) per filtrare. Senza filtraggio, elaborare i risultati in modo asincrono — onScanResult può essere chiamato decine di volte al secondo.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche