美文网首页蓝牙
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