BluetoothLeScanner est une classe Android pour scanner les appareils Bluetooth Low Energy, disponible depuis l'API 21 (Android 5.0). BluetoothLeScanner a remplacé la méthode obsolète startLeScan sur BluetoothAdapter, offrant une API flexible avec configuration de scan (ScanSettings), filtrage (ScanFilter) et prise en charge du mode arrière-plan (PendingIntent). L'instance est obtenue via BluetoothAdapter.getBluetoothLeScanner(). Selon Android Developers, 2026, BluetoothLeScanner prend en charge trois modes d'alimentation et permet de scanner les paquets publicitaires BLE avec filtrage par UUID de service, nom d'appareil ou adresse MAC.
Points Clés
BluetoothLeScanner est une classe système pour gérer le scan BLE sur Android. Contrairement à BluetoothAdapter.startLeScan() qui accepte un simple LeScanCallback, BluetoothLeScanner fournit une API orientée objet avec paramètres, filtres et gestion avancée des erreurs. Cette classe a été introduite dans l'API 21 (Android 5.0) avec le support BLE 4.2 et reste la méthode principale de scan BLE sur toutes les versions modernes d'Android.
L'obtention d'une instance BluetoothLeScanner se fait via BluetoothAdapter.getBluetoothLeScanner(). La méthode retourne null si l'adaptateur Bluetooth est indisponible (Bluetooth désactivé ou appareil ne prend pas en charge BLE). Avant de l'obtenir, vérifiez BluetoothAdapter.isEnabled() et la présence de FEATURE_BLUETOOTH_LE via PackageManager. Une fois le scanner obtenu, le scan peut être lancé sur n'importe quel thread — Android planifie les opérations BLE sur un thread interne de la pile Bluetooth.
// Obtention de 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 {
// Vérifier la disponibilité BLE
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
return false
}
// Vérifier Bluetooth activé
if (adapter?.isEnabled != true) {
return false
}
// Obtenir le scanner
scanner = adapter?.bluetoothLeScanner
return scanner != null
}
// Vérifier la disponibilité du scanner
val isAvailable: Boolean
get() = scanner != null
// Lancer un scan de base sans filtres
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 démontre l'obtention et l'initialisation sécurisées de BluetoothLeScanner. initScanner vérifie la disponibilité BLE via hasSystemFeature, Bluetooth activé et l'obtention réussie du scanner. startBasicScan lance le scan sans paramètres ni filtres — détecte tous les appareils BLE à portée. handleResult analyse ScanResult : BluetoothDevice (nom, adresse), RSSI (intensité du signal), scanRecord (données publicitaires).
ScanSettings est une classe pour la configuration du scan BLE. Le paramètre principal est le mode de scan (scanMode) qui détermine le compromis entre consommation d'énergie et latence de détection. ScanSettings.Builder permet de configurer : scanMode, callbackType (CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH, CALLBACK_TYPE_MATCH_LOST), matchMode (MATCH_MODE_AGGRESSIVE, MATCH_MODE_STICKY), reportDelay (délai de livraison par lot) et phy (PHY_LE_1M, PHY_LE_2M, PHY_LE_CODED).
Trois modes de scan : SCAN_MODE_LOW_POWER (0) — scan en arrière-plan avec faible consommation, délai de détection de plusieurs secondes. SCAN_MODE_BALANCED (1) — mode équilibré pour la plupart des scénarios. SCAN_MODE_LOW_LATENCY (2) — latence de détection minimale (environ 100 ms), consommation maximale. Pour la recherche active d'appareils, utilisez LOW_LATENCY, pour la surveillance en arrière-plan, utilisez LOW_POWER.
reportDelay — délai en millisecondes avant la livraison par lot des résultats. Si reportDelay = 0, les résultats sont envoyés immédiatement à la détection. Si > 0, Android accumule les résultats et envoie un lot via onBatchScanResults. La livraison par lot réduit les appels de callbacks et la consommation d'énergie, adaptée au scan en arrière-plan de faible priorité.
// Configuration ScanSettings pour différents scénarios
class ScanSettingsProvider {
// 1. Scan rapide (recherche active)
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. Scan économe en énergie (surveillance en arrière-plan)
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) // lot toutes les 2 secondes
.build()
}
// 3. Scan 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. Scanner uniquement sur 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 contient des configurations typiques. lowLatencyScan — pour le scan d'interface utilisateur (recherche « ici et maintenant »). lowPowerScan — pour la surveillance en arrière-plan avec lot toutes les 2 secondes et callbackType FIRST_MATCH (déclenché uniquement à la première détection). longRangeScan utilise PHY_LE_CODED (BLE Long Range, jusqu'à 1 km). highSpeedScan — PHY_LE_2M (2 Mbit/s, appareils BLE 5.0+ uniquement).
ScanFilter est une classe pour filtrer les résultats de scan BLE. Sans filtre, BluetoothLeScanner retourne tous les appareils BLE à portée — dans un environnement BLE dense, cela peut être des centaines de paquets par minute. ScanFilter réduit les résultats aux appareils souhaités, diminuant la consommation d'énergie et la charge de l'application. Les filtres sont appliqués au niveau de la pile Bluetooth — les paquets non appropriés sont rejetés avant d'atteindre l'application.
Types de filtres : setServiceUuid — UUID du service (format 128 bits complet requis). setDeviceName — sous-chaîne du nom d'appareil (sensible à la casse, correspondance exacte de sous-chaîne). setDeviceAddress — adresse MAC exacte. setManufacturerData — données fabricant (ID entreprise + masque). Plusieurs filtres peuvent être définis pour un seul scan — l'appareil doit correspondre à tous (logique ET). Pour la logique OU, lancez plusieurs scans.
// Création de ScanFilter pour différents scénarios
class ScanFilterFactory {
// 1. Filtrer par UUID de service (Moniteur de fréquence cardiaque)
fun byHeartRateService(): ScanFilter {
return ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
)
.build()
}
// 2. Filtrer par nom d'appareil ( "iBeacon*")
fun byDeviceName(): ScanFilter {
return ScanFilter.Builder()
.setDeviceName("Sensor")
.build()
}
// 3. MAC- ( appareils)
fun byMacAddress(mac: String): ScanFilter {
return ScanFilter.Builder()
.setDeviceAddress(mac)
.build()
}
// 4. Filtre combiné (UUID + )
fun combinedFilter(): List<ScanFilter> {
return listOf(
ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
)
.setDeviceName("MyDevice")
.build()
)
}
// 5. données fabricant
fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
return ScanFilter.Builder()
.setManufacturerData(companyId, data, mask)
.build()
}
}
La classe ScanFilterFactory montre tous les types de filtres. byHeartRateService filtre les appareils avec le service de fréquence cardiaque 0x180D. byDeviceName trouve les appareils contenant « Sensor » dans le nom (Apple recommande des noms uniques pour le filtrage). byMacAddress — recherche exacte d'un appareil spécifique. combinedFilter — filtre ET par UUID et nom. byManufacturer — filtre par données fabricant (par exemple, pour iBeacon, l'ID entreprise Apple 0x004C est utilisé).
ScanCallback est une classe abstraite pour recevoir les résultats de scan BLE. Elle contient trois méthodes : onScanResult — résultat individuel (type de callback, ScanResult), onBatchScanResults — résultats par lot pour reportDelay > 0, onScanFailed — code d'erreur. Toutes les méthodes sont appelées sur le thread principal d'Android. Pour un traitement long dans onScanResult, utilisez des coroutines ou HandlerThread.
ScanResult contient : BluetoothDevice device, int rssi (niveau du signal en dBm), ScanRecord scanRecord (données publicitaires), long timestampNanos (temps de détection depuis le démarrage du système). ScanRecord fournit : getServiceData() — UUID + données personnalisées, getManufacturerSpecificData() — données fabricant, getAdvertiseFlags() — drapeaux BLE. Le type de callback indique : CALLBACK_TYPE_ALL_MATCHES — correspondance avec le filtre, CALLBACK_TYPE_FIRST_MATCH — première détection, CALLBACK_TYPE_MATCH_LOST — appareil perdu.
Codes d'erreur onScanFailed : SCAN_FAILED_ALREADY_STARTED (1) — scan déjà démarré, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) — échec d'enregistrement de l'application dans la pile Bluetooth, SCAN_FAILED_INTERNAL_ERROR (3) — erreur interne de la pile, SCAN_FAILED_FEATURE_UNSUPPORTED (4) — scan BLE non pris en charge sur l'appareil.
// Résultats complets et gestion des erreurs
class ScanResultHandler {
private val results = mutableListOf<ScanResult>()
val scanCallback = object : ScanCallback() {
// 1. Résultat individuel
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
}
// Ajouter à la liste (déduplication par adresse)
val existingIndex = results.indexOfFirst {
it.device.address == result.device.address
}
if (existingIndex >= 0) {
results[existingIndex] = result // mettre à jour RSSI
} else {
results.add(result)
}
// Extraire les données du paquet publicitaire
val record = result.scanRecord
val serviceData = record?.serviceData
val manufacturerData = record?.manufacturerSpecificData
print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
}
// 2. Résultats par lot (reportDelay > 0)
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.let { batch ->
print("Batch: ${batch.size} devices")
}
}
// 3. Erreur de scan
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 gère tous les types de callbacks BluetoothLeScanner. onScanResult met à jour la liste des appareils avec déduplication par adresse MAC — RSSI est mis à jour pour les appareils déjà trouvés. CALLBACK_TYPE_MATCH_LOST signale la perte d'appareil (suppression de la liste). onBatchScanResults traite les résultats par lot pour reportDelay > 0. onScanFailed mappe les codes d'erreur en messages lisibles — essentiel pour le débogage du scan BLE.
Scan avec PendingIntent est un mécanisme de BluetoothLeScanner pour le scan BLE qui fonctionne même lorsque l'application est en arrière-plan (avec les restrictions Android 8+). Au lieu de ScanCallback, un PendingIntent est utilisé, qui envoie un Broadcast au BroadcastReceiver système lors de la détection d'un appareil BLE. Cela permet à l'application de recevoir des notifications d'appareils BLE sans rester en mémoire (le système crée le processus à la réception du broadcast).
Limitations du scan en arrière-plan : Sur Android 8+ (API 26), les services en arrière-plan sont restreints — le scan avec PendingIntent contourne cette limitation via BroadcastReceiver, que le système peut lancer à la réception d'un événement BLE. Sur Android 10+ (API 29), le scan BLE en arrière-plan est en outre restreint par les politiques d'économie d'énergie des fabricants (Xiaomi, Huawei, Samsung bloquent les opérations BLE en arrière-plan). Pour les scénarios BLE critiques, une notification de service au premier plan est requise.
// Scan BLE en arrière-plan via PendingIntent
class BackgroundBLEScanner(private val context: Context) {
private val scanner: BluetoothLeScanner? by lazy {
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner
}
fun startBackgroundScan() {
// Créer PendingIntent pour BroadcastReceiver
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Paramètres de scan en arrière-plan
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.build()
// Lancer le scan en arrière-plan
scanner?.startScan(
null, // filtres
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) {
// Obtenir les résultats du scan
val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
results?.let { scanResults ->
for (result in scanResults) {
// Envoyer une notification à l'utilisateur
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 lance le scan BLE en arrière-plan via PendingIntent. startBackgroundScan crée un PendingIntent qui, lors de la détection d'un appareil BLE, envoie un Broadcast à BLEBroadcastReceiver. Le BroadcastReceiver extrait ScanResult via getPendingIntentScanResults() et peut afficher une notification ou envoyer des données au serveur. Cette approche fonctionne même si l'application a été terminée par le système — Android redémarre le BroadcastReceiver à la réception du broadcast.
Exemple complet d'un scanner BLE en Kotlin utilisant BluetoothLeScanner avec ScanSettings, ScanFilter et ScanCallback pour trouver des appareils Heart Rate Monitor. Le scanner affiche une liste des appareils trouvés avec RSSI et UUIDs de service, avec la possibilité de se connecter via BluetoothGatt.
// Scanner BLE complet avec coroutines en 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 {
// Vérifier l'état Bluetooth
if (adapter?.isEnabled != true) {
close(IllegalStateException("Bluetooth disabled"))
return@callbackFlow
}
// Configuration du scan
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"))
}
}
// Lancer le scan
scanner?.startScan(filters, settings, callback)
// Arrêt automatique après durée
delay(duration)
scanner?.stopScan(callback)
close()
}.flowOn(Dispatchers.IO)
fun stop() {
scanner?.stopScan(object : ScanCallback() {})
}
}
class BLEException(message: String) : Exception(message)
La classe DeviceScanner utilise Kotlin Flow (callbackFlow) pour le scan BLE réactif. Le scan est lancé avec les paramètres LOW_LATENCY et un filtre UUID du Heart Rate Service. Les résultats sont émis via onScanResult dans un Flow. Arrêt automatique après une durée définie (10 secondes par défaut). FlowOn(Dispatchers.IO) décharge les opérations BLE sur un thread d'arrière-plan. Cette approche permet le scan BLE dans l'architecture MVVM via viewModelScope.launch et collect.
Questions Fréquentes
BluetoothLeScanner est une classe Android (API 21+) pour le scan BLE. Elle est obtenue via BluetoothAdapter.getBluetoothLeScanner(). Prend en charge trois modes de scan (LOW_POWER, BALANCED, LOW_LATENCY), le filtrage par UUID, nom et adresse MAC, les résultats par lot et PendingIntent pour le scan en arrière-plan. Remplace la méthode obsolète BluetoothAdapter.startLeScan().
SCAN_MODE_LOW_POWER — mode arrière-plan avec délai de détection de 5 à 10 secondes, consommation minimale. SCAN_MODE_LOW_LATENCY — mode actif avec délai d'environ 100 ms, consommation maximale. SCAN_MODE_BALANCED — compromis (~2 secondes de délai). Utilisez LOW_LATENCY pour le scan d'interface utilisateur, LOW_POWER avec PendingIntent pour la surveillance en arrière-plan.
Causes : Bluetooth désactivé (vérifiez adapter.isEnabled), permissions manquantes (BLUETOOTH_SCAN sur API 31+, ACCESS_FINE_LOCATION sur API 23–30), scanner = null (adaptateur indisponible), appareil hors de portée, ou filtre incorrect. Vérifiez également onScanFailed — le code d'erreur indique la cause : SCAN_FAILED_ALREADY_STARTED (1) ou SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2).
Utilisez la version PendingIntent de startScan() — passez un PendingIntent au lieu de ScanCallback. Lorsqu'un appareil BLE est détecté, Android envoie un Broadcast au BroadcastReceiver, qui peut être lancé par le système même si l'application est en arrière-plan. Pour Android 8+, ajoutez le BroadcastReceiver au manifeste. Sur Android 10+, tenez compte des restrictions d'économie d'énergie des fabricants.
BluetoothLeScanner n'a pas de limite sur le nombre d'appareils détectés — la limitation dépend de la saturation BLE de l'environnement. Un bureau peut avoir 20 à 50 appareils BLE actifs, un centre commercial des centaines. Utilisez ScanFilter (par UUID, nom) pour le filtrage. Sans filtrage, traitez les résultats de manière asynchrone — onScanResult peut être appelé des dizaines de fois par seconde.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi