BluetoothLeScanner — klasa Androida do skanowania urządzeń Bluetooth Low Energy, dostępna od API 21 (Android 5.0). BluetoothLeScanner zastąpił przestarzałą metodę startLeScan na BluetoothAdapter, zapewniając elastyczne API z konfiguracją skanowania (ScanSettings), filtrowaniem (ScanFilter) i obsługą trybu tła (PendingIntent). Instancję uzyskuje się przez BluetoothAdapter.getBluetoothLeScanner(). Według Android Developers, 2026, BluetoothLeScanner obsługuje trzy tryby zużycia energii i pozwala skanować pakiety reklamowe BLE z filtrowaniem po UUID usługi, nazwie urządzenia lub adresie MAC.
Najważniejsze
BluetoothLeScanner — klasa systemowa do zarządzania skanowaniem BLE na Androidzie. W przeciwieństwie do BluetoothAdapter.startLeScan(), który przyjmuje prosty callback LeScanCallback, BluetoothLeScanner zapewnia obiektowe API z ustawieniami, filtrami i rozszerzoną obsługą błędów. Klasa pojawiła się w API 21 (Android 5.0) wraz z obsługą BLE 4.2 i pozostaje głównym sposobem skanowania BLE na wszystkich nowoczesnych wersjach Androida.
Uzyskanie instancji BluetoothLeScanner wykonuje się przez BluetoothAdapter.getBluetoothLeScanner(). Metoda zwraca null, jeśli adapter Bluetooth jest niedostępny (Bluetooth wyłączone lub urządzenie nie obsługuje BLE). Przed uzyskaniem sprawdź BluetoothAdapter.isEnabled() i obecność FEATURE_BLUETOOTH_LE przez PackageManager. Po uzyskaniu skanera można uruchomić skanowanie w dowolnym wątku — Android sam planuje operacje BLE na wewnętrznym wątku stosu Bluetooth.
// Uzyskiwanie 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 {
// Sprawdź dostępność BLE
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
return false
}
// Sprawdź włączony Bluetooth
if (adapter?.isEnabled != true) {
return false
}
// Pobierz skaner
scanner = adapter?.bluetoothLeScanner
return scanner != null
}
// Sprawdź dostępność skanera
val isAvailable: Boolean
get() = scanner != null
// Uruchom podstawowe skanowanie bez filtrów
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}")
}
}
Klasa BLEScannerManager demonstruje bezpieczne uzyskanie i inicjalizację BluetoothLeScanner. initScanner sprawdza obecność BLE przez hasSystemFeature, włączony Bluetooth i pomyślne uzyskanie skanera. startBasicScan uruchamia skanowanie bez ustawień i filtrów — wykrywa wszystkie urządzenia BLE w zasięgu. handleResult analizuje ScanResult: BluetoothDevice (nazwa, adres), RSSI (poziom sygnału), scanRecord (dane reklamowe).
ScanSettings — klasa do konfiguracji skanowania BLE. Główny parametr — tryb skanowania (scanMode), określający kompromis między zużyciem energii a opóźnieniem wykrywania. ScanSettings.Builder pozwala skonfigurować: scanMode, callbackType (CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH, CALLBACK_TYPE_MATCH_LOST), matchMode (MATCH_MODE_AGGRESSIVE, MATCH_MODE_STICKY), reportDelay (opóźnienie wysyłki wsadowej) i phy (PHY_LE_1M, PHY_LE_2M, PHY_LE_CODED).
Trzy tryby skanowania: SCAN_MODE_LOW_POWER (0) — skanowanie w tle z niskim zużyciem energii, opóźnienie wykrywania kilka sekund. SCAN_MODE_BALANCED (1) — tryb zrównoważony dla większości scenariuszy. SCAN_MODE_LOW_LATENCY (2) — minimalne opóźnienie wykrywania (około 100 ms), maksymalne zużycie energii. Do aktywnego wyszukiwania urządzeń używaj LOW_LATENCY, do monitorowania w tle — LOW_POWER.
reportDelay — opóźnienie w milisekundach przed grupowym wysłaniem wyników. Jeśli reportDelay = 0, wyniki są wysyłane natychmiast po wykryciu. Jeśli > 0, Android gromadzi wyniki i wysyła partię przez onBatchScanResults. Wysyłka wsadowa zmniejsza liczbę wywołań callbacków i obniża zużycie energii, nadaje się do skanowania w tle z niskim priorytetem.
// Konfiguracja ScanSettings dla różnych scenariuszy
class ScanSettingsProvider {
// 1. Szybkie skanowanie (aktywne wyszukiwanie)
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. Energooszczędne skanowanie (monitorowanie w tle)
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) // partia co 2 sekundy
.build()
}
// 3. Skanowanie 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. Skanowanie tylko na 2M PHY (BLE 5.0+)
fun highSpeedScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setPhy(ScanSettings.PHY_LE_2M)
.build()
}
}
Klasa ScanSettingsProvider zawiera typowe konfiguracje. lowLatencyScan — do skanowania UI (wyszukiwanie „tu i teraz”). lowPowerScan — do monitorowania w tle z wsadem co 2 sekundy i callbackType FIRST_MATCH (uruchamia się tylko przy pierwszym wykryciu). longRangeScan używa PHY_LE_CODED (BLE Long Range, do 1 km). highSpeedScan — PHY_LE_2M (2 Mbit/s, tylko urządzenia BLE 5.0+).
ScanFilter — klasa do filtrowania wyników skanowania BLE. Bez filtra BluetoothLeScanner zwraca wszystkie urządzenia BLE w zasięgu — w gęstym środowisku BLE to setki pakietów na minutę. ScanFilter zawęża wyniki do potrzebnych urządzeń, zmniejszając zużycie energii i obciążenie aplikacji. Filtry są stosowane na poziomie stosu Bluetooth — nieodpowiednie pakiety są odrzucane przed dostarczeniem do aplikacji.
Typy filtrów: setServiceUuid — UUID usługi (obowiązkowo pełny format 128-bit). setDeviceName — podciąg nazwy urządzenia (zależne od wielkości liter, dokładne dopasowanie podciągu). setDeviceAddress — dokładny adres MAC. setManufacturerData — dane producenta (ID firmy + maska). Dla jednego skanowania można ustawić kilka filtrów — urządzenie musi odpowiadać wszystkim (logika AND). Dla logiki OR uruchom kilka skanowań.
// Tworzenie ScanFilter dla różnych scenariuszy
class ScanFilterFactory {
// 1. Filtrowanie po UUID usługi (Heart Rate Monitor)
fun byHeartRateService(): ScanFilter {
return ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
)
.build()
}
// 2. Filtrowanie po nazwie urządzenia („iBeacon*")
fun byDeviceName(): ScanFilter {
return ScanFilter.Builder()
.setDeviceName("Sensor")
.build()
}
// 3. MAC- (urządzenia)
fun byMacAddress(mac: String): ScanFilter {
return ScanFilter.Builder()
.setDeviceAddress(mac)
.build()
}
// 4. Filtr kombinowany (UUID + )
fun combinedFilter(): List<ScanFilter> {
return listOf(
ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
)
.setDeviceName("MyDevice")
.build()
)
}
// 5. dane producenta
fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
return ScanFilter.Builder()
.setManufacturerData(companyId, data, mask)
.build()
}
}
Klasa ScanFilterFactory pokazuje wszystkie typy filtrów. byHeartRateService filtruje urządzenia z usługą pulsu 0x180D. byDeviceName znajduje urządzenia zawierające „Sensor” w nazwie (Apple zaleca unikalne nazwy do filtrowania). byMacAddress — dokładne wyszukiwanie konkretnego urządzenia. combinedFilter — filtr AND po UUID i nazwie. byManufacturer — filtr po danych producenta (np. dla iBeacon używane jest company ID Apple 0x004C).
ScanCallback — abstrakcyjna klasa do otrzymywania wyników skanowania BLE. Zawiera trzy metody: onScanResult — pojedynczy wynik (typ callbacku, ScanResult), onBatchScanResults — partia wyników dla reportDelay > 0, onScanFailed — kod błędu. Wszystkie metody są wywoływane na głównym wątku Androida (main thread). Do długotrwałego przetwarzania w onScanResult używaj korutyn lub HandlerThread.
ScanResult zawiera: BluetoothDevice device (urządzenie), int rssi (poziom sygnału w dBm), ScanRecord scanRecord (dane reklamowe), long timestampNanos (czas wykrycia od momentu uruchomienia systemu). ScanRecord udostępnia: getServiceData() — UUID + niestandardowe dane, getManufacturerSpecificData() — dane producenta, getAdvertiseFlags() — flagi BLE. Typ callbacku (callbackType) wskazuje: CALLBACK_TYPE_ALL_MATCHES — dopasowanie z filtrem, CALLBACK_TYPE_FIRST_MATCH — pierwsze wykrycie, CALLBACK_TYPE_MATCH_LOST — utrata urządzenia.
Kody błędów onScanFailed: SCAN_FAILED_ALREADY_STARTED (1) — skanowanie już uruchomione, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) — rejestracja aplikacji w stosie Bluetooth nie powiodła się, SCAN_FAILED_INTERNAL_ERROR (3) — wewnętrzny błąd stosu, SCAN_FAILED_FEATURE_UNSUPPORTED (4) — skanowanie BLE nie jest obsługiwane na urządzeniu.
// Pełne wyniki skanowania i obsługa błędów
class ScanResultHandler {
private val results = mutableListOf<ScanResult>()
val scanCallback = object : ScanCallback() {
// 1. Pojedynczy wynik
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
}
// Dodaj do listy (deduplikacja po adresie)
val existingIndex = results.indexOfFirst {
it.device.address == result.device.address
}
if (existingIndex >= 0) {
results[existingIndex] = result // aktualizuj RSSI
} else {
results.add(result)
}
// Wyodrębnij dane z pakietu reklamowego
val record = result.scanRecord
val serviceData = record?.serviceData
val manufacturerData = record?.manufacturerSpecificData
print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
}
// 2. Wyniki wsadowe (reportDelay > 0)
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.let { batch ->
print("Batch: ${batch.size} devices")
}
}
// 3. Błąd skanowania
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}")
}
}
Klasa ScanResultHandler przetwarza wszystkie typy callbacków BluetoothLeScanner. onScanResult aktualizuje listę urządzeń z deduplikacją po adresie MAC — RSSI jest aktualizowane dla już znalezionych urządzeń. CALLBACK_TYPE_MATCH_LOST sygnalizuje utratę urządzenia (usunięcie z listy). onBatchScanResults przetwarza wyniki wsadowe dla reportDelay > 0. onScanFailed mapuje kody błędów na czytelne komunikaty — kluczowe do debugowania skanowania BLE.
Skanowanie przez PendingIntent — mechanizm BluetoothLeScanner do skanowania BLE działającego nawet gdy aplikacja jest w tle (z ograniczeniami Android 8+). Zamiast ScanCallback używany jest PendingIntent, który wysyła Broadcast do systemowego BroadcastReceiver przy wykryciu urządzenia BLE. Pozwala to aplikacji otrzymywać powiadomienia o urządzeniach BLE, nie będąc w pamięci (system tworzy proces przy otrzymaniu broadcast).
Ograniczenia skanowania w tle: Na Android 8+ (API 26) usługi w tle są ograniczone — skanowanie przez PendingIntent omija to ograniczenie przez BroadcastReceiver, który system może uruchomić przy otrzymaniu zdarzenia BLE. Na Android 10+ (API 29) skanowanie BLE w tle jest dodatkowo ograniczone politykami oszczędzania energii producentów (Xiaomi, Huawei, Samsung blokują operacje BLE w tle). Do krytycznych scenariuszy BLE wymagane jest powiadomienie z foreground service.
// Skanowanie BLE w tle przez PendingIntent
class BackgroundBLEScanner(private val context: Context) {
private val scanner: BluetoothLeScanner? by lazy {
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner
}
fun startBackgroundScan() {
// Utwórz PendingIntent dla BroadcastReceiver
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Ustawienia skanowania w tle
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.build()
// Uruchom skanowanie w tle
scanner?.startScan(
null, // filtry
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) {
// Pobierz wyniki skanowania
val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
results?.let { scanResults ->
for (result in scanResults) {
// Wyślij powiadomienie do użytkownika
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)
}
}
Klasa BackgroundBLEScanner uruchamia skanowanie BLE w tle przez PendingIntent. startBackgroundScan tworzy PendingIntent, który przy wykryciu urządzenia BLE wysyła Broadcast do BLEBroadcastReceiver. BroadcastReceiver wyodrębnia ScanResult przez getPendingIntentScanResults() i może wyświetlić powiadomienie lub wysłać dane na serwer. Takie podejście działa nawet jeśli aplikacja została zakończona przez system — Android uruchamia BroadcastReceiver przy otrzymaniu broadcast.
Pełny przykład skanera BLE w Kotlin, używającego BluetoothLeScanner z ScanSettings, ScanFilter i ScanCallback do wyszukiwania urządzeń Heart Rate Monitor. Skaner pokazuje listę znalezionych urządzeń z RSSI i UUID usług, z możliwością połączenia przez BluetoothGatt.
// Pełny skaner BLE z korutynami w 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 {
// Sprawdź stan Bluetooth
if (adapter?.isEnabled != true) {
close(IllegalStateException("Bluetooth disabled"))
return@callbackFlow
}
// Konfiguracja skanowania
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"))
}
}
// Rozpocznij skanowanie
scanner?.startScan(filters, settings, callback)
// Automatyczne zatrzymanie po czasie
delay(duration)
scanner?.stopScan(callback)
close()
}.flowOn(Dispatchers.IO)
fun stop() {
scanner?.stopScan(object : ScanCallback() {})
}
}
class BLEException(message: String) : Exception(message)
Klasa DeviceScanner używa Kotlin Flow (callbackFlow) do reaktywnego skanowania BLE. Skanowanie uruchamiane jest z ustawieniami LOW_LATENCY i filtrem po UUID Heart Rate Service. Wyniki są emitowane przez onScanResult do Flow. Automatyczne zatrzymanie po zadanym czasie trwania (10 sekund domyślnie). FlowOn(Dispatchers.IO) przenosi operacje BLE do wątku tła. Takie podejście pozwala używać skanowania BLE w architekturze MVVM przez viewModelScope.launch i collect.
Często zadawane pytania
BluetoothLeScanner — klasa Androida (API 21+) do skanowania BLE. Uzyskuje się przez BluetoothAdapter.getBluetoothLeScanner(). Obsługuje trzy tryby skanowania (LOW_POWER, BALANCED, LOW_LATENCY), filtrowanie po UUID, nazwie i adresie MAC, wyniki wsadowe i PendingIntent do skanowania w tle. Zastępuje przestarzałą metodę BluetoothAdapter.startLeScan().
SCAN_MODE_LOW_POWER — tryb tła z opóźnieniem wykrywania 5–10 sekund, minimalne zużycie energii. SCAN_MODE_LOW_LATENCY — tryb aktywny z opóźnieniem około 100 ms, maksymalne zużycie energii. SCAN_MODE_BALANCED — kompromis (~2 sekundy opóźnienia). Do skanowania UI używaj LOW_LATENCY, do monitorowania w tle — LOW_POWER z PendingIntent.
Przyczyny: Bluetooth wyłączony (sprawdź adapter.isEnabled), brak uprawnień (BLUETOOTH_SCAN na API 31+, ACCESS_FINE_LOCATION na API 23–30), scanner = null (adapter niedostępny), urządzenie poza zasięgiem lub użyty nieprawidłowy filtr. Sprawdź też onScanFailed — kod błędu wskaże przyczynę: SCAN_FAILED_ALREADY_STARTED (1) lub SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2).
Użyj wersji PendingIntent startScan() — przekaż PendingIntent zamiast ScanCallback. Przy wykryciu urządzenia BLE Android wysyła Broadcast do BroadcastReceiver, który może być uruchomiony przez system nawet jeśli aplikacja jest w tle. Dla Android 8+ dodaj BroadcastReceiver w manifeście. Na Android 10+ uwzględnij ograniczenia oszczędzania energii producentów.
BluetoothLeScanner nie ma limitu liczby wykrywanych urządzeń — ograniczenie zależy od nasycenia BLE w środowisku. W biurze może być 20–50 aktywnych urządzeń BLE, w centrum handlowym — setki. Do filtrowania używaj ScanFilter (po UUID, nazwie). Bez filtrowania przetwarzaj wyniki asynchronicznie — onScanResult może być wywoływany dziesiątki razy na sekundę.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również