Skip to content

Bluetooth 蓝牙

源:蓝牙概览

Android 平台支持蓝牙网络堆栈,允许设备与其他蓝牙设备进行无线数据交换。应用框架提供了通过 Android Bluetooth API 访问蓝牙功能的途径。

1. 权限声明

Android 12 (API 31) 开始,蓝牙权限模型发生了重大变化。

Android 12 及以上 (API 31+)

如果你的应用目标是 Android 12 或更高版本,需要在 AndroidManifest.xml 中声明以下权限:

xml
<!-- 如果应用寻找蓝牙设备(如 BLE 外围设备) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
                     android:usesPermissionFlags="neverForLocation" />

<!-- 如果应用让当前设备可被其他设备发现 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

<!-- 如果应用与已配对的蓝牙设备通信 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

注意: neverForLocation 标志表示你使用蓝牙扫描不是为了推断用户位置。如果你确实需要通过蓝牙推断位置,则不能添加此标志,并且需要申请定位权限。

Android 11 及以下 (API 30-)

xml
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<!-- 需要定位权限才能进行蓝牙扫描 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

2. 设置蓝牙

在使用蓝牙之前,需要验证设备是否支持蓝牙,并确保蓝牙已启用。

kotlin
val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter

if (bluetoothAdapter == null) {
    // 设备不支持蓝牙
}

if (bluetoothAdapter?.isEnabled == false) {
    val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
    // 启动 Activity 请求用户开启蓝牙 (需要处理 Activity Result)
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}

3. 查找设备 (BLE 扫描)

使用 BluetoothLeScanner 扫描 BLE 设备。

kotlin
private val leScanner = bluetoothAdapter.bluetoothLeScanner
private var scanning = false
private val handler = Handler(Looper.getMainLooper())

// 10秒后停止扫描
private val SCAN_PERIOD: Long = 10000

private fun scanLeDevice() {
    if (!scanning) {
        // 停止扫描的 Runnable
        handler.postDelayed({
            scanning = false
            leScanner.stopScan(leScanCallback)
        }, SCAN_PERIOD)

        scanning = true
        leScanner.startScan(leScanCallback)
    } else {
        scanning = false
        leScanner.stopScan(leScanCallback)
    }
}

// 扫描回调
private val leScanCallback: ScanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        super.onScanResult(callbackType, result)
        val device = result.device
        // 发现设备,可以在这里更新 UI
        Log.d("BLE", "Found device: ${device.name} - ${device.address}")
    }
}

4. 连接到 GATT 服务端

连接到 BLE 设备主要涉及连接到该设备上的 GATT 服务器。

kotlin
var bluetoothGatt: BluetoothGatt? = null

private fun connectToDevice(device: BluetoothDevice) {
    // autoConnect = false 表示立即连接
    bluetoothGatt = device.connectGatt(context, false, gattCallback)
}

private val gattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
        when (newState) {
            BluetoothProfile.STATE_CONNECTED -> {
                Log.i("BLE", "Connected to GATT server.")
                // 连接成功后,开始发现服务
                gatt.discoverServices()
            }
            BluetoothProfile.STATE_DISCONNECTED -> {
                Log.i("BLE", "Disconnected from GATT server.")
            }
        }
    }

    override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            // 服务已发现,可以遍历 gatt.services
            displayGattServices(gatt.services)
        } else {
            Log.w("BLE", "onServicesDiscovered received: $status")
        }
    }
    
    // ... 其他回调,如 onCharacteristicRead, onCharacteristicWrite
}

5. 传输数据

读取特征 (Characteristic)

kotlin
fun readCharacteristic(serviceUUID: UUID, charUUID: UUID) {
    val service = bluetoothGatt?.getService(serviceUUID)
    val characteristic = service?.getCharacteristic(charUUID)
    
    if (characteristic != null) {
        bluetoothGatt?.readCharacteristic(characteristic)
    }
}

// 结果在 gattCallback.onCharacteristicRead 中返回

写入特征

kotlin
fun writeCharacteristic(serviceUUID: UUID, charUUID: UUID, value: ByteArray) {
    val service = bluetoothGatt?.getService(serviceUUID)
    val characteristic = service?.getCharacteristic(charUUID)
    
    if (characteristic != null) {
        // 根据 API 版本设置写入类型和值
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            bluetoothGatt?.writeCharacteristic(characteristic, value, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
        } else {
            characteristic.value = value
            characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
            bluetoothGatt?.writeCharacteristic(characteristic)
        }
    }
}

订阅通知 (Notifications)

kotlin
fun setCharacteristicNotification(characteristic: BluetoothGattCharacteristic, enabled: Boolean) {
    bluetoothGatt?.setCharacteristicNotification(characteristic, enabled)
    
    // 还需要写入 Descriptor 才能真正开启通知
    val uuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") // Client Characteristic Configuration Descriptor
    val descriptor = characteristic.getDescriptor(uuid)
    
    val value = if (enabled) BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        bluetoothGatt?.writeDescriptor(descriptor, value)
    } else {
        descriptor.value = value
        bluetoothGatt?.writeDescriptor(descriptor)
    }
}
// 数据更新在 onCharacteristicChanged 回调中

6. 最佳实践

  1. 权限处理:务必适配 Android 12+ 的新权限模型,并在运行时请求。
  2. 不在主线程操作:所有的蓝牙操作(扫描、连接、读写)都应当放在后台线程执行,虽然回调通常在 Binder 线程,但处理逻辑不应阻塞 UI。
  3. 及时停止扫描:扫描非常耗电,找到设备后应立即停止扫描。
  4. GATT 操作顺序:GATT 操作(读、写)是串行的。如果连续发起多个操作,可能会失败。最好建立一个命令队列。
  5. 资源释放:不再使用时调用 bluetoothGatt?.close() 释放资源。