Merge pull request 'BtLEAction: code review' (#6287)

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6287
This commit is contained in:
José Rebelo
2026-06-14 14:23:48 +02:00
18 changed files with 116 additions and 63 deletions
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2024 Carsten Pfeiffer, Uwe Hermann
/* Copyright (C) 2015-2026 Carsten Pfeiffer, Uwe Hermann, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -19,15 +19,19 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
/**
* The Bluedroid implementation only allows performing one GATT request at a time.
* As they are asynchronous anyway, we encapsulate every GATT request (read and write)
* inside a runnable action.
* <p/>
* <p>
* These actions are then executed one after another, ensuring that every action's result
* has been posted before invoking the next action.
* </p>
*/
public abstract class BtLEAction {
private final BluetoothGattCharacteristic characteristic;
@@ -41,33 +45,36 @@ public abstract class BtLEAction {
/**
* Returns true if this action expects an (async) result which must
* be waited for, before continuing with other actions.
* <p/>
* <p>
* This is needed because the current Bluedroid stack can only deal
* with one single bluetooth operation at a time.
* </p>
*/
public abstract boolean expectsResult();
/**
* Executes this action, e.g. reads or write a GATT characteristic.
* Executes this action, e.g. reads or write a {@link GattCharacteristic}.
*
* @param gatt the characteristic to manipulate, or null if none.
* @return true if the action was successful, false otherwise
* @return {@code true} if the action was successful, {@code false} otherwise
*/
public abstract boolean run(BluetoothGatt gatt);
public abstract boolean run(@NonNull BluetoothGatt gatt);
/**
* Returns the GATT characteristic being read/written/...
*
* @return the GATT characteristic, or <code>null</code>
* @return the GATT characteristic, or {@code null}
*/
@Nullable
public BluetoothGattCharacteristic getCharacteristic() {
return characteristic;
}
@NonNull
protected String getCreationTime() {
return DateTimeUtils.formatLocalTime(creationTimestamp);
}
@NonNull
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2024 Carsten Pfeiffer
/* Copyright (C) 2015-2026 Carsten Pfeiffer
This file is part of Gadgetbridge.
@@ -18,6 +18,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.bluetooth.BluetoothGatt;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,7 +31,7 @@ public abstract class AbortTransactionAction extends PlainAction {
private static final Logger LOG = LoggerFactory.getLogger(AbortTransactionAction.class);
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
if (shouldAbort()) {
LOG.info("Aborting transaction because abort criteria met.");
return false;
@@ -39,6 +41,7 @@ public abstract class AbortTransactionAction extends PlainAction {
protected abstract boolean shouldAbort();
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getSimpleName() + " abort=" + shouldAbort();
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2025 Andreas Böhler, Thomas Kuehne
/* Copyright (C) 2023-2026 Andreas Böhler, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -22,6 +22,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.IntentFilter;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -70,7 +71,7 @@ public class BondAction extends PlainAction implements BondingInterface {
}
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
BluetoothDevice device = gatt.getDevice();
mCandidate = new GBDeviceCandidate(device, GBDevice.RSSI_UNKNOWN, null, null);
BondingUtil.tryBondThenComplete(this, device);
@@ -1,4 +1,4 @@
/* Copyright (C) 2016-2024 Carsten Pfeiffer
/* Copyright (C) 2016-2026 Carsten Pfeiffer
This file is part of Gadgetbridge.
@@ -16,6 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,9 +31,10 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class CheckInitializedAction extends AbortTransactionAction {
private static final Logger LOG = LoggerFactory.getLogger(CheckInitializedAction.class);
@NonNull
private final GBDevice device;
public CheckInitializedAction(GBDevice gbDevice) {
public CheckInitializedAction(@NonNull GBDevice gbDevice) {
device = gbDevice;
}
@@ -39,7 +42,7 @@ public class CheckInitializedAction extends AbortTransactionAction {
protected boolean shouldAbort() {
boolean abort = device.isInitialized();
if (abort) {
LOG.info("Aborting device initialization, because already initialized: " + device);
LOG.info("Aborting device initialization, because already initialized: {}", device);
}
return abort;
}
@@ -1,4 +1,4 @@
/* Copyright (C) 2016-2025 Andreas Shimokawa, Carsten Pfeiffer, Thomas Kuehne
/* Copyright (C) 2016-2026 Andreas Shimokawa, Carsten Pfeiffer, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -19,16 +19,19 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public abstract class ConditionalWriteAction extends WriteAction {
private boolean actualGenerated;
private byte[] actual;
public ConditionalWriteAction(BluetoothGattCharacteristic characteristic) {
public ConditionalWriteAction(@NonNull BluetoothGattCharacteristic characteristic) {
super(characteristic, null);
}
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
byte[] value = getValue();
if (value != null) {
return super.run(gatt);
@@ -47,12 +50,14 @@ public abstract class ConditionalWriteAction extends WriteAction {
/**
* Checks the condition whether the write shall happen or not.
* Returns the actual value to be written or null in case nothing shall be written.
* <p/>
* Note that returning null will not cause {@link #run()} to return false, in other words,
* Returns the actual value to be written or {@code null} in case nothing shall be written.
* <p>
* Note that returning {@code null} will not cause {@link #run(BluetoothGatt)} to return {@code false}, in other words,
* the rest of the queue will still be executed.
* </p>
*
* @return the value to be written or null to not write anything
* @return the value to be written or {@code null} to not write anything
*/
@Nullable
protected abstract byte[] checkCondition();
}
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2025 Johannes Krude, Thomas Kuehne
/* Copyright (C) 2023-2026 Johannes Krude, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -50,7 +50,7 @@ public class FunctionAction extends BtLEAction {
}
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
try {
final boolean success;
if (mRunnable != null) {
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2024 Alicia Hormann, Carsten Pfeiffer, Daniele Gobbetti
/* Copyright (C) 2015-2026 Alicia Hormann, Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@@ -32,6 +32,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
/**
@@ -43,16 +45,19 @@ public class NotifyAction extends BtLEAction {
private static final Logger LOG = LoggerFactory.getLogger(NotifyAction.class);
private final boolean enableFlag;
private boolean hasWrittenDescriptor = false;
private boolean hasWrittenDescriptor;
public NotifyAction(BluetoothGattCharacteristic characteristic, boolean enable) {
public NotifyAction(@NonNull BluetoothGattCharacteristic characteristic, boolean enable) {
super(characteristic);
enableFlag = enable;
hasWrittenDescriptor = false;
}
/// shared write implementation that can be used without a BtLEAction
@SuppressLint("MissingPermission")
public static boolean writeDescriptor(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final byte[] value) {
public static boolean writeDescriptor(@Nullable final BluetoothGatt gatt,
@Nullable final BluetoothGattDescriptor descriptor,
@NonNull final byte[] value) {
if (gatt == null) {
LOG.error("gatt == null");
return false;
@@ -96,7 +101,7 @@ public class NotifyAction extends BtLEAction {
@Override
@RequiresPermission("android.permission.BLUETOOTH_CONNECT")
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
// register gatt's callback to receive notifications
boolean result = gatt.setCharacteristicNotification(getCharacteristic(), enableFlag);
@@ -137,6 +142,7 @@ public class NotifyAction extends BtLEAction {
return hasWrittenDescriptor;
}
@NonNull
@Override
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2024 Carsten Pfeiffer
/* Copyright (C) 2015-2026 Carsten Pfeiffer
This file is part of Gadgetbridge.
@@ -16,6 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
@@ -34,6 +36,7 @@ public abstract class PlainAction extends BtLEAction {
return false;
}
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getSimpleName();
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2024 Carsten Pfeiffer, Daniele Gobbetti
/* Copyright (C) 2015-2026 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@@ -20,6 +20,8 @@ import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,13 +36,13 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
public class ReadAction extends BtLEAction {
private static final Logger LOG = LoggerFactory.getLogger(ReadAction.class);
public ReadAction(BluetoothGattCharacteristic characteristic) {
public ReadAction(@NonNull BluetoothGattCharacteristic characteristic) {
super(characteristic);
}
@SuppressLint("MissingPermission")
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
int properties = getCharacteristic().getProperties();
if ((properties & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
return gatt.readCharacteristic(getCharacteristic());
@@ -54,6 +56,7 @@ public class ReadAction extends BtLEAction {
return true;
}
@NonNull
@Override
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Thomas Kuehne
/* Copyright (C) 2025-2026 Thomas Kuehne
This file is part of Gadgetbridge.
@@ -21,6 +21,7 @@ import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.slf4j.Logger;
@@ -47,7 +48,7 @@ public class ReadPhyAction extends BtLEAction {
@SuppressLint("MissingPermission")
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
try {
gatt.readPhy();
return true;
@@ -57,6 +58,7 @@ public class ReadPhyAction extends BtLEAction {
}
}
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getSimpleName();
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 José Rebelo
/* Copyright (C) 2023-2026 José Rebelo
This file is part of Gadgetbridge.
@@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BleNamesResolver;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
@@ -38,10 +40,11 @@ public class RequestConnectionPriorityAction extends BtLEAction {
@Override
@SuppressLint("MissingPermission")
public boolean run(final BluetoothGatt gatt) {
public boolean run(@NonNull final BluetoothGatt gatt) {
return gatt.requestConnectionPriority(priority);
}
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getSimpleName() + " " +
@@ -1,4 +1,4 @@
/* Copyright (C) 2019-2024 Andreas Shimokawa, Daniel Dakhno
/* Copyright (C) 2019-2026 Andreas Shimokawa, Daniel Dakhno
This file is part of Gadgetbridge.
@@ -19,6 +19,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
@@ -27,7 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
public class RequestMtuAction extends BtLEAction {
private final int mtu;
public RequestMtuAction(int mtu) {
public RequestMtuAction(@IntRange(from = 23L, to = 517L) final int mtu) {
super(null);
this.mtu = mtu;
}
@@ -40,10 +43,11 @@ public class RequestMtuAction extends BtLEAction {
@SuppressLint("MissingPermission")
@Override
public boolean run(BluetoothGatt gatt) {
return gatt.requestMtu(this.mtu);
public boolean run(@NonNull final BluetoothGatt gatt) {
return gatt.requestMtu(mtu);
}
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getSimpleName() + " mtu=" + mtu;
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2025 Carsten Pfeiffer, Thomas Kuehne
/* Copyright (C) 2015-2026 Carsten Pfeiffer, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -45,7 +45,7 @@ public class SetDeviceBusyAction extends PlainAction {
}
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
if (busyTask == 0) {
device.unsetBusyTask();
} else {
@@ -56,6 +56,7 @@ public class SetDeviceBusyAction extends PlainAction {
return true;
}
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getName() + " "
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2025 Andreas Shimokawa, Carsten Pfeiffer, Daniel Dakhno, Thomas Kuehne
/* Copyright (C) 2015-2026 Andreas Shimokawa, Carsten Pfeiffer, Daniel Dakhno, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -28,18 +28,19 @@ public class SetDeviceStateAction extends PlainAction {
private final GBDevice.State deviceState;
private final Context context;
public SetDeviceStateAction(GBDevice device, GBDevice.State deviceState, Context context) {
public SetDeviceStateAction(@NonNull GBDevice device, @NonNull GBDevice.State deviceState, @NonNull Context context) {
this.device = device;
this.deviceState = deviceState;
this.context = context;
}
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
device.setUpdateState(deviceState, context);
return true;
}
@NonNull
public Context getContext() {
return context;
}
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Thomas Kuehne
/* Copyright (C) 2025-2026 Thomas Kuehne
This file is part of Gadgetbridge.
@@ -21,6 +21,7 @@ import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.slf4j.Logger;
@@ -41,7 +42,7 @@ public class SetPreferredPhyAction extends BtLEAction {
private final int mRxPhy;
private final int mPhyOptions;
public SetPreferredPhyAction(int txPhy, int rxPhy, int phyOptions) {
public SetPreferredPhyAction(final int txPhy, final int rxPhy, final int phyOptions) {
super(null);
mTxPhy = txPhy;
mRxPhy = rxPhy;
@@ -57,7 +58,7 @@ public class SetPreferredPhyAction extends BtLEAction {
@SuppressLint("MissingPermission")
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull final BluetoothGatt gatt) {
try {
gatt.setPreferredPhy(mTxPhy, mRxPhy, mPhyOptions);
return true;
@@ -68,6 +69,7 @@ public class SetPreferredPhyAction extends BtLEAction {
}
}
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getSimpleName() + " tx=" + mTxPhy
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2025 Andreas Shimokawa, Carsten Pfeiffer, José Rebelo, Thomas Kuehne
/* Copyright (C) 2015-2026 Andreas Shimokawa, Carsten Pfeiffer, José Rebelo, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothGatt;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -40,15 +41,15 @@ public class SetProgressAction extends PlainAction {
* @param percentage Current percentage indicating how far along the action has progressed
* @param context Context in which to create the notification
*/
public SetProgressAction(@StringRes int textRes, boolean ongoing, int percentage, Context context) {
this.text = context.getString(textRes);;
public SetProgressAction(@StringRes int textRes, boolean ongoing, int percentage, @NonNull Context context) {
this.text = context.getString(textRes);
this.ongoing = ongoing;
this.percentage = percentage;
this.context = context;
}
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
GB.updateInstallNotification(this.text, this.ongoing, this.percentage, this.context);
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(context);
@@ -63,6 +64,7 @@ public class SetProgressAction extends PlainAction {
return true;
}
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getSimpleName() + ": " + text + "; " + percentage + "%";
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2024 Carsten Pfeiffer
/* Copyright (C) 2015-2026 Carsten Pfeiffer
This file is part of Gadgetbridge.
@@ -18,6 +18,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.bluetooth.BluetoothGatt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
/**
* An action that will cause the queue to {@link Thread#sleep(long) sleep} for the specified time.
* Note that this is usually a bad idea, since it will not be able to process messages
@@ -25,22 +28,23 @@ import android.bluetooth.BluetoothGatt;
*/
public class SleepAction extends PlainAction {
private final int mMillis;
private final long mMillis;
public SleepAction(int millis) {
public SleepAction(@IntRange(from = 0L) final long millis) {
mMillis = millis;
}
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull final BluetoothGatt gatt) {
try {
Thread.sleep(mMillis);
return true;
} catch (InterruptedException e) {
} catch (final InterruptedException e) {
return false;
}
}
@NonNull
@Override
public String toString() {
return getCreationTime() + " " + getClass().getSimpleName() + " " + mMillis + " ms";
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2025 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2015-2026 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, José Rebelo, Uwe Hermann, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -22,6 +22,8 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothStatusCodes;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,18 +44,18 @@ public class WriteAction extends BtLEAction {
private final byte[] value;
private final boolean legacyCompat;
public WriteAction(BluetoothGattCharacteristic characteristic, byte[] value) {
public WriteAction(@NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
this(characteristic, value, false);
}
public WriteAction(BluetoothGattCharacteristic characteristic, byte[] value, boolean legacyCompat) {
public WriteAction(@NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value, boolean legacyCompat) {
super(characteristic);
this.value = value;
this.legacyCompat = legacyCompat;
}
@Override
public boolean run(BluetoothGatt gatt) {
public boolean run(@NonNull BluetoothGatt gatt) {
BluetoothGattCharacteristic characteristic = getCharacteristic();
int properties = characteristic.getProperties();
//TODO: expectsResult should return false if PROPERTY_WRITE_NO_RESPONSE is true, but this leads to timing issues
@@ -74,7 +76,7 @@ public class WriteAction extends BtLEAction {
}
@SuppressLint("MissingPermission")
private static boolean writeCharacteristicImp(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value, boolean legacyCompat) {
private static boolean writeCharacteristicImp(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value, boolean legacyCompat) {
if (GBApplication.isRunningTiramisuOrLater() && !legacyCompat) {
// use API introduced in SDK level 33 to catch exceptions and more specific errors
try {
@@ -109,6 +111,7 @@ public class WriteAction extends BtLEAction {
return true;
}
@NonNull
@Override
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();