美文网首页蓝牙
writeCharacteristic() - permissi

writeCharacteristic() - permissi

作者: 冰珊孤雪 | 来源:发表于2020-03-16 14:32 被阅读0次

    Permission check failed

    现象描述

    Android 10 手机连接GATT后,读写characteristic结果返回true,但是确没有callback返回,logcat打印如下:

    02-20 04:26:15.599  3815  3834 W BtGatt.GattService: writeCharacteristic() - permission check failed!
    

    源码分析

    BluetoothGatt#writeCharacteristic

    android.bluetooth.BluetoothGatt.java

    /**
         * Writes a given characteristic and its values to the associated remote device.
         *
         * <p>Once the write operation has been completed, the
         * {@link BluetoothGattCallback#onCharacteristicWrite} callback is invoked,
         * reporting the result of the operation.
         *
         * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
         *
         * @param characteristic Characteristic to write on the remote device
         * @return true, if the write operation was initiated successfully
         */
        public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic) {
            if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0
                    && (characteristic.getProperties()
                    & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) {
                return false;
            }
    
            if (VDBG) Log.d(TAG, "writeCharacteristic() - uuid: " + characteristic.getUuid());
            if (mService == null || mClientIf == 0 || characteristic.getValue() == null) return false;
    
            BluetoothGattService service = characteristic.getService();
            if (service == null) return false;
    
            BluetoothDevice device = service.getDevice();
            if (device == null) return false;
    
            synchronized (mDeviceBusyLock) {
                if (mDeviceBusy) return false;
                mDeviceBusy = true;
            }
    
            try {
                mService.writeCharacteristic(mClientIf, device.getAddress(),
                        characteristic.getInstanceId(), characteristic.getWriteType(),
                        AUTHENTICATION_NONE, characteristic.getValue());
            } catch (RemoteException e) {
                Log.e(TAG, "", e);
                mDeviceBusy = false;
                return false;
            }
    
            return true;
        }
    

    GattService#writeCharacteristic

    /android/platform/packages/apps/Bluetooth/src/com/android/bluetooth/gatt/GattService.java

    void writeCharacteristic(int clientIf, String address, int handle, int writeType, int authReq,
                byte[] value) {
            enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
    
            if (VDBG) {
                Log.d(TAG, "writeCharacteristic() - address=" + address);
            }
    
            if (mReliableQueue.contains(address)) {
                writeType = 3; // Prepared write
            }
    
            Integer connId = mClientMap.connIdByAddress(clientIf, address);
            if (connId == null) {
                Log.e(TAG, "writeCharacteristic() - No connection for " + address + "...");
                return;
            }
    
            if (!permissionCheck(connId, handle)) {
                Log.w(TAG, "writeCharacteristic() - permission check failed!");
                return;
            }
    
            gattClientWriteCharacteristicNative(connId, handle, writeType, authReq, value);
        }
    
    

    从上面的代码可以看到,writeCharacteristic() - permission check failed! 是在permissionCheck(connId, handle)返回失败的情况下才打印的。

    permissionCheck

    /android/platform/packages/apps/Bluetooth/src/com/android/bluetooth/gatt/GattService.java

    private boolean permissionCheck(int connId, int handle) {
            Set<Integer> restrictedHandles = mRestrictedHandles.get(connId);
            if (restrictedHandles == null || !restrictedHandles.contains(handle)) {
                return true;
            }
    
            return (checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)
                    == PERMISSION_GRANTED);
        }
    
    

    从上面的代码可以看出检查流程如下:

    • 首先检查 restrictedHandles 是否包含当前操作的 handle,由于handle对应的UUID在restrictedHandles 中,所以开始检查BLUETOOTH_PRIVILEGED权限

    • 但是只有系统应用才可以申请获取BLUETOOTH_PRIVILEGED权限,所以最后依然返回false.

    第三方应用是无法获取BLUETOOTH_PRIVILEGED权限的,所以问题应该是出在restrictedHandles上。

    restrictedHandles

    /**
    * Set of restricted (which require a BLUETOOTH_PRIVILEGED permission) handles per connectionId.
    */
    private final Map<Integer, Set<Integer>> mRestrictedHandles = new HashMap<>();
    
    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);
                        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));
                        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);
                }
            }
    
            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 */);
        }
    

    从上面的代码看到restrictedHandles里面包含了需要被过滤的UUID,从前面的permissionCheck已经知道,这些被限制的UUID只有系统应用才可以访问。

    我们测试设备的服务里面确实没有需要被限制的UUID为什么也会被过滤?

    继续跟踪定位发现,restrictedHandles只有put操作,没有remove或者clear操作。于是怀疑是restrictedHandles缓存导致的,模拟场景如下:

    • 首先先连接一个HID设备,查询到的服务里面包含被限制的Service(这里以HID为例),连接成功后,connId=0x09.

    • 断开HID设备,connId=0x09被释放。

    • 连接一个新的LE设备,服务里面没有需要被限制的Service.连接成功后,connId也是0x09。

    • 从前面的onGetGattDb可以看出,虽然新的设备没有需要被限制的Service,但是由于restrictedHandles没有被清空,两次的connId也是一样的,导致后面判断的时候依然会被过滤。

      if (!restrictedIds.isEmpty()) {
         mRestrictedHandles.put(connId, restrictedIds);
      }
      

    为什么其他Android设备正常,只有 Android 10有问题?

    前面我们分析的代码就是Android 10 的。mRestrictedHandles也是在Android 10才加上的。

    Android 10 以前版本的 permissionCheck是直接检查UUID,不会受前一次连接的缓存影响。

    boolean permissionCheck(int connId, int handle) {
            List<BluetoothGattService> db = mGattClientDatabases.get(connId);
            if (db == null) {
                return true;
            }
    
            for (BluetoothGattService service : db) {
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    if (handle == characteristic.getInstanceId()) {
                        return !((isRestrictedCharUuid(characteristic.getUuid())
                                || isRestrictedSrvcUuid(service.getUuid()))
                                && (0 != checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)));
                    }
    
                    for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
                        if (handle == descriptor.getInstanceId()) {
                            return !((isRestrictedCharUuid(characteristic.getUuid())
                                    || isRestrictedSrvcUuid(service.getUuid())) && (0
                                    != checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)));
                        }
                    }
                }
            }
    
            return true;
        }
    

    结论

    从上面的分析可以知道,这个问题是Android 10 引入的 BUG,Android 10 以前的系统没有这个问题。第三方APP连接LE设备后,如果前一次连接了一个HID设备,且当前连接的connId和上一次连接的connId相同,就会触发这个BUG。

    解决方案

    • 重新开关蓝牙,这样BluetoothManagerService就会重新bind,缓存会被清除。

    • 确保App 或者第三方应用不会去连接HID设备,减小触发BUG的机率。

    上面两个方法都不能从根本上解决问题,最终我们还是要等到Google更新patch来修复这个BUG。

    相关文章

      网友评论

        本文标题:writeCharacteristic() - permissi

        本文链接:https://www.haomeiwen.com/subject/uvexqhtx.html