Android 获取已连接的经典蓝牙和BLE设备
需求
公司需要获取Android已经连接的蓝牙设备。从公开的API看,无法获取。
我验证其中的代码结果。对于经典蓝牙,因为基本上已连接的设备都会出现在已配对列表,从列表里拿到设备后逐个去判断,确实可行。
但BLE的设备,你只能知道有设备连接在系统里,但无法知道是哪个?
解决思路
- 从上面的文章,我看到经典蓝牙是对 BluetoothDevice 对象反射调用 isConnected 方法判断的,那如果我能拿到BLE的device对象,应该也能判断。我尝试直接用BLE的一个MAC地址使用 BluetoothAdapter.getRemoteDevice(macAddress) 获取到对象,也使用上面的反射方法isConnected,结果证明可行。
- 现在的问题是如何拿到所有可能的device,包括那些已经被连接的BLE或者经典蓝牙。查找BluetoothAdapter源码中,看到一个 getMostRecentlyConnectedDevices,返回的是一个List。大概的意思是返回最近连接过的设备,我理解是:这也包括目前已经连接的设备。 尝试之,此方法有效。
编程实现
- 上面调用会用到反射,由于Android10以后反射调用,会报错:Accessing hidden method ‘xxxx.xxxxxx.xxxxx’ (blacklist, reflection, denied),所以这里我使用 github 上的RestrictionBypass 绕过反射的报错,具体参考它的代码实现。它的库通过内建ContentProvider的attachInfo来调用绕过反射的方法,所以你的app不需要任何一行代码就能生效。
注意: 反射调用BluetoothDevice#isConnected并不会触发这个错误,但BluetoothAdapter#getMostRecentlyConnectedDevices会。如果你只使用我下文中的V2版本,可以不做这一步
- 代码封装
//TODO 根据mac地址判断是否已连接(这里参数可以直接用BluetoothDevice对象)
//但这么写其实更通用。
public boolean isConnected(String macAddress){
if (!BluetoothAdapter.checkBluetoothAddress(macAddress)){
return false;
}
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(macAddress);
Method isConnectedMethod = null;
boolean isConnected;
try {
isConnectedMethod = BluetoothDevice.class.getDeclaredMethod("isConnected", (Class[]) null);
isConnectedMethod.setAccessible(true);
isConnected = (boolean) isConnectedMethod.invoke(device, (Object[]) null);
} catch (NoSuchMethodException e) {
isConnected = false;
} catch (IllegalAccessException e) {
isConnected = false;
} catch (InvocationTargetException e) {
isConnected = false;
}
return isConnected;
}
/**
* 获取系统中已连接的蓝牙设备
* @return
*/
public Set<BluetoothDevice> getConnectedDevicesV1() {
Class<BluetoothAdapter> bluetoothAdapterClass = BluetoothAdapter.class;//得到BluetoothAdapter的Class对象
Set<BluetoothDevice> deviceSet = new HashSet<>();
//是否存在连接的蓝牙设备
try {
Method method = bluetoothAdapterClass.getDeclaredMethod("getMostRecentlyConnectedDevices", (Class[]) null);
//打开权限
method.setAccessible(true);
List<BluetoothDevice> list= (List<BluetoothDevice>) method.invoke(BluetoothAdapter.getDefaultAdapter(), (Object[]) null);
Log.d("zbh","最近连接过的设备:");
for (BluetoothDevice dev:list
) {
String Type = "";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
switch (dev.getType()){
case BluetoothDevice.DEVICE_TYPE_CLASSIC:
Type = "经典";
break;
case BluetoothDevice.DEVICE_TYPE_LE:
Type = "BLE";
break;
case BluetoothDevice.DEVICE_TYPE_DUAL:
Type = "双模";
break;
default:
Type = "未知";
break;
}
}
String connect = "设备未连接";
if (isConnected(dev.getAddress())){
deviceSet.add(dev);
connect = "设备已连接";
}
Log.d("zbh", connect+", address = "+dev.getAddress() + "("+ Type + "), name --> "+dev.getName());
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return deviceSet;
}
- 测试结果截图:
测试机型
从最终测试结果看,不管是TWS蓝牙耳机,还是普通的BLE设备,或者是经典蓝牙都能正确拿到状态。但当我测试了多个平板后,我发现华为系的Android或者鸿蒙大概率报错。
目前我测试的机型有:
- 小米10 (android 11), 测试OK
- 荣耀View7 (Android 11),测试OK
- 华为M6平板SCM-AL09 (Android 9),报错 找不到 getMostRecentlyConnectedDevices方法
- 华为C5平板BZT-AL10 (android 8)报错 找不到 getMostRecentlyConnectedDevices方法
- 华为C5平板BZT3-W09 (android 10)报错 找不到 getMostRecentlyConnectedDevices方法
- 华为荣耀手机TEL-TN00(鸿蒙2.0)报错 找不到 getMostRecentlyConnectedDevices方法
- 华为手机ALP-AL00(鸿蒙2.0)报错 找不到 getMostRecentlyConnectedDevices方法
改进方法
反射getMostRecentlyConnectedDevices方法还是有风险, 不保证能通用。
继续找方法。
终于,我吐了,秃了,佛了
又过了三天,我找到了一个API (BluetoothManager#getConnectedDevices),姑且一试吧
V2版本方法:
/**
* 获取系统中已连接的蓝牙设备
* @return
*/
public Set<BluetoothDevice> getConnectedDevicesV2(Context context){
Set<BluetoothDevice> result = new HashSet<>();
Set<BluetoothDevice> deviceSet = new HashSet<>();
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
//获取BLE的设备, profile只能是GATT或者GATT_SERVER
List GattDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
if (GattDevices!=null && GattDevices.size()>0){
deviceSet.addAll(GattDevices);
}
//获取已配对的设备
Set ClassicDevices = bluetoothManager.getAdapter().getBondedDevices();
if (ClassicDevices!=null && ClassicDevices.size()>0){
deviceSet.addAll(ClassicDevices);
}
for (BluetoothDevice dev:deviceSet
) {
String Type = "";
switch (dev.getType()){
case BluetoothDevice.DEVICE_TYPE_CLASSIC:
Type = "经典";
break;
case BluetoothDevice.DEVICE_TYPE_LE:
Type = "BLE";
break;
case BluetoothDevice.DEVICE_TYPE_DUAL:
Type = "双模";
break;
default:
Type = "未知";
break;
}
String connect = "设备未连接";
if (isConnected(dev.getAddress())){
result.add(dev);
connect = "设备已连接";
}
Log.d("zbh", connect+", address = "+dev.getAddress() + "("+ Type + "), name --> "+dev.getName());
}
return result;
}
结论
- V1 方法在华为平板上可能不生效,但它能找到所有系统连接的蓝牙设备,包括不需要配对的经典设备
- V2 方法目前在华为平板上能生效,但经典蓝牙通过 createInsecureRfcommSocketToServiceRecord建立的连接,在配对列表里看不到它,也就找不出来这个设备。也就是它能找到所有已连接的BLE设备和在配对列表里已连接的设备。
3 当然,可以结合V1 和 V2 做一个兼容的版本。V1版本中一旦找不到getMostRecentlyConnectedDevices就启用V2的方式。
不过对于我来说, 我们都要求客户必须配对,所以V2的方法已经满足我的需求了。