diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index a08bfeec4..d93ed745b 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -45,7 +45,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(62, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(63, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -70,6 +70,7 @@ public class GBDaoGenerator { addHuamiHeartRateRestingSample(schema, user, device); addHuamiPaiSample(schema, user, device); addHuamiSleepRespiratoryRateSample(schema, user, device); + addXiaomiActivitySample(schema, user, device); addPebbleHealthActivitySample(schema, user, device); addPebbleHealthActivityKindOverlay(schema, user, device); addPebbleMisfitActivitySample(schema, user, device); @@ -324,6 +325,19 @@ public class GBDaoGenerator { return sleepRespiratoryRateSample; } + private static Entity addXiaomiActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "XiaomiActivitySample"); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.implementsSerializable(); + activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + addHeartRateProperties(activitySample); + activitySample.addIntProperty("stress"); + activitySample.addIntProperty("spo2"); + return activitySample; + } + private static void addHeartRateProperties(Entity activitySample) { activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleToTimeSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleToTimeSampleProvider.java new file mode 100644 index 000000000..c65b58e61 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleToTimeSampleProvider.java @@ -0,0 +1,93 @@ +/* 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; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.TimeSample; + +/** + * Wraps a {@link SampleProvider} into a {@link TimeSampleProvider}. + */ +public abstract class AbstractSampleToTimeSampleProvider implements TimeSampleProvider { + private final SampleProvider mSampleProvider; + private final DaoSession mSession; + private final GBDevice mDevice; + + protected AbstractSampleToTimeSampleProvider(final SampleProvider sampleProvider, final GBDevice device, final DaoSession session) { + mSampleProvider = sampleProvider; + mDevice = device; + mSession = session; + } + + protected abstract T convertSample(final S sample); + + public GBDevice getDevice() { + return mDevice; + } + + public DaoSession getSession() { + return mSession; + } + + @NonNull + @Override + public List getAllSamples(final long timestampFrom, final long timestampTo) { + final List upstreamSamples = mSampleProvider.getAllActivitySamples((int) (timestampFrom / 1000L), (int) (timestampTo / 1000L)); + final List ret = new ArrayList<>(); + for (final S sample : upstreamSamples) { + ret.add(convertSample(sample)); + } + return ret; + } + + @Override + public void addSample(final T timeSample) { + throw new UnsupportedOperationException("This sample provider is read-only!"); + } + + @Override + public void addSamples(final List timeSamples) { + throw new UnsupportedOperationException("This sample provider is read-only!"); + } + + @Override + public T createSample() { + throw new UnsupportedOperationException("This sample provider is read-only!"); + } + + @Nullable + @Override + public T getLatestSample() { + final S latestSample = mSampleProvider.getLatestActivitySample(); + return convertSample(latestSample); + } + + @Nullable + @Override + public T getFirstSample() { + final S firstSample = mSampleProvider.getFirstActivitySample(); + return convertSample(firstSample); + } +} 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 f7ae7caea..ab52d60fe 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 @@ -70,8 +70,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { @Override public TimeSampleProvider getStressSampleProvider(final GBDevice device, final DaoSession session) { - // TODO XiaomiStressSampleProvider - return super.getStressSampleProvider(device, session); + return new XiaomiStressSampleProvider(device, session); } @Override @@ -182,7 +181,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { @Override public boolean supportsPai() { // TODO does it? - return true; + return false; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiSampleProvider.java index a4ba4e6ab..d25887753 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiSampleProvider.java @@ -23,37 +23,36 @@ import de.greenrobot.dao.AbstractDao; import de.greenrobot.dao.Property; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; -import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySample; -import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -// TODO s/HuamiExtendedActivitySample/XiaomiActivitySample/g -public class XiaomiSampleProvider extends AbstractSampleProvider { +public class XiaomiSampleProvider extends AbstractSampleProvider { public XiaomiSampleProvider(final GBDevice device, final DaoSession session) { super(device, session); } @Override - public AbstractDao getSampleDao() { - return getSession().getHuamiExtendedActivitySampleDao(); + public AbstractDao getSampleDao() { + return getSession().getXiaomiActivitySampleDao(); } @Nullable @Override protected Property getRawKindSampleProperty() { - return HuamiExtendedActivitySampleDao.Properties.RawKind; + return XiaomiActivitySampleDao.Properties.RawKind; } @NonNull @Override protected Property getTimestampSampleProperty() { - return HuamiExtendedActivitySampleDao.Properties.Timestamp; + return XiaomiActivitySampleDao.Properties.Timestamp; } @NonNull @Override protected Property getDeviceIdentifierSampleProperty() { - return HuamiExtendedActivitySampleDao.Properties.DeviceId; + return XiaomiActivitySampleDao.Properties.DeviceId; } @Override @@ -64,16 +63,18 @@ public class XiaomiSampleProvider extends AbstractSampleProvider. */ +package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi; + +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleToTimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; + +public class XiaomiStressSampleProvider extends AbstractSampleToTimeSampleProvider { + public XiaomiStressSampleProvider(final GBDevice device, final DaoSession session) { + super(new XiaomiSampleProvider(device, session), device, session); + } + + @Override + protected StressSample convertSample(final XiaomiActivitySample sample) { + return new XiaomiStressSample( + sample.getTimestamp() * 1000L, + sample.getStress() + ); + } + + protected static class XiaomiStressSample implements StressSample { + private final long timestamp; + private final int stress; + + public XiaomiStressSample(final long timestamp, final int stress) { + this.timestamp = timestamp; + this.stress = stress; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public Type getType() { + return Type.UNKNOWN; + } + + @Override + public int getStress() { + return stress; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/StressSample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/StressSample.java index 807d414cf..6c6827984 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/StressSample.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/StressSample.java @@ -20,6 +20,7 @@ public interface StressSample extends TimeSample { enum Type { MANUAL(0), AUTOMATIC(1), + UNKNOWN(2), ; private final int num; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java index b41c7864d..ee9e27457 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java @@ -117,7 +117,7 @@ public class XiaomiActivityFileFetcher { final XiaomiActivityParser activityParser = XiaomiActivityParser.create(fileId); if (activityParser == null) { - LOG.warn("Failed to find activity parser for {}", fileId); + LOG.warn("Failed to find parser for {}", fileId); triggerNextFetch(); return; } 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 3d84f120f..2f7a72929 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 @@ -114,9 +114,9 @@ public class XiaomiActivityFileId { return getClass().getSimpleName() + "{" + "timestamp=" + DateTimeUtils.formatIso8601(timestamp) + ", timezone=" + timezone + - ", type=" + (typeName != Type.UNKNOWN ? typeName : "UNKNOWN(" + type + ")") + - ", subtype=" + (subtypeName != Subtype.UNKNOWN ? subtypeName : "UNKNOWN(" + subtype + ")") + - ", detailType=" + (detailTypeName != DetailType.UNKNOWN ? detailTypeName : "UNKNOWN(" + detailType + ")") + + ", type=" + (typeName + "(" + type + ")") + + ", subtype=" + (subtypeName + "(" + subtype + ")") + + ", detailType=" + (detailTypeName + "(" + detailType + ")") + ", version=" + version + "}"; } 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 494135e34..edbf395b8 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 @@ -22,6 +22,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.DailyDetailsParser; public abstract class XiaomiActivityParser { private static final Logger LOG = LoggerFactory.getLogger(XiaomiActivityParser.class); @@ -46,26 +47,22 @@ public abstract class XiaomiActivityParser { switch (fileId.getSubtype()) { case ACTIVITY_DAILY: - switch (fileId.getDetailType()) { - case DETAILS: - return null; - case SUMMARY: - return null; + if (fileId.getDetailType() == XiaomiActivityFileId.DetailType.DETAILS) { + return new DailyDetailsParser(); } + break; + case ACTIVITY_SLEEP: + // TODO break; } - LOG.warn("No parser for activity subtype in {}", fileId); - return null; } private static XiaomiActivityParser createForSports(final XiaomiActivityFileId fileId) { assert fileId.getType() == XiaomiActivityFileId.Type.SPORTS; - LOG.warn("No parser for sports subtype in {}", fileId); - return null; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/DailyDetailsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/DailyDetailsParser.java new file mode 100644 index 000000000..0be80fb68 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/DailyDetailsParser.java @@ -0,0 +1,132 @@ +/* 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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +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 DailyDetailsParser extends XiaomiActivityParser { + private static final Logger LOG = LoggerFactory.getLogger(DailyDetailsParser.class); + + @Override + public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) { + final int version = fileId.getVersion(); + final int headerSize; + final int recordSize; + switch (version) { + case 1: + case 2: + headerSize = 4; + recordSize = 10; + break; + case 3: + headerSize = 5; + recordSize = 12; + break; + default: + LOG.warn("Unable to parse daily details version {}", fileId.getVersion()); + return false; + } + + final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + final byte[] header = new byte[headerSize]; + buf.get(header); + + if ((buf.limit() - buf.position()) % recordSize != 0) { + LOG.warn("Remaining data in the buffer is not a multiple of {}", recordSize); + return false; + } + + final List samples = new ArrayList<>(); + + while (buf.position() < buf.limit()) { + final XiaomiActivitySample sample = new XiaomiActivitySample(); + + sample.setSteps(buf.getShort()); + + final byte[] unknown1 = new byte[4]; + buf.get(unknown1); // TODO intensity and kind? + + sample.setHeartRate(buf.get() & 0xff); + + final byte[] unknown2 = new byte[3]; + buf.get(unknown2); // TODO intensity and kind? + + if (version == 3) { + sample.setSpo2(buf.get() & 0xff); + sample.setStress(buf.get() & 0xff); + } + + samples.add(sample); + } + + // save all the samples that we got + final Calendar timestamp = Calendar.getInstance(); + timestamp.setTime(fileId.getTimestamp()); + + try (DBHandler handler = GBApplication.acquireDB()) { + final DaoSession session = handler.getDaoSession(); + + final GBDevice gbDevice = support.getDevice(); + final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator(); + final SampleProvider sampleProvider = (SampleProvider) coordinator.getSampleProvider(gbDevice, session); + final Device device = DBHelper.getDevice(gbDevice, session); + final User user = DBHelper.getUser(session); + + for (final XiaomiActivitySample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000)); + sample.setProvider(sampleProvider); + + timestamp.add(Calendar.MINUTE, 1); + } + sampleProvider.addGBActivitySamples(samples.toArray(new XiaomiActivitySample[0])); + + timestamp.add(Calendar.MINUTE, -1); + + return true; + } catch (final Exception e) { + GB.toast(support.getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR); + LOG.error("Error saving activity samples", e); + return false; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java index 91cc82bea..8bc63b325 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java @@ -44,6 +44,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -543,7 +544,7 @@ public class XiaomiHealthService extends AbstractXiaomiService { previousSteps = realTimeStats.getSteps(); } - final HuamiExtendedActivitySample sample; + final XiaomiActivitySample sample; try (final DBHandler dbHandler = GBApplication.acquireDB()) { final DaoSession session = dbHandler.getDaoSession();