BluetoothLeScanner — клас Android для сканування Bluetooth Low Energy пристроїв, доступний з API 21 (Android 5.0). BluetoothLeScanner замінив застарілий метод startLeScan на BluetoothAdapter, надаючи гнучке API з налаштуванням сканування (ScanSettings), фільтрацією (ScanFilter) та підтримкою фонового режиму (PendingIntent). Екземпляр отримується через BluetoothAdapter.getBluetoothLeScanner(). За даними Android Developers, 2026, BluetoothLeScanner підтримує три режими енергоспоживання та дозволяє сканувати рекламні пакети BLE з фільтрацією за UUID сервісу, імені пристрою або MAC-адресою.
Головне
BluetoothLeScanner — системний клас для керування BLE-скануванням на Android. На відміну від BluetoothAdapter.startLeScan(), який приймає простий LeScanCallback, BluetoothLeScanner надає об'єктне API з налаштуваннями, фільтрами та розширеною обробкою помилок. Клас з'явився в API 21 (Android 5.0) разом з підтримкою BLE 4.2 і залишається основним способом BLE-сканування на всіх сучасних версіях Android.
Отримання екземпляра BluetoothLeScanner виконується через BluetoothAdapter.getBluetoothLeScanner(). Метод повертає null, якщо Bluetooth-адаптер недоступний (Bluetooth вимкнено або пристрій не підтримує BLE). Перед отриманням перевірте BluetoothAdapter.isEnabled() та наявність FEATURE_BLUETOOTH_LE через PackageManager. Після отримання сканера можна запускати сканування в будь-якому потоці — Android сам планує BLE-операції на внутрішньому потоці Bluetooth-стека.
// Отримання 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 {
// Перевірка доступності BLE
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
return false
}
// Перевірка увімкнення Bluetooth
if (adapter?.isEnabled != true) {
return false
}
// Отримати сканер
scanner = adapter?.bluetoothLeScanner
return scanner != null
}
// Перевірка доступності сканера
val isAvailable: Boolean
get() = scanner != null
// Запустити базове сканування без фільтрів
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}")
}
}
Клас BLEScannerManager демонструє безпечне отримання та ініціалізацію BluetoothLeScanner. initScanner перевіряє наявність BLE через hasSystemFeature, увімкнений Bluetooth та успішне отримання сканера. startBasicScan запускає сканування без налаштувань та фільтрів — виявляє всі BLE-пристрої в зоні дії. handleResult розбирає ScanResult: BluetoothDevice (ім'я, адреса), RSSI (рівень сигналу), scanRecord (рекламні дані).
ScanSettings — клас для конфігурації BLE-сканування. Головний параметр — режим сканування (scanMode), що визначає компроміс між енергоспоживанням та затримкою виявлення. ScanSettings.Builder дозволяє налаштувати: scanMode, callbackType (CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH, CALLBACK_TYPE_MATCH_LOST), matchMode (MATCH_MODE_AGGRESSIVE, MATCH_MODE_STICKY), reportDelay (затримка пакетної відправки) та phy (PHY_LE_1M, PHY_LE_2M, PHY_LE_CODED).
Три режими сканування: SCAN_MODE_LOW_POWER (0) — фонове сканування з низьким енергоспоживанням, затримка виявлення кілька секунд. SCAN_MODE_BALANCED (1) — збалансований режим для більшості сценаріїв. SCAN_MODE_LOW_LATENCY (2) — мінімальна затримка виявлення (близько 100 мс), максимальне енергоспоживання. Для активного пошуку пристроїв використовуйте LOW_LATENCY, для фонового моніторингу — LOW_POWER.
reportDelay — затримка в мілісекундах перед груповою відправкою результатів. Якщо reportDelay = 0, результати надсилаються негайно при виявленні. Якщо > 0, Android накопичує результати та надсилає батч через onBatchScanResults. Пакетна відправка зменшує кількість викликів callbackів та знижує енергоспоживання, підходить для сканування у фоні з низьким пріоритетом.
// Конфігурація ScanSettings для різних сценаріїв
class ScanSettingsProvider {
// 1. Швидке сканування (активний пошук)
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. Енергоефективне сканування (фоновий моніторинг)
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) // батч кожні 2 секунди
.build()
}
// 3. 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. Сканування лише на 2M PHY (BLE 5.0+)
fun highSpeedScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setPhy(ScanSettings.PHY_LE_2M)
.build()
}
}
Клас ScanSettingsProvider містить типові конфігурації. lowLatencyScan — для UI-сканування (пошук «тут і зараз»). lowPowerScan — для фонового моніторингу з батчем кожні 2 секунди та callbackType FIRST_MATCH (спрацьовує лише при першому виявленні). longRangeScan використовує PHY_LE_CODED (BLE Long Range, до 1 км). highSpeedScan — PHY_LE_2M (2 Мбіт/с, лише пристрої BLE 5.0+).
ScanFilter — клас для фільтрації результатів BLE-сканування. Без фільтра BluetoothLeScanner повертає всі BLE-пристрої в зоні дії — для щільного BLE-середовища це сотні пакетів на хвилину. ScanFilter звужує результати до потрібних пристроїв, зменшуючи енергоспоживання та навантаження на додаток. Фільтри застосовуються на рівні Bluetooth-стека — непідходящі пакети відкидаються до доставки в додаток.
Типи фільтрів: setServiceUuid — UUID сервісу (обов'язково повний 128-бітний формат). setDeviceName — підрядок імені пристрою (реєстрозалежно, точний збіг підрядка). setDeviceAddress — точна MAC-адреса. setManufacturerData — дані виробника (ID компанії + маска). Для одного сканування можна встановити кілька фільтрів — пристрій повинен відповідати всім (AND-логіка). Для OR-логіки запустіть кілька сканувань.
// Створення ScanFilter для різних сценаріїв
class ScanFilterFactory {
// 1. Фільтр за UUID сервісу (Heart Rate Monitor)
fun byHeartRateService(): ScanFilter {
return ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
)
.build()
}
// 2. Фільтр за іменем пристрою ( "iBeacon*")
fun byDeviceName(): ScanFilter {
return ScanFilter.Builder()
.setDeviceName("Sensor")
.build()
}
// 3. MAC- ( пристроїв)
fun byMacAddress(mac: String): ScanFilter {
return ScanFilter.Builder()
.setDeviceAddress(mac)
.build()
}
// 4. Комбінований фільтр (UUID + )
fun combinedFilter(): List<ScanFilter> {
return listOf(
ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
)
.setDeviceName("MyDevice")
.build()
)
}
// 5. дані виробника
fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
return ScanFilter.Builder()
.setManufacturerData(companyId, data, mask)
.build()
}
}
Клас ScanFilterFactory показує всі типи фільтрів. byHeartRateService відфільтровує пристрої з сервісом пульсу 0x180D. byDeviceName знаходить пристрої, що містять «Sensor» в імені (Apple рекомендує унікальні імена для фільтрації). byMacAddress — точний пошук конкретного девайса. combinedFilter — AND-фільтр за UUID та іменем. byManufacturer — фільтр за даними виробника (наприклад, для iBeacon використовується company ID Apple 0x004C).
ScanCallback — абстрактний клас для отримання результатів BLE-сканування. Містить три методи: onScanResult — одиничний результат (тип callbackа, ScanResult), onBatchScanResults — пакет результатів для reportDelay > 0, onScanFailed — код помилки. Всі методи викликаються на головному потоці Android (main thread). Для тривалої обробки в onScanResult використовуйте корутини або HandlerThread.
ScanResult містить: BluetoothDevice device (пристрій), int rssi (рівень сигналу в dBm), ScanRecord scanRecord (рекламні дані), long timestampNanos (час виявлення з моменту завантаження системи). ScanRecord надає: getServiceData() — UUID + кастомні дані, getManufacturerSpecificData() — дані виробника, getAdvertiseFlags() — BLE-прапорці. Тип callbackа (callbackType) вказує: CALLBACK_TYPE_ALL_MATCHES — збіг з фільтром, CALLBACK_TYPE_FIRST_MATCH — перше виявлення, CALLBACK_TYPE_MATCH_LOST — втрата пристрою.
Коди помилок onScanFailed: SCAN_FAILED_ALREADY_STARTED (1) — сканування вже запущено, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) — реєстрація додатку в Bluetooth-стеку не вдалася, SCAN_FAILED_INTERNAL_ERROR (3) — внутрішня помилка стека, SCAN_FAILED_FEATURE_UNSUPPORTED (4) — BLE-сканування не підтримується на пристрої.
// Повні результати сканування та обробка помилок
class ScanResultHandler {
private val results = mutableListOf<ScanResult>()
val scanCallback = object : ScanCallback() {
// 1. Одиничний результат
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
}
// Додати до списку (дедуплікація за адресою)
val existingIndex = results.indexOfFirst {
it.device.address == result.device.address
}
if (existingIndex >= 0) {
results[existingIndex] = result // оновити RSSI
} else {
results.add(result)
}
// Витягти дані з рекламного пакета
val record = result.scanRecord
val serviceData = record?.serviceData
val manufacturerData = record?.manufacturerSpecificData
print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
}
// 2. Пакетні результати (reportDelay > 0)
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.let { batch ->
print("Batch: ${batch.size} devices")
}
}
// 3. Помилка сканування
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}")
}
}
Клас ScanResultHandler обробляє всі типи callbackів BluetoothLeScanner. onScanResult оновлює список пристроїв з дедуплікацією за MAC-адресою — RSSI оновлюється для вже знайдених пристроїв. CALLBACK_TYPE_MATCH_LOST сигналізує про втрату пристрою (видалення зі списку). onBatchScanResults обробляє пакетні результати для reportDelay > 0. onScanFailed маппить коди помилок у людиночитані повідомлення — критично для налагодження BLE-сканування.
PendingIntent-сканування — механізм BluetoothLeScanner для BLE-сканування, що працює навіть коли додаток у фоні (з обмеженнями Android 8+). Замість ScanCallback використовується PendingIntent, який надсилає Broadcast у системний BroadcastReceiver при виявленні BLE-пристрою. Це дозволяє додатку отримувати сповіщення про BLE-пристрої, не перебуваючи в пам'яті (система створює процес при отриманні broadcast).
Обмеження фонового сканування: На Android 8+ (API 26) фонові сервіси обмежені — PendingIntent-сканування оминає це обмеження через BroadcastReceiver, який система може запустити при отриманні BLE-події. На Android 10+ (API 29) фонове BLE-сканування додатково обмежене політиками енергозбереження виробників (Xiaomi, Huawei, Samsung блокують фонові BLE-операції). Для критичних BLE-сценаріїв потрібне сповіщення з foreground service.
// Фонове BLE-сканування через PendingIntent
class BackgroundBLEScanner(private val context: Context) {
private val scanner: BluetoothLeScanner? by lazy {
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner
}
fun startBackgroundScan() {
// Створити PendingIntent для BroadcastReceiver
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Налаштування фонового сканування
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.build()
// Запустити фонове сканування
scanner?.startScan(
null, // фільтри
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) {
// Отримати результати сканування
val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
results?.let { scanResults ->
for (result in scanResults) {
// Відправити сповіщення користувачеві
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)
}
}
Клас BackgroundBLEScanner запускає фонове BLE-сканування через PendingIntent. startBackgroundScan створює PendingIntent, який при виявленні BLE-пристрою надсилає Broadcast в BLEBroadcastReceiver. BroadcastReceiver витягує ScanResult через getPendingIntentScanResults() та може показувати сповіщення або надсилати дані на сервер. Такий підхід працює навіть якщо додаток було завершено системою — Android перезапускає BroadcastReceiver при отриманні broadcast.
Повний приклад BLE-сканера на Kotlin, що використовує BluetoothLeScanner з ScanSettings, ScanFilter та ScanCallback для пошуку пристроїв Heart Rate Monitor. Сканер показує список знайдених пристроїв з RSSI та UUID сервісів, з можливістю підключення через BluetoothGatt.
// Повний BLE-сканер з корутинами в 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 {
// Перевірити стан Bluetooth
if (adapter?.isEnabled != true) {
close(IllegalStateException("Bluetooth disabled"))
return@callbackFlow
}
// Конфігурація сканування
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"))
}
}
// Запустити сканування
scanner?.startScan(filters, settings, callback)
// Автоостановка після тривалості
delay(duration)
scanner?.stopScan(callback)
close()
}.flowOn(Dispatchers.IO)
fun stop() {
scanner?.stopScan(object : ScanCallback() {})
}
}
class BLEException(message: String) : Exception(message)
Клас DeviceScanner використовує Kotlin Flow (callbackFlow) для реактивного BLE-сканування. Сканування запускається з налаштуваннями LOW_LATENCY та фільтром за UUID Heart Rate Service. Результати емітяться через onScanResult в Flow. Автоостановка через заданий duration (10 секунд за замовчуванням). FlowOn(Dispatchers.IO) виносить BLE-операції у фоновий потік. Такий підхід дозволяє використовувати BLE-сканування в MVVM-архітектурі через viewModelScope.launch та collect.
Часто задавані питання
BluetoothLeScanner — клас Android (API 21+) для BLE-сканування. Отримується через BluetoothAdapter.getBluetoothLeScanner(). Підтримує три режими сканування (LOW_POWER, BALANCED, LOW_LATENCY), фільтрацію за UUID, іменем та MAC-адресою, пакетні результати та PendingIntent для фонового сканування. Замінює застарілий метод BluetoothAdapter.startLeScan().
SCAN_MODE_LOW_POWER — фоновий режим із затримкою виявлення 5–10 секунд, мінімальне енергоспоживання. SCAN_MODE_LOW_LATENCY — активний режим із затримкою близько 100 мс, максимальне енергоспоживання. SCAN_MODE_BALANCED — компроміс (~2 секунди затримки). Для UI-сканування використовуйте LOW_LATENCY, для фонового моніторингу — LOW_POWER з PendingIntent.
Причини: Bluetooth вимкнено (перевірте adapter.isEnabled), не отримано дозволи (BLUETOOTH_SCAN на API 31+, ACCESS_FINE_LOCATION на API 23–30), scanner = null (адаптер недоступний), пристрій поза зоною дії, або використовується неправильний фільтр. Також перевірте onScanFailed — код помилки вкаже на причину: SCAN_FAILED_ALREADY_STARTED (1) або SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2).
Використовуйте PendingIntent версію startScan() — передайте PendingIntent замість ScanCallback. При виявленні BLE-пристрою Android надсилає Broadcast в BroadcastReceiver, який може бути запущений системою навіть якщо додаток у фоні. Для Android 8+ додайте BroadcastReceiver в маніфест. На Android 10+ враховуйте обмеження енергозбереження виробників.
BluetoothLeScanner не має ліміту на кількість виявлених пристроїв — обмеження залежить від BLE-насиченості середовища. В офісі може бути 20–50 активних BLE-пристроїв, у торговому центрі — сотні. Для фільтрації використовуйте ScanFilter (за UUID, іменем). Без фільтрації обробляйте результати асинхронно — onScanResult може викликатися десятки разів на секунду.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.