ble: add ReadPhyAction, SetPreferredPhyAction, onServiceChanged and onReliableWriteCompleted

This commit is contained in:
Thomas Kuehne
2025-06-28 19:34:22 +02:00
committed by José Rebelo
parent e40608dcf6
commit f732ff582a
15 changed files with 488 additions and 112 deletions
@@ -28,6 +28,7 @@ import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
@@ -44,7 +45,6 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitializedAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@@ -52,7 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
/**
* Abstract base class for all devices connected through Bluetooth Low Energy (LE) aka
* Bluetooth Smart.
* <p/>
* <p>
* The connection to the device and all communication is made with a generic {@link BtLEQueue}.
* Messages to the device are encoded as {@link BtLEAction actions} or {@link BtLEServerAction actions}
* that are grouped with a {@link Transaction} or {@link ServerTransaction} and sent via {@link BtLEQueue}.
@@ -554,6 +554,23 @@ public abstract class AbstractBTLEMultiDeviceSupport extends AbstractBTLEDeviceS
return false;
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
}
@Override
public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status){
}
@Override
public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status){
}
@Override
public void onServiceChanged(@NonNull BluetoothGatt gatt){
logger.warn("onServiceChanged is NOT supported by AbstractBTLEMultiDeviceSupport");
}
/**
* Gets the current MTU, or 0 if unknown
*
@@ -1,5 +1,5 @@
/* Copyright (C) 2015-2024 Andreas Shimokawa, Carsten Pfeiffer, Damien
Gaignon, Daniel Dakhno, Uwe Hermann
/* Copyright (C) 2015-2025 Andreas Shimokawa, Carsten Pfeiffer, Damien
Gaignon, Daniel Dakhno, Uwe Hermann, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -21,6 +21,11 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.content.Context;
import android.os.Build;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.io.IOException;
import java.util.UUID;
@@ -29,17 +34,18 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
/**
* Abstract base class for a BTLEOperation, i.e. an operation that does more than
* Abstract base class for a {@link BTLEOperation}, i.e. an operation that does more than
* just sending a few bytes to the device. It typically involves exchanging many messages
* between the mobile and the device.
* <p/>
* One operation may execute multiple @{link Transaction transactions} with each
* multiple @{link BTLEAction actions}.
* <p/>
* This class implements GattCallback so that subclasses may override those methods
* <p>
* One operation may execute multiple {@link Transaction transactions} with each
* multiple {@link BtLEAction actions}.
* <p>
* This class implements {@link GattCallback} so that subclasses may override those methods
* to handle those events.
* Note: by default all Gatt events are forwarded to AbstractBTLEDeviceSupport, subclasses may override
* this behavior.
* <p>
* Note: by default all Gatt events are forwarded to {@link AbstractBTLESingleDeviceSupport},
* subclasses may override this behavior.
*/
public abstract class AbstractBTLEOperation<T extends AbstractBTLESingleDeviceSupport> implements GattCallback, BTLEOperation {
private final T mSupport;
@@ -53,9 +59,7 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLESingleDeviceSu
/**
* Performs this operation. The whole operation is asynchronous, i.e.
* this method quickly returns before the actual operation is finished.
* Calls #prePerform() and, if successful, #doPerform().
*
* @throws IOException
* Calls {@link #prePerform()} and, if successful, {@link #doPerform()}.
*/
@Override
public final void perform() throws IOException {
@@ -66,42 +70,32 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLESingleDeviceSu
}
/**
* Hook for subclasses to perform something before #doPerform() is invoked.
*
* @throws IOException
* Hook for subclasses to perform something before {@link #doPerform()} is invoked.
*/
protected void prePerform() throws IOException {
}
/**
* Subclasses must implement this. When invoked, #prePerform() returned
* Subclasses must implement this. When invoked, {@link #prePerform()} returned
* successfully.
* Note that subclasses HAVE TO call #operationFinished() when the entire
* Note that subclasses HAVE TO call {@link #operationFinished()} when the entire
* operation is done (successful or not).
*
* @throws IOException
*/
protected abstract void doPerform() throws IOException;
/**
* You MUST call this method when the operation has finished, either
* successfully or unsuccessfully.
*
* <p>
* Subclasses must ensure that the {@link BtLEQueue queue's}'s gatt callback (set on the transaction builder by {@link #performInitialized(String)})
* is being unset, otherwise it will continue to receive events until another transaction is being executed by the queue.
*
* @throws IOException
*/
protected void operationFinished() throws IOException {
}
/**
* Delegates to the DeviceSupport instance and additionally sets this instance as the Gatt
* callback for the transaction.
*
* @param taskName
* @return
* @throws IOException
* Delegates to the {@link AbstractBTLESingleDeviceSupport#performInitialized(String)} instance
* and additionally sets this instance as the {@link GattCallback} for the transaction.
*/
public TransactionBuilder performInitialized(String taskName) throws IOException {
TransactionBuilder builder = mSupport.performInitialized(taskName);
@@ -109,20 +103,27 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLESingleDeviceSu
return builder;
}
/**
* Delegates to the {@link AbstractBTLESingleDeviceSupport#createTransactionBuilder(String)}
* instance and additionally sets this instance as the {@link GattCallback} for the transaction.
*/
public TransactionBuilder createTransactionBuilder(String taskName) {
TransactionBuilder builder = getSupport().createTransactionBuilder(taskName);
TransactionBuilder builder = mSupport.createTransactionBuilder(taskName);
builder.setCallback(this);
return builder;
}
/// Delegates to {@link AbstractBTLESingleDeviceSupport#performImmediately(TransactionBuilder)}
public void performImmediately(TransactionBuilder builder) throws IOException {
mSupport.performImmediately(builder);
}
/// Delegates to {@link AbstractBTLESingleDeviceSupport#getContext()}
protected Context getContext() {
return mSupport.getContext();
}
/// Delegates to {@link AbstractBTLESingleDeviceSupport#getDevice()}
protected GBDevice getDevice() {
return mSupport.getDevice();
}
@@ -143,10 +144,12 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLESingleDeviceSu
return getClass().getSimpleName();
}
/// Delegates to {@link AbstractBTLESingleDeviceSupport#getCharacteristic(UUID)}
protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
return mSupport.getCharacteristic(uuid);
}
/// Delegates to {@link AbstractBTLESingleDeviceSupport#getQueue()}
protected BtLEQueue getQueue() {
return mSupport.getQueue();
}
@@ -170,7 +173,6 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLESingleDeviceSu
return mSupport;
}
// All Gatt callbacks delegated to MiBandSupport
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
mSupport.onConnectionStateChange(gatt, status, newState);
@@ -215,4 +217,27 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLESingleDeviceSu
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
mSupport.onMtuChanged(gatt, mtu, status);
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
mSupport.onReliableWriteCompleted(gatt, status);
}
@RequiresApi(Build.VERSION_CODES.O)
@Override
public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
mSupport.onPhyRead(gatt, txPhy, rxPhy, status);
}
@RequiresApi(Build.VERSION_CODES.O)
@Override
public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
mSupport.onPhyUpdate(gatt, txPhy, rxPhy, status);
}
@RequiresApi(Build.VERSION_CODES.S)
@Override
public void onServiceChanged(@NonNull BluetoothGatt gatt) {
mSupport.onServiceChanged(gatt);
}
}
@@ -29,6 +29,7 @@ import android.bluetooth.BluetoothGattService;
import android.content.Context;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
@@ -469,6 +470,27 @@ public abstract class AbstractBTLESingleDeviceSupport extends AbstractBTLEDevice
return false;
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
}
@Override
public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status){
}
@Override
public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status){
}
@Override
public void onServiceChanged(@NonNull BluetoothGatt gatt){
logger.warn("onServiceChanged is NOT supported by AbstractBTLESingleDeviceSupport");
}
/**
* Gets the current MTU, or 0 if unknown
* @return the current MTU, 0 if unknown
@@ -1,4 +1,4 @@
/* Copyright (C) 2015-2024 Carsten Pfeiffer, Daniel Dakhno
/* Copyright (C) 2015-2025 Carsten Pfeiffer, Daniel Dakhno, Thomas Kuehne
This file is part of Gadgetbridge.
@@ -20,6 +20,8 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import androidx.annotation.NonNull;
/**
* Base class for GattCallbacks wishing to just implement a few of the methods.
*/
@@ -64,4 +66,20 @@ public abstract class AbstractGattCallback implements GattCallback {
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
}
@Override
public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status){
}
@Override
public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status){
}
@Override
public void onServiceChanged(@NonNull BluetoothGatt gatt){
}
}
@@ -31,11 +31,13 @@ import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -517,11 +519,11 @@ public final class BtLEQueue {
GattCallback mTransactionGattCallback;
private final GattCallback mExternalGattCallback;
public InternalGattCallback(GattCallback externalGattCallback) {
InternalGattCallback(GattCallback externalGattCallback) {
mExternalGattCallback = externalGattCallback;
}
public void setTransactionGattCallback(@Nullable GattCallback callback) {
void setTransactionGattCallback(@Nullable GattCallback callback) {
mTransactionGattCallback = callback;
}
@@ -540,6 +542,10 @@ public final class BtLEQueue {
BleNamesResolver.getStateString(newState), BleNamesResolver.getStatusString(status),
BleNamesResolver.getBondStateString(bondState));
if (!checkCorrectGattInstance(gatt, "onConnectionStateChange")) {
return;
}
synchronized (mGattMonitor) {
if (mBluetoothGatt == null) {
mBluetoothGatt = gatt;
@@ -597,7 +603,9 @@ public final class BtLEQueue {
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (!checkCorrectGattInstance(gatt, "services discovered: " + BleNamesResolver.getStatusString(status))) {
LOG.debug("services discovered: {}", BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "onServicesDiscovered")) {
return;
}
@@ -630,14 +638,14 @@ public final class BtLEQueue {
checkWaitingCharacteristic(characteristic, status);
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
LOG.debug("mtu changed to {} {}", mtu, BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "onMtuChanged")) {
return;
}
final GattCallback callback = getCallbackToUse();
if (callback != null) {
callback.onMtuChanged(gatt, mtu, status);
@@ -661,13 +669,15 @@ public final class BtLEQueue {
public void onCharacteristicRead(@NonNull BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
@NonNull byte[] value, int status) {
LOG.debug(
"characteristic read: {} {} {}",
characteristic.getUuid(),
BleNamesResolver.getStatusString(status),
status == BluetoothGatt.GATT_SUCCESS ? ": " + Logging.formatBytes(value) : ""
);
if (!checkCorrectGattInstance(gatt, "characteristic read")) {
if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value);
LOG.debug(
"characteristic read: {} {} - {}", characteristic.getUuid(),
BleNamesResolver.getStatusString(status), content
);
}
if (!checkCorrectGattInstance(gatt, "onCharacteristicRead")) {
return;
}
@@ -690,8 +700,13 @@ public final class BtLEQueue {
@Override
public void onDescriptorRead(@NonNull BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status, @NonNull byte[] value) {
LOG.debug("descriptor read: {} {}", descriptor.getUuid(), BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "descriptor read")) {
if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value);
LOG.debug("descriptor read: {} {} - {}", descriptor.getUuid(),
BleNamesResolver.getStatusString(status), content);
}
if (!checkCorrectGattInstance(gatt, "onDescriptorRead")) {
return;
}
@@ -737,7 +752,7 @@ public final class BtLEQueue {
@NonNull byte[] value) {
if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value);
LOG.debug("characteristic changed: {} value: {}", characteristic.getUuid(), content);
LOG.debug("characteristic changed: {} - {}", characteristic.getUuid(), content);
}
if (!checkCorrectGattInstance(gatt, "characteristic changed")) {
return;
@@ -757,7 +772,7 @@ public final class BtLEQueue {
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
LOG.debug("remote rssi: {} {}", rssi, BleNamesResolver.getStatusString(status));
LOG.debug("read remote rssi: {} {}", rssi, BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "remote rssi")) {
return;
}
@@ -772,6 +787,96 @@ public final class BtLEQueue {
}
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
LOG.debug("reliable write completed: {}", BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "onReliableWriteCompleted")) {
return;
}
final GattCallback callback = getCallbackToUse();
if (callback != null) {
try {
callback.onReliableWriteCompleted(gatt, status);
} catch (Throwable ex) {
LOG.error("onReliableWriteCompleted failed", ex);
}
}
final CountDownLatch latch = mWaitForActionResultLatch;
if (latch != null) {
latch.countDown();
}
}
@Override
@RequiresApi(Build.VERSION_CODES.O)
public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
LOG.debug("phy read: tx={} rx={} {}", txPhy, rxPhy,
BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "onPhyRead")) {
return;
}
final GattCallback callback = getCallbackToUse();
if (callback != null) {
try {
callback.onPhyRead(gatt, txPhy, rxPhy, status);
} catch (Throwable ex) {
LOG.error("onPhyRead failed", ex);
}
}
final CountDownLatch latch = mWaitForActionResultLatch;
if (latch != null) {
latch.countDown();
}
}
@Override
@RequiresApi(Build.VERSION_CODES.O)
public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
LOG.debug("phy updated: tx={} rx={} {}", txPhy, rxPhy,
BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "onPhyUpdate")) {
return;
}
final GattCallback callback = getCallbackToUse();
if (callback != null) {
try {
callback.onPhyUpdate(gatt, txPhy, rxPhy, status);
} catch (Throwable ex) {
LOG.error("onPhyUpdate failed", ex);
}
}
// not all updates are triggered by GB:
// can't use mWaitForActionResultLatch here
}
@Override
@RequiresApi(Build.VERSION_CODES.S)
public void onServiceChanged(@NonNull BluetoothGatt gatt) {
LOG.debug("service changed");
if (!checkCorrectGattInstance(gatt, "onServiceChanged")) {
return;
}
final GattCallback callback = getCallbackToUse();
if (callback != null) {
try {
callback.onServiceChanged(gatt);
} catch (Throwable ex) {
LOG.error("onServiceChanged failed", ex);
}
}
}
private void checkWaitingCharacteristic(BluetoothGattCharacteristic characteristic, int status) {
if (status != BluetoothGatt.GATT_SUCCESS) {
if (characteristic != null) {
@@ -779,7 +884,7 @@ public final class BtLEQueue {
}
mAbortTransaction = true;
}
final BluetoothGattCharacteristic waitCharacteristic = BtLEQueue.this.mWaitCharacteristic;
final BluetoothGattCharacteristic waitCharacteristic = mWaitCharacteristic;
if (characteristic != null && waitCharacteristic != null && characteristic.getUuid().equals(waitCharacteristic.getUuid())) {
final CountDownLatch resultLatch = mWaitForActionResultLatch;
if (resultLatch != null) {
@@ -795,7 +900,7 @@ public final class BtLEQueue {
}
}
public void reset() {
void reset() {
if (LOG.isDebugEnabled()) {
LOG.debug("internal gatt callback set to null");
}
@@ -803,7 +908,7 @@ public final class BtLEQueue {
}
/// helper to emulate Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU on older APIs
private byte[] emulateMemorySafeValue(BluetoothGattCharacteristic characteristic,
private static byte[] emulateMemorySafeValue(BluetoothGattCharacteristic characteristic,
int status){
if(status == BluetoothGatt.GATT_SUCCESS) {
byte[] value = characteristic.getValue();
@@ -815,7 +920,7 @@ public final class BtLEQueue {
}
/// helper to emulate Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU on older APIs
private byte[] emulateMemorySafeValue(BluetoothGattDescriptor descriptor,
private static byte[] emulateMemorySafeValue(BluetoothGattDescriptor descriptor,
int status){
if(status == BluetoothGatt.GATT_SUCCESS) {
byte[] value = descriptor.getValue();
@@ -20,12 +20,16 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
/**
* Callback interface handling gatt events.
* Pretty much the same as {@link BluetoothGattCallback}, except it's an interface
* instead of an abstract class. Some handlers commented out, because not used (yet).
*
* <p>
* Note: the boolean return values indicate whether this callback "consumed" this event
* or not. True means, the event was consumed by this instance and no further instances
* shall be notified. False means, this instance could not handle the event.
@@ -33,87 +37,65 @@ import android.bluetooth.BluetoothGattDescriptor;
public interface GattCallback {
/**
* @param gatt
* @param status
* @param newState
* @see BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)
*/
void onConnectionStateChange(BluetoothGatt gatt, int status, int newState);
/**
* @param gatt
* @see BluetoothGattCallback#onServicesDiscovered(BluetoothGatt, int)
*/
void onServicesDiscovered(BluetoothGatt gatt);
/**
* @param gatt
* @param characteristic
* @param value
* @param status
* @see BluetoothGattCallback#onCharacteristicRead(BluetoothGatt, BluetoothGattCharacteristic, byte[], int)
*/
boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
byte[] value, int status);
/**
* @param gatt
* @param characteristic
* @param status
* @see BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)
*/
boolean onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status);
/**
* @param gatt
* @param characteristic
* @param value
* @see BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt, BluetoothGattCharacteristic, byte[])
*/
boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, byte[] value);
/**
* @param gatt
* @param descriptor
* @param status
* @see BluetoothGattCallback#onDescriptorRead(BluetoothGatt, BluetoothGattDescriptor, int, byte[])
*/
boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status, byte[] value);
/**
* @param gatt
* @param descriptor
* @param status
* @see BluetoothGattCallback#onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int)
*/
boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status);
//
// /**
// * @see BluetoothGattCallback#onReliableWriteCompleted(BluetoothGatt, int)
// * @param gatt
// * @param status
// */
// public void onReliableWriteCompleted(BluetoothGatt gatt, int status);
/**
* @param gatt
* @param rssi
* @param status
* @see BluetoothGattCallback#onReadRemoteRssi(BluetoothGatt, int, int)
*/
void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status);
/// @see BluetoothGattCallback#onMtuChanged(BluetoothGatt, int, int)
void onMtuChanged(BluetoothGatt gatt, int mtu, int status);
// /**
// * @see BluetoothGattCallback#onMtuChanged(BluetoothGatt, int, int)
// * @param gatt
// * @param mtu
// * @param status
// */
// public void onMtuChanged(BluetoothGatt gatt, int mtu, int status);
/// @see BluetoothGattCallback#onReliableWriteCompleted(BluetoothGatt, int)
void onReliableWriteCompleted (BluetoothGatt gatt, int status);
/// @see BluetoothGattCallback#onPhyRead(BluetoothGatt, int, int, int)
@RequiresApi(Build.VERSION_CODES.O)
void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status);
/// @see BluetoothGattCallback#onPhyUpdate(BluetoothGatt, int, int, int)
@RequiresApi(Build.VERSION_CODES.O)
void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status);
/// @see BluetoothGattCallback#onServiceChanged(BluetoothGatt)
@RequiresApi(Build.VERSION_CODES.S)
void onServiceChanged(@NonNull BluetoothGatt gatt);
}
@@ -20,12 +20,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.os.Build;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.Arrays;
@@ -34,9 +36,11 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.BondAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.FunctionAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadPhyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.RequestConnectionPriorityAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.RequestMtuAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetPreferredPhyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
@@ -50,6 +54,7 @@ public class TransactionBuilder {
mTransaction = new Transaction(taskName);
}
/// @see ReadAction
public TransactionBuilder read(BluetoothGattCharacteristic characteristic) {
if (characteristic == null) {
LOG.warn("Unable to read characteristic: null");
@@ -59,6 +64,7 @@ public class TransactionBuilder {
return add(action);
}
/// @see WriteAction
public TransactionBuilder write(BluetoothGattCharacteristic characteristic, byte[] data) {
if (characteristic == null) {
LOG.warn("Unable to write characteristic: null");
@@ -79,23 +85,27 @@ public class TransactionBuilder {
return this;
}
/// @see RequestMtuAction
public TransactionBuilder requestMtu(int mtu){
return add(
new RequestMtuAction(mtu)
);
}
/// @see RequestConnectionPriorityAction
public TransactionBuilder requestConnectionPriority(int priority){
return add(
new RequestConnectionPriorityAction(priority)
);
}
/// @see BondAction
public TransactionBuilder bond() {
BondAction action = new BondAction();
return add(action);
}
/// @see NotifyAction
public TransactionBuilder notify(BluetoothGattCharacteristic characteristic, boolean enable) {
if (characteristic == null) {
LOG.warn("Unable to notify characteristic: null");
@@ -120,7 +130,7 @@ public class TransactionBuilder {
return add(action);
}
// Runs the given function/lambda
/// @see FunctionAction
public TransactionBuilder run(FunctionAction.Function function) {
return add(new FunctionAction(function));
}
@@ -138,6 +148,26 @@ public class TransactionBuilder {
return add(action);
}
/**
* Read the current transmitter PHY and receiver PHY of the connection.
* @see ReadPhyAction
*/
@RequiresApi(api = Build.VERSION_CODES.O)
public TransactionBuilder readPhy() {
BtLEAction action = new ReadPhyAction();
return add(action);
}
/**
* Set the preferred PHY of the connection.
* @see SetPreferredPhyAction
*/
@RequiresApi(api = Build.VERSION_CODES.O)
public TransactionBuilder setPreferredPhy(int txPhy, int rxPhy, int phyOptions) {
BtLEAction action = new SetPreferredPhyAction(txPhy, rxPhy, phyOptions);
return add(action);
}
/**
* Sets a GattCallback instance that will be called when the transaction is executed,
* resulting in GattCallback events.
@@ -18,7 +18,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothStatusCodes;
@@ -29,20 +28,21 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BleNamesResolver;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION;
import androidx.annotation.RequiresPermission;
/**
* Enables or disables notifications for a given GATT characteristic.
* Enables or disables notifications for a given {@link BluetoothGattCharacteristic}.
* The result will be made available asynchronously through the
* {@link BluetoothGattCallback}.
* {@link GattCallback#onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int)}.
*/
public class NotifyAction extends BtLEAction {
private static final Logger LOG = LoggerFactory.getLogger(NotifyAction.class);
protected final boolean enableFlag;
private final boolean enableFlag;
private boolean hasWrittenDescriptor = false;
public NotifyAction(BluetoothGattCharacteristic characteristic, boolean enable) {
@@ -107,25 +107,25 @@ public class NotifyAction extends BtLEAction {
int properties = getCharacteristic().getProperties();
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
LOG.debug("use NOTIFICATION for Characteristic " + getCharacteristic().getUuid());
LOG.debug("use NOTIFICATION for Characteristic {}", getCharacteristic().getUuid());
final byte[] value = enableFlag ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
result = writeDescriptor(gatt, clientCharConfigDescriptor, value);
hasWrittenDescriptor = true;
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
LOG.debug("use INDICATION for Characteristic " + getCharacteristic().getUuid());
LOG.debug("use INDICATION for Characteristic {}", getCharacteristic().getUuid());
final byte[] value = enableFlag ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
result = writeDescriptor(gatt, clientCharConfigDescriptor, value);
hasWrittenDescriptor = true;
} else {
LOG.debug("use neither NOTIFICATION nor INDICATION for Characteristic " + getCharacteristic().getUuid());
LOG.debug("use neither NOTIFICATION nor INDICATION for Characteristic {}", getCharacteristic().getUuid());
hasWrittenDescriptor = false;
}
} else {
LOG.warn("Descriptor CLIENT_CHARACTERISTIC_CONFIGURATION for characteristic " + getCharacteristic().getUuid() + " is null");
LOG.warn("Descriptor CLIENT_CHARACTERISTIC_CONFIGURATION for characteristic {}", getCharacteristic().getUuid() + " is null");
hasWrittenDescriptor = false;
}
} else {
LOG.error("Unable to enable notifications for " + getCharacteristic().getUuid());
LOG.error("Unable to enable notifications for {}", getCharacteristic().getUuid());
hasWrittenDescriptor = false;
}
@@ -136,4 +136,13 @@ public class NotifyAction extends BtLEAction {
public boolean expectsResult() {
return hasWrittenDescriptor;
}
@Override
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
return getCreationTime() + ": " + getClass().getSimpleName() + " " + uuid +
(enableFlag ? " enable" : " disable");
}
}
@@ -16,11 +16,13 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.bluetooth.BluetoothGattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
/**
* An abstract non-BTLE action. It performs no bluetooth operation,
* does not have a BluetoothGattCharacteristic instance and expects no result.
* does not have a {@link BluetoothGattCharacteristic} instance and expects no result.
*/
public abstract class PlainAction extends BtLEAction {
@@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -26,7 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
/**
* Invokes a read operation on a given GATT characteristic.
* Invokes a read operation on a given {@link BluetoothGattCharacteristic}.
* The result will be made available asynchronously through
* {@link GattCallback#onCharacteristicRead}
*/
@@ -37,6 +38,7 @@ public class ReadAction extends BtLEAction {
super(characteristic);
}
@SuppressLint("MissingPermission")
@Override
public boolean run(BluetoothGatt gatt) {
int properties = getCharacteristic().getProperties();
@@ -51,4 +53,11 @@ public class ReadAction extends BtLEAction {
public boolean expectsResult() {
return true;
}
@Override
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
return getCreationTime() + ": " + getClass().getSimpleName() + " " + uuid;
}
}
@@ -0,0 +1,64 @@
/* Copyright (C) 2025 Thomas Kuehne
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.os.Build;
import androidx.annotation.RequiresApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
/// Calls {@link BluetoothGatt#readPhy()}. The result will be made available asynchronously through
/// {@link GattCallback#onPhyRead(BluetoothGatt, int, int, int)}
@RequiresApi(api = Build.VERSION_CODES.O)
public class ReadPhyAction extends BtLEAction {
private static final Logger LOG = LoggerFactory.getLogger(ReadPhyAction.class);
public ReadPhyAction() {
super(null);
}
@Override
public boolean expectsResult() {
return true;
}
@SuppressLint("MissingPermission")
@Override
public boolean run(BluetoothGatt gatt) {
try {
gatt.readPhy();
return true;
} catch (final Throwable ex) {
LOG.warn("BluetoothGatt.readPhy failed", ex);
return false;
}
}
@Override
public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName();
}
}
@@ -21,6 +21,7 @@ import android.bluetooth.BluetoothGatt;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
/// Calls {@link BluetoothGatt#requestConnectionPriority(int)}.
public class RequestConnectionPriorityAction extends BtLEAction {
private int priority;
@@ -39,4 +40,9 @@ public class RequestConnectionPriorityAction extends BtLEAction {
public boolean run(final BluetoothGatt gatt) {
return gatt.requestConnectionPriority(priority);
}
@Override
public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " priority=" + priority;
}
}
@@ -16,16 +16,16 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Build;
import androidx.annotation.RequiresApi;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
/// Calls {@link BluetoothGatt#requestMtu(int)}. Results are returned to
/// {@link GattCallback#onMtuChanged(BluetoothGatt, int, int)}
public class RequestMtuAction extends BtLEAction {
private int mtu;
private final int mtu;
public RequestMtuAction(int mtu) {
super(null);
@@ -38,8 +38,14 @@ public class RequestMtuAction extends BtLEAction {
return true;
}
@SuppressLint("MissingPermission")
@Override
public boolean run(BluetoothGatt gatt) {
return gatt.requestMtu(this.mtu);
}
@Override
public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " mtu=" + mtu;
}
}
@@ -0,0 +1,76 @@
/* Copyright (C) 2025 Thomas Kuehne
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.os.Build;
import androidx.annotation.RequiresApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
/// Calls {@link BluetoothGatt#setPreferredPhy(int, int, int)}. The result will be made available
/// asynchronously through {@link GattCallback#onPhyUpdate(BluetoothGatt, int, int, int)}
@RequiresApi(api = Build.VERSION_CODES.O)
public class SetPreferredPhyAction extends BtLEAction {
private static final Logger LOG = LoggerFactory.getLogger(SetPreferredPhyAction.class);
private final int mTxPhy;
private final int mRxPhy;
private final int mPhyOptions;
public SetPreferredPhyAction(int txPhy, int rxPhy, int phyOptions) {
super(null);
mTxPhy = txPhy;
mRxPhy = rxPhy;
mPhyOptions = phyOptions;
}
/// {@link GattCallback#onPhyUpdate(BluetoothGatt, int, int, int)} is also triggered by device
/// activity so can't use {@link BtLEQueue#mWaitForActionResultLatch} to wait for results
@Override
public boolean expectsResult() {
return false;
}
@SuppressLint("MissingPermission")
@Override
public boolean run(BluetoothGatt gatt) {
try {
gatt.setPreferredPhy(mTxPhy, mRxPhy, mPhyOptions);
return true;
} catch (final Throwable ex) {
LOG.warn("BluetoothGatt.setPreferredPhy({}, {}, {}) failed", mTxPhy, mRxPhy,
mPhyOptions, ex);
return false;
}
}
@Override
public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " tx=" + mTxPhy
+ " rx=" + mRxPhy + " opt=" + mPhyOptions;
}
}
@@ -19,7 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.bluetooth.BluetoothGatt;
/**
* An action that will cause the queue to sleep for the specified time.
* 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
* during that time. It is also likely to cause race conditions.
*/
@@ -40,4 +40,9 @@ public class WaitAction extends PlainAction {
return false;
}
}
@Override
public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " " + mMillis + " ms";
}
}