ble: improve exception handling and streamline logged information

This commit is contained in:
Thomas Kuehne
2025-07-04 22:03:15 +02:00
committed by José Rebelo
parent 2709446597
commit b30ede77c7
20 changed files with 417 additions and 75 deletions
@@ -17,6 +17,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge; package nodomain.freeyourgadget.gadgetbridge;
import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -57,7 +58,14 @@ public abstract class Logging {
} else { } else {
stopFileLogger(); stopFileLogger();
} }
getLogger().info("Gadgetbridge version: {}-{}", BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH_SHORT); getLogger().info("Gadgetbridge version: {}-{} {} {}", BuildConfig.VERSION_NAME,
BuildConfig.GIT_HASH_SHORT, BuildConfig.FLAVOR, BuildConfig.BUILD_TYPE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
getLogger().info("Android: SDK_INT={}", Build.VERSION.SDK_INT);
} else {
getLogger().info("Android: SDK_INT_FULL={} SECURITY_PATCH={}", Build.VERSION.SDK_INT_FULL, Build.VERSION.SECURITY_PATCH);
}
} catch (Exception ex) { } catch (Exception ex) {
Log.e("GBApplication", "External files dir not available, cannot log to file", ex); Log.e("GBApplication", "External files dir not available, cannot log to file", ex);
stopFileLogger(); stopFileLogger();
@@ -16,9 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle; package nodomain.freeyourgadget.gadgetbridge.service.btle;
import java.text.DateFormat; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import java.util.Date;
import java.util.Locale;
public abstract class AbstractTransaction { public abstract class AbstractTransaction {
private final String mName; private final String mName;
@@ -33,14 +31,15 @@ public abstract class AbstractTransaction {
} }
protected String getCreationTime() { protected String getCreationTime() {
return DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date(creationTimestamp)); return DateTimeUtils.formatLocalTime(creationTimestamp);
} }
public abstract int getActionCount(); public abstract int getActionCount();
@Override @Override
public String toString() { public String toString() {
return String.format(Locale.US, "%s: Transaction task: %s with %d actions", getCreationTime(), getTaskName(), getActionCount()); return getCreationTime() + " " + getClass().getSimpleName() + " with "
+ getActionCount() + " actions for " + getTaskName();
} }
} }
@@ -25,11 +25,11 @@ import android.util.SparseArray;
import java.util.HashMap; import java.util.HashMap;
public class BleNamesResolver { public class BleNamesResolver {
private static HashMap<String, String> mServices = new HashMap<>(); private static HashMap<String, String> mServices = new HashMap<>(100);
private static HashMap<String, String> mCharacteristics = new HashMap<>(); private static HashMap<String, String> mCharacteristics = new HashMap<>(600);
private static SparseArray<String> mValueFormats = new SparseArray<>(); private static SparseArray<String> mValueFormats = new SparseArray<>(10);
private static SparseArray<String> mAppearance = new SparseArray<>(); private static SparseArray<String> mAppearance = new SparseArray<>(20);
private static SparseArray<String> mHeartRateSensorLocation = new SparseArray<>(); private static SparseArray<String> mHeartRateSensorLocation = new SparseArray<>(10);
static public String resolveServiceName(final String uuid) { static public String resolveServiceName(final String uuid) {
String result = mServices.get(uuid); String result = mServices.get(uuid);
@@ -363,6 +363,17 @@ public class BleNamesResolver {
} }
} }
/// lookup description for numeric {@link BluetoothGatt#requestConnectionPriority} priorities
public static String getConnectionPriorityString(int priority){
return switch (priority) {
case BluetoothGatt.CONNECTION_PRIORITY_BALANCED -> "CONNECTION_PRIORITY_BALANCED";
case BluetoothGatt.CONNECTION_PRIORITY_HIGH -> "CONNECTION_PRIORITY_HIGH";
case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER -> "CONNECTION_PRIORITY_LOW_POWER";
case BluetoothGatt.CONNECTION_PRIORITY_DCK -> "CONNECTION_PRIORITY_DCK";
default -> "priority_" + priority;
};
}
static public String getCharacteristicPropertyString(int property) { static public String getCharacteristicPropertyString(int property) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
if (BluetoothGattCharacteristic.PROPERTY_BROADCAST == (BluetoothGattCharacteristic.PROPERTY_BROADCAST & property)) { if (BluetoothGattCharacteristic.PROPERTY_BROADCAST == (BluetoothGattCharacteristic.PROPERTY_BROADCAST & property)) {
@@ -484,6 +495,8 @@ public class BleNamesResolver {
mServices.put("86f65000-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Request)"); mServices.put("86f65000-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Request)");
mServices.put("86f66000-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Data)"); mServices.put("86f66000-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Data)");
mServices.put("8d53dc1d-1db7-4cd3-868b-8a527460aa84", "(Propr: SMP - Simple Management Protocol)"); mServices.put("8d53dc1d-1db7-4cd3-868b-8a527460aa84", "(Propr: SMP - Simple Management Protocol)");
mServices.put("6e40fff0-b5a3-f393-e0a9-e50e24dcca9e", "(Propr: NUS - Nordic UART Service)");
mServices.put("de5bf728-d711-4e47-af26-65e3012a5dc7", "(Propr: Yawell Serial)");
// source https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/characteristic_uuids.yaml // source https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/characteristic_uuids.yaml
mCharacteristics.put("00002a00-0000-1000-8000-00805f9b34fb", "Device Name"); mCharacteristics.put("00002a00-0000-1000-8000-00805f9b34fb", "Device Name");
@@ -1015,6 +1028,10 @@ public class BleNamesResolver {
mCharacteristics.put("86f65002-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Response)"); mCharacteristics.put("86f65002-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Response)");
mCharacteristics.put("86f66001-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Data)"); mCharacteristics.put("86f66001-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Data)");
mCharacteristics.put("da2e7828-fbce-4e01-ae9e-261174997c48", "(Propr: SMP - Simple Management Protocol)"); mCharacteristics.put("da2e7828-fbce-4e01-ae9e-261174997c48", "(Propr: SMP - Simple Management Protocol)");
mCharacteristics.put("6e400002-b5a3-f393-e0a9-e50e24dcca9e", "(Propr: Nordic UART TX)");
mCharacteristics.put("6e400003-b5a3-f393-e0a9-e50e24dcca9e", "(Propr: Nordic UART RX)");
mCharacteristics.put("de5bf729-d711-4e47-af26-65e3012a5dc7", "(Propr: Yawell Notify)");
mCharacteristics.put("de5bf72a-d711-4e47-af26-65e3012a5dc7", "(Propr: Yawell Write)");
mValueFormats.put(52, "32bit float"); mValueFormats.put(52, "32bit float");
mValueFormats.put(50, "16bit float"); mValueFormats.put(50, "16bit float");
@@ -19,8 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattCharacteristic;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
/** /**
@@ -67,12 +65,12 @@ public abstract class BtLEAction {
} }
protected String getCreationTime() { protected String getCreationTime() {
return DateTimeUtils.formatDateTime(new Date(creationTimestamp)); return DateTimeUtils.formatLocalTime(creationTimestamp);
} }
public String toString() { public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic(); BluetoothGattCharacteristic characteristic = getCharacteristic();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString(); String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
return getCreationTime() + ": " + getClass().getSimpleName() + " on characteristic: " + uuid; return getCreationTime() + " " + getClass().getSimpleName() + " on characteristic " + uuid;
} }
} }
@@ -53,17 +53,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.GBExceptionHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/** /**
* One queue/thread per connectable device. * One queue/thread per connectable device.
*/ */
@SuppressLint("MissingPermission") // if we're using this, we have bluetooth permissions @SuppressLint("MissingPermission") // if we're using this, we have bluetooth permissions
public final class BtLEQueue { public final class BtLEQueue implements Thread.UncaughtExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(BtLEQueue.class); private static final Logger LOG = LoggerFactory.getLogger(BtLEQueue.class);
private static final byte[] EMPTY = new byte[0]; private static final byte[] EMPTY = new byte[0];
private static final AtomicLong THREAD_COUNTER = new AtomicLong(0L); private static final AtomicLong THREAD_COUNTER = new AtomicLong(0L);
@@ -86,7 +87,7 @@ public final class BtLEQueue {
private CountDownLatch mWaitForServerActionResultLatch; private CountDownLatch mWaitForServerActionResultLatch;
private CountDownLatch mConnectionLatch; private CountDownLatch mConnectionLatch;
private BluetoothGattCharacteristic mWaitCharacteristic; private BluetoothGattCharacteristic mWaitCharacteristic;
private final InternalGattCallback internalGattCallback; private final NoThrowBluetoothGattCallback<InternalGattCallback> internalGattCallback;
private final InternalGattServerCallback internalGattServerCallback; private final InternalGattServerCallback internalGattServerCallback;
private final AbstractBTLEDeviceSupport mDeviceSupport; private final AbstractBTLEDeviceSupport mDeviceSupport;
private final boolean mImplicitGattCallbackModify; private final boolean mImplicitGattCallbackModify;
@@ -99,7 +100,7 @@ public final class BtLEQueue {
private class DispatchRunnable implements Runnable { private class DispatchRunnable implements Runnable {
@Override @Override
public void run() { public void run() {
LOG.debug("Queue Dispatch Thread started."); LOG.debug("{} started", Thread.currentThread().getName());
boolean crashed = false; boolean crashed = false;
while (!mDisposed.get() && !crashed) { while (!mDisposed.get() && !crashed) {
@@ -109,7 +110,7 @@ public final class BtLEQueue {
if (!isConnected()) { if (!isConnected()) {
LOG.debug("not connected, waiting for connection..."); LOG.debug("not connected, waiting for connection...");
// TODO: request connection and initialization from the outside and wait until finished // TODO: request connection and initialization from the outside and wait until finished
internalGattCallback.reset(); internalGattCallback.Delegate.reset();
// wait until the connection succeeds before running the actions // wait until the connection succeeds before running the actions
// Note that no automatic connection is performed. This has to be triggered // Note that no automatic connection is performed. This has to be triggered
@@ -131,7 +132,7 @@ public final class BtLEQueue {
break; break;
} }
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("About to run server action: {}", action); LOG.debug("execute server: {}", action);
} }
if (action.run(mBluetoothGattServer)) { if (action.run(mBluetoothGattServer)) {
// check again, maybe due to some condition, action did not need to write, so we can't wait // check again, maybe due to some condition, action did not need to write, so we can't wait
@@ -153,7 +154,7 @@ public final class BtLEQueue {
if (qTransaction instanceof final Transaction transaction) { if (qTransaction instanceof final Transaction transaction) {
LOG.trace("Changing gatt callback for {}? {}", transaction.getTaskName(), transaction.isModifyGattCallback()); LOG.trace("Changing gatt callback for {}? {}", transaction.getTaskName(), transaction.isModifyGattCallback());
if (mImplicitGattCallbackModify || transaction.isModifyGattCallback()) { if (mImplicitGattCallbackModify || transaction.isModifyGattCallback()) {
internalGattCallback.setTransactionGattCallback(transaction.getGattCallback()); internalGattCallback.Delegate.setTransactionGattCallback(transaction.getGattCallback());
} }
mAbortTransaction = false; mAbortTransaction = false;
// Run all actions of the transaction until one doesn't succeed // Run all actions of the transaction until one doesn't succeed
@@ -174,12 +175,12 @@ public final class BtLEQueue {
mWaitCharacteristic = action.getCharacteristic(); mWaitCharacteristic = action.getCharacteristic();
mWaitForActionResultLatch = new CountDownLatch(1); mWaitForActionResultLatch = new CountDownLatch(1);
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("About to run action: {}", action); LOG.debug("execute: {}", action);
} }
if (action instanceof final GattListenerAction listenerAction) { if (action instanceof final GattListenerAction listenerAction) {
// this special action overwrites the transaction gatt listener (if any), it must // this special action overwrites the transaction gatt listener (if any), it must
// always be the last action in the transaction // always be the last action in the transaction
internalGattCallback.setTransactionGattCallback(listenerAction.getGattCallback()); internalGattCallback.Delegate.setTransactionGattCallback(listenerAction.getGattCallback());
} }
if (action.run(mBluetoothGatt)) { if (action.run(mBluetoothGatt)) {
// check again, maybe due to some condition, action did not need to write, so we can't wait // check again, maybe due to some condition, action did not need to write, so we can't wait
@@ -209,7 +210,7 @@ public final class BtLEQueue {
mWaitCharacteristic = null; mWaitCharacteristic = null;
} }
} }
LOG.info("Queue Dispatch Thread terminated."); LOG.debug("{} terminated", Thread.currentThread().getName());
} }
}; };
@@ -230,9 +231,10 @@ public final class BtLEQueue {
mDisposed = new AtomicBoolean(false); mDisposed = new AtomicBoolean(false);
mGattMonitor = new Object(); mGattMonitor = new Object();
mTransactions = new LinkedBlockingQueue<>(); mTransactions = new LinkedBlockingQueue<>();
internalGattCallback = new InternalGattCallback(deviceSupport); internalGattCallback = new NoThrowBluetoothGattCallback<>(new InternalGattCallback(deviceSupport));
internalGattServerCallback = new InternalGattServerCallback(deviceSupport); internalGattServerCallback = new InternalGattServerCallback(deviceSupport);
mDispatchThread = new Thread(new DispatchRunnable(), "BtLEQueue_" + threadIdx + "_out"); mDispatchThread = new Thread(new DispatchRunnable(), "BtLEQueue_" + threadIdx + "_out");
mDispatchThread.setUncaughtExceptionHandler(this);
// 3) start the thread // 3) start the thread
mDispatchThread.start(); mDispatchThread.start();
@@ -240,6 +242,7 @@ public final class BtLEQueue {
// 4) handler thread ensure serial processing and informative thread name in the log // 4) handler thread ensure serial processing and informative thread name in the log
if(GBApplication.isRunningOreoOrLater()){ if(GBApplication.isRunningOreoOrLater()){
mReceiverThread = new HandlerThread("BtLEQueue_" + threadIdx + "_in"); mReceiverThread = new HandlerThread("BtLEQueue_" + threadIdx + "_in");
mReceiverThread.setUncaughtExceptionHandler(this);
mReceiverThread.start(); mReceiverThread.start();
mReceiverHandler = new Handler(mReceiverThread.getLooper()); mReceiverHandler = new Handler(mReceiverThread.getLooper());
} else { } else {
@@ -248,6 +251,14 @@ public final class BtLEQueue {
} }
} }
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
LOG.error("exception in {}", t.getName(), e);
// TODO implement actual exception handling for mDispatchThread and mReceiverThread
new GBExceptionHandler(null, true).uncaughtException(t, e);
}
boolean isConnected() { boolean isConnected() {
State state = mGbDevice.getState(); State state = mGbDevice.getState();
boolean gatt = (mBluetoothGatt != null); boolean gatt = (mBluetoothGatt != null);
@@ -361,7 +372,7 @@ public final class BtLEQueue {
private void handleDisconnected(int status) { private void handleDisconnected(int status) {
LOG.debug("handleDisconnected: {}", BleNamesResolver.getStatusString(status)); LOG.debug("handleDisconnected: {}", BleNamesResolver.getStatusString(status));
internalGattCallback.reset(); internalGattCallback.Delegate.reset();
mTransactions.clear(); mTransactions.clear();
mPauseTransaction = false; mPauseTransaction = false;
mAbortTransaction = true; mAbortTransaction = true;
@@ -464,7 +475,7 @@ public final class BtLEQueue {
* @param transaction * @param transaction
*/ */
void add(Transaction transaction) { void add(Transaction transaction) {
LOG.debug("about to add: {}", transaction); LOG.debug("add: {}", transaction);
if (!transaction.isEmpty()) { if (!transaction.isEmpty()) {
mTransactions.add(transaction); mTransactions.add(transaction);
} }
@@ -485,7 +496,7 @@ public final class BtLEQueue {
* Adds a serverTransaction to the end of the queue * Adds a serverTransaction to the end of the queue
*/ */
void add(ServerTransaction transaction) { void add(ServerTransaction transaction) {
LOG.debug("about to add server: {}", transaction); LOG.debug("add server: {}", transaction);
if(!transaction.isEmpty()) { if(!transaction.isEmpty()) {
mTransactions.add(transaction); mTransactions.add(transaction);
} }
@@ -559,7 +570,7 @@ public final class BtLEQueue {
@Override @Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
final int bondState = gatt.getDevice().getBondState(); final int bondState = gatt.getDevice().getBondState();
LOG.debug("connection state change, newState: {} {} {}", LOG.debug("connection state changed: {} {} {}",
BleNamesResolver.getStateString(newState), BleNamesResolver.getStatusString(status), BleNamesResolver.getStateString(newState), BleNamesResolver.getStatusString(status),
BleNamesResolver.getBondStateString(bondState)); BleNamesResolver.getBondStateString(bondState));
@@ -583,7 +594,11 @@ public final class BtLEQueue {
final GattCallback callback = getCallbackToUse(); final GattCallback callback = getCallbackToUse();
if (callback != null) { if (callback != null) {
callback.onConnectionStateChange(gatt, status, newState); try {
callback.onConnectionStateChange(gatt, status, newState);
} catch (Exception ex) {
LOG.error("onConnectionStateChange failed", ex);
}
} }
switch (newState) { switch (newState) {
@@ -636,7 +651,11 @@ public final class BtLEQueue {
final GattCallback callback = getCallbackToUse(); final GattCallback callback = getCallbackToUse();
if (callback != null) { if (callback != null) {
// only propagate the successful event // only propagate the successful event
callback.onServicesDiscovered(gatt); try {
callback.onServicesDiscovered(gatt);
} catch (Exception ex) {
LOG.error("onServicesDiscovered failed", ex);
}
} }
final CountDownLatch latch = mConnectionLatch; final CountDownLatch latch = mConnectionLatch;
if (latch != null) { if (latch != null) {
@@ -649,14 +668,18 @@ public final class BtLEQueue {
@Override @Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
LOG.debug("characteristic write: {} {}", characteristic.getUuid(), BleNamesResolver.getStatusString(status)); LOG.debug("characteristic written: {} {}", characteristic.getUuid(), BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "characteristic write")) { if (!checkCorrectGattInstance(gatt, "characteristic write")) {
return; return;
} }
final GattCallback callback = getCallbackToUse(); final GattCallback callback = getCallbackToUse();
if (callback != null) { if (callback != null) {
callback.onCharacteristicWrite(gatt, characteristic, status); try {
callback.onCharacteristicWrite(gatt, characteristic, status);
} catch (Exception ex) {
LOG.error("onCharacteristicWrite failed", ex);
}
} }
checkWaitingCharacteristic(characteristic, status); checkWaitingCharacteristic(characteristic, status);
} }
@@ -671,7 +694,11 @@ public final class BtLEQueue {
final GattCallback callback = getCallbackToUse(); final GattCallback callback = getCallbackToUse();
if (callback != null) { if (callback != null) {
callback.onMtuChanged(gatt, mtu, status); try {
callback.onMtuChanged(gatt, mtu, status);
} catch (Exception ex) {
LOG.error("onMtuChanged failed", ex);
}
} }
final CountDownLatch latch = mWaitForActionResultLatch; final CountDownLatch latch = mWaitForActionResultLatch;
@@ -693,7 +720,7 @@ public final class BtLEQueue {
BluetoothGattCharacteristic characteristic, BluetoothGattCharacteristic characteristic,
@NonNull byte[] value, int status) { @NonNull byte[] value, int status) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value); String content = GB.hexdump(value);
LOG.debug( LOG.debug(
"characteristic read: {} {} - {}", characteristic.getUuid(), "characteristic read: {} {} - {}", characteristic.getUuid(),
BleNamesResolver.getStatusString(status), content BleNamesResolver.getStatusString(status), content
@@ -708,8 +735,8 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onCharacteristicRead(gatt, characteristic, value, status); callback.onCharacteristicRead(gatt, characteristic, value, status);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onCharacteristicRead: {}", ex.getMessage(), ex); LOG.error("onCharacteristicRead failed", ex);
} }
} }
checkWaitingCharacteristic(characteristic, status); checkWaitingCharacteristic(characteristic, status);
@@ -724,7 +751,7 @@ public final class BtLEQueue {
@Override @Override
public void onDescriptorRead(@NonNull BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status, @NonNull byte[] value) { public void onDescriptorRead(@NonNull BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status, @NonNull byte[] value) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value); String content = GB.hexdump(value);
LOG.debug("descriptor read: {} {} - {}", descriptor.getUuid(), LOG.debug("descriptor read: {} {} - {}", descriptor.getUuid(),
BleNamesResolver.getStatusString(status), content); BleNamesResolver.getStatusString(status), content);
} }
@@ -737,7 +764,7 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onDescriptorRead(gatt, descriptor, status, value); callback.onDescriptorRead(gatt, descriptor, status, value);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onDescriptorRead failed", ex); LOG.error("onDescriptorRead failed", ex);
} }
} }
@@ -746,7 +773,7 @@ public final class BtLEQueue {
@Override @Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
LOG.debug("descriptor write: {} {}", descriptor.getUuid(), BleNamesResolver.getStatusString(status)); LOG.debug("descriptor written: {} {}", descriptor.getUuid(), BleNamesResolver.getStatusString(status));
if (!checkCorrectGattInstance(gatt, "descriptor write")) { if (!checkCorrectGattInstance(gatt, "descriptor write")) {
return; return;
} }
@@ -755,7 +782,7 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onDescriptorWrite(gatt, descriptor, status); callback.onDescriptorWrite(gatt, descriptor, status);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onDescriptorWrite failed", ex); LOG.error("onDescriptorWrite failed", ex);
} }
} }
@@ -774,7 +801,7 @@ public final class BtLEQueue {
@NonNull BluetoothGattCharacteristic characteristic, @NonNull BluetoothGattCharacteristic characteristic,
@NonNull byte[] value) { @NonNull byte[] value) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value); String content = GB.hexdump(value);
LOG.debug("characteristic changed: {} - {}", characteristic.getUuid(), content); LOG.debug("characteristic changed: {} - {}", characteristic.getUuid(), content);
} }
if (!checkCorrectGattInstance(gatt, "characteristic changed")) { if (!checkCorrectGattInstance(gatt, "characteristic changed")) {
@@ -785,7 +812,7 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onCharacteristicChanged(gatt, characteristic, value); callback.onCharacteristicChanged(gatt, characteristic, value);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onCharacteristicChanged failed", ex); LOG.error("onCharacteristicChanged failed", ex);
} }
} else { } else {
@@ -804,7 +831,7 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onReadRemoteRssi(gatt, rssi, status); callback.onReadRemoteRssi(gatt, rssi, status);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onReadRemoteRssi failed", ex); LOG.error("onReadRemoteRssi failed", ex);
} }
} }
@@ -822,7 +849,7 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onReliableWriteCompleted(gatt, status); callback.onReliableWriteCompleted(gatt, status);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onReliableWriteCompleted failed", ex); LOG.error("onReliableWriteCompleted failed", ex);
} }
} }
@@ -847,7 +874,7 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onPhyRead(gatt, txPhy, rxPhy, status); callback.onPhyRead(gatt, txPhy, rxPhy, status);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onPhyRead failed", ex); LOG.error("onPhyRead failed", ex);
} }
} }
@@ -872,7 +899,7 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onPhyUpdate(gatt, txPhy, rxPhy, status); callback.onPhyUpdate(gatt, txPhy, rxPhy, status);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onPhyUpdate failed", ex); LOG.error("onPhyUpdate failed", ex);
} }
} }
@@ -894,7 +921,7 @@ public final class BtLEQueue {
if (callback != null) { if (callback != null) {
try { try {
callback.onServiceChanged(gatt); callback.onServiceChanged(gatt);
} catch (Throwable ex) { } catch (Exception ex) {
LOG.error("onServiceChanged failed", ex); LOG.error("onServiceChanged failed", ex);
} }
} }
@@ -1000,7 +1027,11 @@ public final class BtLEQueue {
LOG.debug("characteristic read request: {} characteristic: {}", device.getAddress(), characteristic.getUuid()); LOG.debug("characteristic read request: {} characteristic: {}", device.getAddress(), characteristic.getUuid());
final GattServerCallback callback = getCallbackToUse(); final GattServerCallback callback = getCallbackToUse();
if (callback != null) { if (callback != null) {
callback.onCharacteristicReadRequest(device, requestId, offset, characteristic); try {
callback.onCharacteristicReadRequest(device, requestId, offset, characteristic);
} catch (Exception ex) {
LOG.error("onCharacteristicReadRequest failed", ex);
}
} }
} }
@@ -1013,7 +1044,11 @@ public final class BtLEQueue {
boolean success = false; boolean success = false;
final GattServerCallback callback = getCallbackToUse(); final GattServerCallback callback = getCallbackToUse();
if (callback != null) { if (callback != null) {
success = callback.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value); try {
success = callback.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
} catch (Exception ex) {
LOG.error("onCharacteristicWriteRequest failed", ex);
}
} }
if (responseNeeded && mSendWriteRequestResponse) { if (responseNeeded && mSendWriteRequestResponse) {
mBluetoothGattServer.sendResponse(device, requestId, success ? BluetoothGatt.GATT_SUCCESS : BluetoothGatt.GATT_FAILURE, 0, EMPTY); mBluetoothGattServer.sendResponse(device, requestId, success ? BluetoothGatt.GATT_SUCCESS : BluetoothGatt.GATT_FAILURE, 0, EMPTY);
@@ -1028,7 +1063,11 @@ public final class BtLEQueue {
LOG.debug("onDescriptorReadRequest: {}", device.getAddress()); LOG.debug("onDescriptorReadRequest: {}", device.getAddress());
final GattServerCallback callback = getCallbackToUse(); final GattServerCallback callback = getCallbackToUse();
if (callback != null) { if (callback != null) {
callback.onDescriptorReadRequest(device, requestId, offset, descriptor); try {
callback.onDescriptorReadRequest(device, requestId, offset, descriptor);
} catch (Exception ex) {
LOG.error("onDescriptorReadRequest failed", ex);
}
} }
} }
@@ -1041,11 +1080,48 @@ public final class BtLEQueue {
boolean success = false; boolean success = false;
final GattServerCallback callback = getCallbackToUse(); final GattServerCallback callback = getCallbackToUse();
if (callback != null) { if (callback != null) {
success = callback.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); try {
success = callback.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
} catch (Exception ex) {
LOG.error("onDescriptorWriteRequest failed", ex);
}
} }
if (responseNeeded && mSendWriteRequestResponse) { if (responseNeeded && mSendWriteRequestResponse) {
mBluetoothGattServer.sendResponse(device, requestId, success ? BluetoothGatt.GATT_SUCCESS : BluetoothGatt.GATT_FAILURE, 0, EMPTY); mBluetoothGattServer.sendResponse(device, requestId, success ? BluetoothGatt.GATT_SUCCESS : BluetoothGatt.GATT_FAILURE, 0, EMPTY);
} }
} }
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
LOG.debug("server.onServiceAdded {} {}", service.getUuid(), service.getInstanceId());
}
@Override
public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
LOG.debug("server.onExecuteWrite {} {}", requestId, execute);
}
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
LOG.debug("server.onNotificationSent {}",
BleNamesResolver.getStatusString(status));
}
@Override
public void onMtuChanged(BluetoothDevice device, int mtu) {
LOG.debug("server.onMtuChanged mtu={}", mtu);
}
@Override
public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) {
LOG.debug("server.onPhyUpdate tx={} rx={} {}", txPhy, rxPhy,
BleNamesResolver.getStatusString(status));
}
@Override
public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) {
LOG.debug("server.onPhyRead tx={} rx={} {}", txPhy, rxPhy,
BleNamesResolver.getStatusString(status));
}
} }
} }
@@ -0,0 +1,242 @@
/* 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;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/// A {@link BluetoothGattCallback} that ensures no {@link Exception} can kill the current {@link Thread}.
/// All processing and logging must be done in the {@link #Delegate} and not here.
public class NoThrowBluetoothGattCallback<T extends BluetoothGattCallback> extends BluetoothGattCallback {
private static final Logger LOG = LoggerFactory.getLogger(NoThrowBluetoothGattCallback.class);
public final T Delegate;
public NoThrowBluetoothGattCallback(T delegate) {
if (delegate == null) {
throw new IllegalArgumentException("delegate is null");
}
Delegate = delegate;
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
try {
Delegate.onPhyUpdate(gatt, txPhy, rxPhy, status);
} catch (Exception ex) {
LOG.error("onPhyUpdate", ex);
} catch (Throwable t) {
LOG.error("onPhyUpdate", t);
throw t;
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
try {
Delegate.onPhyRead(gatt, txPhy, rxPhy, status);
} catch (Exception ex) {
LOG.error("onPhyRead", ex);
} catch (Throwable t) {
LOG.error("onPhyRead", t);
throw t;
}
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
try {
Delegate.onConnectionStateChange(gatt, status, newState);
} catch (Exception ex) {
LOG.error("onConnectionStateChange", ex);
} catch (Throwable t) {
LOG.error("onConnectionStateChange", t);
throw t;
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
try {
Delegate.onServicesDiscovered(gatt, status);
} catch (Exception ex) {
LOG.error("onServicesDiscovered", ex);
} catch (Throwable t) {
LOG.error("onServicesDiscovered", t);
throw t;
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
try {
Delegate.onCharacteristicRead(gatt, characteristic, status);
} catch (Exception ex) {
LOG.error("onCharacteristicRead-old", ex);
} catch (Throwable t) {
LOG.error("onCharacteristicRead-old", t);
throw t;
}
}
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
@Override
public void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value, int status) {
try {
Delegate.onCharacteristicRead(gatt, characteristic, value, status);
} catch (Exception ex) {
LOG.error("onCharacteristicRead", ex);
} catch (Throwable t) {
LOG.error("onCharacteristicRead", t);
throw t;
}
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
try {
Delegate.onCharacteristicWrite(gatt, characteristic, status);
} catch (Exception ex) {
LOG.error("onCharacteristicWrite", ex);
} catch (Throwable t) {
LOG.error("onCharacteristicWrite", t);
throw t;
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
try {
Delegate.onCharacteristicChanged(gatt, characteristic);
} catch (Exception ex) {
LOG.error("onCharacteristicChanged-old", ex);
} catch (Throwable t) {
LOG.error("onCharacteristicChanged-old", t);
throw t;
}
}
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
@Override
public void onCharacteristicChanged(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
try {
Delegate.onCharacteristicChanged(gatt, characteristic, value);
} catch (Exception ex) {
LOG.error("onCharacteristicChanged", ex);
} catch (Throwable t) {
LOG.error("onCharacteristicChanged", t);
throw t;
}
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
try {
Delegate.onDescriptorRead(gatt, descriptor, status);
} catch (Exception ex) {
LOG.error("onDescriptorRead-old", ex);
} catch (Throwable t) {
LOG.error("onDescriptorRead-old", t);
throw t;
}
}
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
@Override
public void onDescriptorRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattDescriptor descriptor, int status, @NonNull byte[] value) {
try {
Delegate.onDescriptorRead(gatt, descriptor, status, value);
} catch (Exception ex) {
LOG.error("onDescriptorRead", ex);
} catch (Throwable t) {
LOG.error("onDescriptorRead", t);
throw t;
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
try {
Delegate.onDescriptorWrite(gatt, descriptor, status);
} catch (Exception ex) {
LOG.error("onDescriptorWrite", ex);
} catch (Throwable t) {
LOG.error("onDescriptorWrite", t);
throw t;
}
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
try {
Delegate.onReliableWriteCompleted(gatt, status);
} catch (Exception ex) {
LOG.error("onReliableWriteCompleted", ex);
} catch (Throwable t) {
LOG.error("onReliableWriteCompleted", t);
throw t;
}
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
try {
Delegate.onReadRemoteRssi(gatt, rssi, status);
} catch (Exception ex) {
LOG.error("onReadRemoteRssi", ex);
} catch (Throwable t) {
LOG.error("onReadRemoteRssi", t);
throw t;
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
try {
Delegate.onMtuChanged(gatt, mtu, status);
} catch (Exception ex) {
LOG.error("onMtuChanged", ex);
} catch (Throwable t) {
LOG.error("onMtuChanged", t);
throw t;
}
}
@RequiresApi(api = Build.VERSION_CODES.S)
@Override
public void onServiceChanged(@NonNull BluetoothGatt gatt) {
try {
Delegate.onServiceChanged(gatt);
} catch (Exception ex) {
LOG.error("onServiceChanged", ex);
} catch (Throwable t) {
LOG.error("onServiceChanged", t);
throw t;
}
}
}
@@ -19,7 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -51,11 +50,6 @@ public class ServerTransaction extends AbstractTransaction {
return mActions.isEmpty(); return mActions.isEmpty();
} }
@Override
public String toString() {
return String.format(Locale.US, "%s: Transaction task: %s with %d actions", getCreationTime(), getTaskName(), mActions.size());
}
public void setCallback(@Nullable GattServerCallback callback) { public void setCallback(@Nullable GattServerCallback callback) {
gattCallback = callback; gattCallback = callback;
} }
@@ -28,9 +28,6 @@ import org.slf4j.LoggerFactory;
public abstract class AbortTransactionAction extends PlainAction { public abstract class AbortTransactionAction extends PlainAction {
private static final Logger LOG = LoggerFactory.getLogger(AbortTransactionAction.class); private static final Logger LOG = LoggerFactory.getLogger(AbortTransactionAction.class);
public AbortTransactionAction() {
}
@Override @Override
public boolean run(BluetoothGatt gatt) { public boolean run(BluetoothGatt gatt) {
if (shouldAbort()) { if (shouldAbort()) {
@@ -44,6 +41,6 @@ public abstract class AbortTransactionAction extends PlainAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + ": aborting? " + shouldAbort(); return getCreationTime() + " " + getClass().getSimpleName() + " abort=" + shouldAbort();
} }
} }
@@ -142,7 +142,7 @@ public class NotifyAction extends BtLEAction {
BluetoothGattCharacteristic characteristic = getCharacteristic(); BluetoothGattCharacteristic characteristic = getCharacteristic();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString(); String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
return getCreationTime() + ": " + getClass().getSimpleName() + " " + uuid + return getCreationTime() + " " + getClass().getSimpleName() + " " + uuid +
(enableFlag ? " enable" : " disable"); (enableFlag ? " enable" : " disable");
} }
} }
@@ -37,6 +37,6 @@ public abstract class PlainAction extends BtLEAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName(); return getCreationTime() + " " + getClass().getSimpleName();
} }
} }
@@ -58,6 +58,6 @@ public class ReadAction extends BtLEAction {
public String toString() { public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic(); BluetoothGattCharacteristic characteristic = getCharacteristic();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString(); String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
return getCreationTime() + ": " + getClass().getSimpleName() + " " + uuid; return getCreationTime() + " " + getClass().getSimpleName() + " " + uuid;
} }
} }
@@ -59,6 +59,6 @@ public class ReadPhyAction extends BtLEAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName(); return getCreationTime() + " " + getClass().getSimpleName();
} }
} }
@@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGatt;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BleNamesResolver;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
/// Calls {@link BluetoothGatt#requestConnectionPriority(int)}. /// Calls {@link BluetoothGatt#requestConnectionPriority(int)}.
@@ -43,6 +44,7 @@ public class RequestConnectionPriorityAction extends BtLEAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " priority=" + priority; return getCreationTime() + " " + getClass().getSimpleName() + " " +
BleNamesResolver.getConnectionPriorityString(priority);
} }
} }
@@ -46,6 +46,6 @@ public class RequestMtuAction extends BtLEAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " mtu=" + mtu; return getCreationTime() + " " + getClass().getSimpleName() + " mtu=" + mtu;
} }
} }
@@ -48,6 +48,6 @@ public class SetDeviceBusyAction extends PlainAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getName() + ": " + busyTask; return getCreationTime() + " " + getClass().getName() + " " + busyTask;
} }
} }
@@ -70,7 +70,7 @@ public class SetPreferredPhyAction extends BtLEAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " tx=" + mTxPhy return getCreationTime() + " " + getClass().getSimpleName() + " tx=" + mTxPhy
+ " rx=" + mRxPhy + " opt=" + mPhyOptions; + " rx=" + mRxPhy + " opt=" + mPhyOptions;
} }
} }
@@ -64,6 +64,6 @@ public class SetProgressAction extends PlainAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + ": " + text + "; " + percentage + "%"; return getCreationTime() + " " + getClass().getSimpleName() + ": " + text + "; " + percentage + "%";
} }
} }
@@ -43,6 +43,6 @@ public class WaitAction extends PlainAction {
@Override @Override
public String toString() { public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " " + mMillis + " ms"; return getCreationTime() + " " + getClass().getSimpleName() + " " + mMillis + " ms";
} }
} }
@@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BleNamesResolver; import nodomain.freeyourgadget.gadgetbridge.service.btle.BleNamesResolver;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/** /**
* Invokes a write operation on a given {@link BluetoothGattCharacteristic}. * Invokes a write operation on a given {@link BluetoothGattCharacteristic}.
@@ -111,7 +112,7 @@ public class WriteAction extends BtLEAction {
public String toString() { public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic(); BluetoothGattCharacteristic characteristic = getCharacteristic();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString(); String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
return getCreationTime() + ": " + getClass().getSimpleName() + " " + uuid + " - " return getCreationTime() + " " + getClass().getSimpleName() + " " + uuid + " - "
+ Logging.formatBytes(getValue()); + GB.hexdump(getValue());
} }
} }
@@ -327,4 +327,12 @@ public class DateTimeUtils {
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from); String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
return fromFormattedDate + " - " + toFormattedDate; return fromFormattedDate + " - " + toFormattedDate;
} }
private static SimpleDateFormat mFormatLocalTime;
/// format UTC millisecond epoch as local time (e.g. 23:59:59)
public static String formatLocalTime(long epochMilli) {
Date date = new Date(epochMilli);
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss", Locale.ROOT);
return format.format(date);
}
} }