1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-06 21:28:11 +02:00

Huami: Refactor activity data fetching

Activity data fetching on Huami devices was filled with duplicated code,
and the handleActivityFetchFinish was called from multiple places where
it did not make sense. This made us signal to the band that activity
fetch was finished when it sometimes was not, causing some race
condititions that would make activity fetch fail or get stuck.

This refactor defines a clear "processBufferedData" that is called
upstream, signaling to the fetch operation that we have received all
data and the buffer can be processed. All handling of metadata and ack
messages is also delegated to the upstream class.
This commit is contained in:
José Rebelo 2024-02-24 22:35:35 +00:00 committed by José Rebelo
parent 9b0229cdf0
commit 58d4ebf509
9 changed files with 243 additions and 474 deletions

View File

@ -229,8 +229,6 @@ public class HuamiService {
public static final byte COMMAND_FIRMWARE_CHECKSUM = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_REBOOT = 0x05; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS};
public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 };
public static final byte[] WEAR_LOCATION_RIGHT_WRIST = new byte[] { 0x20, 0x00, 0x00, (byte) 0x82};

View File

@ -344,7 +344,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
protected Huami2021ChunkedEncoder huami2021ChunkedEncoder;
protected Huami2021ChunkedDecoder huami2021ChunkedDecoder;
private final Queue<AbstractFetchOperation> fetchOperationQueue = new LinkedList<>();
private final LinkedList<AbstractFetchOperation> fetchOperationQueue = new LinkedList<>();
public HuamiSupport() {
this(LOG);
@ -1725,6 +1725,10 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
return fetchOperationQueue.poll();
}
public LinkedList<AbstractFetchOperation> getFetchOperationQueue() {
return fetchOperationQueue;
}
@Override
public void onEnableRealtimeSteps(boolean enable) {
try {

View File

@ -28,6 +28,7 @@ import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Arrays;
@ -42,39 +43,41 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
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.SetDeviceBusyAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
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
* be created, i.e. an operation may not be reused for multiple fetches.
* An operation that fetches activity data.
*/
public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
private static final Logger LOG = LoggerFactory.getLogger(AbstractFetchOperation.class);
protected byte lastPacketCounter;
int fetchCount;
protected BluetoothGattCharacteristic characteristicActivityData;
protected BluetoothGattCharacteristic characteristicFetch;
Calendar startTimestamp;
int expectedDataLength = 0;
public AbstractFetchOperation(HuamiSupport support) {
protected Calendar startTimestamp;
protected int fetchCount;
protected byte lastPacketCounter;
protected int expectedDataLength = 0;
protected final ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
protected boolean operationValid = true; // to mark operation failed midway (eg. out of sync)
public AbstractFetchOperation(final HuamiSupport support) {
super(support);
}
@Override
protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
protected void enableNeededNotifications(final TransactionBuilder builder, final boolean enable) {
if (!enable) {
// dynamically enabled, but always disabled on finish
builder.notify(characteristicFetch, enable);
builder.notify(characteristicActivityData, enable);
builder.notify(characteristicFetch, false);
builder.notify(characteristicActivityData, false);
}
}
@ -87,7 +90,7 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
expectedDataLength = 0;
lastPacketCounter = -1;
TransactionBuilder builder = performInitialized(getName());
final TransactionBuilder builder = performInitialized(getName());
if (fetchCount == 0) {
builder.add(new SetDeviceBusyAction(getDevice(), taskDescription(), getContext()));
}
@ -114,11 +117,11 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
protected abstract String getLastSyncTimeKey();
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
public boolean onCharacteristicChanged(final BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic) {
final UUID characteristicUUID = characteristic.getUuid();
if (HuamiService.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) {
handleActivityNotif(characteristic.getValue());
handleActivityData(characteristic.getValue());
return true;
} else if (HuamiService.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) {
handleActivityMetadata(characteristic.getValue());
@ -129,18 +132,15 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
}
/**
* Handles the finishing of fetching the activity.
* @param success whether fetching was successful
* @return whether handling the activity fetch finish was successful
* Handles the finishing of fetching the activity. This signals the actual end of this operation.
*/
@CallSuper
protected boolean handleActivityFetchFinish(boolean success) {
protected final void onOperationFinished() {
final AbstractFetchOperation nextFetchOperation = getSupport().getNextFetchOperation();
if (nextFetchOperation != null) {
LOG.debug("Performing next operation {}", nextFetchOperation.getName());
try {
nextFetchOperation.perform();
return true;
return;
} catch (final IOException e) {
GB.toast(
getContext(),
@ -148,17 +148,16 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
Toast.LENGTH_SHORT,
GB.ERROR, e
);
return false;
return;
}
}
LOG.debug("All operations finished");
GB.updateTransferNotification(null, "", false, 100, getContext());
GB.signalActivityDataFinish();
operationFinished();
unsetBusy();
return true;
}
/**
@ -168,72 +167,54 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
* @param crc32 the expected checksum
* @return whether the checksum was valid
*/
protected abstract boolean validChecksum(int crc32);
protected boolean validChecksum(int crc32) {
return crc32 == CheckSums.getCRC32(buffer.toByteArray());
}
/**
* Method to handle the incoming activity data.
* There are two kind of messages we currently know:
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
* - the second one is 20 bytes long and contains the actual activity data
* <p/>
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
*
* @param value
*/
protected abstract void handleActivityNotif(byte[] value);
protected abstract boolean processBufferedData();
protected abstract void bufferActivityData(byte[] value);
protected void handleActivityData(final byte[] value) {
LOG.debug("{} data: {}", getName(), Logging.formatBytes(value));
protected void startFetching(TransactionBuilder builder, byte fetchType, GregorianCalendar sinceWhen) {
final String taskName = StringUtils.ensureNotNull(builder.getTaskName());
if (!isOperationRunning()) {
LOG.error("ignoring {} notification because operation is not running. Data length: {}", getName(), value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
// TODO we should handle skipped or repeated bytes more gracefully
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR);
operationValid = false;
}
}
protected void bufferActivityData(byte[] value) {
buffer.write(value, 1, value.length - 1); // skip the counter
}
protected void startFetching(final TransactionBuilder builder, final byte fetchType, final GregorianCalendar sinceWhen) {
final HuamiSupport support = getSupport();
final boolean isZeppOs = support instanceof ZeppOsSupport;
byte[] fetchBytes = BLETypeConversions.join(new byte[]{
HuamiService.COMMAND_ACTIVITY_DATA_START_DATE,
fetchType},
support.getTimeBytes(sinceWhen, support.getFetchOperationsTimeUnit()));
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);
if (expectedDataLength == 0 && isZeppOs) {
// Nothing to receive, if we try to fetch data it will fail
sendAckZeppOs(true);
} else if (expectedDataLength != 0) {
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;
}
});
builder.write(characteristicFetch, fetchBytes);
}
private void handleActivityMetadata(byte[] value) {
if (value.length < 3) {
LOG.warn("Activity metadata too short: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
if (value[0] != HuamiService.RESPONSE) {
LOG.warn("Activity metadata not a response: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
@ -245,26 +226,26 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
handleFetchDataResponse(value);
return;
case HuamiService.COMMAND_ACK_ACTIVITY_DATA:
// ignore, this is just the reply to the COMMAND_ACK_ACTIVITY_DATA
LOG.info("Got reply to COMMAND_ACK_ACTIVITY_DATA");
onOperationFinished();
return;
default:
LOG.warn("Unexpected activity metadata: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
}
}
private void handleStartDateResponse(final byte[] value) {
if (value[2] != HuamiService.SUCCESS) {
LOG.warn("Start date unsuccessful response: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
// it's 16 on the MB7, with a 0 at the end
if (value.length != 15 && (value.length != 16 && value[15] != 0x00)) {
LOG.warn("Start date response length: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
@ -277,7 +258,8 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
if (expectedDataLength == 0) {
LOG.info("No data to fetch since {}", startTimestamp.getTime());
handleActivityFetchFinish(true);
sendAck(true);
// do not finish the operation - do it in the ack response
return;
}
@ -287,85 +269,108 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
GB.updateTransferNotification(taskDescription(),
getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since,
DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), true, 0, getContext());
// Trigger the actual data fetch
final TransactionBuilder step2builder = createTransactionBuilder(getName() + " Step 2");
step2builder.notify(characteristicActivityData, true);
step2builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA});
try {
performImmediately(step2builder);
} catch (final IOException e) {
GB.toast(getContext(), "Error starting fetch step 2: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
onOperationFinished();
}
}
private void handleFetchDataResponse(final byte[] value) {
if (value[2] != HuamiService.SUCCESS) {
LOG.warn("Fetch data unsuccessful response: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
if (value.length != 3 && value.length != 7) {
LOG.warn("Fetch data unexpected metadata length: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
if (value.length == 7 && !validChecksum(BLETypeConversions.toUint32(value, 3))) {
LOG.warn("Data checksum invalid");
handleActivityFetchFinish(false);
sendAckZeppOs(true);
// If we're on Zepp OS, ack but keep data on device
if (isZeppOs()) {
sendAck(true);
// do not finish the operation - do it in the ack response
return;
}
onOperationFinished();
return;
}
boolean handleFinishSuccess;
try {
handleFinishSuccess = handleActivityFetchFinish(true);
} catch (final Exception e) {
LOG.warn("Failed to handle activity fetch finish", e);
handleFinishSuccess = false;
final boolean success = operationValid && processBufferedData();
final boolean keepActivityDataOnDevice = !success || HuamiCoordinator.getKeepActivityDataOnDevice(getDevice().getAddress());
if (isZeppOs() || !keepActivityDataOnDevice) {
sendAck(keepActivityDataOnDevice);
// do not finish the operation - do it in the ack response
return;
}
final boolean keepActivityDataOnDevice = HuamiCoordinator.getKeepActivityDataOnDevice(getDevice().getAddress());
sendAckZeppOs(keepActivityDataOnDevice || !handleFinishSuccess);
onOperationFinished();
}
protected void sendAckZeppOs(final boolean keepDataOnDevice) {
if (!(getSupport() instanceof ZeppOsSupport)) {
return;
protected void sendAck(final boolean keepDataOnDevice) {
final byte[] ackBytes;
if (isZeppOs()) {
LOG.debug("Sending ack, keepDataOnDevice = {}", keepDataOnDevice);
// 0x01 to ACK, mark as saved on phone (drop from band)
// 0x09 to ACK, but keep it marked as not saved
// If 0x01 is sent, detailed information seems to be discarded, and is not sent again anymore
final byte ackByte = (byte) (keepDataOnDevice ? 0x09 : 0x01);
ackBytes = new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA, ackByte};
} else {
LOG.debug("Sending ack, simple");
ackBytes = new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA};
}
LOG.debug("Sending Zepp OS ack, keepDataOnDevice = {}", keepDataOnDevice);
// 0x01 to ACK, mark as saved on phone (drop from band)
// 0x09 to ACK, but keep it marked as not saved
// If 0x01 is sent, detailed information seems to be discarded, and is not sent again anymore
final byte ackByte = (byte) (keepDataOnDevice ? 0x09 : 0x01);
try {
final TransactionBuilder builder = performInitialized(getName() + " end");
builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA, ackByte});
final TransactionBuilder builder = createTransactionBuilder(getName() + " end");
builder.write(characteristicFetch, ackBytes);
performImmediately(builder);
} catch (final IOException e) {
LOG.error("Ending failed", e);
LOG.error("Failed to send ack", e);
}
}
private void setStartTimestamp(Calendar startTimestamp) {
private void setStartTimestamp(final Calendar startTimestamp) {
this.startTimestamp = startTimestamp;
}
Calendar getLastStartTimestamp() {
protected Calendar getLastStartTimestamp() {
return startTimestamp;
}
void saveLastSyncTimestamp(@NonNull GregorianCalendar timestamp) {
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit();
protected void saveLastSyncTimestamp(@NonNull final GregorianCalendar timestamp) {
final SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit();
editor.putLong(getLastSyncTimeKey(), timestamp.getTimeInMillis());
editor.apply();
}
protected GregorianCalendar getLastSuccessfulSyncTime() {
long timeStampMillis = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getLong(getLastSyncTimeKey(), 0);
final long timeStampMillis = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getLong(getLastSyncTimeKey(), 0);
if (timeStampMillis != 0) {
GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.setTimeInMillis(timeStampMillis);
return calendar;
}
GregorianCalendar calendar = BLETypeConversions.createCalendar();
final GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.add(Calendar.DAY_OF_MONTH, -100);
return calendar;
}
protected boolean isZeppOs() {
return getSupport() instanceof ZeppOsSupport;
}
}

View File

@ -16,30 +16,20 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A repeating fetch operation. This operation repeats the fetch up to a certain number of times, or
@ -49,8 +39,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(AbstractRepeatingFetchOperation.class);
private final ByteArrayOutputStream byteStreamBuffer = new ByteArrayOutputStream(140);
protected final HuamiFetchDataType dataType;
public AbstractRepeatingFetchOperation(final HuamiSupport support, final HuamiFetchDataType dataType) {
@ -77,21 +65,14 @@ public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOpera
protected abstract boolean handleActivityData(GregorianCalendar timestamp, byte[] bytes);
@Override
protected boolean handleActivityFetchFinish(final boolean success) {
LOG.info("{} has finished round {}: {}, got {} bytes in buffer", getName(), fetchCount, success, byteStreamBuffer.size());
protected boolean processBufferedData() {
LOG.info("{} has finished round {}, got {} bytes in buffer", getName(), fetchCount, buffer.size());
if (!success) {
// We need to explicitly ack this, or the next operation will fail fetch will become stuck
sendAckZeppOs(true);
super.handleActivityFetchFinish(false);
return false;
if (buffer.size() == 0) {
return true;
}
if (byteStreamBuffer.size() == 0) {
return super.handleActivityFetchFinish(true);
}
final byte[] bytes = byteStreamBuffer.toByteArray();
final byte[] bytes = buffer.toByteArray();
final GregorianCalendar timestamp = (GregorianCalendar) this.startTimestamp.clone();
// Uncomment to dump the bytes to external storage for debugging
@ -100,7 +81,6 @@ public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOpera
final boolean handleSuccess = handleActivityData(timestamp, bytes);
if (!handleSuccess) {
super.handleActivityFetchFinish(false);
return false;
}
@ -108,63 +88,12 @@ public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOpera
saveLastSyncTimestamp(timestamp);
if (needsAnotherFetch(timestamp)) {
byteStreamBuffer.reset();
buffer.reset();
try {
final boolean keepActivityDataOnDevice = HuamiCoordinator.getKeepActivityDataOnDevice(getDevice().getAddress());
sendAckZeppOs(keepActivityDataOnDevice);
startFetching();
return true;
} catch (final IOException ex) {
LOG.error("Error starting another round of " + getName(), ex);
super.handleActivityFetchFinish(false);
return false;
}
getSupport().getFetchOperationQueue().add(0, this);
}
final boolean superSuccess = super.handleActivityFetchFinish(true);
postActivityFetchFinish(superSuccess);
return superSuccess;
}
protected void postActivityFetchFinish(final boolean success) {
}
@Override
protected boolean validChecksum(final int crc32) {
return crc32 == CheckSums.getCRC32(byteStreamBuffer.toByteArray());
}
@Override
public boolean onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
LOG.debug("characteristic read: {}: {}", characteristic.getUuid(), Logging.formatBytes(characteristic.getValue()));
return super.onCharacteristicRead(gatt, characteristic, status);
}
@Override
protected void handleActivityNotif(final byte[] value) {
LOG.debug("{} data: {}", getName(), Logging.formatBytes(value));
if (!isOperationRunning()) {
LOG.error("ignoring {} notification because operation is not running. Data length: {}", getName(), value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
// TODO we should handle skipped or repeated bytes more gracefully
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR);
handleActivityFetchFinish(false);
}
}
@Override
protected void bufferActivityData(final byte[] value) {
byteStreamBuffer.write(value, 1, value.length - 1); // skip the counter
return true;
}
private boolean needsAnotherFetch(final GregorianCalendar lastSyncTimestamp) {

View File

@ -129,11 +129,6 @@ public class FetchActivityOperation extends AbstractRepeatingFetchOperation {
}
}
@Override
protected void postActivityFetchFinish(final boolean success) {
GB.signalActivityDataFinish();
}
@Override
protected boolean validChecksum(final int crc32) {
// TODO actually check it

View File

@ -37,7 +37,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FetchDebugLogsOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchDebugLogsOperation.class);
@ -81,11 +80,11 @@ public class FetchDebugLogsOperation extends AbstractFetchOperation {
@Override
protected String getLastSyncTimeKey() {
return null;
return "lastDebugTimeMillis";
}
@Override
protected boolean handleActivityFetchFinish(boolean success) {
protected boolean processBufferedData() {
LOG.info("{} data has finished", getName());
try {
logOutputStream.close();
@ -95,7 +94,7 @@ public class FetchDebugLogsOperation extends AbstractFetchOperation {
return false;
}
return super.handleActivityFetchFinish(success);
return true;
}
@Override
@ -105,30 +104,13 @@ public class FetchDebugLogsOperation extends AbstractFetchOperation {
return true;
}
@Override
protected void handleActivityNotif(byte[] value) {
if (!isOperationRunning()) {
LOG.error("ignoring notification because operation is not running. Data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + " invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR);
handleActivityFetchFinish(false);
}
}
@Override
protected void bufferActivityData(@NonNull byte[] value) {
try {
logOutputStream.write(value, 1, value.length - 1);
} catch (final IOException e) {
LOG.warn("could not write to output stream", e);
handleActivityFetchFinish(false);
operationValid = false;
}
}
}

View File

@ -23,7 +23,6 @@ import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -32,7 +31,6 @@ import java.util.GregorianCalendar;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
@ -45,7 +43,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -60,8 +57,6 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
private final BaseActivitySummary summary;
private final String lastSyncTimeKey;
private ByteArrayOutputStream buffer;
FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary,
@NonNull AbstractHuamiActivityDetailsParser detailsParser,
@NonNull HuamiSupport support,
@ -83,93 +78,88 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
@Override
protected void startFetching(TransactionBuilder builder) {
LOG.info("start " + getName());
buffer = new ByteArrayOutputStream(1024);
GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
final GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
startFetching(builder, HuamiFetchDataType.SPORTS_DETAILS.getCode(), sinceWhen);
}
@Override
protected boolean handleActivityFetchFinish(boolean success) {
LOG.info(getName() + " has finished round " + fetchCount);
protected boolean processBufferedData() {
LOG.info("{} has finished round {}", getName(), fetchCount);
boolean parseSuccess = true;
if (buffer.size() == 0) {
LOG.warn("Buffer is empty");
return false;
}
if (success && buffer.size() > 0) {
if (detailsParser instanceof HuamiActivityDetailsParser) {
((HuamiActivityDetailsParser) detailsParser).setSkipCounterByte(false); // is already stripped
if (detailsParser instanceof HuamiActivityDetailsParser) {
((HuamiActivityDetailsParser) detailsParser).setSkipCounterByte(false); // is already stripped
}
try {
final ActivityTrack track = detailsParser.parse(buffer.toByteArray());
final ActivityTrackExporter exporter = createExporter();
final String trackType;
switch (summary.getActivityKind()) {
case ActivityKind.TYPE_CYCLING:
trackType = getContext().getString(R.string.activity_type_biking);
break;
case ActivityKind.TYPE_RUNNING:
trackType = getContext().getString(R.string.activity_type_running);
break;
case ActivityKind.TYPE_WALKING:
trackType = getContext().getString(R.string.activity_type_walking);
break;
case ActivityKind.TYPE_HIKING:
trackType = getContext().getString(R.string.activity_type_hiking);
break;
case ActivityKind.TYPE_CLIMBING:
trackType = getContext().getString(R.string.activity_type_climbing);
break;
case ActivityKind.TYPE_SWIMMING:
trackType = getContext().getString(R.string.activity_type_swimming);
break;
default:
trackType = "track";
break;
}
final String rawBytesPath = saveRawBytes();
final String fileName = FileUtils.makeValidFileName("gadgetbridge-" + trackType.toLowerCase() + "-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
final File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
boolean exportGpxSuccess = true;
try {
ActivityTrack track = detailsParser.parse(buffer.toByteArray());
ActivityTrackExporter exporter = createExporter();
String trackType = "track";
switch (summary.getActivityKind()) {
case ActivityKind.TYPE_CYCLING:
trackType = getContext().getString(R.string.activity_type_biking);
break;
case ActivityKind.TYPE_RUNNING:
trackType = getContext().getString(R.string.activity_type_running);
break;
case ActivityKind.TYPE_WALKING:
trackType = getContext().getString(R.string.activity_type_walking);
break;
case ActivityKind.TYPE_HIKING:
trackType = getContext().getString(R.string.activity_type_hiking);
break;
case ActivityKind.TYPE_CLIMBING:
trackType = getContext().getString(R.string.activity_type_climbing);
break;
case ActivityKind.TYPE_SWIMMING:
trackType = getContext().getString(R.string.activity_type_swimming);
break;
}
final String rawBytesPath = saveRawBytes();
String fileName = FileUtils.makeValidFileName("gadgetbridge-" + trackType.toLowerCase() + "-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
boolean exportGpxSuccess = true;
try {
exporter.performExport(track, targetFile);
} catch (ActivityTrackExporter.GPXTrackEmptyException ex) {
exportGpxSuccess = false;
GB.toast(getContext(), "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
if (exportGpxSuccess) {
summary.setGpxTrack(targetFile.getAbsolutePath());
}
if (rawBytesPath != null) {
summary.setRawDetailsPath(rawBytesPath);
}
dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
}
} catch (Exception ex) {
GB.toast(getContext(), "Error getting activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
parseSuccess = false;
exporter.performExport(track, targetFile);
} catch (final ActivityTrackExporter.GPXTrackEmptyException ex) {
exportGpxSuccess = false;
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
if (exportGpxSuccess) {
summary.setGpxTrack(targetFile.getAbsolutePath());
}
if (rawBytesPath != null) {
summary.setRawDetailsPath(rawBytesPath);
}
dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
}
} catch (final Exception e) {
GB.toast(getContext(), "Error saving activity details: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
final boolean superSuccess = super.handleActivityFetchFinish(success);
// Always increment the sync timestamp on success, even if we did not get data
final GregorianCalendar endTime = BLETypeConversions.createCalendar();
endTime.setTime(summary.getEndTime());
saveLastSyncTimestamp(endTime);
if (success && parseSuccess) {
// Always increment the sync timestamp on success, even if we did not get data
GregorianCalendar endTime = BLETypeConversions.createCalendar();
endTime.setTime(summary.getEndTime());
saveLastSyncTimestamp(endTime);
if (needsAnotherFetch(endTime)) {
FetchSportsSummaryOperation nextOperation = new FetchSportsSummaryOperation(getSupport(), fetchCount);
try {
nextOperation.perform();
} catch (IOException ex) {
LOG.error("Error starting another round of fetching activity data", ex);
}
}
if (needsAnotherFetch(endTime)) {
final FetchSportsSummaryOperation nextOperation = new FetchSportsSummaryOperation(getSupport(), fetchCount);
getSupport().getFetchOperationQueue().add(0, nextOperation);
}
return superSuccess && parseSuccess;
return true;
}
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
@ -183,78 +173,30 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
LOG.info("Hopefully no further fetch needed, last synced timestamp is from today.");
return false;
}
if (lastSyncTimestamp.getTimeInMillis() > System.currentTimeMillis()) {
LOG.warn("Not doing another fetch since last synced timestamp is in the future: {}", DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return false;
}
LOG.info("Doing another fetch since last sync timestamp is still too old: {}", DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return true;
}
@Override
protected boolean validChecksum(int crc32) {
return crc32 == CheckSums.getCRC32(buffer.toByteArray());
}
private ActivityTrackExporter createExporter() {
GPXExporter exporter = new GPXExporter();
final GPXExporter exporter = new GPXExporter();
exporter.setCreator(GBApplication.app().getNameAndVersion());
return exporter;
}
/**
* Method to handle the incoming activity data.
* There are two kind of messages we currently know:
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
* - the second one is 20 bytes long and contains the actual activity data
* <p/>
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
*
* @param value
*/
@Override
protected void handleActivityNotif(byte[] value) {
LOG.warn("sports details: " + Logging.formatBytes(value));
if (!isOperationRunning()) {
LOG.error("ignoring sports details notification because operation is not running. Data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if (value.length < 2) {
LOG.error("unexpected sports details data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR);
handleActivityFetchFinish(false);
}
}
/**
* Buffers the given activity summary data. If the total size is reached,
* it is converted to an object and saved in the database.
*
* @param value
*/
@Override
protected void bufferActivityData(byte[] value) {
buffer.write(value, 1, value.length - 1); // skip the counter
}
@Override
protected String getLastSyncTimeKey() {
return lastSyncTimeKey;
}
@Override
protected GregorianCalendar getLastSuccessfulSyncTime() {
GregorianCalendar calendar = BLETypeConversions.createCalendar();
final GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.setTime(summary.getStartTime());
return calendar;
}

View File

@ -17,19 +17,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
@ -43,7 +38,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -53,7 +47,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FetchSportsSummaryOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchSportsSummaryOperation.class);
private ByteArrayOutputStream buffer = new ByteArrayOutputStream(140);
public FetchSportsSummaryOperation(HuamiSupport support, int fetchCount) {
super(support);
setName("fetching sport summaries");
@ -68,126 +61,57 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
@Override
protected void startFetching(TransactionBuilder builder) {
LOG.info("start" + getName());
GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
final GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
startFetching(builder, HuamiFetchDataType.SPORTS_SUMMARIES.getCode(), sinceWhen);
}
@Override
protected boolean handleActivityFetchFinish(boolean success) {
LOG.info(getName() + " has finished round " + fetchCount);
protected boolean processBufferedData() {
LOG.info("{} has finished round {}", getName(), fetchCount);
if (buffer.size() < 2) {
LOG.warn("Buffer size {} too small for activity summary", buffer.size());
return false;
}
BaseActivitySummary summary = null;
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(getDevice());
boolean parseSummarySuccess = true;
BaseActivitySummary summary = new BaseActivitySummary();
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
summary.setRawSummaryData(buffer.toByteArray());
try {
summary = summaryParser.parseBinaryData(summary);
} catch (final Exception e) {
GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
if (success && buffer.size() > 0) {
summary = new BaseActivitySummary();
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
if (summary == null) {
return false;
}
summary.setSummaryData(null); // remove json before saving to database,
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(getDevice(), session);
final User user = DBHelper.getUser(session);
summary.setDevice(device);
summary.setUser(user);
summary.setRawSummaryData(buffer.toByteArray());
try {
summary = summaryParser.parseBinaryData(summary);
} catch (final Exception e) {
GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
summary = null;
parseSummarySuccess = false;
}
if (summary != null) {
summary.setSummaryData(null); // remove json before saving to database,
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
Device device = DBHelper.getDevice(getDevice(), session);
User user = DBHelper.getUser(session);
summary.setDevice(device);
summary.setUser(user);
summary.setRawSummaryData(buffer.toByteArray());
session.getBaseActivitySummaryDao().insertOrReplace(summary);
} catch (Exception ex) {
GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
parseSummarySuccess = false;
}
}
session.getBaseActivitySummaryDao().insertOrReplace(summary);
} catch (final Exception ex) {
GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
return false;
}
final boolean superSuccess = super.handleActivityFetchFinish(success);
boolean getDetailsSuccess = true;
final AbstractHuamiActivityDetailsParser detailsParser = ((HuamiActivitySummaryParser) summaryParser).getDetailsParser(summary);
final FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, detailsParser, getSupport(), getLastSyncTimeKey(), fetchCount);
getSupport().getFetchOperationQueue().add(0, nextOperation);
if (summary != null) {
final AbstractHuamiActivityDetailsParser detailsParser = ((HuamiActivitySummaryParser) summaryParser).getDetailsParser(summary);
FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, detailsParser, getSupport(), getLastSyncTimeKey(), fetchCount);
try {
nextOperation.perform();
} catch (IOException ex) {
GB.toast(getContext(), "Unable to fetch activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
getDetailsSuccess = false;
}
}
return parseSummarySuccess && superSuccess && getDetailsSuccess;
return true;
}
@Override
protected boolean validChecksum(int crc32) {
return crc32 == CheckSums.getCRC32(buffer.toByteArray());
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
LOG.warn("characteristic read: " + characteristic.getUuid() + ": " + Logging.formatBytes(characteristic.getValue()));
return super.onCharacteristicRead(gatt, characteristic, status);
}
/**
* Method to handle the incoming activity data.
* There are two kind of messages we currently know:
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
* - the second one is 20 bytes long and contains the actual activity data
* <p/>
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
*
* @param value
*/
@Override
protected void handleActivityNotif(byte[] value) {
LOG.warn("sports summary data: " + Logging.formatBytes(value));
if (!isOperationRunning()) {
LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if (value.length < 2) {
LOG.error("unexpected sports summary data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR);
handleActivityFetchFinish(false);
}
}
/**
* Buffers the given activity summary data. If the total size is reached,
* it is converted to an object and saved in the database.
*
* @param value
*/
@Override
protected void bufferActivityData(byte[] value) {
buffer.write(value, 1, value.length - 1); // skip the counter
}
@Override
protected String getLastSyncTimeKey() {
return "lastSportsActivityTimeMillis";

View File

@ -19,12 +19,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fe
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
/**
@ -52,14 +50,6 @@ public class FetchStatisticsOperation extends AbstractRepeatingFetchOperation {
return true;
}
@Override
protected GregorianCalendar getLastSuccessfulSyncTime() {
// One minute ago - we don't really care about this data, and we don't want to fetch it all
final GregorianCalendar sinceWhen = BLETypeConversions.createCalendar();
sinceWhen.add(Calendar.MINUTE, -1);
return sinceWhen;
}
@Override
protected String getLastSyncTimeKey() {
return "lastStatisticsTimeMillis";