---
id: android-bluetooth-le-scanning
title: Android BLE — Scan / Connect / GATT
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [android, bluetooth, ble, gatt, vibe-coding]
tech_stack: { language: "Kotlin / Bluetooth LE", applicable_to: ["Android"] }
applied_in: []
aliases: [BLE, GATT, characteristic, scan filter, ScanCallback]
---

# Android Bluetooth LE

> **Scan → Connect → Discover Services → Read/Write/Notify** 흐름. 권한 / lifecycle / state machine 까다로움. Nordic / Polidea 같은 라이브러리 권장.

## 📖 핵심 개념
- BLE: low energy. peripherals (sensor, watch).
- GATT: 서비스 / characteristic / descriptor.
- Scan filter: UUID / name / MAC.
- Permission (Android 12+): BLUETOOTH_SCAN, BLUETOOTH_CONNECT.

## 💻 코드 패턴

### Permission
```xml
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Android 11- -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
```

### Scan
```kotlin
@SuppressLint("MissingPermission") // Permission check 별도
class BleScanner(private val ctx: Context) {
    private val adapter = (ctx.getSystemService(BluetoothManager::class.java)).adapter
    private val scanner: BluetoothLeScanner? get() = adapter?.bluetoothLeScanner

    fun scan(serviceUuid: UUID): Flow<ScanResult> = callbackFlow {
        val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(serviceUuid)).build()
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .build()

        val cb = object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                trySend(result)
            }
            override fun onScanFailed(errorCode: Int) {
                close(IllegalStateException("scan failed: $errorCode"))
            }
        }
        scanner?.startScan(listOf(filter), settings, cb)
        awaitClose { scanner?.stopScan(cb) }
    }
}
```

### Connect + GATT
```kotlin
class BleClient(private val ctx: Context, private val device: BluetoothDevice) {

    private var gatt: BluetoothGatt? = null

    @SuppressLint("MissingPermission")
    suspend fun connect(): BluetoothGatt = suspendCancellableCoroutine { cont ->
        gatt = device.connectGatt(ctx, false, object : BluetoothGattCallback() {
            override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    g.discoverServices()
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    g.close()
                    cont.resumeWithException(Exception("disconnected"))
                }
            }
            override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
                if (status == BluetoothGatt.GATT_SUCCESS) cont.resume(g)
                else cont.resumeWithException(Exception("services discovery failed"))
            }
        })
    }

    suspend fun read(serviceUuid: UUID, charUuid: UUID): ByteArray { ... }
    suspend fun write(serviceUuid: UUID, charUuid: UUID, data: ByteArray) { ... }
    fun notifications(serviceUuid: UUID, charUuid: UUID): Flow<ByteArray> { ... }

    fun disconnect() {
        gatt?.disconnect()
        gatt?.close()
        gatt = null
    }
}
```

### MTU / connection priority
```kotlin
gatt.requestMtu(247)                                            // 큰 패킷
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)  // 빠른 throughput
```

## 🤔 의사결정 기준
| 상황 | 도구 |
|---|---|
| Direct BLE | Android API + wrapper |
| 복잡 GATT 시나리오 | Nordic/Polidea/Kable 라이브러리 |
| Companion Device Pairing (Android 8+) | CompanionDeviceManager (자동 pair UI) |
| BLE Beacon scanning | iBeacon / Eddystone 라이브러리 |
| BLE peripheral (앱이 server) | BluetoothGattServer |

## ❌ 안티패턴
- **Scan 무한**: 배터리 폭발 + Android 가 throttle (5회/30s). 짧게 + filter.
- **discoverServices 없이 read/write**: characteristic null.
- **gatt.close() 누락**: 메모리 + 다음 connect 실패.
- **메인 스레드 callback 안에서 무거운 작업**: 다른 callback 못 받음.
- **권한 체크 안 함 Android 12+**: SecurityException.
- **여러 callback 같은 gatt**: 마지막 것만 호출.
- **MTU 협상 안 함**: 작은 패킷 만. 247 바이트 설정.
- **disconnect 후 즉시 connect**: race. delay 또는 state machine.

## 🤖 LLM 활용 힌트
- 권한 + Scan filter + state machine + close 4종 강조.
- 라이브러리 권장 — Kable (Kotlin Multiplatform) 또는 Nordic.

## 🔗 관련 문서
- [[Android_Lifecycle_Aware_Components]]
- [[Native_Battery_Network_Profiling]]
