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/>. */
package nodomain.freeyourgadget.gadgetbridge;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -57,7 +58,14 @@ public abstract class Logging {
} else {
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) {
Log.e("GBApplication", "External files dir not available, cannot log to file", ex);
stopFileLogger();
@@ -16,9 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public abstract class AbstractTransaction {
private final String mName;
@@ -33,14 +31,15 @@ public abstract class AbstractTransaction {
}
protected String getCreationTime() {
return DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date(creationTimestamp));
return DateTimeUtils.formatLocalTime(creationTimestamp);
}
public abstract int getActionCount();
@Override
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;
public class BleNamesResolver {
private static HashMap<String, String> mServices = new HashMap<>();
private static HashMap<String, String> mCharacteristics = new HashMap<>();
private static SparseArray<String> mValueFormats = new SparseArray<>();
private static SparseArray<String> mAppearance = new SparseArray<>();
private static SparseArray<String> mHeartRateSensorLocation = new SparseArray<>();
private static HashMap<String, String> mServices = new HashMap<>(100);
private static HashMap<String, String> mCharacteristics = new HashMap<>(600);
private static SparseArray<String> mValueFormats = new SparseArray<>(10);
private static SparseArray<String> mAppearance = new SparseArray<>(20);
private static SparseArray<String> mHeartRateSensorLocation = new SparseArray<>(10);
static public String resolveServiceName(final String 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) {
StringBuilder builder = new StringBuilder();
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("86f66000-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Data)");
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
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("86f66001-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Data)");
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(50, "16bit float");
@@ -19,8 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
/**
@@ -67,12 +65,12 @@ public abstract class BtLEAction {
}
protected String getCreationTime() {
return DateTimeUtils.formatDateTime(new Date(creationTimestamp));
return DateTimeUtils.formatLocalTime(creationTimestamp);
}
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();
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 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.State;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* One queue/thread per connectable device.
*/
@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 byte[] EMPTY = new byte[0];
private static final AtomicLong THREAD_COUNTER = new AtomicLong(0L);
@@ -86,7 +87,7 @@ public final class BtLEQueue {
private CountDownLatch mWaitForServerActionResultLatch;
private CountDownLatch mConnectionLatch;
private BluetoothGattCharacteristic mWaitCharacteristic;
private final InternalGattCallback internalGattCallback;
private final NoThrowBluetoothGattCallback<InternalGattCallback> internalGattCallback;
private final InternalGattServerCallback internalGattServerCallback;
private final AbstractBTLEDeviceSupport mDeviceSupport;
private final boolean mImplicitGattCallbackModify;
@@ -99,7 +100,7 @@ public final class BtLEQueue {
private class DispatchRunnable implements Runnable {
@Override
public void run() {
LOG.debug("Queue Dispatch Thread started.");
LOG.debug("{} started", Thread.currentThread().getName());
boolean crashed = false;
while (!mDisposed.get() && !crashed) {
@@ -109,7 +110,7 @@ public final class BtLEQueue {
if (!isConnected()) {
LOG.debug("not connected, waiting for connection...");
// 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
// Note that no automatic connection is performed. This has to be triggered
@@ -131,7 +132,7 @@ public final class BtLEQueue {
break;
}
if (LOG.isDebugEnabled()) {
LOG.debug("About to run server action: {}", action);
LOG.debug("execute server: {}", action);
}
if (action.run(mBluetoothGattServer)) {
// 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) {
LOG.trace("Changing gatt callback for {}? {}", transaction.getTaskName(), transaction.isModifyGattCallback());
if (mImplicitGattCallbackModify || transaction.isModifyGattCallback()) {
internalGattCallback.setTransactionGattCallback(transaction.getGattCallback());
internalGattCallback.Delegate.setTransactionGattCallback(transaction.getGattCallback());
}
mAbortTransaction = false;
// Run all actions of the transaction until one doesn't succeed
@@ -174,12 +175,12 @@ public final class BtLEQueue {
mWaitCharacteristic = action.getCharacteristic();
mWaitForActionResultLatch = new CountDownLatch(1);
if (LOG.isDebugEnabled()) {
LOG.debug("About to run action: {}", action);
LOG.debug("execute: {}", action);
}
if (action instanceof final GattListenerAction listenerAction) {
// this special action overwrites the transaction gatt listener (if any), it must
// always be the last action in the transaction
internalGattCallback.setTransactionGattCallback(listenerAction.getGattCallback());
internalGattCallback.Delegate.setTransactionGattCallback(listenerAction.getGattCallback());
}
if (action.run(mBluetoothGatt)) {
// 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;
}
}
LOG.info("Queue Dispatch Thread terminated.");
LOG.debug("{} terminated", Thread.currentThread().getName());
}
};
@@ -230,9 +231,10 @@ public final class BtLEQueue {
mDisposed = new AtomicBoolean(false);
mGattMonitor = new Object();
mTransactions = new LinkedBlockingQueue<>();
internalGattCallback = new InternalGattCallback(deviceSupport);
internalGattCallback = new NoThrowBluetoothGattCallback<>(new InternalGattCallback(deviceSupport));
internalGattServerCallback = new InternalGattServerCallback(deviceSupport);
mDispatchThread = new Thread(new DispatchRunnable(), "BtLEQueue_" + threadIdx + "_out");
mDispatchThread.setUncaughtExceptionHandler(this);
// 3) start the thread
mDispatchThread.start();
@@ -240,6 +242,7 @@ public final class BtLEQueue {
// 4) handler thread ensure serial processing and informative thread name in the log
if(GBApplication.isRunningOreoOrLater()){
mReceiverThread = new HandlerThread("BtLEQueue_" + threadIdx + "_in");
mReceiverThread.setUncaughtExceptionHandler(this);
mReceiverThread.start();
mReceiverHandler = new Handler(mReceiverThread.getLooper());
} 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() {
State state = mGbDevice.getState();
boolean gatt = (mBluetoothGatt != null);
@@ -361,7 +372,7 @@ public final class BtLEQueue {
private void handleDisconnected(int status) {
LOG.debug("handleDisconnected: {}", BleNamesResolver.getStatusString(status));
internalGattCallback.reset();
internalGattCallback.Delegate.reset();
mTransactions.clear();
mPauseTransaction = false;
mAbortTransaction = true;
@@ -464,7 +475,7 @@ public final class BtLEQueue {
* @param transaction
*/
void add(Transaction transaction) {
LOG.debug("about to add: {}", transaction);
LOG.debug("add: {}", transaction);
if (!transaction.isEmpty()) {
mTransactions.add(transaction);
}
@@ -485,7 +496,7 @@ public final class BtLEQueue {
* Adds a serverTransaction to the end of the queue
*/
void add(ServerTransaction transaction) {
LOG.debug("about to add server: {}", transaction);
LOG.debug("add server: {}", transaction);
if(!transaction.isEmpty()) {
mTransactions.add(transaction);
}
@@ -559,7 +570,7 @@ public final class BtLEQueue {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
final int bondState = gatt.getDevice().getBondState();
LOG.debug("connection state change, newState: {} {} {}",
LOG.debug("connection state changed: {} {} {}",
BleNamesResolver.getStateString(newState), BleNamesResolver.getStatusString(status),
BleNamesResolver.getBondStateString(bondState));
@@ -583,7 +594,11 @@ public final class BtLEQueue {
final GattCallback callback = getCallbackToUse();
if (callback != null) {
callback.onConnectionStateChange(gatt, status, newState);
try {
callback.onConnectionStateChange(gatt, status, newState);
} catch (Exception ex) {
LOG.error("onConnectionStateChange failed", ex);
}
}
switch (newState) {
@@ -636,7 +651,11 @@ public final class BtLEQueue {
final GattCallback callback = getCallbackToUse();
if (callback != null) {
// only propagate the successful event
callback.onServicesDiscovered(gatt);
try {
callback.onServicesDiscovered(gatt);
} catch (Exception ex) {
LOG.error("onServicesDiscovered failed", ex);
}
}
final CountDownLatch latch = mConnectionLatch;
if (latch != null) {
@@ -649,14 +668,18 @@ public final class BtLEQueue {
@Override
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")) {
return;
}
final GattCallback callback = getCallbackToUse();
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);
}
@@ -671,7 +694,11 @@ public final class BtLEQueue {
final GattCallback callback = getCallbackToUse();
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;
@@ -693,7 +720,7 @@ public final class BtLEQueue {
BluetoothGattCharacteristic characteristic,
@NonNull byte[] value, int status) {
if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value);
String content = GB.hexdump(value);
LOG.debug(
"characteristic read: {} {} - {}", characteristic.getUuid(),
BleNamesResolver.getStatusString(status), content
@@ -708,8 +735,8 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onCharacteristicRead(gatt, characteristic, value, status);
} catch (Throwable ex) {
LOG.error("onCharacteristicRead: {}", ex.getMessage(), ex);
} catch (Exception ex) {
LOG.error("onCharacteristicRead failed", ex);
}
}
checkWaitingCharacteristic(characteristic, status);
@@ -724,7 +751,7 @@ public final class BtLEQueue {
@Override
public void onDescriptorRead(@NonNull BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status, @NonNull byte[] value) {
if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value);
String content = GB.hexdump(value);
LOG.debug("descriptor read: {} {} - {}", descriptor.getUuid(),
BleNamesResolver.getStatusString(status), content);
}
@@ -737,7 +764,7 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onDescriptorRead(gatt, descriptor, status, value);
} catch (Throwable ex) {
} catch (Exception ex) {
LOG.error("onDescriptorRead failed", ex);
}
}
@@ -746,7 +773,7 @@ public final class BtLEQueue {
@Override
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")) {
return;
}
@@ -755,7 +782,7 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onDescriptorWrite(gatt, descriptor, status);
} catch (Throwable ex) {
} catch (Exception ex) {
LOG.error("onDescriptorWrite failed", ex);
}
}
@@ -774,7 +801,7 @@ public final class BtLEQueue {
@NonNull BluetoothGattCharacteristic characteristic,
@NonNull byte[] value) {
if (LOG.isDebugEnabled()) {
String content = Logging.formatBytes(value);
String content = GB.hexdump(value);
LOG.debug("characteristic changed: {} - {}", characteristic.getUuid(), content);
}
if (!checkCorrectGattInstance(gatt, "characteristic changed")) {
@@ -785,7 +812,7 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onCharacteristicChanged(gatt, characteristic, value);
} catch (Throwable ex) {
} catch (Exception ex) {
LOG.error("onCharacteristicChanged failed", ex);
}
} else {
@@ -804,7 +831,7 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onReadRemoteRssi(gatt, rssi, status);
} catch (Throwable ex) {
} catch (Exception ex) {
LOG.error("onReadRemoteRssi failed", ex);
}
}
@@ -822,7 +849,7 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onReliableWriteCompleted(gatt, status);
} catch (Throwable ex) {
} catch (Exception ex) {
LOG.error("onReliableWriteCompleted failed", ex);
}
}
@@ -847,7 +874,7 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onPhyRead(gatt, txPhy, rxPhy, status);
} catch (Throwable ex) {
} catch (Exception ex) {
LOG.error("onPhyRead failed", ex);
}
}
@@ -872,7 +899,7 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onPhyUpdate(gatt, txPhy, rxPhy, status);
} catch (Throwable ex) {
} catch (Exception ex) {
LOG.error("onPhyUpdate failed", ex);
}
}
@@ -894,7 +921,7 @@ public final class BtLEQueue {
if (callback != null) {
try {
callback.onServiceChanged(gatt);
} catch (Throwable ex) {
} catch (Exception ex) {
LOG.error("onServiceChanged failed", ex);
}
}
@@ -1000,7 +1027,11 @@ public final class BtLEQueue {
LOG.debug("characteristic read request: {} characteristic: {}", device.getAddress(), characteristic.getUuid());
final GattServerCallback callback = getCallbackToUse();
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;
final GattServerCallback callback = getCallbackToUse();
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) {
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());
final GattServerCallback callback = getCallbackToUse();
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;
final GattServerCallback callback = getCallbackToUse();
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) {
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.Collections;
import java.util.List;
import java.util.Locale;
import androidx.annotation.Nullable;
@@ -51,11 +50,6 @@ public class ServerTransaction extends AbstractTransaction {
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) {
gattCallback = callback;
}
@@ -28,9 +28,6 @@ import org.slf4j.LoggerFactory;
public abstract class AbortTransactionAction extends PlainAction {
private static final Logger LOG = LoggerFactory.getLogger(AbortTransactionAction.class);
public AbortTransactionAction() {
}
@Override
public boolean run(BluetoothGatt gatt) {
if (shouldAbort()) {
@@ -44,6 +41,6 @@ public abstract class AbortTransactionAction extends PlainAction {
@Override
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();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
return getCreationTime() + ": " + getClass().getSimpleName() + " " + uuid +
return getCreationTime() + " " + getClass().getSimpleName() + " " + uuid +
(enableFlag ? " enable" : " disable");
}
}
@@ -37,6 +37,6 @@ public abstract class PlainAction extends BtLEAction {
@Override
public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName();
return getCreationTime() + " " + getClass().getSimpleName();
}
}
@@ -58,6 +58,6 @@ public class ReadAction extends BtLEAction {
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();
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
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.bluetooth.BluetoothGatt;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BleNamesResolver;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
/// Calls {@link BluetoothGatt#requestConnectionPriority(int)}.
@@ -43,6 +44,7 @@ public class RequestConnectionPriorityAction extends BtLEAction {
@Override
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
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
public String toString() {
return getCreationTime() + ": " + getClass().getName() + ": " + busyTask;
return getCreationTime() + " " + getClass().getName() + " " + busyTask;
}
}
@@ -70,7 +70,7 @@ public class SetPreferredPhyAction extends BtLEAction {
@Override
public String toString() {
return getCreationTime() + ": " + getClass().getSimpleName() + " tx=" + mTxPhy
return getCreationTime() + " " + getClass().getSimpleName() + " tx=" + mTxPhy
+ " rx=" + mRxPhy + " opt=" + mPhyOptions;
}
}
@@ -64,6 +64,6 @@ public class SetProgressAction extends PlainAction {
@Override
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
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.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* Invokes a write operation on a given {@link BluetoothGattCharacteristic}.
@@ -111,7 +112,7 @@ public class WriteAction extends BtLEAction {
public String toString() {
BluetoothGattCharacteristic characteristic = getCharacteristic();
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
return getCreationTime() + ": " + getClass().getSimpleName() + " " + uuid + " - "
+ Logging.formatBytes(getValue());
return getCreationTime() + " " + getClass().getSimpleName() + " " + uuid + " - "
+ GB.hexdump(getValue());
}
}
@@ -327,4 +327,12 @@ public class DateTimeUtils {
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
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);
}
}