From 372cf563ea9c2e9b4af4c00b5da9222ef21fabfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Tue, 12 Dec 2023 20:27:15 +0000 Subject: [PATCH] Xiaomi: Add Vitality Score (PAI-like metric) --- .../charts/ActivityChartsActivity.java | 2 +- .../activities/charts/PaiChartFragment.java | 7 +- .../DeviceSpecificSettingsFragment.java | 14 ++ .../devices/AbstractDeviceCoordinator.java | 10 ++ .../devices/DeviceCoordinator.java | 12 ++ .../devices/xiaomi/XiaomiCoordinator.java | 17 ++- .../XiaomiDailySummarySampleProvider.java | 56 +++++++ .../xiaomi/XiaomiPaiSampleProvider.java | 144 ++++++++++++++++++ app/src/main/res/values/strings.xml | 4 +- 9 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiDailySummarySampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiPaiSampleProvider.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java index d3d818bd7..3f82d9133 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java @@ -172,7 +172,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity { case "stress": return getString(R.string.menuitem_stress); case "pai": - return getString(R.string.menuitem_pai); + return getString(getDevice().getDeviceCoordinator().getPaiName()); case "stepsweek": return getStepsTitle(); case "speedzones": diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/PaiChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/PaiChartFragment.java index ece6f2817..71a58c2cd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/PaiChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/PaiChartFragment.java @@ -58,7 +58,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; -import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.Optional; public class PaiChartFragment extends AbstractChartFragment { @@ -118,6 +117,12 @@ public class PaiChartFragment extends AbstractChartFragment { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 66ca3dfb2..0e6f3e411 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -398,6 +398,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public int getPaiName() { + return R.string.menuitem_pai; + } + + @Override + public boolean supportsPaiTime() { + return supportsPai(); + } + @Override public boolean supportsSleepRespiratoryRate() { return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index d39090e32..1e7b31548 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -209,6 +209,18 @@ public interface DeviceCoordinator { */ boolean supportsPai(); + /** + * Returns the device-specific name for PAI (eg. Vitality Score). + */ + @StringRes + int getPaiName(); + + /** + * Returns true if the device is capable of providing the time contribution for each PAI type + * (light, moderate, high). + */ + boolean supportsPaiTime(); + /** * Returns true if sleep respiratory rate measurement and fetching is supported by * the device (with this coordinator). 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 d5dc940a2..70b4db328 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 @@ -140,8 +140,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { @Override public TimeSampleProvider getPaiSampleProvider(final GBDevice device, final DaoSession session) { - // TODO XiaomiPaiSampleProvider - return super.getPaiSampleProvider(device, session); + return new XiaomiPaiSampleProvider(device, session); } @Override @@ -245,13 +244,23 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { @Override public boolean supportsHeartRateStats() { - // TODO it does - see DailySummaryParser + // TODO it does, and they're persisted - see DailySummaryParser return false; } @Override public boolean supportsPai() { - // TODO it does - vitality score + // Vitality Score + return true; + } + + @Override + public int getPaiName() { + return R.string.pref_vitality_score_title; + } + + @Override + public boolean supportsPaiTime() { return false; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiDailySummarySampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiDailySummarySampleProvider.java new file mode 100644 index 000000000..7543694d8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiDailySummarySampleProvider.java @@ -0,0 +1,56 @@ +/* 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 androidx.annotation.NonNull; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiDailySummarySample; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiDailySummarySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class XiaomiDailySummarySampleProvider extends AbstractTimeSampleProvider { + public XiaomiDailySummarySampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getXiaomiDailySummarySampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return XiaomiDailySummarySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return XiaomiDailySummarySampleDao.Properties.DeviceId; + } + + @Override + public XiaomiDailySummarySample createSample() { + return new XiaomiDailySummarySample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiPaiSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiPaiSampleProvider.java new file mode 100644 index 000000000..037598948 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiPaiSampleProvider.java @@ -0,0 +1,144 @@ +/* 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiDailySummarySample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; + +public class XiaomiPaiSampleProvider implements TimeSampleProvider { + private final XiaomiDailySummarySampleProvider dailySummarySampleProvider; + + public XiaomiPaiSampleProvider(final GBDevice device, final DaoSession session) { + this.dailySummarySampleProvider = new XiaomiDailySummarySampleProvider(device, session); + } + + @NonNull + @Override + public List getAllSamples(final long timestampFrom, final long timestampTo) { + final List allSamples = dailySummarySampleProvider.getAllSamples(timestampFrom, timestampTo); + final List ret = new ArrayList<>(allSamples.size()); + for (final XiaomiDailySummarySample sample : allSamples) { + ret.add(new XiaomiPaiSample(sample)); + } + return ret; + } + + @Override + public void addSample(final PaiSample 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 PaiSample createSample() { + throw new UnsupportedOperationException("This sample provider is read-only!"); + } + + @Nullable + @Override + public PaiSample getLatestSample() { + final XiaomiDailySummarySample sample = dailySummarySampleProvider.getLatestSample(); + if (sample != null) { + return new XiaomiPaiSample(sample); + } + return null; + } + + @Nullable + @Override + public PaiSample getFirstSample() { + final XiaomiDailySummarySample sample = dailySummarySampleProvider.getFirstSample(); + if (sample != null) { + return new XiaomiPaiSample(sample); + } + return null; + } + + public static class XiaomiPaiSample implements PaiSample { + private final long timestamp; + private final int paiLow; + private final int paiModerate; + private final int paiHigh; + private final int paiTotal; + + public XiaomiPaiSample(final XiaomiDailySummarySample sample) { + this.timestamp = sample.getTimestamp(); + this.paiLow = sample.getVitalityIncreaseLight(); + this.paiModerate = sample.getVitalityIncreaseModerate(); + this.paiHigh = sample.getVitalityIncreaseHigh(); + this.paiTotal = sample.getVitalityCurrent(); + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public float getPaiLow() { + return paiLow; + } + + @Override + public float getPaiModerate() { + return paiModerate; + } + + @Override + public float getPaiHigh() { + return paiHigh; + } + + @Override + public int getTimeLow() { + return 0; // not supported + } + + @Override + public int getTimeModerate() { + return 0; // not supported + } + + @Override + public int getTimeHigh() { + return 0; // not supported + } + + @Override + public float getPaiToday() { + return paiLow + paiModerate + paiHigh; + } + + @Override + public float getPaiTotal() { + return paiTotal; + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d6862b3e2..31c2ebd81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2043,8 +2043,8 @@ Mild Moderate High - PAI Total - Day PAI increase + Total + Day increase Mode Off Noise Cancelling