Xiaomi-protobuf: Fix activity sync stuck on duplicated or invalid files

Ref: #4305
This commit is contained in:
José Rebelo 2024-12-03 21:04:25 +00:00
parent 44d50e6246
commit 9748f389dd
3 changed files with 114 additions and 62 deletions

View File

@ -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<XiaomiActivityFileId> 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);
}

View File

@ -42,6 +42,10 @@ public abstract class AbstractXiaomiService {
}
public void dispose() {
}
/**
* Handle a preference change.
* @param config the preference key

View File

@ -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) {