BluetoothLeScanner — класс Android для сканирования Bluetooth Low Energy devices, доступный с API 21 (Android 5.0). BluetoothLeScanner заменил устаревший метод startLeScan на BluetoothAdapter, предоставляя гибкое API с настройкой сканирования (ScanSettings), фильтрацией (ScanFilter) и поддержкой фонового режима (PendingIntent). Экземпляр получается через BluetoothAdapter.getBluetoothLeScanner(). По данным Android Developers, 2026, BluetoothLeScanner поддерживает три режима энергопотребления и позволяет сканировать рекламные пакеты BLE с фильтрацией по UUID сервиса, имени devicesа или MAC-адресу.
Главное
BluetoothLeScanner — системный класс для управления BLE-сканированием на Android. В отличие от BluetoothAdapter.startLeScan(), который принимает простой callback LeScanCallback, BluetoothLeScanner предоставляет объектный API с настройками, фильтрами и расширенной обработкой ошибок. Класс появился в API 21 (Android 5.0) вместе с поддержкой BLE 4.2 и остаётся основным способом BLE-сканирования на всех современных версиях Android.
Получение экземпляра BluetoothLeScanner выполняется через BluetoothAdapter.getBluetoothLeScanner(). Метод возвращает null, если Bluetooth-адаптер недоступен (Bluetooth disabled или devicesо не поддерживает BLE). Перед получением проверьте BluetoothAdapter.isEnabled() и наличие FEATURE_BLUETOOTH_LE через PackageManager. После получения сканера можно запускать сканирование в любом потоке — Android сам планирует BLE-операции на внутреннем потоке Bluetooth-стека.
// Getting 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 {
// Check BLE availability
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
return false
}
// Check Bluetooth enabled
if (adapter?.isEnabled != true) {
return false
}
// Get scanner
scanner = adapter?.bluetoothLeScanner
return scanner != null
}
// Check scanner availability
val isAvailable: Boolean
get() = scanner != null
// Start basic scan without filters
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-devicesа в зоне действия. 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) — balanced режим для большинства сценариев. SCAN_MODE_LOW_LATENCY (2) — минимальная задержка обнаружения (около 100 мс), максимальное энергопотребление. Для активного поиска devices используйте LOW_LATENCY, для фонового мониторинга — LOW_POWER.
reportDelay — задержка в миллисекундах перед групповой отправкой результатов. Если reportDelay = 0, результаты отправляются немедленно при обнаружении. Если > 0, Android накапливает результаты и отправляет батч через onBatchScanResults. Пакетная отправка уменьшает количество вызовов callbackов и снижает энергопотребление, подходит для сканирования в фоне с низким приоритетом.
// ScanSettings configuration for different scenarios
class ScanSettingsProvider {
// 1. Fast scan (active search)
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. Power efficient scan (background monitoring)
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 every 2 seconds
.build()
}
// 3. BLE Long Range scan (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. Scan only on 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+ devicesа).
ScanFilter — класс для фильтрации результатов BLE-сканирования. Без фильтра BluetoothLeScanner возвращает все BLE-devicesа в зоне действия — для плотной BLE-среды это сотни пакетов в минуту. ScanFilter сужает результаты до нужных devices, уменьшая энергопотребление и нагрузку на приложение. Фильтры применяются на уровне Bluetooth-стека — неподходящие пакеты отбрасываются до доставки в приложение.
Типы фильтров: setServiceUuid — UUID сервиса (обязательно полный 128-bit формат). setDeviceName — подстрока имени devicesа (регистрозависимо, точное совпадение подстроки). setDeviceAddress — точный MAC-адрес. setManufacturerData — данные производителя (ID компании + маска). Для одного сканирования можно установить несколько фильтров — devicesо должно соответствовать всем (AND-логика). Для OR-логики запустите несколько сканирований.
// ScanFilter creation for different scenarios
class ScanFilterFactory {
// 1. Filter by service UUID (Heart Rate Monitor)
fun byHeartRateService(): ScanFilter {
return ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
)
.build()
}
// 2. Filter by device name ( "iBeacon*")
fun byDeviceName(): ScanFilter {
return ScanFilter.Builder()
.setDeviceName("Sensor")
.build()
}
// 3. MAC- ( devices)
fun byMacAddress(mac: String): ScanFilter {
return ScanFilter.Builder()
.setDeviceAddress(mac)
.build()
}
// 4. Combined filter (UUID + )
fun combinedFilter(): List<ScanFilter> {
return listOf(
ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
)
.setDeviceName("MyDevice")
.build()
)
}
// 5. manufacturer data
fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
return ScanFilter.Builder()
.setManufacturerData(companyId, data, mask)
.build()
}
}
Класс ScanFilterFactory показывает все типы фильтров. byHeartRateService отфильтровывает devicesа с сервисом пульса 0x180D. byDeviceName находит devicesа, содержащие "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 (devicesо), int rssi (уровень сигнала в dBm), ScanRecord scanRecord (рекламные данные), long timestampNanos (время обнаружения с момента загрузки системы). ScanRecord предоставляет: getServiceData() — UUID + кастомные данные, getManufacturerSpecificData() — данные производителя, getAdvertiseFlags() — BLE-флаги. Тип callbackа (callbackType) указывает: CALLBACK_TYPE_ALL_MATCHES — совпадение with filter, CALLBACK_TYPE_FIRST_MATCH — первое обнаружение, CALLBACK_TYPE_MATCH_LOST — потеря devicesа.
Коды ошибок onScanFailed: SCAN_FAILED_ALREADY_STARTED (1) — сканирование уже запущено, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) — регистрация приложения в Bluetooth-стеке не удалась, SCAN_FAILED_INTERNAL_ERROR (3) — внутренняя ошибка стека, SCAN_FAILED_FEATURE_UNSUPPORTED (4) — BLE-сканирование не поддерживается на devicesе.
// Full scan results and error handling
class ScanResultHandler {
private val results = mutableListOf<ScanResult>()
val scanCallback = object : ScanCallback() {
// 1. Single result
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
}
// Add to list (dedup by address)
val existingIndex = results.indexOfFirst {
it.device.address == result.device.address
}
if (existingIndex >= 0) {
results[existingIndex] = result // update RSSI
} else {
results.add(result)
}
// Extract data from advertising packet
val record = result.scanRecord
val serviceData = record?.serviceData
val manufacturerData = record?.manufacturerSpecificData
print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
}
// 2. Batch results (reportDelay > 0)
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.let { batch ->
print("Batch: ${batch.size} devices")
}
}
// 3. Scan error
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 обновляет список devices с дедупликацией по MAC-адресу — RSSI обновляется для уже найденных devices. CALLBACK_TYPE_MATCH_LOST сигнализирует о потере devicesа (удаление из списка). onBatchScanResults обрабатывает пакетные результаты для reportDelay > 0. onScanFailed маппит коды ошибок в человекочитаемые сообщения — критично для отладки BLE-сканирования.
PendingIntent-сканирование — механизм BluetoothLeScanner для BLE-сканирования, работающего даже когда приложение в background (с ограничениями Android 8+). Вместо ScanCallback используется PendingIntent, который отправляет Broadcast в системный BroadcastReceiver при обнаружении BLE-devicesа. Это позволяет приложению получать уведомления о BLE-devicesах, не находясь в памяти (система создаёт процесс при получении broadcast).
Ограничения фонового сканирования: На Android 8+ (API 26) фоновые сервисы ограничены — PendingIntent-сканирование обходит это ограничение через BroadcastReceiver, который система может запустить при получении BLE-события. На Android 10+ (API 29) фоновое BLE-сканирование дополнительно ограничено политиками энергосбережения производителей (Xiaomi, Huawei, Samsung блокируют фоновые BLE-операции). Для критических BLE-сценариев требуется уведомление с foreground service.
// Background BLE scanning via PendingIntent
class BackgroundBLEScanner(private val context: Context) {
private val scanner: BluetoothLeScanner? by lazy {
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner
}
fun startBackgroundScan() {
// Create PendingIntent for BroadcastReceiver
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Background scan settings
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.build()
// Start background scan
scanner?.startScan(
null, // filters
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) {
// Get scan results
val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
results?.let { scanResults ->
for (result in scanResults) {
// Send notification to user
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-devicesа отправляет Broadcast в BLEBroadcastReceiver. BroadcastReceiver извлекает ScanResult через getPendingIntentScanResults() и может показывать уведомление или отправлять данные на сервер. Такой подход работает даже если приложение было завершено системой — Android перезапускает BroadcastReceiver при получении broadcast.
Полный пример BLE-сканера на Kotlin, использующего BluetoothLeScanner с ScanSettings, ScanFilter и ScanCallback для поиска devices Heart Rate Monitor. Сканер показывает список найденных devices с RSSI и UUID сервисов, с возможностью подключения через BluetoothGatt.
// Full BLE scanner with coroutines 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 {
// Check Bluetooth state
if (adapter?.isEnabled != true) {
close(IllegalStateException("Bluetooth disabled"))
return@callbackFlow
}
// Scan configuration
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"))
}
}
// Start scanning
scanner?.startScan(filters, settings, callback)
// Auto-stop after duration
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 disabled (проверьте adapter.isEnabled), не получены разрешения (BLUETOOTH_SCAN на API 31+, ACCESS_FINE_LOCATION на API 23–30), scanner = null (адаптер недоступен), devicesо вне зоны действия, или используется неправильный фильтр. Также проверьте onScanFailed — код ошибки укажет на причину: SCAN_FAILED_ALREADY_STARTED (1) или SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2).
Используйте PendingIntent версию startScan() — передайте PendingIntent вместо ScanCallback. При обнаружении BLE-devicesа Android отправляет Broadcast в BroadcastReceiver, который может быть запущен системой даже если приложение в background. Для Android 8+ добавьте BroadcastReceiver в манифест. На Android 10+ учитывайте ограничения энергосбережения производителей.
BluetoothLeScanner не имеет лимита на количество обнаруженных devices — ограничение зависит от BLE-насыщенности среды. В офисе может быть 20–50 активных BLE-devices, в торговом центре — сотни. Для фильтрации используйте ScanFilter (по UUID, имени). Без фильтрации обрабатывайте результаты асинхронно — onScanResult может вызываться десятки раз в секунду.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также