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:
parent
7712ea773a
commit
0188820048
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
@ -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());
|
||||||
|
Loading…
Reference in New Issue
Block a user