1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 04:46:51 +01:00

Xiaomi: Fix off-by-one workout summary error (#3916)

This commit is contained in:
José Rebelo 2024-07-26 16:09:53 +01:00
parent 7712ea773a
commit 0188820048
9 changed files with 71 additions and 17 deletions

View File

@ -501,12 +501,11 @@ public class XiaomiSupport extends AbstractDeviceSupport {
try (InputStream in = new FileInputStream(activityFile)) { try (InputStream in = new FileInputStream(activityFile)) {
data = FileUtils.readAll(in, 999999); data = FileUtils.readAll(in, 999999);
} catch (final IOException ioe) { } catch (final IOException ioe) {
LOG.error("Failed to read " + activityFile, ioe); LOG.error("Failed to read {}", activityFile, ioe);
continue; continue;
} }
final byte[] fileIdBytes = Arrays.copyOfRange(data, 0, 7); final byte[] fileIdBytes = Arrays.copyOfRange(data, 0, 7);
final byte[] activityData = Arrays.copyOfRange(data, 8, data.length - 4);
final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(fileIdBytes); final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(fileIdBytes);
final XiaomiActivityParser activityParser = XiaomiActivityParser.create(fileId); final XiaomiActivityParser activityParser = XiaomiActivityParser.create(fileId);
@ -516,7 +515,7 @@ public class XiaomiSupport extends AbstractDeviceSupport {
} }
try { try {
if (activityParser.parse(this, fileId, activityData)) { if (activityParser.parse(this, fileId, data)) {
LOG.info("Successfully parsed {}", fileId); LOG.info("Successfully parsed {}", fileId);
} else { } else {
LOG.warn("Failed to parse {}", fileId); LOG.warn("Failed to parse {}", fileId);

View File

@ -25,14 +25,10 @@ import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.PriorityQueue; import java.util.PriorityQueue;
import java.util.Queue; import java.util.Queue;
import java.util.TimeZone;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
@ -98,7 +94,6 @@ public class XiaomiActivityFileFetcher {
} }
final byte[] fileIdBytes = Arrays.copyOfRange(data, 0, 7); final byte[] fileIdBytes = Arrays.copyOfRange(data, 0, 7);
final byte[] activityData = Arrays.copyOfRange(data, 8, data.length - 4);
final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(fileIdBytes); final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(fileIdBytes);
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@ -120,7 +115,7 @@ public class XiaomiActivityFileFetcher {
} }
try { try {
if (activityParser.parse(mHealthService.getSupport(), fileId, activityData)) { if (activityParser.parse(mHealthService.getSupport(), fileId, data)) {
LOG.info("Successfully parsed {}", fileId); LOG.info("Successfully parsed {}", fileId);
} else { } else {
LOG.warn("Failed to parse {}", fileId); LOG.warn("Failed to parse {}", fileId);

View File

@ -63,6 +63,12 @@ public class DailyDetailsParser extends XiaomiActivityParser {
} }
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
buf.get(new byte[7]); // skip fileId bytes
final byte fileIdPadding = buf.get();
if (fileIdPadding != 0) {
LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding);
}
final byte[] header = new byte[headerSize]; final byte[] header = new byte[headerSize];
buf.get(header); buf.get(header);
@ -74,7 +80,7 @@ public class DailyDetailsParser extends XiaomiActivityParser {
timestamp.setTime(fileId.getTimestamp()); timestamp.setTime(fileId.getTimestamp());
final List<XiaomiActivitySample> samples = new ArrayList<>(); final List<XiaomiActivitySample> samples = new ArrayList<>();
while (buf.position() < buf.limit()) { while (buf.position() < buf.limit() - 4 /* crc at the end */) {
complexParser.reset(); complexParser.reset();
final XiaomiActivitySample sample = new XiaomiActivitySample(); final XiaomiActivitySample sample = new XiaomiActivitySample();

View File

@ -53,6 +53,12 @@ public class DailySummaryParser extends XiaomiActivityParser {
} }
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
buf.get(new byte[7]); // skip fileId bytes
final byte fileIdPadding = buf.get();
if (fileIdPadding != 0) {
LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding);
}
final byte[] header = new byte[headerSize]; final byte[] header = new byte[headerSize];
buf.get(header); buf.get(header);

View File

@ -51,6 +51,11 @@ public class ManualSamplesParser extends XiaomiActivityParser {
} }
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
buf.get(new byte[7]); // skip fileId bytes
final byte fileIdPadding = buf.get();
if (fileIdPadding != 0) {
LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding);
}
// Looks like there is no header, it starts right away with samples: // Looks like there is no header, it starts right away with samples:
// 8A90A965 12 63 <- spo2 // 8A90A965 12 63 <- spo2
@ -62,7 +67,7 @@ public class ManualSamplesParser extends XiaomiActivityParser {
final List<XiaomiManualSample> samples = new ArrayList<>(); final List<XiaomiManualSample> samples = new ArrayList<>();
while (buf.position() < buf.limit()) { while (buf.position() < buf.limit() - 4 /* crc at the end */) {
final int timestamp = buf.getInt(); final int timestamp = buf.getInt();
final int type = buf.get() & 0xff; final int type = buf.get() & 0xff;

View File

@ -59,6 +59,12 @@ public class SleepDetailsParser extends XiaomiActivityParser {
int versionDependentFields = 0; int versionDependentFields = 0;
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
buf.get(new byte[7]); // skip fileId bytes
final byte fileIdPadding = buf.get();
if (fileIdPadding != 0) {
LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding);
}
final byte header = buf.get(); final byte header = buf.get();
final int isAwake = buf.get() & 0xff; // 0/1 - more correctly this would be !isSleepFinish final int isAwake = buf.get() & 0xff; // 0/1 - more correctly this would be !isSleepFinish

View File

@ -53,6 +53,11 @@ public class SleepStagesParser extends XiaomiActivityParser {
} }
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
buf.get(new byte[7]); // skip fileId bytes
final byte fileIdPadding = buf.get();
if (fileIdPadding != 0) {
LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding);
}
// over 4 days // over 4 days
// first 2 bytes: always FF FF // first 2 bytes: always FF FF

View File

@ -68,6 +68,12 @@ public class WorkoutGpsParser extends XiaomiActivityParser {
} }
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
buf.get(new byte[7]); // skip fileId bytes
final byte fileIdPadding = buf.get();
if (fileIdPadding != 0) {
LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding);
}
final byte[] header = new byte[headerSize]; final byte[] header = new byte[headerSize];
buf.get(header); buf.get(header);
@ -80,7 +86,7 @@ public class WorkoutGpsParser extends XiaomiActivityParser {
final ActivityTrack activityTrack = new ActivityTrack(); final ActivityTrack activityTrack = new ActivityTrack();
while (buf.position() < buf.limit()) { while (buf.position() < buf.limit() - 4 /* crc at the end */) {
final int ts = buf.getInt(); final int ts = buf.getInt();
final float longitude = buf.getFloat(); final float longitude = buf.getFloat();
final float latitude = buf.getFloat(); final float latitude = buf.getFloat();

View File

@ -73,6 +73,7 @@ import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
@ -83,9 +84,11 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class WorkoutSummaryParser extends XiaomiActivityParser implements ActivitySummaryParser { public class WorkoutSummaryParser extends XiaomiActivityParser implements ActivitySummaryParser {
@ -98,7 +101,7 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
summary.setStartTime(fileId.getTimestamp()); // due to a bug this has to be set summary.setStartTime(fileId.getTimestamp()); // due to a bug this has to be set
summary.setEndTime(fileId.getTimestamp()); // due to a bug this has to be set summary.setEndTime(fileId.getTimestamp()); // due to a bug this has to be set
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN); summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
summary.setRawSummaryData(ArrayUtils.addAll(fileId.toBytes(), bytes)); summary.setRawSummaryData(bytes);
try { try {
summary = parseBinaryData(summary); summary = parseBinaryData(summary);
@ -138,10 +141,33 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
@Override @Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) { public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
final ByteBuffer buf = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN); final byte[] data = summary.getRawSummaryData();
final int arrCrc32 = CheckSums.getCRC32(data, 0, data.length - 4);
final int expectedCrc32 = BLETypeConversions.toUint32(data, data.length - 4);
final ByteBuffer buf;
if (arrCrc32 != expectedCrc32) {
// If the CRC32 is not valid, we're missing 1 header padding byte due to a previous bug
// This previous version also did not include the CRC at the end
// More info: https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/3916
buf = ByteBuffer.allocate(data.length + 1).order(ByteOrder.LITTLE_ENDIAN);
buf.put(data, 0, 7);
buf.put((byte) 0);
buf.put(data, 7, data.length - 7);
buf.flip();
} else {
// Valid full file, skip crc
buf = ByteBuffer.wrap(data, 0, data.length - 4).order(ByteOrder.LITTLE_ENDIAN);
}
final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(buf); final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(buf);
final byte fileIdPadding = buf.get();
if (fileIdPadding != 0) {
LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding);
}
XiaomiSimpleActivityParser parser = null; XiaomiSimpleActivityParser parser = null;
switch (fileId.getSubtype()) { switch (fileId.getSubtype()) {
@ -151,7 +177,7 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
break; break;
case SPORTS_OUTDOOR_RUNNING: case SPORTS_OUTDOOR_RUNNING:
summary.setActivityKind(ActivityKind.TYPE_RUNNING); summary.setActivityKind(ActivityKind.TYPE_RUNNING);
// TODO parser = getOutdoorWalkingV1Parser(fileId);
break; break;
case SPORTS_INDOOR_CYCLING: case SPORTS_INDOOR_CYCLING:
summary.setActivityKind(ActivityKind.TYPE_INDOOR_CYCLING); summary.setActivityKind(ActivityKind.TYPE_INDOOR_CYCLING);
@ -197,7 +223,7 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
final int headerSize; final int headerSize;
switch (version) { switch (version) {
case 8: case 8:
headerSize = 5; headerSize = 6;
break; break;
default: default:
LOG.warn("Unable to parse workout summary version {}", fileId.getVersion()); LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());
@ -433,7 +459,7 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
final int headerSize; final int headerSize;
switch (version) { switch (version) {
case 5: case 5:
headerSize = 3; headerSize = 4;
break; break;
default: default:
LOG.warn("Unable to parse workout summary version {}", fileId.getVersion()); LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());