From d6f5e36e12104fd489dd06702c12175fb9e5c893 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Fri, 14 Sep 2018 20:11:27 +0200 Subject: [PATCH] Huami devices: Fix seldom fetch failures (better support for app level ble feedback) Fixes #1264 --- .../service/btle/AbstractBTLEOperation.java | 10 +++++ .../gadgetbridge/service/btle/BtLEQueue.java | 5 +++ .../service/btle/GattListenerAction.java | 5 +++ .../service/btle/TransactionBuilder.java | 4 ++ .../AbstractGattListenerWriteAction.java | 33 +++++++++++++++ .../service/btle/actions/WaitAction.java | 5 +++ .../AmazfitBipFetchLogsOperation.java | 39 +++++++++++++++--- .../operations/FetchActivityOperation.java | 40 +++++++++++++++++-- .../huami/operations/InitOperation.java | 6 --- 9 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattListenerAction.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/AbstractGattListenerWriteAction.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java index 5dc29645f..c0c61756a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEOperation.java @@ -108,6 +108,16 @@ public abstract class AbstractBTLEOperation return builder; } + public TransactionBuilder createTransactionBuilder(String taskName) { + TransactionBuilder builder = getSupport().createTransactionBuilder(taskName); + builder.setGattCallback(this); + return builder; + } + + public void performImmediately(TransactionBuilder builder) throws IOException { + mSupport.performImmediately(builder); + } + protected Context getContext() { return mSupport.getContext(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index 8ca171816..542e92fb8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java @@ -107,6 +107,11 @@ public final class BtLEQueue { if (LOG.isDebugEnabled()) { LOG.debug("About to run action: " + action); } + if (action instanceof GattListenerAction) { + // this special action overwrites the transaction gatt listener (if any), it must + // always be the last action in the transaction + internalGattCallback.setTransactionGattCallback(((GattListenerAction)action).getGattCallback()); + } if (action.run(mBluetoothGatt)) { // check again, maybe due to some condition, action did not need to write, so we can't wait boolean waitForResult = action.expectsResult(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattListenerAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattListenerAction.java new file mode 100644 index 000000000..733ca2795 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattListenerAction.java @@ -0,0 +1,5 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle; + +public interface GattListenerAction { + GattCallback getGattCallback(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java index cef217495..37dad0bb8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java @@ -110,4 +110,8 @@ public class TransactionBuilder { public Transaction getTransaction() { return mTransaction; } + + public String getTaskName() { + return mTransaction.getTaskName(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/AbstractGattListenerWriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/AbstractGattListenerWriteAction.java new file mode 100644 index 000000000..18916f15e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/AbstractGattListenerWriteAction.java @@ -0,0 +1,33 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.actions; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +import java.util.Objects; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractGattCallback; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattListenerAction; + +public abstract class AbstractGattListenerWriteAction extends WriteAction implements GattListenerAction { + private final BtLEQueue queue; + + public AbstractGattListenerWriteAction(BtLEQueue queue, BluetoothGattCharacteristic characteristic, byte[] value) { + super(characteristic, value); + this.queue = queue; + Objects.requireNonNull(queue, "queue must not be null"); + } + + @Override + public GattCallback getGattCallback() { + return new AbstractGattCallback() { + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + return AbstractGattListenerWriteAction.this.onCharacteristicChanged(gatt, characteristic); + } + }; + } + + protected abstract boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WaitAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WaitAction.java index edfe25158..841b0ac55 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WaitAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WaitAction.java @@ -18,6 +18,11 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.actions; import android.bluetooth.BluetoothGatt; +/** + * An action that will cause the queue to 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. + */ public class WaitAction extends PlainAction { private final int mMillis; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/operations/AmazfitBipFetchLogsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/operations/AmazfitBipFetchLogsOperation.java index bbaeb2c85..cee4c9449 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/operations/AmazfitBipFetchLogsOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/operations/AmazfitBipFetchLogsOperation.java @@ -16,6 +16,8 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.operations; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; import android.support.annotation.NonNull; import android.widget.Toast; @@ -30,17 +32,21 @@ import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; +import java.util.UUID; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbstractGattListenerWriteAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.AbstractFetchOperation; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipFetchLogsOperation.class); @@ -72,15 +78,38 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { return; } + final String taskName = StringUtils.ensureNotNull(builder.getTaskName()); GregorianCalendar sinceWhen = BLETypeConversions.createCalendar(); sinceWhen.add(Calendar.DAY_OF_MONTH, -10); - builder.write(characteristicFetch, BLETypeConversions.join(new byte[]{ + byte[] fetchBytes = BLETypeConversions.join(new byte[]{ HuamiService.COMMAND_ACTIVITY_DATA_START_DATE, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_DEBUGLOGS}, - getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); - builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply - builder.notify(characteristicActivityData, true); - builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA}); + getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES)); + builder.add(new AbstractGattListenerWriteAction(getQueue(), characteristicFetch, fetchBytes) { + @Override + protected boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (HuamiService.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { + byte[] value = characteristic.getValue(); + + if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) { + handleActivityMetadata(value); + TransactionBuilder newBuilder = createTransactionBuilder(taskName + " Step 2"); + newBuilder.notify(characteristicActivityData, true); + newBuilder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA}); + try { + performImmediately(newBuilder); + } catch (IOException ex) { + GB.toast(getContext(), "Error fetching debug logs: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); + } + return true; + } else { + handleActivityMetadata(value); + } + } + return false; + } + }); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java index 650bc2406..fe1f3951b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java @@ -16,6 +16,8 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; import android.text.format.DateUtils; import android.widget.Toast; @@ -24,9 +26,11 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; +import java.util.UUID; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -34,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -41,10 +46,14 @@ import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbstractGattListenerWriteAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.PlainAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; /** * An operation that fetches activity data. For every fetch, a new operation must @@ -68,11 +77,34 @@ public class FetchActivityOperation extends AbstractFetchOperation { @Override protected void startFetching(TransactionBuilder builder) { + final String taskName = StringUtils.ensureNotNull(builder.getTaskName()); GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); - builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { HuamiService.COMMAND_ACTIVITY_DATA_START_DATE, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); - builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply - builder.notify(characteristicActivityData, true); - builder.write(characteristicFetch, new byte[] { HuamiService.COMMAND_FETCH_DATA}); + byte[] fetchBytes = BLETypeConversions.join(new byte[] { HuamiService.COMMAND_ACTIVITY_DATA_START_DATE, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES)); + builder.add(new AbstractGattListenerWriteAction(getQueue(), characteristicFetch, fetchBytes) { + @Override + protected boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (HuamiService.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { + byte[] value = characteristic.getValue(); + + if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) { + handleActivityMetadata(value); + TransactionBuilder newBuilder = createTransactionBuilder(taskName + " Step 2"); + newBuilder.notify(characteristicActivityData, true); + newBuilder.write(characteristicFetch, new byte[] { HuamiService.COMMAND_FETCH_DATA}); + try { + performImmediately(newBuilder); + } catch (IOException ex) { + GB.toast(getContext(), "Error fetching activity data: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); + } + return true; + } else { + handleActivityMetadata(value); + } + } + return false; + } + }); } protected void handleActivityFetchFinish(boolean success) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java index a7d64a135..06917ddb8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java @@ -137,12 +137,6 @@ public class InitOperation extends AbstractBTLEOperation { } } - private TransactionBuilder createTransactionBuilder(String task) { - TransactionBuilder builder = getSupport().createTransactionBuilder(task); - builder.setGattCallback(this); - return builder; - } - private byte[] getMD5(byte[] message) throws NoSuchAlgorithmException { MessageDigest md5 = MessageDigest.getInstance("MD5"); return md5.digest(message);