diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java index 57cdea2fa..688391c02 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java @@ -17,6 +17,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity; import android.content.Context; +import android.os.Handler; +import android.os.Looper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,84 +50,120 @@ public class XiaomiActivityFileFetcher { private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream(); private boolean isFetching = false; + private final Handler timeoutHandler = new Handler(Looper.getMainLooper()); + public XiaomiActivityFileFetcher(final XiaomiHealthService healthService) { this.mHealthService = healthService; } + public void dispose() { + clearTimeout(); + } + + private void clearTimeout() { + this.timeoutHandler.removeCallbacksAndMessages(null); + } + + private void setTimeout() { + // #4305 - Set the timeout in case the watch does not send the file + this.timeoutHandler.postDelayed(() -> { + LOG.warn("Timed out waiting for activity file with {} bytes in the buffer", mBuffer.size()); + triggerNextFetch(); + }, 5000L); + } + public void addChunk(final byte[] chunk) { + clearTimeout(); + final int total = BLETypeConversions.toUint16(chunk, 0); final int num = BLETypeConversions.toUint16(chunk, 2); + if (num == 1) { + // reset buffer + mBuffer = new ByteArrayOutputStream(); + } + LOG.debug("Got activity chunk {}/{}", num, total); mBuffer.write(chunk, 4, chunk.length - 4); - if (num == total) { - final byte[] data = mBuffer.toByteArray(); - mBuffer = new ByteArrayOutputStream(); - - if (data.length < 13) { - LOG.warn("Activity data length of {} is too short", data.length); - // FIXME this may mess up the order.. maybe we should just abort - triggerNextFetch(); - return; - } - - final int arrCrc32 = CheckSums.getCRC32(data, 0, data.length - 4); - final int expectedCrc32 = BLETypeConversions.toUint32(data, data.length - 4); - - if (arrCrc32 != expectedCrc32) { - LOG.warn( - "Invalid activity data checksum: got {}, expected {}", - String.format("%08X", arrCrc32), - String.format("%08X", expectedCrc32) - ); - // FIXME this may mess up the order.. maybe we should just abort - triggerNextFetch(); - return; - } - - if (data[7] != 0) { - LOG.warn( - "Unexpected activity payload byte {} at position 7 - parsing might fail", - String.format("0x%02X", data[7]) - ); - } - - final byte[] fileIdBytes = Arrays.copyOfRange(data, 0, 7); - final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(fileIdBytes); - - dumpBytesToExternalStorage(fileId, data); - - if (!XiaomiPreferences.keepActivityDataOnDevice(mHealthService.getSupport().getDevice())) { - LOG.debug("Acking recorded data {}", fileId); - // TODO is this too early? - mHealthService.ackRecordedData(fileId); - } - - final XiaomiActivityParser activityParser = XiaomiActivityParser.create(fileId); - if (activityParser == null) { - LOG.warn("Failed to find parser for {}", fileId); - triggerNextFetch(); - return; - } - - try { - if (activityParser.parse(mHealthService.getSupport(), fileId, data)) { - LOG.info("Successfully parsed {}", fileId); - } else { - LOG.warn("Failed to parse {}", fileId); - } - } catch (final Exception ex) { - LOG.error("Exception while parsing {}", fileId, ex); - } - - triggerNextFetch(); + if (num != total) { + setTimeout(); + return; } + + final byte[] data = mBuffer.toByteArray(); + mBuffer = new ByteArrayOutputStream(); + + if (data.length < 13) { + LOG.warn("Activity data length of {} is too short", data.length); + // FIXME this may mess up the order.. maybe we should just abort + triggerNextFetch(); + return; + } + + final int arrCrc32 = CheckSums.getCRC32(data, 0, data.length - 4); + final int expectedCrc32 = BLETypeConversions.toUint32(data, data.length - 4); + + if (arrCrc32 != expectedCrc32) { + LOG.warn( + "Invalid activity data checksum: got {}, expected {}", + String.format("%08X", arrCrc32), + String.format("%08X", expectedCrc32) + ); + // FIXME this may mess up the order.. maybe we should just abort + triggerNextFetch(); + return; + } + + if (data[7] != 0) { + LOG.warn( + "Unexpected activity payload byte {} at position 7 - parsing might fail", + String.format("0x%02X", data[7]) + ); + } + + final byte[] fileIdBytes = Arrays.copyOfRange(data, 0, 7); + final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(fileIdBytes); + + dumpBytesToExternalStorage(fileId, data); + + if (!XiaomiPreferences.keepActivityDataOnDevice(mHealthService.getSupport().getDevice())) { + LOG.debug("Acking recorded data {}", fileId); + // TODO is this too early? + mHealthService.ackRecordedData(fileId); + } + + final XiaomiActivityParser activityParser = XiaomiActivityParser.create(fileId); + if (activityParser == null) { + LOG.warn("Failed to find parser for {}", fileId); + triggerNextFetch(); + return; + } + + try { + if (activityParser.parse(mHealthService.getSupport(), fileId, data)) { + LOG.info("Successfully parsed {}", fileId); + } else { + LOG.warn("Failed to parse {}", fileId); + } + } catch (final Exception ex) { + LOG.error("Exception while parsing {}", fileId, ex); + } + + triggerNextFetch(); } public void fetch(final List fileIds) { - mFetchQueue.addAll(fileIds); + // #4305 - ensure unique files + for (final XiaomiActivityFileId fileId : fileIds) { + if (!mFetchQueue.contains(fileId)) { + mFetchQueue.add(fileId); + } else { + LOG.warn("Ignoring duplicated file {}", fileId); + } + } + if (!isFetching) { // Currently not fetching anything, fetch the next isFetching = true; @@ -139,6 +177,9 @@ public class XiaomiActivityFileFetcher { } private void triggerNextFetch() { + clearTimeout(); + mBuffer = new ByteArrayOutputStream(); + final XiaomiActivityFileId fileId = mFetchQueue.poll(); if (fileId == null) { @@ -153,6 +194,8 @@ public class XiaomiActivityFileFetcher { LOG.debug("Triggering next fetch for: {}", fileId); + setTimeout(); + mHealthService.requestRecordedData(fileId); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/AbstractXiaomiService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/AbstractXiaomiService.java index aa800ab77..90f29ad59 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/AbstractXiaomiService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/AbstractXiaomiService.java @@ -42,6 +42,10 @@ public abstract class AbstractXiaomiService { } + public void dispose() { + + } + /** * Handle a preference change. * @param config the preference key diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java index 558523eed..189da3ce4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java @@ -203,6 +203,11 @@ public class XiaomiHealthService extends AbstractXiaomiService { getSupport().sendCommand("get vitality score config", COMMAND_TYPE, CMD_CONFIG_VITALITY_SCORE_GET); } + @Override + public void dispose() { + activityFetcher.dispose(); + } + @Override public boolean onSendConfiguration(final String config, final Prefs prefs) { switch (config) {