Android 获取已连接的经典蓝牙和BLE设备

需求

公司需要获取Android已经连接的蓝牙设备。从公开的API看,无法获取。

我验证其中的代码结果。对于经典蓝牙,因为基本上已连接的设备都会出现在已配对列表,从列表里拿到设备后逐个去判断,确实可行。
但BLE的设备,你只能知道有设备连接在系统里,但无法知道是哪个?

解决思路

  • 从上面的文章,我看到经典蓝牙是对 BluetoothDevice 对象反射调用 isConnected 方法判断的,那如果我能拿到BLE的device对象,应该也能判断。我尝试直接用BLE的一个MAC地址使用 BluetoothAdapter.getRemoteDevice(macAddress) 获取到对象,也使用上面的反射方法isConnected,结果证明可行。
  • 现在的问题是如何拿到所有可能的device,包括那些已经被连接的BLE或者经典蓝牙。查找BluetoothAdapter源码中,看到一个 getMostRecentlyConnectedDevices,返回的是一个List。大概的意思是返回最近连接过的设备,我理解是:这也包括目前已经连接的设备。 尝试之,此方法有效。

编程实现

  1. 上面调用会用到反射,由于Android10以后反射调用,会报错:Accessing hidden method ‘xxxx.xxxxxx.xxxxx’ (blacklist, reflection, denied),所以这里我使用 github 上的RestrictionBypass 绕过反射的报错,具体参考它的代码实现。它的库通过内建ContentProvider的attachInfo来调用绕过反射的方法,所以你的app不需要任何一行代码就能生效。

注意: 反射调用BluetoothDevice#isConnected并不会触发这个错误,但BluetoothAdapter#getMostRecentlyConnectedDevices会。如果你只使用我下文中的V2版本,可以不做这一步

  1. 代码封装
//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;
}
  1. 测试结果截图:
  2. Android 蓝牙获取不到name android获取已连接蓝牙设备_Android 蓝牙获取不到name

测试机型

从最终测试结果看,不管是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;
    }

结论

  1. V1 方法在华为平板上可能不生效,但它能找到所有系统连接的蓝牙设备,包括不需要配对的经典设备
  2. V2 方法目前在华为平板上能生效,但经典蓝牙通过 createInsecureRfcommSocketToServiceRecord建立的连接,在配对列表里看不到它,也就找不出来这个设备。也就是它能找到所有已连接的BLE设备和在配对列表里已连接的设备。

3 当然,可以结合V1 和 V2 做一个兼容的版本。V1版本中一旦找不到getMostRecentlyConnectedDevices就启用V2的方式。

不过对于我来说, 我们都要求客户必须配对,所以V2的方法已经满足我的需求了。