1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-20 03:50:43 +02:00

Xiaomi: Parse daily summary and workout gps tracks

This commit is contained in:
José Rebelo 2023-12-12 14:12:00 +00:00
parent 9e2e3bbebc
commit 128aed005b
13 changed files with 383 additions and 69 deletions

View File

@ -229,7 +229,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public boolean supportsActivityTracks() {
// TODO It does, but not yet fully working
// TODO It does, but not yet fully working - only in Mi Band 8
return false;
}
@ -245,25 +245,19 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public boolean supportsHeartRateStats() {
// TODO does it?
// TODO it does - see DailySummaryParser
return false;
}
@Override
public boolean supportsPai() {
// TODO does it?
// TODO it does - vitality score
return false;
}
@Override
public boolean supportsSleepRespiratoryRate() {
// TODO does it?
return false;
}
@Override
public boolean supportsAlarmDescription(final GBDevice device) {
// TODO does it?
// TODO it does
return false;
}

View File

@ -73,6 +73,12 @@ public class MiBand8Coordinator extends XiaomiCoordinator {
return true;
}
@Override
public boolean supportsActivityTracks() {
// FIXME still has some issues
return true;
}
@Override
public boolean supportsMultipleWeatherLocations() {
return true;

View File

@ -19,13 +19,9 @@ package nodomain.freeyourgadget.gadgetbridge.export;
import java.io.File;
import java.io.IOException;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
public interface ActivityTrackExporter {
@NonNull
String getDefaultFileName(@NonNull ActivityTrack track);
void performExport(ActivityTrack track, File targetFile) throws IOException, GPXTrackEmptyException;
class GPXTrackEmptyException extends Exception {

View File

@ -19,7 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.export;
import android.util.Xml;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.xmlpull.v1.XmlSerializer;
@ -39,7 +38,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class GPXExporter implements ActivityTrackExporter {
private static final String NS_GPX_URI = "http://www.topografix.com/GPX/1/1";
@ -56,12 +54,6 @@ public class GPXExporter implements ActivityTrackExporter {
private boolean includeHeartRate = true;
private boolean includeHeartRateOfNearestSample = true;
@NonNull
@Override
public String getDefaultFileName(@NonNull ActivityTrack track) {
return FileUtils.makeValidFileName(track.getName());
}
@Override
public void performExport(ActivityTrack track, File targetFile) throws IOException, GPXTrackEmptyException {
String encoding = StandardCharsets.UTF_8.name();

View File

@ -38,9 +38,7 @@ public class ActivityPoint {
private Date time;
private GPSCoordinate location;
private int heartRate;
private long speed4;
private long speed5;
private long speed6;
private float speed = -1;
// e.g. to describe a pause during the activity
private @Nullable String description;
@ -85,27 +83,11 @@ public class ActivityPoint {
this.heartRate = heartRate;
}
public long getSpeed4() {
return speed4;
public float getSpeed() {
return speed;
}
public void setSpeed4(long speed4) {
this.speed4 = speed4;
}
public long getSpeed5() {
return speed5;
}
public void setSpeed5(long speed5) {
this.speed5 = speed5;
}
public long getSpeed6() {
return speed6;
}
public void setSpeed6(long speed6) {
this.speed6 = speed6;
public void setSpeed(float speed) {
this.speed = speed;
}
}

View File

@ -28,7 +28,9 @@ import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.TimeZone;
@ -47,7 +49,7 @@ public class XiaomiActivityFileFetcher {
private final XiaomiHealthService mHealthService;
private final Queue<XiaomiActivityFileId> mFetchQueue = new LinkedList<>();
private final Queue<XiaomiActivityFileId> mFetchQueue = new PriorityQueue<>();
private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream();
private boolean isFetching = false;
@ -124,15 +126,15 @@ public class XiaomiActivityFileFetcher {
LOG.warn("Failed to parse {}", fileId);
}
} catch (final Exception ex) {
LOG.error("addChunk(): failed to parse activity: ", ex);
LOG.error("Exception while parsing " + fileId, ex);
}
triggerNextFetch();
}
}
public void fetch(final XiaomiActivityFileId fileId) {
mFetchQueue.add(fileId);
public void fetch(final List<XiaomiActivityFileId> fileIds) {
mFetchQueue.addAll(fileIds);
if (!isFetching) {
// Currently not fetching anything, fetch the next
isFetching = true;
@ -163,23 +165,12 @@ public class XiaomiActivityFileFetcher {
}
protected void dumpBytesToExternalStorage(final XiaomiActivityFileId fileId, final byte[] bytes) {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US);
try {
final File externalFilesDir = FileUtils.getExternalFilesDir();
final File targetDir = new File(externalFilesDir, "rawFetchOperations");
targetDir.mkdirs();
final String filename = String.format(
Locale.ROOT, "xiaomi_%s_%02X_%02X_%02X_v%d.bin",
sdf.format(fileId.getTimestamp()),
fileId.getTypeCode(),
fileId.getSubtypeCode(),
fileId.getDetailTypeCode(),
fileId.getVersion()
);
final File outputFile = new File(targetDir, filename);
final File outputFile = new File(targetDir, fileId.getFilename());
final OutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(bytes);

View File

@ -18,13 +18,17 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.builder.CompareToBuilder;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class XiaomiActivityFileId {
public class XiaomiActivityFileId implements Comparable<XiaomiActivityFileId> {
private final Date timestamp;
private final int timezone;
private final int type;
@ -133,6 +137,32 @@ public class XiaomiActivityFileId {
"}";
}
@Override
public int compareTo(final XiaomiActivityFileId o) {
return new CompareToBuilder()
.append(timestamp, o.timestamp)
.append(timezone, o.timezone)
.append(type, o.type)
.append(subtype, o.subtype)
.append(getDetailType().getFetchOrder(), o.getDetailType().getFetchOrder())
.append(version, o.version)
.build();
}
public String getFilename() {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US);
return String.format(
Locale.ROOT,
"xiaomi_%s_%02X_%02X_%02X_v%d.bin",
sdf.format(getTimestamp()),
getTypeCode(),
getSubtypeCode(),
getDetailTypeCode(),
getVersion()
);
}
public enum Type {
UNKNOWN(-1),
ACTIVITY(0),
@ -166,6 +196,7 @@ public class XiaomiActivityFileId {
SPORTS_OUTDOOR_RUNNING(Type.SPORTS, 0x01),
SPORTS_FREESTYLE(Type.SPORTS, 0x08),
SPORTS_ELLIPTICAL(Type.SPORTS, 0x0B),
SPORTS_OUTDOOR_WALKING(Type.SPORTS, 0x16),
SPORTS_OUTDOOR_CYCLING(Type.SPORTS, 0x17),
;
@ -208,6 +239,21 @@ public class XiaomiActivityFileId {
return code;
}
public int getFetchOrder() {
// Fetch summary first, so we have the summary data for workouts
// before parsing the gps track
switch (this) {
case SUMMARY:
return 0;
case DETAILS:
return 1;
case GPS_TRACK:
return 2;
}
return 3;
}
public static DetailType fromCode(final int code) {
for (final DetailType detailType : values()) {
if (detailType.getCode() == code) {

View File

@ -21,9 +21,20 @@ import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.DailyDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.DailySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.SleepDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.WorkoutGpsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.WorkoutSummaryParser;
public abstract class XiaomiActivityParser {
@ -31,6 +42,34 @@ public abstract class XiaomiActivityParser {
public abstract boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes);
protected BaseActivitySummary findOrCreateBaseActivitySummary(final DaoSession session,
final Device device,
final User user,
final XiaomiActivityFileId fileId) {
final BaseActivitySummaryDao summaryDao = session.getBaseActivitySummaryDao();
final QueryBuilder<BaseActivitySummary> qb = summaryDao.queryBuilder();
qb.where(BaseActivitySummaryDao.Properties.StartTime.eq(fileId.getTimestamp()));
qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(device.getId()));
qb.where(BaseActivitySummaryDao.Properties.UserId.eq(user.getId()));
final List<BaseActivitySummary> summaries = qb.build().list();
if (summaries.isEmpty()) {
final BaseActivitySummary summary = new BaseActivitySummary();
summary.setStartTime(fileId.getTimestamp());
summary.setDevice(device);
summary.setUser(user);
// These will be set later, once we parse the summary
summary.setEndTime(fileId.getTimestamp());
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
return summary;
}
if (summaries.size() > 1) {
LOG.warn("Found multiple summaries for {}", fileId);
}
return summaries.get(0);
}
@Nullable
public static XiaomiActivityParser create(final XiaomiActivityFileId fileId) {
switch (fileId.getType()) {
@ -52,6 +91,9 @@ public abstract class XiaomiActivityParser {
if (fileId.getDetailType() == XiaomiActivityFileId.DetailType.DETAILS) {
return new DailyDetailsParser();
}
if (fileId.getDetailType() == XiaomiActivityFileId.DetailType.SUMMARY) {
return new DailySummaryParser();
}
break;
case ACTIVITY_SLEEP:
@ -71,6 +113,8 @@ public abstract class XiaomiActivityParser {
switch (fileId.getDetailType()) {
case SUMMARY:
return new WorkoutSummaryParser();
case GPS_TRACK:
return new WorkoutGpsParser();
}
return null;

View File

@ -69,6 +69,8 @@ public class DailyDetailsParser extends XiaomiActivityParser {
final byte[] header = new byte[headerSize];
buf.get(header);
LOG.debug("Daily Details Header: {}", GB.hexdump(header));
if ((buf.limit() - buf.position()) % sampleSize != 0) {
LOG.warn("Remaining data in the buffer is not a multiple of {}", sampleSize);
return false;
@ -85,8 +87,11 @@ public class DailyDetailsParser extends XiaomiActivityParser {
sample.setSteps(buf.getShort());
final byte[] unknown1 = new byte[4];
buf.get(unknown1); // TODO intensity and kind?
final int calories = buf.get() & 0xff;
final int unk2 = buf.get() & 0xff;
final int distance = buf.getShort(); // not just walking, includes workouts like cycling
// TODO persist calories and distance, add UI
sample.setHeartRate(buf.get() & 0xff);

View File

@ -0,0 +1,88 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
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.XiaomiActivityParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DailySummaryParser extends XiaomiActivityParser {
private static final Logger LOG = LoggerFactory.getLogger(DailySummaryParser.class);
@Override
public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
final int version = fileId.getVersion();
final int headerSize;
switch (version) {
case 5:
headerSize = 4;
break;
default:
LOG.warn("Unable to parse daily summary version {}", fileId.getVersion());
return false;
}
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
final byte[] header = new byte[headerSize];
buf.get(header);
LOG.debug("Header: {}", GB.hexdump(header));
final int steps = buf.getInt();
final int unk1 = buf.get() & 0xff; // 0
final int unk2 = buf.get() & 0xff; // 0
final int unk3 = buf.get() & 0xff; // 0
final int hrResting = buf.get() & 0xff;
final int hrMax = buf.get() & 0xff;
final int hrMaxTs = buf.getInt();
final int hrMin = buf.get() & 0xff;
final int hrMinTs = buf.getInt();
final int hrAvg = buf.get() & 0xff;
final int stressAvg = buf.get() & 0xff;
final int stressMax = buf.get() & 0xff;
final int stressMin = buf.get() & 0xff;
final int unk4 = buf.get() & 0xff; // 0
final int unk5 = buf.get() & 0xff; // 0
final int unk6 = buf.get() & 0xff; // 0
final int calories = buf.getShort();
final int unk7 = buf.get() & 0xff; // 0
final int unk8 = buf.get() & 0xff; // 0
final int unk9 = buf.get() & 0xff; // 0
final int spo2Max = buf.get() & 0xff;
final int spo2MaxTs = buf.getInt();
final int spo2Min = buf.get() & 0xff;
final int spo2MinTs = buf.getInt();
final int spo2Avg = buf.get() & 0xff;
final int trainingLoadMaybe1 = buf.getShort();
final int trainingLoadMaybe2 = buf.getShort();
final int trainingLoadMaybe3 = buf.get() & 0xff;
// TODO vitality somewhere?
// TODO persist everything
LOG.warn("Persisting daily summary is not implemented");
return true;
}
}

View File

@ -0,0 +1,159 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
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.XiaomiActivityParser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class WorkoutGpsParser extends XiaomiActivityParser {
private static final Logger LOG = LoggerFactory.getLogger(WorkoutGpsParser.class);
@Override
public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
final int version = fileId.getVersion();
final int headerSize;
final int sampleSize;
switch (version) {
case 1:
case 2:
headerSize = 1;
sampleSize = 18;
break;
default:
LOG.warn("Unable to parse workout gps version {}", fileId.getVersion());
return false;
}
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
final byte[] header = new byte[headerSize];
buf.get(header);
LOG.debug("Workout gps Header: {}", GB.hexdump(header));
if ((buf.limit() - buf.position()) % sampleSize != 0) {
LOG.warn("Remaining data in the buffer is not a multiple of {}", sampleSize);
return false;
}
final ActivityTrack activityTrack = new ActivityTrack();
while (buf.position() < buf.limit()) {
final int ts = buf.getInt();
final float longitude = buf.getFloat();
final float latitude = buf.getFloat();
final int unk1 = buf.getInt(); // 0
final float speed = (buf.getShort() >> 2) / 10.0f;
final ActivityPoint ap = new ActivityPoint(new Date(ts * 1000L));
ap.setLocation(new GPSCoordinate(longitude, latitude, 0));
activityTrack.addTrackPoint(ap);
LOG.trace("ActivityPoint: ts={} lon={} lat={} unk1={} speed={}", ts, longitude, latitude, unk1, speed);
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(support.getDevice(), session);
final User user = DBHelper.getUser(session);
// Find the matching summary
final BaseActivitySummary summary = findOrCreateBaseActivitySummary(session, device, user, fileId);
// Set the info on the activity track
activityTrack.setUser(user);
activityTrack.setDevice(device);
activityTrack.setName(ActivityKind.asString(summary.getActivityKind(), support.getContext()));
// Save the raw bytes
final String rawBytesPath = saveRawBytes(fileId, bytes);
// Save the gpx file
final GPXExporter exporter = new GPXExporter();
exporter.setCreator(GBApplication.app().getNameAndVersion());
final String gpxFileName = FileUtils.makeValidFileName("gadgetbridge-" + DateTimeUtils.formatIso8601(fileId.getTimestamp()) + ".gpx");
final File gpxTargetFile = new File(FileUtils.getExternalFilesDir(), gpxFileName);
boolean exportGpxSuccess = true;
try {
exporter.performExport(activityTrack, gpxTargetFile);
} catch (final ActivityTrackExporter.GPXTrackEmptyException ex) {
exportGpxSuccess = false;
GB.toast(support.getContext(), "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
}
if (exportGpxSuccess) {
summary.setGpxTrack(gpxTargetFile.getAbsolutePath());
}
if (rawBytesPath != null) {
summary.setRawDetailsPath(rawBytesPath);
}
session.getBaseActivitySummaryDao().insertOrReplace(summary);
} catch (final Exception e) {
GB.toast(support.getContext(), "Error saving workout gps", Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
return true;
}
private String saveRawBytes(final XiaomiActivityFileId fileId, final byte[] bytes) {
try {
final File targetFolder = new File(FileUtils.getExternalFilesDir(), "rawDetails");
targetFolder.mkdirs();
final File targetFile = new File(targetFolder, fileId.getFilename());
FileOutputStream outputStream = new FileOutputStream(targetFile);
outputStream.write(fileId.toBytes());
outputStream.write(bytes);
outputStream.close();
return targetFile.getAbsolutePath();
} catch (final IOException e) {
LOG.error("Failed to save raw bytes", e);
}
return null;
}
}

View File

@ -71,9 +71,14 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(support.getDevice(), session);
final User user = DBHelper.getUser(session);
summary.setDevice(device);
summary.setUser(user);
session.getBaseActivitySummaryDao().insertOrReplace(summary);
final BaseActivitySummary existingSummary = findOrCreateBaseActivitySummary(session, device, user, fileId);
existingSummary.setEndTime(summary.getEndTime());
existingSummary.setActivityKind(summary.getActivityKind());
existingSummary.setRawSummaryData(summary.getRawSummaryData());
existingSummary.setSummaryData(null); // remove json before saving to database
session.getBaseActivitySummaryDao().insertOrReplace(existingSummary);
} catch (final Exception e) {
GB.toast(support.getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
return false;
@ -117,7 +122,9 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
final int startTime = buf.getInt();
final int endTime = buf.getInt();
summary.setStartTime(new Date(startTime * 1000L));
// We don't set the start time, since we need it to match the fileId for the WorkoutGpsParser
// to find it. They also seem to match.
//summary.setStartTime(new Date(startTime * 1000L));
summary.setEndTime(new Date(endTime * 1000L));
final int duration = buf.getInt();

View File

@ -29,9 +29,11 @@ import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -775,12 +777,14 @@ public class XiaomiHealthService extends AbstractXiaomiService {
LOG.debug("Got {} activity file IDs", recordIds.length / 7);
final ByteBuffer buf = ByteBuffer.wrap(recordIds).order(ByteOrder.LITTLE_ENDIAN);
final List<XiaomiActivityFileId> fileIds = new ArrayList<>();
while (buf.position() < buf.limit()) {
final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(buf);
LOG.debug("Got activity to fetch: {}", fileId);
activityFetcher.fetch(fileId);
fileIds.add(fileId);
}
activityFetcher.fetch(fileIds);
if (subtype == CMD_ACTIVITY_FETCH_TODAY) {
LOG.debug("Fetch recorded data from the past");