Content
概述
- Android应用可通过Bluetooth API执行以下操作
- 扫描其他蓝牙设备
- 查询本地蓝牙适配器的配对蓝牙设备
- 建立 RFCOMM 通道
- 通过服务发现连接到其他设备
- 与其他设备进行双向数据传输
- 管理多个连接
- 蓝牙进行通信的四大必须任务
- 设置蓝牙
- 查找局部区域内的配对设备或可用设备
- 连接设备
- 在设备之间传输数据
BLUETOOTH: 执行任何蓝牙通信,例如请求连接、接受连接和传输数据等。ACCESS_FINE_LOCATION: 蓝牙扫描可用于手机用户信息ACCESS_COARSE_LOCATIONBLUETOOTH_ADMIN: (optional) 如果您想让应用启动设备发现或操纵蓝牙设置,则除了 BLUETOOTH权限以外,您还必须声明 BLUETOOTH_ADMIN权限。大多数应用只是需利用此权限发现本地蓝牙设备。除非应用是根据用户请求修改蓝牙设置的“超级管理员”,否则不应使用此权限所授予的其他功能。<manifest ... > <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- If your app targets Android 9 or lower, you can declare ACCESS_COARSE_LOCATION instead. --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> ... </manifest>
蓝牙配置文件 是适用于设备间蓝牙通信的无线接口规范。
BluetoothHeadset 类 — 用于控制蓝牙耳机服务的代理BluetoothA2dp 类 — 用于控制蓝牙A2DP服务的代理BluetoothProfile.ServiceListener。此侦听器会在 BluetoothProfile 客户端连接到服务或断开服务连接时向其发送通知。getProfileProxy() 与配置文件所关联的配置文件代理对象建立连接。在以下示例中,配置文件代理对象是一个 BluetoothHeadset 实例。onServiceConnected() 中,获取配置文件代理对象的句柄。从 Android 3.0(API 级别 11)开始,应用可注册接收耳机发送的预定义供应商特定 AT 命令(例如 Plantronics +XEVENT 命令)的系统广播。例如,应用可接收指示所连接设备电池电量的广播,并根据需要通知用户或采取其他操作。为 ACTION_VENDOR_SPECIFIC_HEADSET_EVENT
Intent 创建广播接收器,以处理耳机的供应商特定 AT 命令。BluetoothAdapter ,分两步:BluetoothAdatpterBluetoothAdatpter 。BluetoothAdatpter,调用静态的 getDefaultAdapter() 方法,返回一个 BluetoothAdatpter 对象,表示设备自身的蓝牙适配器。getDefaultAdapter() 返回 null ,则表示设备不支持蓝牙。val bluetoothAdapter: BluetoothAdapter? = Bluetooth.getDefaultAdapter() if (bluetoothAdapter == null) { // Device does not support Bluetooth }
isEnabled(),以检查当前是否启用蓝牙。若返回 false ,则表示处于停用状态。startActivityForResult() ,传入一个 ACTION_REQUEST_ENABLE Intent操作,此调用会发出系统设置启用蓝牙的请求(无需停止应用)if (bluetoothAdapter?.isEnabled == false) { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) }
startActivityForResult() 的 REQUEST_ENABLE_BT 常量为局部定义的整型数(必须大于 0)。系统会以 onActivityResult() 实现中的 requestCode 参数形式,向您传回该常量。onActivityResult() 回调中收到 RESULT_OK 结果代码。如果由于某个错误(或用户响应“No”)未成功启用蓝牙,则结果代码为 RESULT_CANCELED。ACTION_STATE_CHANGEED 广播Intent,每当蓝牙状态发生变化时,系统都会广播此Intent。EXTRA_STATE 和 EXTRA_PREVIOUS_STATE ,二者分别包含新的和旧的蓝牙状态。STATE_TURNING_ON 和STATE_ON 和STATE_TURNING_OFF 和STATE_OFF 提示:启用可检测性即可自动启用蓝牙。如果您计划在执行蓝牙 Activity 之前一直启用设备的可检测性,则可以跳过上述步骤 2。如需了解详情,请阅读的启用可检测性部分。
getBondedDevices() ,会返回一组表示已配对设备的 BluetoothDevice 对象。val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices pairedDevices?.forEach { device -> val deviceName = device.name val deviceHardwareAddress = device.address // MAC address }
BluetoothDevice 对象中获取MAC地址,可的通过调用 getAddress() 检索此地址。有关创建连接的详情,请参阅连接设备部分。注意:
startDiscovery() 开始发现设备。该进程为异步操作,返回一个布尔值,指示进程是否成功启动。ACTION_FOUND Intent注册一个 BroadcastReceiver ,以便接收每台发现的设备的相关信息。系统会为每台设备广播此Intent。Intent包含额外字段 EXTRA_DEVICE 和 EXTRA_CLASS ,二者又分别包含 BluetoothDevice 和BluetoothClassoverride fun onCreate(savedInstanceState: Bundle?) { ... // Register for broadcasts when a device is discovered. val filter = IntentFilter(BluetoothDevice.ACTION_FOUND) registerReceiver(receiver, filter) } // Create a BroadcastReceiver for ACTION_FOUND. private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action: String = intent.action when(action) { BluetoothDevice.ACTION_FOUND -> { // Discovery has found a device. Get the BluetoothDevice // object and its info from the Intent. val device: BluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) val deviceName = device.name val deviceHardwareAddress = device.address // MAC address } } } } override fun onDestroy() { super.onDestroy() ... // Don't forget to unregister the ACTION_FOUND receiver. unregisterReceiver(receiver) }
ACTION_REQUEST_DISCOVERABLE Intent调用 startActivityForResult(Intent, int) ,这样便可发出启用系统可检测到模式的请求。EXTRA_DISCOVERABLE_DURATION Extra属性,定义不同的持续时间,最长可达3600s。若设置为0,则设备始终处于可检测到模式(安全性低,不建议使用)val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply { putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300) } startActivity(discoverableIntent)
onActivityResult() 回调的调用。RESULT_CANCELED注意:如果尚未在设备上启用蓝牙,则启用设备可检测性会自动启用蓝牙。
ACTION_SCAN_MODE_CHANGED Intent注册BroadcastReceiver。EXTRA_SCAN_MODE 和 EXTRA_PREVIOUT_SCAN_MODE 二者分别提供新的和旧的扫描模式。每个Extra属性可能拥有以下值:SCAN_MODE_CONNECTABLE_DISCOVERABLE 设备处于可检测到模式。SCAN_MODE_CONNECTABLE 设备未处于可检测到模式,但仍能收到连接。SCAN_MODE_NONE 设备未处于可检测到模式,且无法收到连接。注意: 如果两台设备之前尚未配对,则在连接过程中,Android 框架会自动向用户显示配对请求通知或对话框。因此,在尝试连接设备时,您的应用无需担心设备是否已配对。
BluetoothServerSocket ,从而充当服务器。服务器套接字涌入是监听传入的连接请求,并在接受请求后提供已连接的 BluetoothSocket 。从BluetoothServerSocket 中获取BluetoothSocket 后,应该丢弃 BluetoothServerSocket ,除非设备需要接受更多的连接。listenUsingRfcommWithServiceRecord()获取 BluetoothServerSocket。fromString(String) 初始化一个 UUID。accept()开始侦听连接请求。accept()将返回已连接的 BluetoothSocket。close()。accept()所返回的已连接的 BluetoothSocket。与 TCP/IP 不同,RFCOMM 一次只允许每个通道有一个已连接的客户端,因此大多数情况下,在接受已连接的套接字后,您可以立即在 BluetoothServerSocket上调用 close()。accept() 为阻塞调用,因此不应在主Activity界面线程中执行该应用。accept() 等被阻塞的调用,需要通过另一个线程,在BluetoothServerSocket 或BluetoothSocket 上调用 close() 。请注意,BluetoothServerSocket 或 BluetoothSocket 中的所有方法都是线程安全的方法。private inner class AcceptThread : Thread() { private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) { bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID) } override fun run() { // Keep listening until exception occurs or a socket is returned. var shouldLoop = true // 只需要一个传入连接,因此在接受连接并获取 BluetoothSocket 之后,应用会立即将获取的 BluetoothSocket 传送到单独的线程、关闭 BluetoothServerSocket 并中断循环。 while (shouldLoop) { val socket: BluetoothSocket? = try { // 如果 accept() 返回 BluetoothSocket,则表示已连接套接字。因此,您不应像从客户端那样调用 connect()。 mmServerSocket?.accept() } catch (e: IOException) { Log.e(TAG, "Socket's accept() method failed", e) shouldLoop = false null } socket?.also { // 应用特定的 manageMyConnectedSocket() 方法旨在启动用于传输数据的线程。 manageMyConnectedSocket(it) mmServerSocket?.close() shouldLoop = false } } } // Closes the connect socket and causes the thread to finish. fun cancel() { // 通常,在完成传入连接的侦听后,您应立即关闭您的 BluetoothServerSocket。在此示例中,获取 BluetoothSocket 后会立即调用 close()。此外,您可能还希望在线程中提供一个公共方法,以便在需要停止侦听服务器套接字时关闭私有 BluetoothSocket。 try { mmServerSocket?.close() } catch (e: IOException) { Log.e(TAG, "Could not close the connect socket", e) } } }
BluetoothDevice 对象。然后,必须使用该对象来获取 BluetoothSocket 并发起连接BluetoothDevice,通过调用 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket。BluetoothSocket 对象,以便客户端连接至 BluetoothDevice。此处传递的 UUID 必须与服务器设备在调用 listenUsingRfcommWithServiceRecord(String, UUID) 开放其 BluetoothServerSocket 时所用的 UUID 相匹配。如要使用匹配的 UUID,请通过硬编码方式将 UUID 字符串写入您的应用,然后通过服务器和客户端代码引用该字符串。connect() 发起连接。请注意,此方法为阻塞调用。connect() 方法将会返回。如果连接失败,或者 connect() 方法超时(约 12 秒后),则此方法将引发 IOException。connect() 是阻塞调用,因此您应始终在主 Activity(界面)线程以外的线程中执行此连接步骤。注意:您应始终调用cancelDiscovery(),以确保设备在您调用connect()之前不会执行设备发现。如果正在执行发现操作,则会大幅降低连接尝试的速度,并增加连接失败的可能性。
private inner class ConnectThread(device: BluetoothDevice) : Thread() { private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) { device.createRfcommSocketToServiceRecord(MY_UUID) } public override fun run() { // Cancel discovery because it otherwise slows down the connection. bluetoothAdapter?.cancelDiscovery() mmSocket?.use { socket -> // Connect to the remote device through the socket. This call blocks // until it succeeds or throws an exception. socket.connect() // The connection attempt succeeded. Perform work associated with // the connection in a separate thread. // 应用特定 manageMyConnectedSocket() 方法旨在启动用于传输数据的线程 manageMyConnectedSocket(socket) } } // Closes the client socket and causes the thread to finish. fun cancel() { try { // 使用完 BluetoothSocket 后,请务必调用 close()。这样,您便可立即关闭连接的套接字,并释放所有相关的内部资源。 mmSocket?.close() } catch (e: IOException) { Log.e(TAG, "Could not close the client socket", e) } } }
BluetoothSocket ,可以在设备之间共享信息。BluetoothSocket 传输数据的一般过程如下:getInputStream() 和 getOutputStream(),分别获取通过套接字处理数据传输的 InputStream 和 OutputStream。read(byte[]) 和 write(byte[]) 读取数据以及将其写入数据流。read(byte[]) 和 write(byte[]) 方法都是阻塞调用。read(byte[]) 方法将会阻塞,直至从数据流中读取数据。write(byte[]) 方法通常不会阻塞,但若远程设备调用 read(byte[]) 方法的速度不够快,进而导致中间缓冲区已满,则该方法可能会保持阻塞状态以实现流量控制。因此,线程中的主循环应专门用于从 InputStream 中读取数据。您可使用线程中单独的公共方法,发起对 OutputStream 的写入操作。private const val TAG = "MY_APP_DEBUG_TAG" // Defines several constants used when transmitting messages between the // service and the UI. const val MESSAGE_READ: Int = 0 const val MESSAGE_WRITE: Int = 1 const val MESSAGE_TOAST: Int = 2 // ... (Add other message types here as needed.) class MyBluetoothService( // handler that gets info from Bluetooth service private val handler: Handler ) { private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() { private val mmInStream: InputStream = mmSocket.inputStream private val mmOutStream: OutputStream = mmSocket.outputStream private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer store for the stream // 当构造函数获取必要的数据流后,线程会等待通过 InputStream 传入的数据。当 read(byte[]) 返回数据流中的数据时,将使用来自父类的 Handler 成员将数据发送到主 Activity。然后,线程会等待从 InputStream 中读取更多字节。 override fun run() { var numBytes: Int // bytes returned from read() // Keep listening to the InputStream until an exception occurs. while (true) { // Read from the InputStream. numBytes = try { mmInStream.read(mmBuffer) } catch (e: IOException) { Log.d(TAG, "Input stream was disconnected", e) break } // Send the obtained bytes to the UI activity. val readMsg = handler.obtainMessage( MESSAGE_READ, numBytes, -1, mmBuffer) readMsg.sendToTarget() } } // Call this from the main activity to send data to the remote device. // 发送传出数据不外乎从主 Activity 调用线程的 write() 方法,并传入要发送的字节。此方法会调用 write(byte[]),从而将数据发送到远程设备。如果在调用 write(byte[]) 时引发 IOException,则线程会发送一条 Toast 至主 Activity,向用户说明设备无法将给定的字节发送到另一台(连接的)设备。 fun write(bytes: ByteArray) { try { mmOutStream.write(bytes) } catch (e: IOException) { Log.e(TAG, "Error occurred when sending data", e) // Send a failure message back to the activity. val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST) val bundle = Bundle().apply { putString("toast", "Couldn't send data to the other device") } writeErrorMsg.data = bundle handler.sendMessage(writeErrorMsg) return } // Share the sent message with the UI activity. val writtenMsg = handler.obtainMessage( MESSAGE_WRITE, -1, -1, mmBuffer) writtenMsg.sendToTarget() } // Call this method from the main activity to shut down the connection. // 借助线程的 cancel() 方法,您可通过关闭 BluetoothSocket 随时终止连接。当您结束蓝牙连接的使用时,应始终调用此方法。 fun cancel() { try { mmSocket.close() } catch (e: IOException) { Log.e(TAG, "Could not close the connect socket", e) } } } }
BluetoothAdapter表示本地蓝牙适配器(蓝牙无线装置)。BluetoothAdapter 是所有蓝牙交互的入口点。借助该类,您可以发现其他蓝牙设备、查询已绑定(已配对)设备的列表、使用已知的 MAC 地址实例化 BluetoothDevice,以及通过创建 BluetoothServerSocket 侦听来自其他设备的通信。BluetoothDevice表示远程蓝牙设备。借助该类,您可以通过 BluetoothSocket 请求与某个远程设备建立连接,或查询有关该设备的信息,例如设备的名称、地址、类和绑定状态等。BluetoothSocket表示蓝牙套接字接口(类似于 TCP Socket)。这是允许应用使用 InputStream 和 OutputStream 与其他蓝牙设备交换数据的连接点。BluetoothServerSocket表示用于侦听传入请求的开放服务器套接字(类似于 TCP ServerSocket)。如要连接两台 Android 设备,其中一台设备必须使用此类开放一个服务器套接字。当远程蓝牙设备向此设备发出连接请求时,该设备接受连接,然后返回已连接的 BluetoothSocket。BluetoothClass描述蓝牙设备的一般特征和功能。这是一组只读属性,用于定义设备的类和服务。虽然这些信息会提供关于设备类型的有用提示,但该类的属性未必描述设备支持的所有蓝牙配置文件和服务。BluetoothProfile表示蓝牙配置文件的接口。蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范。举个例子:免提配置文件。如需了解有关配置文件的详细讨论,请参阅使用配置文件。BluetoothHeadset提供蓝牙耳机支持,以便与手机配合使用。这包括蓝牙耳机配置文件和免提 (v1.5) 配置文件。BluetoothA2dp定义如何使用蓝牙立体声音频传输配置文件 (A2DP),通过蓝牙连接将高质量音频从一个设备流式传输至另一个设备。BluetoothHealth表示用于控制蓝牙服务的健康设备配置文件代理。BluetoothHealthCallback用于实现 BluetoothHealth 回调的抽象类。您必须扩展此类并实现回调方法,以接收关于应用注册状态和蓝牙通道状态变化的更新内容。BluetoothHealthAppConfiguration表示第三方蓝牙健康应用注册的应用配置,该配置旨在实现与远程蓝牙健康设备的通信。BluetoothProfile.ServiceListener当 BluetoothProfile 进程间通信 (IPC) 客户端连接到运行特定配置文件的内部服务或断开该服务连接时,向该客户端发送通知的接口。