Android gatt.writeDescriptor 无法触发onDescriptorWrite回调
背景
作为一位蓝牙设备厂商Android SDK开发工程师,时常被问到这个问题:为什么这部手机一直连接不上这个蓝牙设备???
我:
根据以往经验,大部分都是以下几个原因造成蓝牙设备连接不成功。在看问题前,先简单了解蓝牙基本流程
蓝牙连接基本流程
- 扫描设备,获取设备
mBluetoothAdapter.startLeScan(mLeScanCallback);
- 发起连接请求
mBluetoothGatt=device.connectGatt(mContext,false,BluetoothGattCallback);
- 发现设备服务,获取特征UUID
mBluetoothGatt.discoverServices();
- 订阅通知,特征使能
//订阅特征的通知 并使能 成功onDescriptorWrite将回调
gatt.setCharacteristicNotification(characteristic,true);
final BluetoothGattDescriptor descriptor=characteristic.getDescriptor("00002902-0000-1000-8000-00805f9b34fb");
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
- 数据交互
//数据写入
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数据
- 断开连接
//mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt=null;
问题
一、蓝牙设备已经被连接了
可能被别的手机,或者本手机别的应用连接了。
解决方法
- 查看蓝牙设备是否有连接标志,连接标志是否显示已连接。如果已经连接,可以使用另外一个手机,打开第三方连接工具,扫描设备查看是否可以扫描到。再用无法连接设备的手机一个一个强行停止可以连接设备的app,直到另外一部手机能够扫描出设备,可以确认是哪个app连走了设备
- 重启蓝牙设备后,再次尝试连接
- 关闭并重新打开手机蓝牙,再次尝试连接
二、低性能手机连接蓝牙开始至成功时间太长
该问题只出现在我们自己开发的SDK,SDK内部连接时长为10秒,10秒内如果没有连接上,就会断开本次连接后重新连接。
如果遇到连接时长超过10秒的设备或者手机,就会造成一直连接中且连接大概率不成功。
-
如何确认是低性能手机?
使用该手机再用第三方连接工具例如 nRF Connect
nRF Connect Google play
nRF Connect github
尝试搜索并连接。如果发现开始连接至连接成功需要大于10秒,则是这个问题。 -
为什么SDK不增加连接超时时长?
低性能手机数量少,其它正常手机通常10秒内能够成功,修改连接超时时长会影响正常手机连接的时长。
三、GATT 133 异常
-
什么是 GATT ?
低功耗蓝牙的基本通讯协议
通用属性规范GATT(Generic Attribute Profile)将ATT层定义的属性打包成不同的属性实体,包括服务项、特征项和描述符,这些属性实体组合在一起组成规范,即GATT规范。GATT规范是服务项的集合,服务项是特征项的集合,特征项携带了属性参数和数据,描述符协助特征项描述特征值的形式和功能。 GATT层按照命令的传输方向将设备分成GATT客户端和GATT服务端。客户端发起命令,服务端发出数据。
GATT规范定义了客户端设备发现服务端设备的服务项的方法,建立连接以后,客户端设备可以通过发现方法检索服务端设备的GATT服务项和特征项,进而发送或接受数据。
-
什么是 133 ?
GATT出现异常的状态码
在Android底层源码中定义的GATT一些状态码,其中GATT_ERROR = 0x85为十进制状态码:133
系统源码GattStatus
-
出现GATT 133为什么连接不成功?
可以理解gatt为蓝牙间客户端与服务端通讯的桥梁,桥梁坏了还怎么交互数据,故我们SDK做了断连重连操作。以祈祷它恢复正常,但这并不理想…
可能原因
参考Google官方蓝牙连接DEMO提交的问题记录Google samples Issues
- gatt连接释放不规范导致gatt连接数量超过限制1
- gatt断连未完成又再次发起连接
- 在子线程执行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服务的蓝牙产品后,出现概率无法使用其它蓝牙设备
- 什么是HID蓝牙?
The Human Interface Device HID协议
定义了蓝牙在人机接口设备中的协议、特征和使用规程。典型的应用包括蓝牙鼠标、蓝牙键盘、蓝牙游戏手柄等。可以使人们无连线烦恼地控制他们的计算机、游戏操作杆、远程监控设备等。
问题原因分析
-
在Android系统中,HID蓝牙是可以自由控制手机的某种按键或者功能,例如我们的一款设备可以控制系统相机的拍照行为。
Google认为这是一个危险的行为。故对HID相关应用层的操作添加了系统权限限制android.Manifest.permission.BLUETOOTH_PRIVILEGED。仅拥有系统权限的APP才能控制HID蓝牙。
自Android 10.0开始,对系统蓝牙连接连接过HID服务的蓝牙GATT服务、特征和描述符的handleId2进行限制,然而确由此衍生出了一个系统级的BUG,此问题直至android 12还未得到解决。 -
当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
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集合里,所以一切正常。
-
为什么普通(不带HID服务)蓝牙设备不重启蓝牙偶尔能成功???
由问题原因分析可知,只有当普通蓝牙设备连接上底层蓝牙时,由底层分配的connId刚好是上一次系统保存的MAP中HID蓝牙设备的connId,才会出现异常错误! -
为什么普通(不带HID服务)蓝牙设备不能成功???
日志中可以看到,普通蓝牙设备数据交互相关的特征使能所用到的描述符handleId,居然是19 ,赫然是刚刚被底层加入到受限制的id中的一个!
然后底层系统就因为BUG用一个未清除的错误限制集合去判断了这是一个受限制的handleId!!!Android OS:赶紧把它禁用了!我再报个权限异常错误
至此,最终导致普通蓝牙设备的连接基本流程走不下去,就像是坐地铁却因为健康码错误显示了红码,被赶出了地铁站…
我们SDK内部由于使能超时未成功就执行了断连后再重连,故导致了连接异常现象,如果不进行解决方案中的其中一种,将会一直进入这个系统错误的循环中。 -
为什么重启蓝牙后能成功???
GattService重新创建分配新的内存地址,内部mRestrictedHandles也重新创建分配新的内存地址,缓存被清空
解决方法
-
占用问题connid
遇到gatt.writeDescriptor异常后,断开异常连接,执行重连,并在重连时生成一个假的gatt连接,使它占用掉问题connid。
这样writeDescriptor就不会有问题,然而却有一个隐患,由1可知,gatt连接就会创建累加clientIf,而clientIf所能分配的最大值为32,如果不释放连接达到上限后就无法创建新的连接了。该操作违反了GATT规范,如果反复这样操作,一定会超过最大限制就会引发GATT 133/257的问题。
如果出现了此问题,耶稣都保不住!我说的!只有重启蓝牙大法能救了//如果使能超时,连接个虚拟设备,暂用问题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; }
-
避免手机连接带HID服务的蓝牙设备
-
错开HID相关handleId,所有蓝牙设备产品都增加HID服务以及相关特性和描述符,并且HID服务和数据描述符等顺序保持一致
-
错开HID相关handleId,蓝牙设备的数据服务尽量避免与HID服务出现的顺序一样
-
关闭并重新打开手机蓝牙
connid:由底层分配给每个连接的蓝牙设备的一个整数值‘连接id’。Android Gatt连接流程源码分析之ClientIf注册 ↩︎ ↩︎ ↩︎ ↩︎