Android 蓝牙设备连接广播详解-新
文章目录
- [Android 蓝牙设备连接广播详解-新](#Android 蓝牙设备连接广播详解-新)
-
- 一、前言
- 二、蓝牙权限配置
-
- [2.1 基础蓝牙权限](#2.1 基础蓝牙权限)
- [2.2 Android 12 及以上版本权限](#2.2 Android 12 及以上版本权限)
- 三、蓝牙连接状态相关广播
-
- [3.1 主要广播 Action](#3.1 主要广播 Action)
- [3.2 各广播的区别与使用场景(重要)](#3.2 各广播的区别与使用场景(重要))
-
- [3.2.1 ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED](#3.2.1 ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED)
- [3.2.2 BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED](#3.2.2 BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
- [3.2.3 BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED(推荐)](#3.2.3 BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED(推荐))
- [3.2.4 广播选择对比总结](#3.2.4 广播选择对比总结)
- [3.3 ACL 连接广播详解](#3.3 ACL 连接广播详解)
- 四、广播接收器实现
-
- [4.1 静态注册方式](#4.1 静态注册方式)
- [4.2 动态注册方式](#4.2 动态注册方式)
- [4.3 广播接收器完整实现](#4.3 广播接收器完整实现)
- 五、获取广播中的额外数据
-
- [5.1 常用 Extra 字段](#5.1 常用 Extra 字段)
- [5.2 获取设备信息示例](#5.2 获取设备信息示例)
- [六、经典蓝牙与 BLE 的区别](#六、经典蓝牙与 BLE 的区别)
-
- [6.1 经典蓝牙连接广播](#6.1 经典蓝牙连接广播)
- [6.2 BLE 连接状态监听](#6.2 BLE 连接状态监听)
- 七、注意事项
-
- [7.1 Android 版本适配](#7.1 Android 版本适配)
- [7.2 动态注册优先](#7.2 动态注册优先)
- [7.3 内存泄漏问题](#7.3 内存泄漏问题)
- [7.4 广播接收时机](#7.4 广播接收时机)
- [八、完整工具类封装(推荐使用 A2DP 广播)](#八、完整工具类封装(推荐使用 A2DP 广播))
-
- [8.1 使用示例](#8.1 使用示例)
- 九、常见问题与解决方案
-
- [9.1 为什么 ACTION_ACL_CONNECTED 不准确?](#9.1 为什么 ACTION_ACL_CONNECTED 不准确?)
- [9.2 为什么多设备连接时第二个设备收不到广播?](#9.2 为什么多设备连接时第二个设备收不到广播?)
- [9.3 如何判断蓝牙设备的连接类型?](#9.3 如何判断蓝牙设备的连接类型?)
- [十、蓝牙 Profile 详解:A2DP、Headset、HID 的区别](#十、蓝牙 Profile 详解:A2DP、Headset、HID 的区别)
-
- [10.1 BluetoothA2dp(Advanced Audio Distribution Profile)](#10.1 BluetoothA2dp(Advanced Audio Distribution Profile))
-
- [10.1.1 简介](#10.1.1 简介)
- [10.1.2 核心特点](#10.1.2 核心特点)
- [10.1.3 应用场景](#10.1.3 应用场景)
- [10.1.4 代码示例](#10.1.4 代码示例)
- [10.2 BluetoothHeadset(Headset Profile / Hands-Free Profile)](#10.2 BluetoothHeadset(Headset Profile / Hands-Free Profile))
-
- [10.2.1 简介](#10.2.1 简介)
- [10.2.2 核心特点](#10.2.2 核心特点)
- [10.2.3 A2DP 与 Headset 的区别](#10.2.3 A2DP 与 Headset 的区别)
- [10.2.4 应用场景](#10.2.4 应用场景)
- [10.2.5 代码示例](#10.2.5 代码示例)
- [10.3 BluetoothHidDevice(Human Interface Device Profile)](#10.3 BluetoothHidDevice(Human Interface Device Profile))
-
- [10.3.1 简介](#10.3.1 简介)
- [10.3.2 核心特点](#10.3.2 核心特点)
- [10.3.3 应用场景](#10.3.3 应用场景)
- [10.3.4 代码示例](#10.3.4 代码示例)
- [10.4 三种 Profile 对比总结](#10.4 三种 Profile 对比总结)
-
- [10.4.1 功能对比表](#10.4.1 功能对比表)
- [10.4.2 设备连接流程对比](#10.4.2 设备连接流程对比)
- [10.4.3 选择建议](#10.4.3 选择建议)
- [10.5 综合使用示例:监听所有类型的蓝牙设备](#10.5 综合使用示例:监听所有类型的蓝牙设备)
- 十一、总结
- 参考资料
一、前言
在 Android 蓝牙开发过程中,监听蓝牙设备的连接状态变化是一项非常常见的需求。
无论是开发蓝牙音频设备、健康监测设备还是智能家居应用,都需要实时获取蓝牙设备的连接状态。
Android 系统通过广播机制将蓝牙设备的各种状态变化通知给应用程序,
蓝牙开关和发现那些广播这里就不介绍了,主要介绍蓝牙状态的广播;
问题:多个蓝牙设备连接的时候,有些设备断开连接就没有状态广播了,这个需要如何解决?
如果不想看过程,直接后面总结的关键点,就知道答案了。
本文将详细介绍 Android 蓝牙设备连接相关的广播及其使用方法。
二、蓝牙权限配置
在监听蓝牙广播之前,首先需要在 AndroidManifest.xml 文件中声明相应的权限。
2.1 基础蓝牙权限
xml
<!-- 基础蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
2.2 Android 12 及以上版本权限
从 Android 12 (API 31) 开始,蓝牙权限进行了细化,需要申请新的权限:
xml
<!-- Android 12+ 蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
注意 :如果应用不需要物理定位功能,建议添加
android:usesPermissionFlags="neverForLocation"属性,以便在应用商店中避免不必要的隐私声明。
三、蓝牙连接状态相关广播
3.1 主要广播 Action
以下是蓝牙设备连接状态相关的核心广播:
| 广播 Action | 说明 |
|---|---|
BluetoothDevice.ACTION_ACL_CONNECTED |
蓝牙设备 ACL 链路已连接 |
BluetoothDevice.ACTION_ACL_DISCONNECTED |
蓝牙设备 ACL 链路已断开连接 |
BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED |
蓝牙设备正在请求断开连接 |
BluetoothAdapter.ACTION_STATE_CHANGED |
蓝牙适配器开关状态改变 |
BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED |
蓝牙适配器整体连接状态改变 |
BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED |
A2DP 音频设备连接状态改变 |
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED |
蓝牙耳机连接状态改变 |
BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED |
HID 设备连接状态改变 |
3.2 各广播的区别与使用场景(重要)
在实际开发中,正确选择广播类型非常重要,不同的广播有不同的触发时机和适用场景:
3.2.1 ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED
触发时机:设备完成配对后建立 ACL 物理链路时触发。
重要特点:
- 该广播表示蓝牙底层物理链路的连接状态,不是真正的音频连接状态
- 设备配对后就会触发
ACTION_ACL_CONNECTED,此时音频通道可能还未建立 - 如果使用此广播来刷新蓝牙音箱的连接状态,会导致状态不准确
适用场景:需要监听蓝牙设备物理链路连接状态的场景。
java
// ⚠️ 注意:不建议用此广播判断蓝牙音箱是否真正连接成功
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
// 此时设备可能只是配对连接,音频通道可能还未建立
// 对于蓝牙音箱场景,这并不是最佳的连接状态判断方式
}
3.2.2 BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED
触发时机:蓝牙适配器的整体连接状态变化时触发。
重要特点:
- 表示整个蓝牙适配器的连接状态,而非具体某个设备
- 在多设备连接场景下存在问题 :当第一个蓝牙音箱已连接,第二个蓝牙音箱连接时,此广播不会触发
- 只能监听是否有设备连接,无法区分具体是哪个设备
适用场景:只需知道蓝牙是否有设备连接的场景,不适合多设备管理。
java
// ⚠️ 注意:多设备连接场景下,第二个设备连接时不会收到此广播
if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE,
BluetoothAdapter.ERROR);
// 只能判断整体是否有设备连接,无法区分具体设备
// 第一个设备连接后,第二个设备连接不会触发此广播
}
3.2.3 BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED(推荐)
触发时机:A2DP 音频设备(如蓝牙音箱、蓝牙耳机)的音频连接状态变化时触发。
重要特点:
- 推荐使用此广播监听蓝牙音箱/耳机的连接状态
- 能够正确接收多个蓝牙音箱同时连接时的状态变化
- 每个设备的连接和断开都能准确接收到
- 需要获取
BluetoothA2dp服务实例才能正常工作
适用场景:蓝牙音箱、蓝牙耳机等 A2DP 音频设备的连接状态监听。
java
// ✅ 推荐:使用 A2DP 广播监听蓝牙音箱连接状态
public static final String ACTION_A2DP_CONNECTION_STATE_CHANGED =
"android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED";
// 注册广播时需要添加此 Action
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
// 处理 A2DP 连接状态变化
if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
BluetoothProfile.STATE_DISCONNECTED);
if (state == BluetoothProfile.STATE_CONNECTED) {
// 音频设备真正连接成功,可以播放音频
Log.d(TAG, "A2DP 设备已连接: " + device.getName());
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
// 音频设备断开连接
Log.d(TAG, "A2DP 设备已断开: " + device.getName());
}
}
3.2.4 广播选择对比总结
| 广播类型 | 多设备支持 | 音频连接准确性 | 推荐场景 |
|---|---|---|---|
| ACTION_ACL_CONNECTED | ✅ 支持 | ❌ 不准确(配对即触发) | 物理链路监听 |
| ACTION_CONNECTION_STATE_CHANGED (Adapter) | ❌ 不支持 | ⚠️ 有限支持 | 单设备简单场景 |
| ACTION_CONNECTION_STATE_CHANGED (A2DP) | ✅ 支持 | ✅ 准确 | 蓝牙音箱/耳机(推荐) |
| ACTION_CONNECTION_STATE_CHANGED (Headset) | ✅ 支持 | ✅ 准确 | 蓝牙通话耳机 |
3.3 ACL 连接广播详解
ACL(Asynchronous Connection-Less)是蓝牙底层连接,表示蓝牙设备之间的物理链路连接状态。
.
ACTION_ACL_CONNECTED
当蓝牙设备成功建立 ACL 连接时,系统会发送此广播。
java
// 广播 Action
public static final String ACTION_ACL_CONNECTED = "android.bluetooth.device.action.ACL_CONNECTED";
ACTION_ACL_DISCONNECTED
当蓝牙设备 ACL 连接断开时,系统会发送此广播。
java
// 广播 Action
public static final String ACTION_ACL_DISCONNECTED = "android.bluetooth.device.action.ACL_DISCONNECTED";
四、广播接收器实现
4.1 静态注册方式
在 AndroidManifest.xml 中静态注册广播接收器:
xml
<receiver
android:name=".receiver.BluetoothStateReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<action android:name="android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED" />
<!-- A2DP 音频设备连接状态广播(推荐用于蓝牙音箱) -->
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED" />
<!-- 蓝牙耳机连接状态广播 -->
<action android:name="android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED" />
</intent-filter>
</receiver>
4.2 动态注册方式
在代码中动态注册广播接收器(推荐):
java
public class BluetoothManager {
private BluetoothStateReceiver mReceiver;
private Context mContext;
/**
* 注册蓝牙状态广播接收器
*/
public void registerReceiver() {
if (mReceiver == null) {
mReceiver = new BluetoothStateReceiver();
}
IntentFilter filter = new IntentFilter();
// ACL 连接广播
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
// 蓝牙适配器状态广播
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
// A2DP 音频设备连接状态广播(推荐用于蓝牙音箱)
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
// 蓝牙耳机连接状态广播
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
mContext.registerReceiver(mReceiver, filter);
}
}
/**
* 注销广播接收器
*/
public void unregisterReceiver() {
if (mReceiver != null) {
mContext.unregisterReceiver(mReceiver);
mReceiver = null;
}
}
}
4.3 广播接收器完整实现
java
public class BluetoothStateReceiver extends BroadcastReceiver {
private static final String TAG = "BluetoothStateReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
String action = intent.getAction();
Log.d(TAG, "Received action: " + action);
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
handleAclConnected(intent);
} else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) {
handleAclDisconnected(intent);
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
handleAdapterStateChanged(intent);
} else if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
handleAdapterConnectionStateChanged(intent);
} else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
handleA2dpConnectionStateChanged(intent);
} else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
handleHeadsetConnectionStateChanged(intent);
}
}
/**
* 处理 ACL 连接成功
* 注意:此广播在设备配对后就会触发,不代表音频通道已建立
*/
private void handleAclConnected(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null) {
Log.d(TAG, "ACL 链路已连接: " + device.getName() + " - " + device.getAddress());
Log.d(TAG, "注意:此状态不代表音频通道已建立,建议使用 A2DP 广播判断音频连接");
}
}
/**
* 处理 ACL 断开连接
*/
private void handleAclDisconnected(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null) {
Log.d(TAG, "ACL 链路已断开: " + device.getName() + " - " + device.getAddress());
}
}
/**
* 处理蓝牙适配器开关状态变化
*/
private void handleAdapterStateChanged(Intent intent) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
switch (state) {
case BluetoothAdapter.STATE_OFF:
Log.d(TAG, "蓝牙已关闭");
break;
case BluetoothAdapter.STATE_ON:
Log.d(TAG, "蓝牙已开启");
break;
case BluetoothAdapter.STATE_TURNING_OFF:
Log.d(TAG, "蓝牙正在关闭");
break;
case BluetoothAdapter.STATE_TURNING_ON:
Log.d(TAG, "蓝牙正在开启");
break;
}
}
/**
* 处理蓝牙适配器整体连接状态变化
* 注意:多设备连接时,第二个设备连接不会触发此广播
*/
private void handleAdapterConnectionStateChanged(Intent intent) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE,
BluetoothAdapter.ERROR);
int prevState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_CONNECTION_STATE,
BluetoothAdapter.ERROR);
Log.d(TAG, "适配器连接状态变化: " + prevState + " -> " + state);
Log.d(TAG, "注意:多设备场景下,第二个设备连接时不会触发此广播");
switch (state) {
case BluetoothAdapter.STATE_CONNECTED:
Log.d(TAG, "有蓝牙设备已连接");
break;
case BluetoothAdapter.STATE_DISCONNECTED:
Log.d(TAG, "所有蓝牙设备已断开");
break;
case BluetoothAdapter.STATE_CONNECTING:
Log.d(TAG, "蓝牙正在连接");
break;
case BluetoothAdapter.STATE_DISCONNECTING:
Log.d(TAG, "蓝牙正在断开");
break;
}
}
/**
* 处理 A2DP 音频设备连接状态变化(推荐用于蓝牙音箱)
* 特点:支持多设备连接,状态准确
*/
private void handleA2dpConnectionStateChanged(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
BluetoothProfile.STATE_DISCONNECTED);
if (device != null) {
Log.d(TAG, "A2DP 设备连接状态变化: " + device.getName());
Log.d(TAG, "状态: " + prevState + " -> " + state);
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
Log.d(TAG, "A2DP 设备已连接(音频通道已建立)");
// 发送连接成功通知
EventBus.getDefault().post(new BluetoothConnectEvent(device, true));
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "A2DP 设备已断开");
// 发送断开连接通知
EventBus.getDefault().post(new BluetoothConnectEvent(device, false));
break;
case BluetoothProfile.STATE_CONNECTING:
Log.d(TAG, "A2DP 设备正在连接");
break;
case BluetoothProfile.STATE_DISCONNECTING:
Log.d(TAG, "A2DP 设备正在断开");
break;
}
}
}
/**
* 处理蓝牙耳机连接状态变化
*/
private void handleHeadsetConnectionStateChanged(Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
if (device != null) {
Log.d(TAG, "Headset 设备连接状态: " + state);
if (state == BluetoothProfile.STATE_CONNECTED) {
Log.d(TAG, "蓝牙耳机已连接");
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "蓝牙耳机已断开");
}
}
}
}
五、获取广播中的额外数据
蓝牙广播中包含了丰富的额外数据,可以通过 Intent 的 Extra 字段获取。
5.1 常用 Extra 字段
| Extra 字段 | 说明 | 适用广播 |
|---|---|---|
BluetoothDevice.EXTRA_DEVICE |
蓝牙设备对象 | 所有蓝牙广播 |
BluetoothAdapter.EXTRA_STATE |
当前状态 | Adapter 状态广播 |
BluetoothAdapter.EXTRA_PREVIOUS_STATE |
之前的状态 | Adapter 状态广播 |
BluetoothAdapter.EXTRA_CONNECTION_STATE |
当前连接状态 | Adapter 连接广播 |
BluetoothAdapter.EXTRA_PREVIOUS_CONNECTION_STATE |
之前的连接状态 | Adapter 连接广播 |
BluetoothProfile.EXTRA_STATE |
当前 Profile 状态 | A2DP/Headset 广播 |
BluetoothProfile.EXTRA_PREVIOUS_STATE |
之前的 Profile 状态 | A2DP/Headset 广播 |
具体字段新可以看源码中的定义。
5.2 获取设备信息示例
java
/**
* 从 Intent 中获取蓝牙设备详细信息
*/
private void parseDeviceInfo(Intent intent) {
// 获取蓝牙设备对象
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null) {
String name = device.getName(); // 设备名称
String address = device.getAddress(); // MAC 地址
int bondState = device.getBondState(); // 配对状态
int type = device.getType(); // 设备类型
Log.d(TAG, "设备名称: " + name);
Log.d(TAG, "设备地址: " + address);
Log.d(TAG, "配对状态: " + bondStateToString(bondState));
Log.d(TAG, "设备类型: " + deviceTypeToString(type));
}
}
/**
* 配对状态转字符串
*/
private String bondStateToString(int bondState) {
switch (bondState) {
case BluetoothDevice.BOND_NONE:
return "未配对";
case BluetoothDevice.BOND_BONDING:
return "正在配对";
case BluetoothDevice.BOND_BONDED:
return "已配对";
default:
return "未知";
}
}
/**
* 设备类型转字符串
*/
private String deviceTypeToString(int type) {
switch (type) {
case BluetoothDevice.DEVICE_TYPE_CLASSIC:
return "经典蓝牙";
case BluetoothDevice.DEVICE_TYPE_LE:
return "低功耗蓝牙(BLE)";
case BluetoothDevice.DEVICE_TYPE_DUAL:
return "双模蓝牙";
case BluetoothDevice.DEVICE_TYPE_UNKNOWN:
default:
return "未知类型";
}
}
六、经典蓝牙与 BLE 的区别
在实际开发中,经典蓝牙和低功耗蓝牙(BLE)的广播处理有所区别。
6.1 经典蓝牙连接广播
经典蓝牙使用 ACL 连接广播:
ACTION_ACL_CONNECTEDACTION_ACL_DISCONNECTED
6.2 BLE 连接状态监听
对于 BLE 设备,需要使用 BluetoothGatt 回调来监听连接状态:
java
private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d(TAG, "BLE 设备已连接");
// 发现服务
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "BLE 设备已断开");
gatt.close();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "服务发现成功");
// 处理服务发现结果
}
}
};
七、注意事项
7.1 Android 版本适配
不同 Android 版本对蓝牙权限和广播的处理有所差异,需要注意以下要点:
| Android 版本 | 注意事项 |
|---|---|
| Android 6.0+ | 需要动态申请位置权限才能扫描蓝牙设备 |
| Android 10+ | 需要申请后台位置权限 |
| Android 12+ | 需要申请新的蓝牙权限(BLUETOOTH_SCAN/CONNECT) |
| Android 13+ | 建议使用 RECEIVER_NOT_EXPORTED 注册广播 |
7.2 动态注册优先
建议使用动态注册方式,原因如下:
- 静态注册在应用被系统杀死后可能无法接收到广播
- Android 8.0+ 对静态注册广播有诸多限制
- 动态注册更加灵活,可以控制注册和注销时机
7.3 内存泄漏问题
在使用动态注册时,务必在合适的生命周期注销广播接收器:
java
@Override
protected void onDestroy() {
super.onDestroy();
// 注销广播接收器,防止内存泄漏
if (mBluetoothReceiver != null) {
unregisterReceiver(mBluetoothReceiver);
}
}
7.4 广播接收时机
部分广播可能需要在应用运行时才能接收,建议结合以下方式:
- 使用前台服务保持应用活跃
- 使用 WorkManager 定期检查蓝牙状态
- 结合 JobScheduler 进行状态同步
八、完整工具类封装(推荐使用 A2DP 广播)
以下是封装好的蓝牙状态监听工具类,推荐使用 A2DP 广播来监听蓝牙音箱/耳机的连接状态:
java
/**
* 蓝牙状态监听工具类
* 推荐使用 A2DP 广播监听蓝牙音箱/耳机的连接状态
*/
public class BluetoothStateHelper {
private static final String TAG = "BluetoothStateHelper";
private Context mContext;
private BluetoothStateReceiver mReceiver;
private OnBluetoothStateListener mListener;
/**
* 蓝牙状态监听接口
*/
public interface OnBluetoothStateListener {
/**
* A2DP 设备连接成功(推荐使用)
*/
void onA2dpDeviceConnected(BluetoothDevice device);
/**
* A2DP 设备断开连接
*/
void onA2dpDeviceDisconnected(BluetoothDevice device);
/**
* 蓝牙适配器开关状态变化
*/
void onAdapterStateChanged(int state);
}
public BluetoothStateHelper(Context context) {
this.mContext = context.getApplicationContext();
}
/**
* 设置监听器
*/
public void setOnBluetoothStateListener(OnBluetoothStateListener listener) {
this.mListener = listener;
}
/**
* 开始监听
*/
public void startListen() {
if (mReceiver == null) {
mReceiver = new BluetoothStateReceiver();
IntentFilter filter = new IntentFilter();
// 蓝牙适配器状态广播
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
// A2DP 音频设备连接状态广播(推荐)
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
// 蓝牙耳机连接状态广播
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
// ACL 连接广播(可选,用于底层链路监听)
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
mContext.registerReceiver(mReceiver, filter);
}
}
}
/**
* 停止监听
*/
public void stopListen() {
if (mReceiver != null) {
mContext.unregisterReceiver(mReceiver);
mReceiver = null;
}
}
/**
* 内部广播接收器
*/
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
String action = intent.getAction();
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// A2DP 音频设备连接状态变化(推荐用于蓝牙音箱)
if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
if (state == BluetoothProfile.STATE_CONNECTED) {
Log.d(TAG, "A2DP 设备已连接: " + getDeviceInfo(device));
if (mListener != null && device != null) {
mListener.onA2dpDeviceConnected(device);
}
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "A2DP 设备已断开: " + getDeviceInfo(device));
if (mListener != null && device != null) {
mListener.onA2dpDeviceDisconnected(device);
}
}
// 蓝牙适配器开关状态变化
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR);
Log.d(TAG, "蓝牙适配器状态: " + state);
if (mListener != null) {
mListener.onAdapterStateChanged(state);
}
// ACL 链路连接(仅供参考,不建议用于判断音频连接状态)
} else if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
Log.d(TAG, "ACL 链路已连接: " + getDeviceInfo(device) +
"(注意:此状态不代表音频已连接)");
// ACL 链路断开
} else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) {
Log.d(TAG, "ACL 链路已断开: " + getDeviceInfo(device));
}
}
}
/**
* 获取设备信息字符串
*/
private String getDeviceInfo(BluetoothDevice device) {
if (device == null) {
return "null";
}
return device.getName() + " [" + device.getAddress() + "]";
}
}
8.1 使用示例
java
public class MainActivity extends AppCompatActivity {
private BluetoothStateHelper mBluetoothHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化蓝牙状态监听
mBluetoothHelper = new BluetoothStateHelper(this);
mBluetoothHelper.setOnBluetoothStateListener(new BluetoothStateHelper.OnBluetoothStateListener() {
@Override
public void onA2dpDeviceConnected(BluetoothDevice device) {
// 蓝牙音箱真正连接成功,可以开始播放音频
runOnUiThread(() -> {
Toast.makeText(MainActivity.this,
"蓝牙音箱已连接: " + device.getName(),
Toast.LENGTH_SHORT).show();
updateUI(device, true);
});
}
@Override
public void onA2dpDeviceDisconnected(BluetoothDevice device) {
// 蓝牙音箱断开连接
runOnUiThread(() -> {
Toast.makeText(MainActivity.this,
"蓝牙音箱已断开: " + device.getName(),
Toast.LENGTH_SHORT).show();
updateUI(device, false);
});
}
@Override
public void onAdapterStateChanged(int state) {
// 蓝牙开关状态变化
if (state == BluetoothAdapter.STATE_OFF) {
// 蓝牙已关闭,清空设备列表
runOnUiThread(() -> clearDeviceList());
}
}
});
// 开始监听
mBluetoothHelper.startListen();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 停止监听,防止内存泄漏
if (mBluetoothHelper != null) {
mBluetoothHelper.stopListen();
}
}
private void updateUI(BluetoothDevice device, boolean connected) {
// 更新 UI 显示
}
private void clearDeviceList() {
// 清空设备列表
}
}
九、常见问题与解决方案
9.1 为什么 ACTION_ACL_CONNECTED 不准确?
问题 :使用 ACTION_ACL_CONNECTED 判断蓝牙音箱连接状态,发现有时已经收到广播,但音频还不能播放。
原因 :ACTION_ACL_CONNECTED 只表示蓝牙底层的 ACL 物理链路已连接,这是在设备配对成功后就会触发的。此时音频通道(A2DP)可能还未完全建立。
解决方案 :使用 BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED 广播,它在音频通道真正建立连接时才会触发。
9.2 为什么多设备连接时第二个设备收不到广播?
问题:第一个蓝牙音箱连接正常,第二个蓝牙音箱连接时收不到任何广播。
原因 :BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED 表示的是蓝牙适配器的整体连接状态。当第一个设备连接后,适配器状态已经是 "已连接",第二个设备连接时状态没有变化,所以不会触发广播。
解决方案 :使用 BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED 广播,它可以正确接收每个 A2DP 设备的连接状态变化。
9.3 如何判断蓝牙设备的连接类型?
不同类型的蓝牙设备使用不同的 Profile,可以通过注册不同的广播来监听:
| 设备类型 | Profile | 推荐广播 |
|---|---|---|
| 蓝牙音箱 | A2DP | BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED |
| 蓝牙耳机(通话) | HFP/HSP | BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED |
| 蓝牙键盘鼠标 | HID | BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED |
| 健康设备 | HDP | BluetoothHealth.ACTION_CONNECTION_STATE_CHANGED |
十、蓝牙 Profile 详解:A2DP、Headset、HID 的区别
在 Android 蓝牙开发中,不同的蓝牙设备使用不同的 Profile(配置文件)进行通信。理解这些 Profile 的区别对于正确监听设备连接状态至关重要。
10.1 BluetoothA2dp(Advanced Audio Distribution Profile)
10.1.1 简介
A2DP(高级音频分发配置文件)是用于传输高质量立体声音频流的蓝牙 Profile。
10.1.2 核心特点
| 特性 | 说明 |
|---|---|
| 全称 | Advanced Audio Distribution Profile |
| 用途 | 传输高质量立体声音频 |
| 音频质量 | 高质量(支持 AAC、SBC、aptX、LDAC 等编码) |
| 传输方向 | 单向(从手机到播放设备) |
| 典型设备 | 蓝牙音箱、蓝牙耳机(听歌模式) |
10.1.3 应用场景
┌─────────────────────────────────────────────────────────┐
│ A2DP 应用场景 │
├─────────────────────────────────────────────────────────┤
│ 手机 ──────A2DP──────► 蓝牙音箱(播放音乐) │
│ 手机 ──────A2DP──────► 蓝牙耳机(听歌、看视频) │
│ 电脑 ──────A2DP──────► 蓝牙音响(无线播放) │
└─────────────────────────────────────────────────────────┘
10.1.4 代码示例
java
/**
* 监听 A2DP 设备连接状态
* 适用设备:蓝牙音箱、蓝牙耳机(听歌模式)
*/
public class A2dpStateHelper {
private Context mContext;
private A2dpReceiver mReceiver;
private BluetoothA2dp mBluetoothA2dp;
/**
* 注册 A2DP 服务监听
*/
public void registerA2dpService() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
return;
}
// 获取 A2DP Profile 代理
adapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (profile == BluetoothProfile.A2DP) {
mBluetoothA2dp = (BluetoothA2dp) proxy;
Log.d(TAG, "A2DP 服务已连接");
// 获取已连接的 A2DP 设备列表
List<BluetoothDevice> connectedDevices = mBluetoothA2dp.getConnectedDevices();
for (BluetoothDevice device : connectedDevices) {
Log.d(TAG, "已连接的 A2DP 设备: " + device.getName());
}
}
}
@Override
public void onServiceDisconnected(int profile) {
if (profile == BluetoothProfile.A2DP) {
mBluetoothA2dp = null;
Log.d(TAG, "A2DP 服务已断开");
}
}
}, BluetoothProfile.A2DP);
}
/**
* 注册 A2DP 连接状态广播
*/
public void registerReceiver() {
mReceiver = new A2dpReceiver();
IntentFilter filter = new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
mContext.registerReceiver(mReceiver, filter);
}
}
/**
* A2DP 连接状态广播接收器
*/
private class A2dpReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
Log.d(TAG, "A2DP 设备已连接: " + device.getName());
// 此时可以开始播放音乐
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "A2DP 设备已断开: " + device.getName());
// 暂停音乐播放
break;
}
}
}
}
10.2 BluetoothHeadset(Headset Profile / Hands-Free Profile)
10.2.1 简介
Headset Profile 包含 HSP(Headset Profile)和 HFP(Hands-Free Profile),主要用于语音通话场景。
10.2.2 核心特点
| 特性 | 说明 |
|---|---|
| 全称 | Headset Profile / Hands-Free Profile |
| 用途 | 语音通话、电话音频传输 |
| 音频质量 | 较低(主要用于语音,带宽约 8kHz) |
| 传输方向 | 双向(双方都可以说话和听) |
| 典型设备 | 蓝牙耳机(通话模式)、车载蓝牙、蓝牙耳麦 |
10.2.3 A2DP 与 Headset 的区别
| 对比项 | A2DP | Headset (HFP/HSP) |
|---|---|---|
| 主要用途 | 音乐播放、媒体音频 | 电话通话、语音输入 |
| 音频质量 | 高质量立体声 | 单声道语音质量 |
| 传输方向 | 单向(源→接收端) | 双向(双方通话) |
| 延迟要求 | 相对宽松 | 要求低延迟 |
| 典型设备 | 蓝牙音箱 | 车载蓝牙、蓝牙耳麦 |
10.2.4 应用场景
┌─────────────────────────────────────────────────────────┐
│ Headset 应用场景 │
├─────────────────────────────────────────────────────────┤
│ 手机 ←──Headset──► 蓝牙耳机(接听电话) │
│ 手机 ←──Headset──► 车载蓝牙(免提通话) │
│ 手机 ←──Headset──► 蓝牙耳麦(语音通话、语音助手) │
└─────────────────────────────────────────────────────────┘
10.2.5 代码示例
java
/**
* 监听 Headset 设备连接状态
* 适用设备:蓝牙耳机(通话模式)、车载蓝牙、蓝牙耳麦
*/
public class HeadsetStateHelper {
private Context mContext;
private HeadsetReceiver mReceiver;
private BluetoothHeadset mBluetoothHeadset;
/**
* 注册 Headset 服务监听
*/
public void registerHeadsetService() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
return;
}
// 获取 Headset Profile 代理
adapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = (BluetoothHeadset) proxy;
Log.d(TAG, "Headset 服务已连接");
// 获取已连接的 Headset 设备列表
List<BluetoothDevice> connectedDevices = mBluetoothHeadset.getConnectedDevices();
for (BluetoothDevice device : connectedDevices) {
Log.d(TAG, "已连接的 Headset 设备: " + device.getName());
// 检查音频连接状态
int audioState = mBluetoothHeadset.isAudioConnected(device) ? 1 : 0;
Log.d(TAG, "音频连接状态: " + audioState);
}
}
}
@Override
public void onServiceDisconnected(int profile) {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = null;
Log.d(TAG, "Headset 服务已断开");
}
}
}, BluetoothProfile.HEADSET);
}
/**
* 注册 Headset 连接状态广播
*/
public void registerReceiver() {
mReceiver = new HeadsetReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
mContext.registerReceiver(mReceiver, filter);
}
}
/**
* Headset 连接状态广播接收器
*/
private class HeadsetReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// Headset 设备连接状态变化
if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
Log.d(TAG, "Headset 设备已连接: " + device.getName());
// 设备已连接,可以接听电话
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "Headset 设备已断开: " + device.getName());
break;
}
}
// Headset 音频连接状态变化(通话音频通道)
if (BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
switch (state) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
Log.d(TAG, "通话音频已连接: " + device.getName());
// 正在通过蓝牙耳机通话
break;
case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
Log.d(TAG, "通话音频已断开: " + device.getName());
// 通话结束或切换到其他音频设备
break;
}
}
}
}
}
10.3 BluetoothHidDevice(Human Interface Device Profile)
10.3.1 简介
HID(人机接口设备)Profile 用于连接键盘、鼠标、游戏手柄等输入设备。
10.3.2 核心特点
| 特性 | 说明 |
|---|---|
| 全称 | Human Interface Device Profile |
| 用途 | 输入设备的数据传输 |
| 传输数据 | 按键事件、鼠标移动、手势等 |
| 延迟要求 | 极低延迟(毫秒级响应) |
| 典型设备 | 蓝牙键盘、蓝牙鼠标、蓝牙游戏手柄、蓝牙遥控器 |
10.3.3 应用场景
┌─────────────────────────────────────────────────────────┐
│ HID 应用场景 │
├─────────────────────────────────────────────────────────┤
│ 蓝牙键盘 ───HID───► 手机/平板(文字输入) │
│ 蓝牙鼠标 ───HID───► 手机/平板(光标控制) │
│ 游戏手柄 ───HID───► 手机/平板(游戏控制) │
│ 蓝牙遥控器 ───HID──► 电视盒子/智能电视(远程控制) │
└─────────────────────────────────────────────────────────┘
10.3.4 代码示例
java
/**
* 监听 HID 设备连接状态
* 适用设备:蓝牙键盘、蓝牙鼠标、游戏手柄、遥控器
*/
public class HidDeviceHelper {
private Context mContext;
private HidReceiver mReceiver;
private BluetoothHidDevice mBluetoothHidDevice;
/**
* 注册 HID 服务监听
*/
public void registerHidService() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
return;
}
// 获取 HID Profile 代理
adapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (profile == BluetoothProfile.HID_DEVICE) {
mBluetoothHidDevice = (BluetoothHidDevice) proxy;
Log.d(TAG, "HID 服务已连接");
// 获取已连接的 HID 设备列表
List<BluetoothDevice> connectedDevices = mBluetoothHidDevice.getConnectedDevices();
for (BluetoothDevice device : connectedDevices) {
Log.d(TAG, "已连接的 HID 设备: " + device.getName());
}
}
}
@Override
public void onServiceDisconnected(int profile) {
if (profile == BluetoothProfile.HID_DEVICE) {
mBluetoothHidDevice = null;
Log.d(TAG, "HID 服务已断开");
}
}
}, BluetoothProfile.HID_DEVICE);
}
/**
* 注册 HID 连接状态广播
*/
public void registerReceiver() {
mReceiver = new HidReceiver();
IntentFilter filter = new IntentFilter(BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
mContext.registerReceiver(mReceiver, filter);
}
}
/**
* HID 连接状态广播接收器
*/
private class HidReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
Log.d(TAG, "HID 设备已连接: " + device.getName());
// 键盘/鼠标已连接,可以使用输入功能
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "HID 设备已断开: " + device.getName());
break;
}
}
}
}
10.4 三种 Profile 对比总结
10.4.1 功能对比表
| 对比项 | BluetoothA2dp | BluetoothHeadset | BluetoothHidDevice |
|---|---|---|---|
| Profile 名称 | Advanced Audio Distribution | Headset / Hands-Free | Human Interface Device |
| 主要用途 | 媒体音频播放 | 语音通话 | 输入设备控制 |
| 传输数据 | 立体声音频流 | 单声道语音 | 按键/移动数据 |
| 数据方向 | 单向 | 双向 | 单向或双向 |
| 典型设备 | 蓝牙音箱 | 蓝牙耳机/车载 | 键盘/鼠标/手柄 |
| 延迟要求 | 一般 | 低 | 极低 |
| 音频质量 | 高 | 中(语音级别) | 不适用 |
10.4.2 设备连接流程对比
A2DP 连接流程:
配对 → ACL连接 → A2DP连接 → 音频通道建立 → 可播放音乐
↑
监听此广播最准确
Headset 连接流程:
配对 → ACL连接 → Headset连接 → (来电时)音频通道建立 → 可通话
↑
监听此广播最准确
HID 连接流程:
配对 → ACL连接 → HID连接 → 可接收输入数据
↑
监听此广播即可
10.4.3 选择建议
| 你的需求 | 推荐使用的 Profile |
|---|---|
| 监听蓝牙音箱连接状态 | BluetoothA2dp |
| 监听蓝牙耳机通话状态 | BluetoothHeadset |
| 监听车载蓝牙连接状态 | BluetoothHeadset |
| 监听蓝牙键盘/鼠标连接 | BluetoothHidDevice |
| 监听游戏手柄连接状态 | BluetoothHidDevice |
| 同时监听音乐和通话 | A2dp + Headset 都需要注册 |
10.5 综合使用示例:监听所有类型的蓝牙设备
java
/**
* 综合蓝牙设备监听器
* 同时监听 A2DP、Headset、HID 设备的连接状态
*/
public class ComprehensiveBluetoothHelper {
private static final String TAG = "ComprehensiveBluetooth";
private Context mContext;
private ComprehensiveReceiver mReceiver;
/**
* 设备类型枚举
*/
public enum DeviceType {
A2DP, // 音频播放设备
HEADSET, // 通话设备
HID // 输入设备
}
/**
* 监听接口
*/
public interface OnDeviceStateListener {
void onDeviceConnected(BluetoothDevice device, DeviceType type);
void onDeviceDisconnected(BluetoothDevice device, DeviceType type);
}
private OnDeviceStateListener mListener;
public void setOnDeviceStateListener(OnDeviceStateListener listener) {
this.mListener = listener;
}
/**
* 注册所有蓝牙设备类型的广播监听
*/
public void registerAllReceivers() {
mReceiver = new ComprehensiveReceiver();
IntentFilter filter = new IntentFilter();
// A2DP 音频设备
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
// Headset 通话设备
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
// HID 输入设备
filter.addAction(BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
mContext.registerReceiver(mReceiver, filter);
}
}
/**
* 注销广播监听
*/
public void unregisterAllReceivers() {
if (mReceiver != null) {
mContext.unregisterReceiver(mReceiver);
mReceiver = null;
}
}
/**
* 综合广播接收器
*/
private class ComprehensiveReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
String action = intent.getAction();
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
if (device == null) return;
DeviceType type = null;
// 判断是哪种类型的设备
if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
type = DeviceType.A2DP;
} else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
type = DeviceType.HEADSET;
} else if (BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
type = DeviceType.HID;
}
if (type == null) return;
// 回调状态变化
if (mListener != null) {
if (state == BluetoothProfile.STATE_CONNECTED) {
mListener.onDeviceConnected(device, type);
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
mListener.onDeviceDisconnected(device, type);
}
}
}
}
}
十一、总结
本文详细介绍了 Android 蓝牙设备连接广播的相关知识,包括:
-
蓝牙权限的正确配置方式
-
蓝牙连接状态相关的广播 Action 及其区别
-
关键要点:
ACTION_ACL_CONNECTED在配对后触发,不适合用于判断音频连接状态BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED不支持多设备场景BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED推荐使用,支持多设备且状态准确
-
广播接收器的静态和动态注册方法
其实很多蓝牙的广播是不能静态注册的,上面有些内容是AI生成的。
可以具体看:
Android13 不能静态注册的几个广播:
https://blog.csdn.net/wenzhi20102321/article/details/13495609
Android13 研究可以静态注册的广播:
https://blog.csdn.net/wenzhi20102321/article/details/149268983
-
如何从广播中获取蓝牙设备信息
-
经典蓝牙与 BLE 的区别
-
三种蓝牙 Profile 详解:
- A2DP:用于蓝牙音箱、耳机听歌,高质量立体声音频
- Headset:用于蓝牙耳机通话、车载蓝牙,双向语音通信
- HID:用于蓝牙键盘、鼠标、游戏手柄,低延迟输入设备
-
Android 版本适配注意事项
-
完整的工具类封装及使用示例
掌握这些知识,可以帮助开发者正确处理蓝牙设备的连接状态变化,根据不同设备类型选择合适的监听方式,避免常见的问题和陷阱。
参考资料
内网有时候打不开,或者打开很慢。