前言
- 最近业务上需要用到蓝牙与硬件交互,经了解,现有分为传统蓝牙和低功耗蓝牙(BLE),本篇讲解传统蓝牙使用
- 现在市面上蓝牙模块大多数都支持低功耗蓝牙,传统蓝牙适用于较为耗电的操作,如 Android 设备之间的流式传输和通信等,使用场景有限。
- Android 平台包含蓝牙网络堆栈支持,此支持能让设备以无线方式与其他蓝牙设备交换数据。应用框架提供通过 Android Bluetooth API 访问蓝牙功能的权限。这些 API 允许应用以无线方式连接到其他蓝牙设备,从而实现点到点和多点无线功能。
权限
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
设备是否支持蓝牙
- BluetoothAdapter表示设备自身的蓝牙适配器,整个系统只有一个蓝牙适配器,使用此对象进行交互。
private val mBluetoothAdapter: BluetoothAdapter? by lazy {
BluetoothAdapter.getDefaultAdapter()
}
fun isSupportBluetooth(): Boolean {
return mBluetoothAdapter != null
}
蓝牙是否打开
fun isEnabled(): Boolean {
return mBluetoothAdapter?.isEnabled == true
}
开启蓝牙
- 这里使用了透明的fragment获取 onActivityResult() 返回的结果,方便处理
fun openBluetooth(activity: FragmentActivity, callback: ((Boolean) -> Unit)? = null) {
if (!isEnabled()) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
if (callback != null) {
InvisibleFragment.instance(activity).apply {
callResult(REQUEST_ENABLE_BT) { _, _ -> callback.invoke(isEnabled()) }
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
} else {
activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
} else {
Timber.tag(TAG).d("蓝牙已开启")
}
}
蓝牙改变监听
fun registerBluetoothChangeListener(listener: (Boolean) -> Unit) {
this.mBluetoothChangeListener = listener
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
getAppContext().registerReceiver(mBluetoothChangeReceiver, filter)
}
fun unregisterBluetoothChangeListener() {
getAppContext().unregisterReceiver(mBluetoothChangeReceiver)
}
private val mBluetoothChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)) {
BluetoothAdapter.STATE_TURNING_ON -> {
Timber.tag(TAG).d("蓝牙正在打开")
}
BluetoothAdapter.STATE_TURNING_OFF -> {
Timber.tag(TAG).d("蓝牙正在关闭")
}
BluetoothAdapter.STATE_ON -> {
Timber.tag(TAG).d("蓝牙已开启")
mBluetoothChangeListener?.invoke(true)
}
BluetoothAdapter.STATE_OFF -> {
Timber.tag(TAG).d("蓝牙已关闭")
mBluetoothChangeListener?.invoke(false)
}
}
}
}
}
获取已配对设备
- 被配对是指两台设备知晓彼此的存在,具有可用于身份验证的共享链路密钥,并且能够与彼此建立加密连接。
- 被连接是指设备当前共享一个 RFCOMM 通道,并且能够向彼此传输数据。当前的 Android Bluetooth API 要求规定,只有先对设备进行配对,然后才能建立 RFCOMM 连接。在使用 Bluetooth API 发起加密连接时,系统会自动执行配对。
fun getPairedDevices(): MutableSet<BluetoothDevice>? {
return mBluetoothAdapter?.bondedDevices
}
val list = BluetoothUtils.getPairedDevices()
?.map { BluetoothDeviceVo(it.name, it.address) }
?.toMutableList()
扫描 / 发现设备
- 执行设备发现将消耗蓝牙适配器的大量资源。在找到要连接的设备后,使用 cancelDiscovery() 停止发现,然后再尝试连接。此外,不应在连接到设备的情况下执行设备发现,因为发现过程会大幅减少可供任何现有连接使用的带宽。
- 发现设备,只需调用 startDiscovery()。该进程为异步操作,并且会返回一个布尔值,判断发现进程是否已成功启动。发现进程通常包含约 12 秒钟的查询扫描,随后会对发现的每台设备进行页面扫描,以检索其蓝牙名称。
- Android 6.0后需要开启定位信息,才可以扫描蓝牙设备。
fun scanDevice(callback: (BluetoothDevice?) -> Unit) {
this.mScanDeviceCallback = callback
if (mBluetoothAdapter?.startDiscovery() == true) {
val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
getAppContext().registerReceiver(mScanDeviceReceiver, filter)
} else {
Timber.tag(TAG).d("开启扫描设备失败")
}
}
private val mScanDeviceReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == BluetoothDevice.ACTION_FOUND) {
val device: BluetoothDevice? =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
mScanDeviceCallback?.invoke(device)
}
}
}
fun cancelScanDevice() {
try {
mBluetoothAdapter?.cancelDiscovery()
getAppContext().unregisterReceiver(mScanDeviceReceiver)
} catch (e: Exception) {
e.printStackTrace()
}
}
位置信息
fun checkLocation(): Boolean {
val manager = getAppContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
return LocationManagerCompat.isLocationEnabled(manager)
}
fun openLocation(activity: FragmentActivity,callback: ((Boolean) -> Unit)? = null) {
val locationIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
if (callback != null) {
InvisibleFragment.instance(activity).apply {
callResult(REQUEST_ENABLE_LOCATION) { _, _ -> callback.invoke(checkLocation()) }
startActivityForResult(locationIntent, REQUEST_ENABLE_LOCATION)
}
} else {
activity.startActivityForResult(locationIntent, REQUEST_ENABLE_BT)
}
}
启用可检测性(可被其他设备发现)
- 启用可检测性若蓝牙未开启可自动开启蓝牙。
- 默认情况下,设备处于可检测到模式的时间为 120 秒(2 分钟),最高可为3600秒(一小时)。
- 若可检测时间设为0,则设备将始终处于可检测到模式。此配置安全性低,非常不建议使用。
- 如果要发起对远程设备的连接,则无需启用设备可检测性。只有当应用对接受传入连接的服务器套接字进行托管时,才有必要启用可检测性,因为在发起对其他设备的连接之前,远程设备必须能够发现这些设备。
fun enableDiscoverable(activity: FragmentActivity,
time: Int? = null,
callback: ((Boolean) -> Unit)? = null) {
val discoverableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
time?.let {
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, it)
}
if (callback != null) {
InvisibleFragment.instance(activity).apply {
callResult(REQUEST_ENABLE_DISCOVERABLE) { _, resultCode ->
callback.invoke(resultCode != Activity.RESULT_CANCELED)
}
startActivityForResult(discoverableIntent, REQUEST_ENABLE_DISCOVERABLE)
}
} else {
activity.startActivityForResult(discoverableIntent, REQUEST_ENABLE_DISCOVERABLE)
}
}
可检测状态监听
fun registerBluetoothDiscoverableListener(listener: (Boolean) -> Unit) {
this.mBluetoothDiscoverableListener = listener
val filter = IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)
getAppContext().registerReceiver(mBluetoothDiscoverableReceiver, filter)
}
fun unregisterBluetoothDiscoverableListener() {
getAppContext().unregisterReceiver(mBluetoothDiscoverableReceiver)
}
private val mBluetoothDiscoverableReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == BluetoothAdapter.ACTION_SCAN_MODE_CHANGED) {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, 0)) {
BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE -> {
Timber.tag(TAG).d("设备处于可检测到模式")
mBluetoothChangeListener?.invoke(true)
}
BluetoothAdapter.SCAN_MODE_CONNECTABLE -> {
Timber.tag(TAG).d("设备未处于可检测到模式,但仍能收到连接")
}
BluetoothAdapter.SCAN_MODE_NONE -> {
Timber.tag(TAG).d("设备未处于可检测到模式,且无法收到连接")
mBluetoothChangeListener?.invoke(false)
}
}
}
}
}
连接设备
- 如果在两台设备之间创建连接,必须同时实现客户端和服务端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须使用服务器设备的MAC地址发起连接。服务端设备和客户端设备均会以不同方法获取所需的BluetoothSocket。接受传入连接后,服务器会收到套接字信息。在开启与服务器连接的RFCOMM通道时,客户端会提供套接字信息。
- 当服务器和客户端在同一 RFCOMM 通道上分别拥有已连接的 BluetoothSocket 时,即可将二者视为彼此连接。这种情况下,每台设备都能获得输入和输出流式传输,并开始传输数据。
- 如果两台设备之前尚未配对,则在连接过程中,Android 框架会自动向用户显示配对请求通知或对话框。因此,在尝试连接设备时,应用无需担心设备是否已配对。在成功配对两台设备之前, RFCOMM 连接尝试会一直阻塞,并且如果用户拒绝配对,或者配对过程失败或超时,则该尝试便会失败。
- 一种实现技术是自动将每台设备准备为一个服务器,从而使每台设备开放一个服务器套接字并侦听连接。在此情况下,任一设备都可发起与另一台设备的连接,并成为客户端。或者,其中一台设备可显式托管连接并按需开放一个服务器套接字,而另一台设备则发起连接。
启动服务
- 通过调用 listenUsingRfcommWithServiceRecord() 获取 BluetoothServerSocket,这里uuid双方使用同一标识。
- 开启子线程调用 accept() 阻塞侦听连接请求,接收请求后关闭当前服务。
- 通过keep和mIsAccept判断是否需要保持服务。
fun start(name: String, keep: Boolean = false) {
close()
mIsAccept = true
mServerSocket = BtUtils.mBluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(name, mUUID)
mPool.execute {
do {
while (mIsAccept) {
try {
mServerSocket?.accept()
} catch (e: IOException) {
Timber.tag(BtUtils.TAG).d(e, "socket的accept()方法失败")
try {
mServerSocket?.close()
} catch (e: IOException) {
Timber.tag(BtUtils.TAG).e(e, "关闭ServerSocket失败")
}
break
}?.also {
connected(it)
}
}
} while (keep && mIsAccept)
}
}
fun close() {
mIsAccept = false
try {
mServerSocket?.close()
} catch (e: IOException) {
Timber.tag(BtUtils.TAG).e(e, "关闭ServerSocket失败")
}
}
连接服务/设备
- 开启子线程,关闭扫描,通过调用 device.createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket对象,UUID双方保持一致,确保device已启动服务。
- 通过调用 connect() 发起连接。
fun connect(device: BluetoothDevice) {
mPool.execute {
BtUtils.cancelScanDevice()
val socket = device.createRfcommSocketToServiceRecord(mUUID)
try {
socket.connect()
} catch (e: IOException) {
Timber.tag(TAG).e(e, "连接失败-> deviceName:${device.name}")
ThreadUtils.runOnUiThread {
mStatusListener?.invoke(device, BtAction.STATE_CONNECT_FAILED, e)
}
return@execute
}
connected(socket)
}
}
管理连接
- 通过socket.remoteDevice获取远程的BluetoothDevice对象,获取address(mac)地址,通过map保存已连接的socket。
- 开启子线程接收对方传过来的消息
private fun connected(socket: BluetoothSocket) {
val device = socket.remoteDevice
ThreadUtils.runOnUiThread { mStatusListener?.invoke(device, BtAction.STATE_CONNECT, null) }
Timber.tag(TAG).d("已连接-> deviceName:${device.name};deviceAddress:${device.address}")
mBtMap[socket.remoteDevice.address] = socket
mPool.execute {
receiveMessage(socket)
}
}
private fun receiveMessage(socket: BluetoothSocket) {
val inputStream = socket.inputStream
val buffer = ByteArray(1024)
val device = socket.remoteDevice
val name = device.name
var numBytes: Int
while (true) {
numBytes = try {
inputStream.read(buffer)
} catch (e: IOException) {
ThreadUtils.runOnUiThread { mStatusListener?.invoke(device, BtAction.STATE_DISCONNECT, e) }
Timber.tag(TAG).d(e, "设备 $name 断开连接")
mBtMap.remove(device.address)
break
}
val message = String(buffer, 0, numBytes)
Timber.tag(TAG).d("接收到设备 $name 的消息:${message}")
ThreadUtils.runOnUiThread { mReceiveListener?.invoke(device, message) }
}
}
发送消息
fun send(address: String, data: ByteArray, callback: ((Boolean) -> Unit)? = null) {
val socket = mBtMap[address]
if (socket == null) {
Timber.tag(TAG).e("未获取到设备mac地址为 $address 的socket,发送消息失败")
callback?.invoke(false)
return
}
if (!socket.isConnected) {
Timber.tag(TAG).e("未连接到设备mac地址为 $address 的socket,发送消息失败")
callback?.invoke(false)
return
}
mPool.execute {
try {
socket.outputStream.write(data)
Timber.tag(TAG).d("发送到设备mac地址为 $address 的消息成功")
callback?.let { ThreadUtils.runOnUiThread { it.invoke(true) } }
} catch (e: IOException) {
callback?.let { ThreadUtils.runOnUiThread { it.invoke(false) } }
Timber.tag(TAG).e(e, "发送到设备mac地址为 $address 的消息失败")
}
}
}
断开连接
fun disconnect(address: String) {
val socket = mBtMap[address] ?: return Timber.tag(TAG).e("未获取到设备mac地址为 $address 的socket,断开连接失败")
try {
socket.close()
} catch (e: IOException) {
Timber.tag(BtUtils.TAG).e(e, "关闭bluetoothSocket失败")
}
}
结束语