BluetoothLeScanner は、Bluetooth Low EnergyデバイスをスキャンするためのAndroidクラスで、API 21(Android 5.0)から利用可能です。BluetoothLeScannerは、BluetoothAdapterの非推奨メソッドstartLeScanを置き換え、スキャン設定(ScanSettings)、フィルタリング(ScanFilter)、バックグラウンドモードサポート(PendingIntent)を備えた柔軟なAPIを提供します。インスタンスはBluetoothAdapter.getBluetoothLeScanner()を介して取得されます。Android Developers, 2026によると、BluetoothLeScannerは3つの電力モードをサポートし、サービスUUID、デバイス名、MACアドレスによるフィルタリングでBLEアドバタイジングパケットをスキャンできます。
重要なポイント
BluetoothLeScanner は、AndroidでBLEスキャンを管理するシステムクラスです。単純なLeScanCallbackを受け入れるBluetoothAdapter.startLeScan()とは異なり、BluetoothLeScannerは設定、フィルター、高度なエラー処理を備えたオブジェクト指向APIを提供します。このクラスはAPI 21(Android 5.0)でBLE 4.2サポートとともに導入され、すべての最新AndroidバージョンでBLEスキャンの主要な方法として残っています。
BluetoothLeScannerインスタンスの取得は、BluetoothAdapter.getBluetoothLeScanner()を介して行われます。Bluetoothアダプターが利用不可の場合(Bluetoothが無効、またはデバイスがBLEをサポートしていない)、メソッドはnullを返します。取得する前に、PackageManagerを介してBluetoothAdapter.isEnabled()とFEATURE_BLUETOOTH_LEの存在を確認してください。スキャナーを取得した後、任意のスレッドでスキャンを開始できます。Androidは内部のBluetoothスタックスレッドでBLE操作をスケジュールします。
// 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は、hasSystemFeatureによるBLEの可用性、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)を設定できます。
3つのスキャンモード:SCAN_MODE_LOW_POWER(0)—低電力消費のバックグラウンドスキャン、検出遅延は数秒。SCAN_MODE_BALANCED(1)—ほとんどのシナリオに対応するバランスモード。SCAN_MODE_LOW_LATENCY(2)—最小検出遅延(約100 ms)、最大電力消費。アクティブなデバイス検出にはLOW_LATENCY、バックグラウンド監視にはLOW_POWERを使用してください。
reportDelay —結果のバッチ配信前のミリ秒単位の遅延。reportDelay = 0の場合、結果は検出されるとすぐに送信されます。> 0の場合、Androidは結果を蓄積し、onBatchScanResultsを介してバッチを送信します。バッチ配信はコールバックの呼び出しと電力消費を削減し、低優先度のバックグラウンドスキャンに適しています。
// さまざまなシナリオの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 km)を使用。highSpeedScanはPHY_LE_2M(2 Mbit/s、BLE 5.0+デバイスのみ)。
ScanFilter はBLEスキャン結果をフィルタリングするためのクラスです。フィルターがない場合、BluetoothLeScannerは範囲内のすべてのBLEデバイスを返します。密集したBLE環境では、毎分数百のパケットになる可能性があります。ScanFilterは結果を目的のデバイスに絞り込み、電力消費とアプリの負荷を軽減します。フィルターはBluetoothスタックレベルで適用され、不適切なパケットはアプリに到達する前に破棄されます。
フィルターの種類:setServiceUuid —サービスUUID(完全な128ビット形式が必要)。setDeviceName —デバイス名の部分文字列(大文字小文字を区別、完全な部分文字列一致)。setDeviceAddress —正確なMACアドレス。setManufacturerData —メーカーデータ(会社ID + マスク)。1回のスキャンに複数のフィルターを設定でき、デバイスはすべてに一致する必要があります(AND論理)。OR論理の場合は、複数のスキャンを開始してください。
// さまざまなシナリオのScanFilter作成
class ScanFilterFactory {
// 1. サービスUUIDでフィルター(心拍数モニター)
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はUUIDと名前によるANDフィルター。byManufacturerはメーカーデータによるフィルター(例:iBeaconの場合、Apple会社ID 0x004Cが使用されます)。
ScanCallback はBLEスキャン結果を受信するための抽象クラスです。3つのメソッドが含まれています:onScanResult —個別結果(コールバックタイプ、ScanResult)、onBatchScanResults —reportDelay > 0用のバッチ結果、onScanFailed —エラーコード。すべてのメソッドはAndroidメインスレッドで呼び出されます。onScanResultでの長時間処理には、coroutinesまたはHandlerThreadを使用してください。
ScanResult には以下が含まれます:BluetoothDevice device、int rssi(dBm単位の信号レベル)、ScanRecord scanRecord(アドバタイジングデータ)、long timestampNanos(システム起動からの検出時間)。ScanRecordが提供するもの:getServiceData() —UUID + カスタムデータ、getManufacturerSpecificData() —メーカーデータ、getAdvertiseFlags() —BLEフラグ。コールバックタイプは以下を示します: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 クラスはすべてのBluetoothLeScannerコールバックタイプを処理します。onScanResultはMACアドレスによる重複排除でデバイスリストを更新し、既に見つかったデバイスのRSSIを更新します。CALLBACK_TYPE_MATCH_LOSTはデバイス喪失(リストからの削除)を示します。onBatchScanResultsはreportDelay > 0用のバッチ結果を処理します。onScanFailedはエラーコードを人間が読めるメッセージにマッピングします。これはBLEスキャンデバッグに重要です。
PendingIntentスキャン は、アプリがバックグラウンドにある場合でも機能するBLEスキャンのためのBluetoothLeScannerのメカニズムです(Android 8+の制限あり)。ScanCallbackの代わりにPendingIntentが使用され、BLEデバイスが検出されるとシステムBroadcastReceiverにBroadcastを送信します。これにより、アプリはメモリに残らなくてもBLEデバイス通知を受信できます(システムはブロードキャスト受信時にプロセスを作成します)。
バックグラウンドスキャンの制限:Android 8+(API 26)では、バックグラウンドサービスが制限されています。PendingIntentスキャンは、BLEイベント受信時にシステムが起動できるBroadcastReceiverを介してこの制限を回避します。Android 10+(API 29)では、バックグラウンドBLEスキャンはメーカーの省電力ポリシー(Xiaomi、Huawei、SamsungがバックグラウンドBLE操作をブロック)によりさらに制限されています。重要なBLEシナリオでは、フォアグラウンドサービス通知が必要です。
// PendingIntentを介したバックグラウンドBLEスキャン
class BackgroundBLEScanner(private val context: Context) {
private val scanner: BluetoothLeScanner? by lazy {
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner
}
fun startBackgroundScan() {
// BroadcastReceiver用のPendingIntentを作成
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 クラスは、PendingIntentを介してバックグラウンドBLEスキャンを開始します。startBackgroundScanはPendingIntentを作成し、BLEデバイス検出時にBLEBroadcastReceiverにBroadcastを送信します。BroadcastReceiverはgetPendingIntentScanResults()を介してScanResultを抽出し、通知を表示したりサーバーにデータを送信したりできます。このアプローチは、アプリがシステムによって終了された場合でも機能します。Androidはブロードキャスト受信時にBroadcastReceiverを再起動します。
完全な例:BluetoothLeScannerをScanSettings、ScanFilter、ScanCallbackとともに使用してHeart Rate Monitorデバイスを検出するKotlinのBLEスキャナー。スキャナーはRSSIとサービスUUIDを含む検出されたデバイスのリストを表示し、BluetoothGattを介した接続が可能です。
// Kotlinでcoroutinesを使用した完全なBLEスキャナー
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 クラスは、リアクティブBLEスキャンにKotlin Flow(callbackFlow)を使用します。スキャンはLOW_LATENCY設定とHeart Rate Service UUIDフィルターで開始されます。結果はonScanResultを介してFlowに出力されます。指定された期間(デフォルト10秒)後に自動停止。FlowOn(Dispatchers.IO)はBLE操作をバックグラウンドスレッドにオフロードします。このアプローチにより、viewModelScope.launchとcollectを介したMVVMアーキテクチャでのBLEスキャンが可能になります。
よくある質問
BluetoothLeScanner はBLEスキャンのためのAndroidクラス(API 21+)です。BluetoothAdapter.getBluetoothLeScanner()を介して取得されます。3つのスキャンモード(LOW_POWER、BALANCED、LOW_LATENCY)、UUID、名前、MACアドレスによるフィルタリング、バッチ結果、バックグラウンドスキャン用のPendingIntentをサポートします。非推奨のBluetoothAdapter.startLeScan()メソッドを置き換えます。
SCAN_MODE_LOW_POWER —5~10秒の検出遅延があるバックグラウンドモード、最小電力消費。SCAN_MODE_LOW_LATENCY —約100 msの遅延があるアクティブモード、最大電力消費。SCAN_MODE_BALANCED —妥協点(約2秒の遅延)。UIスキャンにはLOW_LATENCY、バックグラウンド監視にはPendingIntentとLOW_POWERを使用してください。
原因:Bluetoothが無効(adapter.isEnabledを確認)、権限がない(API 31+ではBLUETOOTH_SCAN、API 23–30ではACCESS_FINE_LOCATION)、scanner = null(アダプター利用不可)、デバイスが範囲外、またはフィルターが間違っている。onScanFailedも確認してください。エラーコードが原因を示します:SCAN_FAILED_ALREADY_STARTED(1)またはSCAN_FAILED_APPLICATION_REGISTRATION_FAILED(2)。
startScan()のPendingIntentバージョンを使用してください。ScanCallbackの代わりにPendingIntentを渡します。BLEデバイスが検出されると、AndroidはBroadcastReceiverにBroadcastを送信します。BroadcastReceiverはアプリがバックグラウンドでもシステムが起動できます。Android 8+の場合は、BroadcastReceiverをマニフェストに追加してください。Android 10+では、メーカーの省電力制限を考慮してください。
BluetoothLeScannerに検出可能なデバイス数の制限はありません。制限は環境のBLE飽和度に依存します。オフィスには20~50のアクティブなBLEデバイス、ショッピングセンターには数百のデバイスがある可能性があります。フィルタリングにはScanFilter(UUID、名前で)を使用してください。フィルタリングなしの場合は、結果を非同期で処理してください。onScanResultは1秒間に何十回も呼び出される可能性があります。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。