diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiActivitySummaryParser.java
deleted file mode 100644
index dbb782df1..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiActivitySummaryParser.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/* 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 . */
-package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
-
-import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
-
-public class XiaomiActivitySummaryParser implements ActivitySummaryParser {
- @Override
- public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
- // TODO parse it
- return summary;
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java
index c83914ef6..0060f251d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java
@@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.WorkoutSummaryParser;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
@@ -111,7 +112,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
@Nullable
@Override
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
- return new XiaomiActivitySummaryParser();
+ return new WorkoutSummaryParser();
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java
index dfcbd8ed2..3db718cbd 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java
@@ -154,6 +154,7 @@ public class XiaomiActivityFileId {
SPORTS_OUTDOOR_RUNNING(Type.SPORTS, 1),
SPORTS_FREESTYLE(Type.SPORTS, 8),
SPORTS_ELLIPTICAL(Type.SPORTS, 11),
+ SPORTS_OUTDOOR_CYCLING(Type.SPORTS, 23),
;
private final Type type;
@@ -182,6 +183,7 @@ public class XiaomiActivityFileId {
UNKNOWN(-1),
DETAILS(0),
SUMMARY(1),
+ GPS_TRACK(2),
;
private final int code;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java
index 5cbca661c..9de6aeb38 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java
@@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
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.SleepDetailsParser;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.WorkoutSummaryParser;
public abstract class XiaomiActivityParser {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiActivityParser.class);
@@ -67,6 +68,11 @@ public abstract class XiaomiActivityParser {
private static XiaomiActivityParser createForSports(final XiaomiActivityFileId fileId) {
assert fileId.getType() == XiaomiActivityFileId.Type.SPORTS;
+ switch (fileId.getDetailType()) {
+ case SUMMARY:
+ return new WorkoutSummaryParser();
+ }
+
return null;
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutSummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutSummaryParser.java
new file mode 100644
index 000000000..e59869215
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutSummaryParser.java
@@ -0,0 +1,169 @@
+/* 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl;
+
+import android.widget.Toast;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
+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 WorkoutSummaryParser extends XiaomiActivityParser implements ActivitySummaryParser {
+ private static final Logger LOG = LoggerFactory.getLogger(WorkoutSummaryParser.class);
+
+ @Override
+ public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
+ BaseActivitySummary summary = new BaseActivitySummary();
+
+ summary.setStartTime(fileId.getTimestamp()); // due to a bug this has to be set
+ summary.setRawSummaryData(ArrayUtils.addAll(fileId.toBytes(), bytes));
+
+ try {
+ summary = parseBinaryData(summary);
+ } catch (final Exception e) {
+ LOG.error("Failed to parse workout summary", e);
+ GB.toast(support.getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e);
+ return false;
+ }
+
+ summary.setSummaryData(null); // remove json before saving to database
+
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ 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);
+ } catch (final Exception e) {
+ GB.toast(support.getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
+ final JSONObject summaryData = new JSONObject();
+
+ final ByteBuffer buf = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN);
+
+ final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(buf);
+
+ final int version = fileId.getVersion();
+ final int headerSize;
+ switch (version) {
+ case 4:
+ headerSize = 6;
+ break;
+ default:
+ LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());
+ return null;
+ }
+
+ final byte[] header = new byte[headerSize];
+ buf.get(header);
+
+ final short workoutType = buf.getShort();
+
+ switch (workoutType) {
+ case 6:
+ summary.setActivityKind(ActivityKind.TYPE_CYCLING);
+ break;
+ default:
+ summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
+ }
+
+ final int startTime = buf.getInt();
+ final int endTime = buf.getInt();
+
+ summary.setStartTime(new Date(startTime * 1000L));
+ summary.setEndTime(new Date(endTime * 1000L));
+
+ final int duration = buf.getInt();
+ addSummaryData(summaryData, "activeSeconds", duration, "seconds");
+
+ final int unknown1 = buf.getInt();
+ final int distance = buf.getInt();
+ addSummaryData(summaryData, "distanceMeters", distance, "meters");
+
+ final int unknown2 = buf.getShort();
+
+ final int calories = buf.getShort();
+ addSummaryData(summaryData, "caloriesBurnt", calories, "calories_unit");
+
+ final int unknown3 = buf.getInt();
+ final int unknown4 = buf.getInt();
+ final float maxSpeed = buf.getFloat();
+
+ final float avgHr = buf.get() & 0xff;
+ final float maxHr = buf.get() & 0xff;
+ final float minHr = buf.get() & 0xff;
+ addSummaryData(summaryData, "averageHR", avgHr, "bpm");
+ addSummaryData(summaryData, "maxHR", maxHr, "bpm");
+ addSummaryData(summaryData, "minHR", minHr, "bpm");
+
+ summary.setSummaryData(summaryData.toString());
+
+ return summary;
+ }
+
+ protected void addSummaryData(final JSONObject summaryData, final String key, final float value, final String unit) {
+ if (value > 0) {
+ try {
+ final JSONObject innerData = new JSONObject();
+ innerData.put("value", value);
+ innerData.put("unit", unit);
+ summaryData.put(key, innerData);
+ } catch (final JSONException ignore) {
+ }
+ }
+ }
+
+ protected void addSummaryData(final JSONObject summaryData, final String key, final String value) {
+ if (key != null && !key.equals("") && value != null && !value.equals("")) {
+ try {
+ final JSONObject innerData = new JSONObject();
+ innerData.put("value", value);
+ innerData.put("unit", "string");
+ summaryData.put(key, innerData);
+ } catch (final JSONException ignore) {
+ }
+ }
+ }
+}