配对
device.createBond()
device.createBond():- 这是 Android SDK 提供的标准 API,用于异步发起配对请求。
- 非阻塞:调用此方法后,函数会立即返回,不会等待配对完成。实际的配对过程(包括弹出系统对话框让用户输入 PIN 码、底层密钥交换)是在后台进行的。
- 系统交互:如果设备需要 PIN 码且未缓存,Android 系统会自动弹出一个原生对话框,要求用户输入密码。开发者无法直接在这个函数中自定义这个对话框(除非使用隐藏 API 或辅助广播,但标准做法是依赖系统弹窗)。
- 权限检查 (
BLUETOOTH_CONNECT):- 从 Android 12 (API 31) 开始,发起配对操作必须持有
BLUETOOTH_CONNECT权限,否则会抛出SecurityException。
- 从 Android 12 (API 31) 开始,发起配对操作必须持有
- 局限性:
- 这个函数没有返回值告诉你配对是否成功。它只是发出了'我想配对'的指令。
- 要得知配对结果(成功、失败、用户取消),必须依赖广播接收器。
- 发起配对函数(
pairDevice):
fun pairDevice(device: BluetoothDevice) {
// 1. 权限检查
if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return
try {
Log.d("BluetoothScanner", "Attempting to pair with ${device.address}...")
// 2. 核心调用:发起配对请求
device.createBond()
} catch (e: SecurityException) {
Log.e("BluetoothScanner", "Pairing failed: ${e.message}")
}
}
- 监听配对状态 (
ACTION_BOND_STATE_CHANGED):由于createBond()是异步的,代码通过注册广播接收器 (scanReceiver) 来监听配对状态的变化。
private val scanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// ... 其他动作 ...
// 监听配对状态变化
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
val device: BluetoothDevice? = getDeviceFromIntent(intent)
// 获取新的配对状态
val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
// 回调通知 UI 层
device?.let { onBondStateChanged?.invoke(it, bondState) }
}
}
}
}
总结流程图
蓝牙配对流程简述:
- 用户触发配对请求。
- 调用
pairDevice(device)->device.createBond()。 - 弹出 PIN 码输入框(如需)。
- 广播
ACTION_BOND_STATE_CHANGED (BONDING)。 - 验证通过,交换密钥。
- 广播
ACTION_BOND_STATE_CHANGED (BONDED)。 - 若验证失败或用户取消,广播
ACTION_BOND_STATE_CHANGED (NONE)。
'隐式'配对
传统的配对流程(搜索 -> 点击 -> 输入 PIN 码/确认数字)对物联网(IoT)设备太繁琐。下面是几种'隐式'方案:
| 方案名称 | 实现方式 | 底层蓝牙机制 | 例子 |
|---|---|---|---|
| NFC 触碰配对 | 手机贴设备,自动连 | OOB (NFC 传密钥) | NFC Touch-to-Pair |
| 二维码配对 | App 扫码,自动连 | OOB (视觉传密钥) | Apple HomeKit |
| 物理按钮配对 | 长按设备按钮,手机自动连 | Just Works (依赖物理操作作为验证) | |
| 邻近自动配对 | 打开盒子,手机弹窗秒连 (如 AirPods) | OOB (利用私有芯片/协议交换密钥) | |
| 无感重连 | 以前配过,下次自动连 | Bonding (使用已存储的密钥) |
在大多数标准的蓝牙开发库(如 Android 的 BluetoothDevice.createBond() 或 iOS 的 CBCentralManager)中,无 pairImplicitly() 方法。一般是在代码中将获取到的 OOB 数据传入标准的配对接口(例如 Android 的 setPairingConfirmation 或 createBond 配合 OOB 数据)。在 UI 层不显示系统默认的配对弹窗(部分系统允许 suppress 弹窗,或者通过自定义 UI 覆盖)。
连接
- 蓝牙 SPP (Serial Port Profile) 是蓝牙技术中最经典、应用最广泛的配置文件之一。它的核心作用是在两个蓝牙设备之间模拟一条传统的串行电缆(如 RS-232),从而让现有的串口应用程序能够无需修改或仅需少量修改,即可通过无线方式进行数据传输。
| 区别项 | 低功耗蓝牙 (BLE) | 经典蓝牙 (SPP 串行端口协议) |
|---|---|---|
| 蓝牙版本 | 蓝牙版本 >= 4.0,又称蓝牙低功耗、蓝牙智能 | 经典蓝牙2.0 或更早版本,经典配对模式在两台蓝牙设备之间建立虚拟串口数据连接,提供一种简单而直接的数据传输通路用于数据交换。 |
| 传输距离 | 距离较短,通信范围一般为10 米左右。 | 通信距离较远,可达100 米。 |
| 传输速率 | 低带宽:BLE 数据速率通常只有1Mbps或更低,主要用于低速度的控制类或监测类数据交换 | SPP 可以达到3Mbps或更高。提供较高的数据传输速率,适用于需要快速传输大量数据的应用,例如音频流、文件传输等。 |
| 蓝牙功耗 | 极低功耗:可达数月或几年的电池续航时间 | 较高功耗:通常不及 BLE 省电,电池续航时间较短。经典蓝牙设计注重速度和传输容量,而忽略了功耗的优化。 |
| 硬件成本 | 低成本:BLE 芯片和模块价格低廉。 | 相比 BLE价格稍高一些。 |
| 开发难度 | 低复杂度:协议栈简单,易于开发。 | 较高复杂度:协议栈较复杂,开发难度较大。 |
| 典型应用 | 可穿戴设备(智能手表、手环)、智能家居、智能门锁、健康监测、环境监测设备等。 | 蓝牙打印机、蓝牙串口替代、蓝牙 POS 机、蓝牙耳机、高速数据交换(图像传输、文件传输)等。 |
连接建立流程简述
- 初始化:双方设备开启蓝牙。
- 广播与扫描:服务端处于可被发现状态,客户端扫描设备。
- 配对 (Pairing):输入 PIN 码(如'1234'或'0000')进行身份验证和密钥交换。
- 服务发现 (SDP):客户端查询服务端的 SDP 记录,找到 SPP 服务对应的 RFCOMM 信道号。
- 建立连接:客户端向该信道发起 RFCOMM 连接请求。
- 数据传输:连接成功后,双方即可像操作串口一样收发数据。
- 断开:任意一方关闭连接。
SPP 规范架构
- 工作模式:通常采用 C/S (客户端/服务器) 模式。
- 依赖关系:SPP 基于 RFCOMM 协议实现,而 RFCOMM 又依赖于 L2CAP(逻辑链路控制与适配协议)。
- L2CAP:负责数据的分段与重组,提供多路复用。
- RFCOMM:模拟串行端口信号(如 RTS, CTS 等控制信号),提供可靠的面向连接的数据传输,本质上是在蓝牙上仿真 RS-232 串口。
- SDP (服务发现协议):在连接建立前,主设备通过 SDP 查询从设备是否支持 SPP 服务,并获取对应的信道号(Channel Number)。
- 标准连接 (Standard Connection):
// 尝试标准连接 (会自动进行 SDP 查询获取通道号)
try {
socket = device.createRfcommSocketToServiceRecord(DEFAULT_UUID)
socket?.connect()
Log.d("BluetoothScanner", "Standard connection successful.")
} catch (e: IOException) {
// ...进入 fallback 策略
}
createRfcommSocketToServiceRecord(DEFAULT_UUID):- 这是 Android 提供的标准 API。
- 关键动作:调用此方法时,Android 蓝牙栈会自动向目标设备发起 SDP 查询。它会问:'嘿,你的 UUID
...1101(SPP) 服务在哪个 RFCOMM 通道上?' - 目标设备回复一个端口号(例如 Channel 1, 5, 等)。
- 然后
connect()会尝试连接到该特定端口。
- 优点:符合规范,自动适配不同设备的端口配置。
- 透明传输:一旦连接建立,数据就像通过物理串口线一样传输,应用层只需读写数据流,无需关心蓝牙包的结构。
- 可靠性高:基于 RFCOMM 的确认机制,保证数据不丢失、顺序正确(适合控制指令、日志传输等场景)。
- 兼容性强:完美兼容传统的串口应用(如 Putty, 串口调试助手,以及各类嵌入式 UART 程序)。
- 点对点通信:标准的 SPP 通常是一对一连接(虽然蓝牙底层支持微网,但 SPP 逻辑上多为直连)。
- 带宽与功耗:
- 带宽:经典蓝牙(BR/EDR)的 SPP 理论速率可达 2-3 Mbps,实际应用中通常在 100KB/s - 200KB/s 左右,远高于 BLE。
- 功耗:由于属于经典蓝牙,其功耗显著高于 BLE (Bluetooth Low Energy),不适合电池供电且需长期待机的设备。
- 缺点:在某些安卓版本或特定的蓝牙硬件固件上,SDP 查询可能会超时、失败或被防火墙拦截,导致抛出
IOException。
SPP 的唯一标识符 (UUID)
// Standard SPP UUID
private val DEFAULT_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
- 含义:
00001101-0000-1000-8000-00805F9B34FB是蓝牙技术联盟(SIG)为 Serial Port Profile (SPP) 分配的官方标准 UUID。 - 作用:当客户端(手机)想要连接一个 SPP 服务时,必须使用这个 UUID 告诉系统:'我要找的是串口服务,而不是耳机(HFP)或文件传输(FTP)服务'。
- 底层机制:这个 UUID 用于后续的 SDP (Service Discovery Protocol) 查询,以找到对方设备开放的具体通信通道号(RFCOMM Channel)。
'端口号'(RFCOMM Channel Number)
- 端口号是一个整数,范围通常在 1 到 30 之间(蓝牙协议规范允许 1-30,部分实现可能支持更多,但 30 是通用标准上限),用于在单个蓝牙物理设备上区分不同的串行服务。
- 蓝牙设备可以同时运行多个服务(例如:一个 SPP 串口服务、一个文件传输服务、一个耳机服务)。
- 每个服务都必须绑定到一个唯一的 RFCOMM 通道号上。
- 当客户端(手机)想要连接 SPP 服务时,它必须知道目标设备把 SPP 服务'挂'在了哪个房间号(通道号)上。
- 为了方便理解,可以将其与网络编程中的 TCP 端口做类比,但它们处于不同的协议层:
| 特性 | TCP/IP 端口 (Port) | 蓝牙 RFCOMM 通道 (Channel) |
|---|---|---|
| 所属协议 | TCP/UDP | RFCOMM (基于 L2CAP) |
| 范围 | 0 - 65535 | 1 - 30 (SPP 常用范围) |
| 功能 | 区分同一 IP 下的不同应用程序 | 区分同一蓝牙设备下的不同串行服务 |
| 常见默认值 | HTTP(80), SSH(22) | SPP 通常为 1 |
| 发现方式 | 已知或通过扫描 | 通过 SDP (服务发现协议) 查询,或暴力遍历 |
相关代码
- 如果标准方法失败,代码进入
catch块,尝试更'暴力'但往往更有效的方法:
// Fallback 策略 A: 尝试通过 SDP 记录手动获取通道号 (更稳健的反射)
val method = device.javaClass.getMethod("createRfcommSocket", Int::class.javaPrimitiveType)
// 尝试常见的通道号 1 到 30
for (channel in 1..30) {
try {
socket = method.invoke(device, channel) as BluetoothSocket
socket?.connect()
Log.d("BluetoothScanner", "Fallback successful on channel: $channel")
break
} catch (e2: IOException) {
socket?.close()
socket = null
}
}
createRfcommSocket(int channel)(反射调用):- 这是一个 隐藏 API (Hidden API),不在公开的 Android SDK 中,但在底层存在。
- 区别:它跳过 SDP 查询,直接尝试连接指定的 RFCOMM 通道号。
- 遍历通道 (1-30):
- 由于跳过了 SDP,代码不知道具体的端口号。大多数蓝牙模块(如 HC-05, JDY 系列)默认将 SPP 服务绑定在 Channel 1,但也有部分设备使用其他端口(如 3, 5, 7 等)。
- 代码通过循环尝试 1 到 30 之间的所有可能端口,一旦某个端口连接成功 (
connect()不抛异常),就立即停止循环并认定连接成功。
Android 项目代码
bluetoothscan
package com.example.bluetoothscan
import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import java.io.IOException
import java.util.UUID
class BluetoothScanner(val context: Context) {
private val bluetoothAdapter: BluetoothAdapter? by lazy {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
private var onDeviceFound: ((ScanDevice) -> Unit)? = null
private var onScanStarted: (() -> Unit)? = null
private var onBondStateChanged: ((BluetoothDevice, Int) -> Unit)? = null
private var onConnectionStateChanged: ((BluetoothDevice, Boolean) -> Unit)? =
isAutoRepeat =
handler = Handler(Looper.getMainLooper())
reScanDelayMs =
DEFAULT_UUID: UUID = UUID.fromString()
bluetoothSocket: BluetoothSocket? =
scanReceiver = : BroadcastReceiver() {
{
(intent.action) {
BluetoothDevice.ACTION_FOUND -> {
device: BluetoothDevice? = getDeviceFromIntent(intent)
rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, .MIN_VALUE).toInt()
device?.let { onDeviceFound?.invoke(ScanDevice(it, rssi, it.bondState)) }
}
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
(isAutoRepeat) {
handler.postDelayed({
(isAutoRepeat) startDiscoveryProcess()
}, reScanDelayMs)
}
}
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
device: BluetoothDevice? = getDeviceFromIntent(intent)
bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
device?.let { onBondStateChanged?.invoke(it, bondState) }
}
}
}
}
: BluetoothDevice? {
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::.java)
} {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
}
}
{
onDeviceFound = listener
}
{
onScanStarted = listener
}
{
onBondStateChanged = listener
}
{
onConnectionStateChanged = listener
}
{
.isAutoRepeat = autoRepeat
filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_FOUND)
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
}
{
context.registerReceiver(scanReceiver, filter)
} (e: Exception) {
}
startDiscoveryProcess()
}
{
adapter = bluetoothAdapter ?:
(!adapter.isEnabled || ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
(adapter.isDiscovering) adapter.cancelDiscovery()
onScanStarted?.invoke()
adapter.startDiscovery()
}
{
isAutoRepeat =
handler.removeCallbacksAndMessages()
(bluetoothAdapter?.isDiscovering == ) {
(ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) {
bluetoothAdapter?.cancelDiscovery()
}
}
{
context.unregisterReceiver(scanReceiver)
} (e: Exception) {
}
}
{
(ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED)
{
Log.d(, )
device.createBond()
} (e: SecurityException) {
Log.e(, )
}
}
{
(ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
Log.e(, )
}
Thread {
disconnect()
socket: BluetoothSocket? =
{
(bluetoothAdapter?.isDiscovering == ) {
bluetoothAdapter?.cancelDiscovery()
}
Log.d(, )
{
socket = device.createRfcommSocketToServiceRecord(DEFAULT_UUID)
socket?.connect()
Log.d(, )
} (e: IOException) {
Log.w(, )
socket?.close()
socket =
method = device.javaClass.getMethod(, ::.javaPrimitiveType)
(channel ) {
{
Log.d(, )
socket = method.invoke(device, channel) BluetoothSocket
socket?.connect()
Log.d(, )
} (e2: IOException) {
socket?.close()
socket =
}
}
(socket == || !socket.isConnected) {
IOException()
}
}
bluetoothSocket = socket
Log.d(, )
handler.post { onConnectionStateChanged?.invoke(device, ) }
} (e: Exception) {
Log.e(, , e)
{
socket?.close()
} (closeException: IOException) {
}
handler.post { onConnectionStateChanged?.invoke(device, ) }
}
}.start()
}
{
{
bluetoothSocket?.close()
bluetoothSocket =
} (e: IOException) {
Log.e(, )
}
}
: List<ScanDevice> {
adapter = bluetoothAdapter ?: emptyList()
(ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
emptyList()
}
adapter.bondedDevices.map { ScanDevice(it, -, it.bondState) }
}
}
MainActivity.kt
package com.example.bluetoothscan
import android.Manifest
import android.bluetooth.BluetoothDevice
import android.content.pm.PackageManager
import android.media.MediaPlayer
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import com.example.bluetoothscan.ui.theme.BluetoothscanTheme
class MainActivity : ComponentActivity() {
private lateinit var bluetoothScanner: BluetoothScanner
private val discoveredDevices = mutableStateListOf<ScanDevice>()
private var isCyclingConnections = false
private var bondedDevicesArray: Array<BluetoothDevice> = arrayOf()
private var currentConnectionIndex =
mediaPlayer: MediaPlayer? =
handler = Handler(Looper.getMainLooper())
cycleRunnable = : Runnable {
{
(bondedDevicesArray.isNotEmpty()) {
device = bondedDevicesArray[currentConnectionIndex]
bluetoothScanner.connectToDevice(device)
currentConnectionIndex = (currentConnectionIndex + ) % bondedDevicesArray.size
handler.postDelayed(, )
} {
isCyclingConnections =
}
}
}
{
.onCreate(savedInstanceState)
mediaPlayer = MediaPlayer.create(, R.raw.connection_failed)
bluetoothScanner = BluetoothScanner()
bluetoothScanner.setOnDeviceFoundListener { device ->
index = discoveredDevices.indexOfFirst { it.device.address == device.device.address }
(index == -) {
discoveredDevices.add(device)
}
}
bluetoothScanner.setOnScanStartedListener {
discoveredDevices.clear()
bondedDevices = bluetoothScanner.getBondedDevices()
discoveredDevices.addAll(bondedDevices)
}
bluetoothScanner.setOnBondStateChangedListener { device, state ->
index = discoveredDevices.indexOfFirst { it.device.address == device.address }
(index != -) {
discoveredDevices[index] = discoveredDevices[index].copy(bondState = state)
}
}
bluetoothScanner.setOnConnectionStateChangedListener { device, connected ->
(!connected) {
message =
Toast.makeText(, message, Toast.LENGTH_SHORT).show()
mediaPlayer?.start()
}
}
setContent {
BluetoothscanTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
BluetoothScanScreen(
devices = discoveredDevices,
onPairDevice = { bluetoothScanner.pairDevice(it) },
onConnectDevice = {
(isCyclingConnections) {
Toast.makeText(, , Toast.LENGTH_SHORT).show()
} {
bluetoothScanner.connectToDevice(it)
}
},
onStartScan = { bluetoothScanner.startScan() },
onStopScan = {
bluetoothScanner.stopScan()
stopCycleConnect()
},
onCycleConnect = { startCycleConnect() }
)
}
}
}
}
{
(isCyclingConnections) {
stopCycleConnect()
Toast.makeText(, , Toast.LENGTH_SHORT).show()
}
bonded = bluetoothScanner.getBondedDevices().map { it.device }
(bonded.isEmpty()) {
Toast.makeText(, , Toast.LENGTH_SHORT).show()
}
isCyclingConnections =
bondedDevicesArray = bonded.toTypedArray()
currentConnectionIndex =
Toast.makeText(, , Toast.LENGTH_SHORT).show()
handler.post(cycleRunnable)
}
{
isCyclingConnections =
handler.removeCallbacks(cycleRunnable)
bluetoothScanner.disconnect()
}
{
.onResume()
checkPermissions()
}
{
.onPause()
bluetoothScanner.stopScan()
stopCycleConnect()
}
{
.onDestroy()
mediaPlayer?.release()
mediaPlayer =
}
: {
requiredPermissions = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
} {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
(requiredPermissions.all { checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }) {
}
ActivityCompat.requestPermissions(, requiredPermissions, )
}
}
{
isScanning remember { mutableStateOf() }
hideUnknownDevices remember { mutableStateOf() }
filterLowRssi remember { mutableStateOf() }
(bondedDevices, availableDevices) = devices.partition { it.bondState == BluetoothDevice.BOND_BONDED }
filteredAvailableDevices = availableDevices.filter {
keep =
(hideUnknownDevices && (it.device.name == || it.device.name.isEmpty())) keep =
(filterLowRssi && it.rssi <= -) keep =
keep
}
Column(modifier = Modifier.padding(dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Button(onClick = {
isScanning = !isScanning
(isScanning) onStartScan()
onStopScan()
}) {
Text( (isScanning) )
}
Spacer(modifier = Modifier.width(dp))
Button(onClick = onCycleConnect) {
Text()
}
}
Spacer(modifier = Modifier.height(dp))
LazyColumn(modifier = Modifier.padding(top = dp)) {
(bondedDevices.isNotEmpty()) {
stickyHeader {
ListHeader()
}
items(bondedDevices, key = { it.device.address }) {
DeviceCard(it, onAction = onConnectDevice)
}
}
(filteredAvailableDevices.isNotEmpty()) {
stickyHeader {
ListHeader()
}
items(filteredAvailableDevices, key = { it.device.address }) {
DeviceCard(it, onAction = onPairDevice)
}
}
}
}
}
{
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(vertical = dp)
)
}
{
Card(modifier = Modifier.fillMaxWidth().padding(vertical = dp)) {
Row(
modifier = Modifier.padding(dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight()) {
context = LocalContext.current
(ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
Text(scanDevice.device.name ?: , style = MaterialTheme.typography.titleMedium)
Text(scanDevice.device.address, style = MaterialTheme.typography.bodySmall)
} {
Text(, style = MaterialTheme.typography.titleMedium)
}
(scanDevice.rssi != -) {
Text(, style = MaterialTheme.typography.bodySmall)
}
}
ActionButton(scanDevice, onAction)
}
}
}
{
(text, enabled) = (scanDevice.bondState) {
BluetoothDevice.BOND_BONDED -> to
BluetoothDevice.BOND_BONDING -> to
-> to
}
Button(onClick = { onAction(scanDevice.device) }, enabled = enabled) {
Text(text)
}
}
bluetoothscan
package com.example.bluetoothscan
import android.bluetooth.BluetoothDevice
/**
* 保存蓝牙设备信息的数据类。
*
* @param device 系统的 BluetoothDevice 对象。
* @param rssi 信号强度 (Received Signal Strength Indication)。
* @param bondState 设备的配对状态,默认为 BOND_NONE。
*/
data class ScanDevice(
val device: BluetoothDevice,
val rssi: Int,
var bondState: Int = BluetoothDevice.BOND_NONE
)
注:后续再优化和解释项目代码。


