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) { + } + } + } +}