I am writing an Android app that can connect to a BLE device. There are multiple types of devices handled by my application and I have to know the type of device I am connecting on.

The entire connection process is handle by a Service object and described below:

  1. Call of connectToGatt() method with the BluetoothDevice object that represent the device I am trying to connect to
  2. The GattCallback.onConnectionStateChanged is triggered. If the device is already bond, go to 4.
  3. Broadcast receiver that handle bond state changes is triggered. If the device is bond, go to 4.
  4. Call of discoverService() method
  5. When Services are discovered, I get my characteristic and enable notifications using CCCD. Consequently, I write to CCCD the activation of notifications.
  6. GattCallback.onDescriptorWrite is triggered. There I create a command object that aims to get the model number of my device, and add it to a queue of command.
  7. A separate thread activated before handle the queue and execute the write operation.

And there my issue: On Danew DSlide 1020 Pro under Android 12, GattCallback.onCharacteristicWrite is called but GattCallback.onCharacteristicChanged is never called (I tested on 3 similar tablets). However, the two callbacks are triggered on my Samsung tablet under Android 13 and I can complete the connection process.

Notice that on BLE device side, the write operation is successfully executed on both Android devices.

Here is the code of my service (I only put the relevant stuff i.e the connection process). Notice that sendEvent() is a method to trigger observers of my service (I do not use broadcast on my activities for personnal reasons)

connectToGatt method:

@SuppressLint("MissingPermission")
    public void connectToGatt(BluetoothDevice device) {
        if (!mIsBleDeviceConnected) {
            this.mBleDevice = device;
            this.mBleDeviceGatt = this.mBleDevice.connectGatt(this, false, this.mBleDeviceGattCallback, BluetoothDevice.TRANSPORT_LE);
        }
    }

GattCallback:

private final BluetoothGattCallback mBleDeviceGattCallback = new BluetoothGattCallback() {
    @SuppressLint("MissingPermission")
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        switch (newState) {
            case BluetoothProfile.STATE_CONNECTED:
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    mIsBleDeviceConnected = true;
                    if (mBleDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
                        Log.d(TAG, "onConnectionStateChange: test");
                        startThread();  //Start the thread that handles the queue of BLE operations such as write on characteristics
                        mBleDeviceGatt.discoverServices();
                    }
                } else {
                    Log.e(TAG, "onConnectionStateChange: Connection to GATT failed with status "+status);
                    sendEvent(new EventGattConnectionFailed());
                }
                break;
            case BluetoothProfile.STATE_DISCONNECTED:
                // handle disconnection
        }
    }

    @SuppressLint("MissingPermission")
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            BluetoothGattService service = gatt.getService(Constants.UUID_SERVICE);
            mBleDeviceCharacteristic = service.getCharacteristic(Constants.UUID_CHARACTERISTIC);
            if (mBleDeviceCharacteristic == null) {
                Log.e(TAG, "onServicesDiscovered: The service does not provide characteristic "+Constants.UUID_CHARACTERISTIC.toString());
                sendEvent(new EventGattConnectionFailed());
                mBleDeviceGatt.disconnect();
            } else {
                boolean isNoCccd = enableNotifications(mBleDeviceCharacteristic);
                if (isNoCccd) {
                    Log.e(TAG, "onServicesDiscovered: CCCD not found on characteristic "+mBleDeviceCharacteristic.getUuid());
                    sendEvent(new EventGattConnectionFailed());
                    mBleDeviceGatt.disconnect();
                }
            }
        } else {
            Log.e(TAG, "Service discovery failed with status: " + status);
            sendEvent(new EventGattConnectionFailed());
            mBleDeviceGatt.disconnect();
        }
    }

    @SuppressLint("MissingPermission")
    @Override
    public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        super.onDescriptorWrite(gatt, descriptor, status);
        if (descriptor.getUuid().toString().equals(Constants.UUID_CCCD.toString())) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (descriptor.getValue() == BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) {
                    addCommandToQueue(new ModelNumberCommand((byte) 0x06,null));
                } else if (descriptor.getValue() == BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE){
                    mBleDeviceGatt.disconnect();
                }
            } else {
                Log.e(TAG, "onDescriptorWrite: Enabling notification process failed with status "+status);
                sendEvent(new EventGattConnectionFailed());
                mBleDeviceGatt.disconnect();
            }
        }
    }

    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        Log.d(TAG, "onCharacteristicWrite: test1 "+status);
        super.onCharacteristicWrite(gatt, characteristic, status);
        try {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.d(TAG, "onCharacteristicWrite: test2");
                // some unrelevant code for this issue here
            } else {
                if (mDevicesInstance == null) {
                    sendEvent(new EventGattConnectionFailed());
                    Log.e(TAG, "onCharacteristicWrite: Get model number command failed with status "+status);
                    disconnectFromGatt();
                } else {
                    // some unrelevant code for this issue here
                }
                mBleCommandAwaitedDataSize = 0;
                mBleCommandSemaphore.release();
            }
        } catch (Exception e) {
            if (mDevicesInstance == null) {
                sendEvent(new EventGattConnectionFailed());
                Log.e(TAG, "onCharacteristicWrite: Get model number command failed with exception "+e);
                disconnectFromGatt();
            } else {
                // some unrelevant code for this issue here
            }
            mBleCommandAwaitedDataSize = 0;
            mBleCommandSemaphore.release();
        }
    }

    @Override
    public void onCharacteristicChanged(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
        Log.d(TAG, "onCharacteristicChanged: test");
        super.onCharacteristicChanged(gatt, characteristic, value);
        byte[] data;
        if (mBleCommandAwaitedDataSize == 0) {
            mBleCommandAwaitedDataSize = value[1];
            mBleCommandCursor = 0;
            mBleCommandResult = new byte[mBleCommandAwaitedDataSize];
            data = Arrays.copyOfRange(value,2,value.length);
        } else {
            data = value;
        }
        System.arraycopy(data,0,mBleCommandResult,mBleCommandCursor,data.length);
        mBleCommandCursor = mBleCommandCursor+data.length;
        if (mBleCommandCursor >= mBleCommandAwaitedDataSize) {
            if (mDevicesInstance == null) {
                finalizeConnection(mBleCommandResult); // End the connection process
            } else {
                // some unrelevant code for this issue here
            }
            mBleCommandSemaphore.release();
        }
    }

Broadcast receiver that handles bonding state

private final BroadcastReceiver mBleDeviceGattReceiver = new BroadcastReceiver() {
        @SuppressLint("MissingPermission")
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
                int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR);
                switch (bondState) {
                    case BluetoothDevice.BOND_BONDED:
                        mBleServiceInstance.sendEvent(new EventGattBondSuccess());
                        startThread();
                        mBleDeviceGatt.discoverServices();
                        break;
                    case BluetoothDevice.ERROR:
                        Log.e(TAG, "mBleDeviceGattReceiver.onReceive: Device bonding failed.");
                        sendEvent(new EventGattConnectionFailed());
                        mBleDeviceGatt.disconnect();
                        break;
                }
            }
        }
    };

Thread object that handles the queue

private class BleCommandExecutor extends Thread {
        private boolean isRunning;
        @SuppressLint("MissingPermission")
        @Override
        public void run() {
            while (isRunning) {
                if (!mBleCommandQueue.isEmpty()) {
                    Log.d(TAG, "run: test");
                    Commands nextCommand = mBleCommandQueue.poll();
                    if (!(nextCommand == null)) {
                        try {
                            mBleCommandSemaphore.acquire();
                            mBleCommandRunning = nextCommand;
                            byte[] payload = mBleCommandRunning.serializeCommand();
                            Log.d(TAG, "run: "+ Arrays.toString(payload));
                            mBleDeviceCharacteristic.setValue(payload);
                            mBleDeviceGatt.writeCharacteristic(mBleDeviceCharacteristic);
                        } catch (InterruptedException e) {
                            Log.e(TAG, "BleCommandExecutor: Exception raised with semaphore: "+e);
                            sendEvent(new EventCommandFailed());
                        }
                    }
                }
            }
        }

        public void setRunningState(boolean newRunningState) {
            this.isRunning = newRunningState;
        }
    }
private BleCommandExecutor mBleCommandExecutor;
public void startThread() {
        this.mBleCommandExecutor = new BleCommandExecutor();
        this.mBleCommandExecutor.setRunningState(true);
        this.mBleCommandExecutor.start();
    }
public void addCommandToQueue(Commands command) {
        this.mBleCommandQueue.add(command);
    }

enableNotifications() method:

@SuppressLint("MissingPermission")
    public boolean enableNotifications(BluetoothGattCharacteristic characteristic) {
        BluetoothGattDescriptor cccd = characteristic.getDescriptor(Constants.UUID_CCCD);
        if (cccd == null) {
            Log.w(TAG, "enableNotifications: CCCD has not been found in the descriptors of the characteristic "+Constants.UUID_CHARACTERISTIC);
            return true;
        }
        if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
            this.mBleDeviceGatt.setCharacteristicNotification(characteristic, true);
            cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            this.mBleDeviceGatt.writeDescriptor(cccd);
        }
        return false;
    }

All this code belong to my Service object.

Note that the command added to the queue (ModelNumber((byte) 0x06, null)) is a getter. If the parameter is not null, onCharacteristicChanged is not triggered (characteristic has PROPERTY_WRITE_NO_RESPONSE), that seems to be normal behavior.

The exact payload written on BLE device is [(byte) 0x06, (byte) 0x00] And the result expected (get from onCharacteristicChanged) is [(byte) 0x07, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00]

The tests I have done enables me to ensure that I reach the write operation execution because in all cases, notifications are enabled and then I receive the onCharacteristicWrite callback.

[EDIT]

I have tested on an Android 11 device and I have the same issue.

However, I discovered an option on Android 11 and 12's developer mode that is the possibility to use Gabeldorsche, the new Bluetooth Stack that is implemented by default on Android 13+.

And enabling Gabeldorsche on my devices under Android 11 and 12 triggers the "old" onCharacteristicChanged callback (see below) when writing on my characteristic.

The "old" callback correspond to the one with the following signature and that has been deprecated on API level 33:

public void onCharacteristicChanged (BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)

I implemented this old callback in my code. It has exactly the same code as the new callback except an addition of a local variable that replace the byte[] value parameter of the new callback:

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    super.onCharacteristicChanged(gatt, characteristic);
    byte[] value = characteristic.getValue(); // replace value parameter of the new callback

    // code of the callback
}

Then, I'm looking for a solution that does not need the manual activation of Gabeldorsche.

0

There are 0 best solutions below