Android gatt.writeDescriptor 无法触发onDescriptorWrite回调

背景

作为一位蓝牙设备厂商Android SDK开发工程师,时常被问到这个问题:为什么这部手机一直连接不上这个蓝牙设备???

我:avatar

根据以往经验,大部分都是以下几个原因造成蓝牙设备连接不成功。在看问题前,先简单了解蓝牙基本流程

蓝牙连接基本流程

  1. 扫描设备,获取设备
   mBluetoothAdapter.startLeScan(mLeScanCallback);
  1. 发起连接请求
   mBluetoothGatt=device.connectGatt(mContext,false,BluetoothGattCallback);
  1. 发现设备服务,获取特征UUID
   mBluetoothGatt.discoverServices();
  1. 订阅通知,特征使能
    //订阅特征的通知 并使能 成功onDescriptorWrite将回调
    gatt.setCharacteristicNotification(characteristic,true);
    final BluetoothGattDescriptor descriptor=characteristic.getDescriptor("00002902-0000-1000-8000-00805f9b34fb");
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    gatt.writeDescriptor(descriptor);
  1. 数据交互
    //数据写入
    BluetoothGattService service=mBluetoothGatt.getService(serviceUUid);
    BluetoothGattCharacteristic characteristic=service.getCharacteristic(characteristicUUid);
    characteristic.setValue(byte[]{0x00,...................});
    mBluetoothGatt.writeCharacteristic(characteristic);
    //数据读取
    BluetoothGattCallback回调onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic)
    byte[]data=characteristic.getValue();
    //TODO 处理data数据
  1. 断开连接
    //mBluetoothGatt.disconnect();
    mBluetoothGatt.close();
    mBluetoothGatt=null;

问题

一、蓝牙设备已经被连接了

可能被别的手机,或者本手机别的应用连接了。

解决方法

  1. 查看蓝牙设备是否有连接标志,连接标志是否显示已连接。如果已经连接,可以使用另外一个手机,打开第三方连接工具,扫描设备查看是否可以扫描到。再用无法连接设备的手机一个一个强行停止可以连接设备的app,直到另外一部手机能够扫描出设备,可以确认是哪个app连走了设备
  2. 重启蓝牙设备后,再次尝试连接
  3. 关闭并重新打开手机蓝牙,再次尝试连接

二、低性能手机连接蓝牙开始至成功时间太长

该问题只出现在我们自己开发的SDK,SDK内部连接时长为10秒,10秒内如果没有连接上,就会断开本次连接后重新连接。

如果遇到连接时长超过10秒的设备或者手机,就会造成一直连接中且连接大概率不成功。

  1. 如何确认是低性能手机?
    使用该手机再用第三方连接工具例如 nRF Connect
    nRF Connect Google play
    nRF Connect github
    尝试搜索并连接。如果发现开始连接至连接成功需要大于10秒,则是这个问题。

  2. 为什么SDK不增加连接超时时长?
    低性能手机数量少,其它正常手机通常10秒内能够成功,修改连接超时时长会影响正常手机连接的时长。

三、GATT 133 异常

  1. 什么是 GATT ?
    低功耗蓝牙的基本通讯协议
    通用属性规范GATT(Generic Attribute Profile)将ATT层定义的属性打包成不同的属性实体,包括服务项、特征项和描述符,这些属性实体组合在一起组成规范,即GATT规范。

    GATT规范是服务项的集合,服务项是特征项的集合,特征项携带了属性参数和数据,描述符协助特征项描述特征值的形式和功能。 GATT层按照命令的传输方向将设备分成GATT客户端和GATT服务端。客户端发起命令,服务端发出数据。

    GATT规范定义了客户端设备发现服务端设备的服务项的方法,建立连接以后,客户端设备可以通过发现方法检索服务端设备的GATT服务项和特征项,进而发送或接受数据。

    GATT协议相关介绍

  2. 什么是 133 ?
    GATT出现异常的状态码
    在Android底层源码中定义的GATT一些状态码,其中GATT_ERROR = 0x85为十进制状态码:133
    系统源码GattStatus
    avatar

  3. 出现GATT 133为什么连接不成功?
    可以理解gatt为蓝牙间客户端与服务端通讯的桥梁,桥梁坏了还怎么交互数据,故我们SDK做了断连重连操作。以祈祷它恢复正常,但这并不理想…

可能原因

参考Google官方蓝牙连接DEMO提交的问题记录Google samples Issues

  1. gatt连接释放不规范导致gatt连接数量超过限制1
  2. gatt断连未完成又再次发起连接
  3. 在子线程执行gatt相关操作

解决方法

Google官方蓝牙连接DEMO都出现大量133异常现象,由于Android碎片化,并没有一种比较好的代码方式去解决这个问题。我们开发蓝牙相关程序,只有尽可能的去符合一些流程规范,使用规范以减少133出现的概率。
我们SDK内部处理方法为断开本次异常连接后,重新执行连接。这只能有概率恢复非133状态,并不能彻底解决,甚至可能一直处于异常循环,只有重启蓝牙才能恢复。sdk并未执行关闭蓝牙等操作,毕竟重启蓝牙大法对用户并不友好。
引用上面Google samples Issues中的一条回答:“resolves my problem on XXX devices but still have the same problem on million other devices”
对于这个行业级难题,我只能给出最佳解决方案

关闭并重新打开手机蓝牙

四、连接过带HID服务的蓝牙产品后,出现概率无法使用其它蓝牙设备

  1. 什么是HID蓝牙?
    The Human Interface Device HID协议
    定义了蓝牙在人机接口设备中的协议、特征和使用规程。典型的应用包括蓝牙鼠标、蓝牙键盘、蓝牙游戏手柄等。可以使人们无连线烦恼地控制他们的计算机、游戏操作杆、远程监控设备等。

问题原因分析

  1. 在Android系统中,HID蓝牙是可以自由控制手机的某种按键或者功能,例如我们的一款设备可以控制系统相机的拍照行为。
    Google认为这是一个危险的行为。故对HID相关应用层的操作添加了系统权限限制android.Manifest.permission.BLUETOOTH_PRIVILEGED。仅拥有系统权限的APP才能控制HID蓝牙。
    自Android 10.0开始,对系统蓝牙连接连接过HID服务的蓝牙GATT服务、特征和描述符的handleId2进行限制,然而确由此衍生出了一个系统级的BUG,此问题直至android 12还未得到解决。

  2. 当Android10以上手机连接过带HID服务的蓝牙设备,系统会收集HID服务、特征、描述符等的handleId集合2,并用当前连接设备的connId1作为KEY保存于一个受限制的MAP对象mRestrictedHandles中。
    当使用gatt.writeDescriptor写入描述符数据时,在发送数据前,底层将判断当前connId的写入数据的描述符handleId是否存在于受限制的集合中,如果是受限制handleId,则直接判断当前应用是否拥有系统权限,未授权则爆出权限异常,从而阻止数据的发送。
    原本该逻辑处理是没有问题,但是mRestrictedHandles只有put操作,没有remove或者clear操作,只要本次连接的HID蓝牙断开,底层并未清除受限制MAP,就会导致MAP中存储了错误的connId-handleId集合。
    当当前连接的带HID服务的蓝牙设备断连后,connId被释放(假设是6),下一次连接的设备connId由底层规则1分配后也是6,此时就会导致系统对受限制的handleId判断失误,导致异常!
    参考:3
    GattService.java源码:4

源码

    // 异常关键信息
    W/BtGatt.GattService: writeDescriptor() - permission check failed!
        //GattService.java中关键代码
        /**
         * GattService.java中定义的一些受限制的UUID
         */
        private static final UUID HID_SERVICE_UUID =UUID.fromString("00001812-0000-1000-8000-00805F9B34FB");
        private static final UUID[] HID_UUIDS = {
            UUID.fromString("00002A4A-0000-1000-8000-00805F9B34FB"),
            UUID.fromString("00002A4B-0000-1000-8000-00805F9B34FB"),
            UUID.fromString("00002A4C-0000-1000-8000-00805F9B34FB"),
            UUID.fromString("00002A4D-0000-1000-8000-00805F9B34FB")
        };
        private static final UUID ANDROID_TV_REMOTE_SERVICE_UUID =UUID.fromString("AB5E0001-5A21-4F05-BC7D-AF01F617B664");
        private static final UUID FIDO_SERVICE_UUID =UUID.fromString("0000FFFD-0000-1000-8000-00805F9B34FB"); // U2F
        
        
       /**
        * 由蓝牙连接流程中第3步发现设备服务:mBluetoothGatt.discoverServices();后在GattService中回调
        */
        void onGetGattDb(int connId,ArrayList<GattDbElement> db)throws RemoteException{
            String address=mClientMap.addressByConnId(connId);
            if(DBG){
                Log.d(TAG,"onGetGattDb() - address="+address);
            }
            ClientMap.App app=mClientMap.getByConnId(connId);
            if(app==null||app.callback==null){
                Log.e(TAG,"app or callback is null");
                return;
            }
            List<BluetoothGattService> dbOut=new ArrayList<BluetoothGattService>();
            Set<Integer> restrictedIds=new HashSet<>();
            BluetoothGattService currSrvc=null;
            BluetoothGattCharacteristic currChar=null;
            boolean isRestrictedSrvc=false;
            boolean isHidSrvc=false;
            boolean isRestrictedChar=false;
            for(GattDbElement el:db){
                switch(el.type){
                    case GattDbElement.TYPE_PRIMARY_SERVICE:
                    case GattDbElement.TYPE_SECONDARY_SERVICE:
                        if(DBG){
                            Log.d(TAG,"got service with UUID="+el.uuid+" id: "+el.id);
                        }
                        currSrvc=new BluetoothGattService(el.uuid,el.id,el.type);
                        dbOut.add(currSrvc);
                        isRestrictedSrvc=isFidoSrvcUuid(el.uuid)||isAndroidTvRemoteSrvcUuid(el.uuid);
                        isHidSrvc=isHidSrvcUuid(el.uuid);
                        if(isRestrictedSrvc){
                            restrictedIds.add(el.id);
                        }
                    break;
                    case GattDbElement.TYPE_CHARACTERISTIC:
                        if(DBG){
                            Log.d(TAG,"got characteristic with UUID="+el.uuid+" id: "+el.id);
                        }
                        currChar=new BluetoothGattCharacteristic(el.uuid,el.id,el.properties,0);
                        currSrvc.addCharacteristic(currChar);
                        //!!!如果是需要受限制(eg.HID服务)的特征uuid
                        //!!!则把特征的handleId加入到受限制的集合中
                        isRestrictedChar=isRestrictedSrvc||(isHidSrvc&&isHidCharUuid(el.uuid));
                        if(isRestrictedChar){
                            restrictedIds.add(el.id);
                        }
                    break;
                    case GattDbElement.TYPE_DESCRIPTOR:
                        if(DBG){
                            Log.d(TAG,"got descriptor with UUID="+el.uuid+" id: "+el.id);
                        }
                        currChar.addDescriptor(new BluetoothGattDescriptor(el.uuid,el.id,0));
                        //!!!如果是需要受限制(eg.HID服务)的描述符uuid
                        //!!!则把描述符的handleId加入到受限制的集合中
                        if(isRestrictedChar){
                            restrictedIds.add(el.id);
                        }
                    break;
                    case GattDbElement.TYPE_INCLUDED_SERVICE:
                        if(DBG){
                            Log.d(TAG,"got included service with UUID="+el.uuid+" id: "+el.id+" startHandle: "+el.startHandle);
                        }
                        currSrvc.addIncludedService(
                        new BluetoothGattService(el.uuid,el.startHandle,el.type));
                    break;
                    default:
                        Log.e(TAG,"got unknown element with type="+el.type+" and UUID="+el.uuid+" id: "+el.id);
                }
            }
            //将受限制的handleid集合restrictedIds,以connid作为Key保存进mRestrictedHandles
            if(!restrictedIds.isEmpty()){
                mRestrictedHandles.put(connId,restrictedIds);
            }
            // Search is complete when there was error, or nothing more to process
            app.callback.onSearchComplete(address,dbOut,0 /* status */);
        }

        /**
         * 当调用APP使能调用gatt.writeDescriptor(descriptor);后最终走向下面这段代码
         * 只有当权限检测通过后,才会向底层写入数据,否则报出 ‘writeDescriptor() - permission check failed!’
         */
        void writeDescriptor(int clientIf, String address, int handle, int authReq, byte[] value) {
            enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
            if (VDBG) {
                Log.d(TAG, "writeDescriptor() - address=" + address);
            }
            //获取本次连接的connId
            Integer connId = mClientMap.connIdByAddress(clientIf, address);
            if (connId == null) {
                Log.e(TAG, "writeDescriptor() - No connection for " + address + "...");
                return;
            }
            //以connid作为Key从mRestrictedHandles中获取受限制的handleId集合,并将当前待处理的handleId传入方法内部判断权限
            if (!permissionCheck(connId, handle)) {
                Log.w(TAG, "writeDescriptor() - permission check failed!");
                return;
            }
            //非受限制handleid 或者 系统权限正常 才允许真正写入数据
            gattClientWriteDescriptorNative(connId, handle, authReq, value);
        }


        /**
         * 检测权限的关键代码
         * 问题出在此处,使用了异常的mRestrictedHandles中的受限制handleId集合
         * 最终导致权限失败,数据写入失败
         */
        private boolean permissionCheck(int connId, int handle) {
            //根据connid 获取到受限制的handleid集合
            Set<Integer> restrictedHandles = mRestrictedHandles.get(connId);
            //*********如果当前handleid不存在于受限制的集合中,则权限正常。否则是受限制的handleId,需要拥有系统权限才允许继续*******
            if (restrictedHandles == null || !restrictedHandles.contains(handle)) {
                return true;
            }
            //handleid是受限制的,直接判断是否拥有系统权限‘BLUETOOTH_PRIVILEGED’
            return (checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)== PERMISSION_GRANTED);
        }

为什么出现writeDescriptor失败会导致连接失败???

我们SDK内部处理了连接异常断连
看下蓝牙连接基本流程第4步最后一行代码就知道了,特征使能是通过gatt.writeDescriptor(descriptor)成功才算成功,也就是特征使能失败,SDK内部使能超时。
蓝牙连接基本流程第5步走不下去,基本数据收发不了,虽然连接成功了但sdk内部判断连接出现异常,执行了断连后重连。

为什么有时连接成功,有时连接异常???

我们从日志一步步分析: 过滤一级关键字:GattService 二级关键字:onGetGattDb
avatar
1.连接带HID服务蓝牙设备为什么一直可以正常?
在这里插入图片描述
日志中可以看到,因为系统发现了HID服务,所以就会把 11 13 15 17 19 20 21 23 24 26 27 28 30 31 33 这些handleId存入mRestrictedHandles
再进一步从日志分析: 过滤一级关键字:GattService 二级关键字:registerForNotification
在这里插入图片描述
发现带HID服务蓝牙设备的数据交互相关的特征使能所用到的描述符handleId 48 45 51 54并不在被上面被禁用的id集合里,所以一切正常。

  1. 为什么普通(不带HID服务)蓝牙设备不重启蓝牙偶尔能成功???
    由问题原因分析可知,只有当普通蓝牙设备连接上底层蓝牙时,由底层分配的connId刚好是上一次系统保存的MAP中HID蓝牙设备的connId,才会出现异常错误!

  2. 为什么普通(不带HID服务)蓝牙设备不能成功???
    在这里插入图片描述

    日志中可以看到,普通蓝牙设备数据交互相关的特征使能所用到的描述符handleId,居然是19 ,赫然是刚刚被底层加入到受限制的id中的一个!
    然后底层系统就因为BUG用一个未清除的错误限制集合去判断了这是一个受限制的handleId!!!Android OS:赶紧把它禁用了!我再报个权限异常错误
    avatar
    至此,最终导致普通蓝牙设备的连接基本流程走不下去,就像是坐地铁却因为健康码错误显示了红码,被赶出了地铁站…
    我们SDK内部由于使能超时未成功就执行了断连后再重连,故导致了连接异常现象,如果不进行解决方案中的其中一种,将会一直进入这个系统错误的循环中。

  3. 为什么重启蓝牙后能成功???
    GattService重新创建分配新的内存地址,内部mRestrictedHandles也重新创建分配新的内存地址,缓存被清空

解决方法

  1. 占用问题connid
    遇到gatt.writeDescriptor异常后,断开异常连接,执行重连,并在重连时生成一个假的gatt连接,使它占用掉问题connid。
    这样writeDescriptor就不会有问题,然而却有一个隐患,由1可知,gatt连接就会创建累加clientIf,而clientIf所能分配的最大值为32,如果不释放连接达到上限后就无法创建新的连接了。该操作违反了GATT规范,如果反复这样操作,一定会超过最大限制就会引发GATT 133/257的问题。
    avatar
    如果出现了此问题,耶稣都保不住!我说的!只有重启蓝牙大法能救了

                 //如果使能超时,连接个虚拟设备,暂用问题coonId
                 BluetoothGatt virtualGatt = null;
                 BluetoothGattCallback virtualCallBack = null;
                 if (isEnableNotificationTimout) {
                     isEnableNotificationTimout = false;
                     BluetoothDevice virtualDevice = mBluetoothAdapter.getRemoteDevice("AA:AA:AA:AA:AA:AA");
                     virtualCallBack = new BluetoothGattCallback() {
                     };
                     if (virtualDevice != null) {
                     	//BluetoothGattCallback不能用mGattCallback
                         virtualGatt = virtualDevice.connectGatt(mContext, false, virtualCallBack);
                     }
                 }
     			 //再连接真实需要连接的设备
                 mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback);
                 //释放虚拟设备
                 if (virtualGatt != null) {
                     //不及时释放会造成clientIf累加>=32后,就 会报133异常,不能再发起连接了
                     virtualGatt.close();
                     virtualGatt = null;
                     virtualCallBack = null;
                 }
    
  2. 避免手机连接带HID服务的蓝牙设备

  3. 错开HID相关handleId,所有蓝牙设备产品都增加HID服务以及相关特性和描述符,并且HID服务和数据描述符等顺序保持一致

  4. 错开HID相关handleId,蓝牙设备的数据服务尽量避免与HID服务出现的顺序一样

  5. 关闭并重新打开手机蓝牙


  1. connid:由底层分配给每个连接的蓝牙设备的一个整数值‘连接id’。Android Gatt连接流程源码分析之ClientIf注册 ↩︎ ↩︎ ↩︎ ↩︎

  2. handleId:句柄id,由底层为每个连接的蓝牙设备的服务、特征、描述符等分配的整数值‘句柄id’。 ↩︎ ↩︎

  3. writeDescriptor() - permission check failed! ↩︎

  4. GattService.java源码 ↩︎

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值