diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index 3330d03390..0d02b60513 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -1,5 +1,6 @@ -/* Copyright (C) 2015-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, Dikay900, José Rebelo, Pavel Elagin, Petr Vaněk, walkjivefly +/* Copyright (C) 2015-2026 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, Dikay900, José Rebelo, Pavel Elagin, Petr Vaněk, walkjivefly, + Thomas Kuehne This file is part of Gadgetbridge. @@ -48,7 +49,9 @@ import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; import nodomain.freeyourgadget.gadgetbridge.database.DBAccess; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericMetricSampleProvider; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.MetricSample; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; /** @@ -422,4 +425,9 @@ public abstract class AbstractChartFragment extends Abstra getChartsHost().setDateInfo(DateTimeUtils.formatDateRange(from, to, dateFlags)); } } + + public boolean supportsMetrics(MetricSample.Metric metric) { + final GBDevice device = getChartsHost().getDevice(); + return GenericMetricSampleProvider.supportsMetrics(device, metric); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesDailyFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesDailyFragment.java index 889fb88f75..9f82774458 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesDailyFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesDailyFragment.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2024-2025 a0z, José Rebelo, Martin.JM, Thomas Kuehne +/* Copyright (C) 2024-2026 a0z, José Rebelo, Martin.JM, Thomas Kuehne This file is part of Gadgetbridge. @@ -16,6 +16,9 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; +import static nodomain.freeyourgadget.gadgetbridge.devices.GenericMetricSampleProvider.getLatestMetricSample; +import static nodomain.freeyourgadget.gadgetbridge.model.MetricSample.Metric.GENERIC_RESTING_METABOLIC_RATE; + import android.os.Bundle; import android.util.TypedValue; import android.view.LayoutInflater; @@ -30,6 +33,8 @@ import androidx.core.content.ContextCompat; import com.github.mikephil.charting.charts.Chart; import org.apache.commons.lang3.EnumUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -48,9 +53,11 @@ import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.MetricSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; public class CaloriesDailyFragment extends AbstractChartFragment { + private static final Logger LOG = LoggerFactory.getLogger(CaloriesDailyFragment.class); private ImageView caloriesGauge; private TextView dateView; @@ -67,6 +74,7 @@ public class CaloriesDailyFragment extends AbstractChartFragment samples = getActivitySamples(db, device, startTs, endTs); - RestingMetabolicRateSample metabolicRate = getRestingMetabolicRate(db, device); - if (metabolicRate == null) { + final Integer restingMetabolicRate; + if (metricMetabolicResting) { + MetricSample sample = getLatestMetricSample(db, device, GENERIC_RESTING_METABOLIC_RATE); + restingMetabolicRate = (sample == null) ? null : (int) sample.getMetricScore(); + } else { + RestingMetabolicRateSample sample = getRestingMetabolicRate(db, device); + restingMetabolicRate = (sample == null) ? null : sample.getRestingMetabolicRate(); + } + if (restingMetabolicRate == null) { return new CaloriesData(0, 0, 0, 0); } int totalBurnt; @@ -179,7 +203,7 @@ public class CaloriesDailyFragment extends AbstractChartFragment. */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; +import static nodomain.freeyourgadget.gadgetbridge.devices.GenericMetricSampleProvider.getLatestMetricSample; +import static nodomain.freeyourgadget.gadgetbridge.model.MetricSample.Metric.GENERIC_RESTING_METABOLIC_RATE; + import android.app.Activity; import org.slf4j.Logger; @@ -19,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount; import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.MetricSample; import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; @@ -47,8 +67,14 @@ abstract class CaloriesFragment extends AbstractChartFragm } protected List getMyCaloriesDaysData(DBHandler db, Calendar day, GBDevice device) { - RestingMetabolicRateSample metabolicRate = getRestingMetabolicRate(db, device); - int restingCalories = metabolicRate.getRestingMetabolicRate(); + int restingCalories; + if (GBApplication.getPrefs().experimentalMetrics() && supportsMetrics(GENERIC_RESTING_METABOLIC_RATE)) { + MetricSample sample = getLatestMetricSample(db, device, GENERIC_RESTING_METABOLIC_RATE); + restingCalories = (sample == null) ? 0 : (int) sample.getMetricScore(); + } else { + RestingMetabolicRateSample metabolicRate = getRestingMetabolicRate(db, device); + restingCalories = metabolicRate.getRestingMetabolicRate(); + } day = (Calendar) day.clone(); // do not modify the caller's argument day.add(Calendar.DATE, -TOTAL_DAYS + 1); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LoadFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LoadFragment.java index f95a893ae6..2b01fb5d04 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LoadFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LoadFragment.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 a0z, Thomas Kuehne +/* Copyright (C) 2025-2026 a0z, Thomas Kuehne This file is part of Gadgetbridge. @@ -16,6 +16,11 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; +import static nodomain.freeyourgadget.gadgetbridge.devices.GenericMetricSampleProvider.getLatestMetricSampleBefore; +import static nodomain.freeyourgadget.gadgetbridge.devices.GenericMetricSampleProvider.getMetricSamples; +import static nodomain.freeyourgadget.gadgetbridge.model.MetricSample.Metric.GENERIC_TRAINING_LOAD_ACUTE; +import static nodomain.freeyourgadget.gadgetbridge.model.MetricSample.Metric.GENERIC_TRAINING_LOAD_CHRONIC; + import android.content.Context; import android.content.res.Resources; import android.graphics.Color; @@ -67,6 +72,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.GenericTrainingLoadAcuteSample; import nodomain.freeyourgadget.gadgetbridge.entities.GenericTrainingLoadChronicSample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.MetricSample; import nodomain.freeyourgadget.gadgetbridge.model.WorkoutLoadSample; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; @@ -92,6 +98,8 @@ public class LoadFragment extends AbstractChartFragment protected int TEXT_COLOR; protected int LOAD_COLOR; + private boolean metricTrainingLoad; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_load, container, false); @@ -104,6 +112,13 @@ public class LoadFragment extends AbstractChartFragment dailyLoadChart = rootView.findViewById(R.id.daily_load_chart); thisWeekTotal = rootView.findViewById(R.id.this_week_total); lastWeekTotal = rootView.findViewById(R.id.last_week_total); + + metricTrainingLoad = GBApplication.getPrefs().experimentalMetrics() + && supportsMetrics(GENERIC_TRAINING_LOAD_ACUTE); + if (metricTrainingLoad) { + LOG.info("using experimental MetricSample for training load"); + } + if (supportsTrainingLoad()) { acuteLoadChart = rootView.findViewById(R.id.acute_load_chart); acuteLoad = rootView.findViewById(R.id.acute_load); @@ -123,6 +138,9 @@ public class LoadFragment extends AbstractChartFragment } public boolean supportsTrainingLoad() { + if (metricTrainingLoad) { + return true; + } final GBDevice device = getChartsHost().getDevice(); return device.getDeviceCoordinator().supportsTrainingLoad(device); } @@ -295,13 +313,24 @@ public class LoadFragment extends AbstractChartFragment load = workoutLoadSamples.stream().mapToInt(WorkoutLoadSample::getValue).sum(); } if (supportsTrainingLoad()) { - List workoutTrainingAcuteLoadSamples = getTrainingLoadAcuteSamples(db, device, startTs, endTs); - if (!workoutTrainingAcuteLoadSamples.isEmpty()) { - acuteLoad = workoutTrainingAcuteLoadSamples.get(workoutTrainingAcuteLoadSamples.size() - 1).getValue(); - } - List workoutTrainingChronicLoadSamples = getTrainingLoadChronicSamples(db, device, startTs, endTs); - if (!workoutTrainingChronicLoadSamples.isEmpty()) { - chronicLoad = workoutTrainingChronicLoadSamples.get(workoutTrainingChronicLoadSamples.size() - 1).getValue(); + if (metricTrainingLoad) { + List workoutTrainingAcuteLoadSamples = getMetricSamples(db, device, GENERIC_TRAINING_LOAD_ACUTE, startTs * 1000L, endTs * 1000L); + if (!workoutTrainingAcuteLoadSamples.isEmpty()) { + acuteLoad = (int) workoutTrainingAcuteLoadSamples.get(workoutTrainingAcuteLoadSamples.size() - 1).getMetricScore(); + } + List workoutTrainingChronicLoadSamples = getMetricSamples(db, device, GENERIC_TRAINING_LOAD_CHRONIC, startTs * 1000L, endTs * 1000L); + if (!workoutTrainingChronicLoadSamples.isEmpty()) { + chronicLoad = (int) workoutTrainingChronicLoadSamples.get(workoutTrainingChronicLoadSamples.size() - 1).getMetricScore(); + } + } else { + List workoutTrainingAcuteLoadSamples = getTrainingLoadAcuteSamples(db, device, startTs, endTs); + if (!workoutTrainingAcuteLoadSamples.isEmpty()) { + acuteLoad = workoutTrainingAcuteLoadSamples.get(workoutTrainingAcuteLoadSamples.size() - 1).getValue(); + } + List workoutTrainingChronicLoadSamples = getTrainingLoadChronicSamples(db, device, startTs, endTs); + if (!workoutTrainingChronicLoadSamples.isEmpty()) { + chronicLoad = workoutTrainingChronicLoadSamples.get(workoutTrainingChronicLoadSamples.size() - 1).getValue(); + } } } data.add(new LoadData((Calendar) day.clone(), load, acuteLoad, chronicLoad, i)); @@ -320,13 +349,27 @@ public class LoadFragment extends AbstractChartFragment int latestChronicLoad = 0; if (supportsTrainingLoad()) { final Date dayEnd = DateTimeUtils.dayEnd(getEndDate()); - GenericTrainingLoadAcuteSample latestAcuteLoadSample = getLatestTrainingLoadAcuteSample(db, device, dayEnd.getTime()); - if (latestAcuteLoadSample != null) { - latestAcuteLoad = latestAcuteLoadSample.getValue(); - } - GenericTrainingLoadChronicSample latestChronicLoadSample = getLatestTrainingLoadChronicSample(db, device, dayEnd.getTime()); - if (latestChronicLoadSample != null) { - latestChronicLoad = latestChronicLoadSample.getValue(); + + if (metricTrainingLoad) { + MetricSample latestAcuteLoadSample = getLatestMetricSampleBefore(db, device, GENERIC_TRAINING_LOAD_ACUTE, dayEnd.getTime()); + if (latestAcuteLoadSample != null) { + latestAcuteLoad = (int) latestAcuteLoadSample.getMetricScore(); + } + + MetricSample latestChronicLoadSample = getLatestMetricSampleBefore(db, device, GENERIC_TRAINING_LOAD_CHRONIC, dayEnd.getTime()); + if (latestChronicLoadSample != null) { + latestChronicLoad = (int) latestChronicLoadSample.getMetricScore(); + } + } else { + GenericTrainingLoadAcuteSample latestAcuteLoadSample = getLatestTrainingLoadAcuteSample(db, device, dayEnd.getTime()); + if (latestAcuteLoadSample != null) { + latestAcuteLoad = latestAcuteLoadSample.getValue(); + } + + GenericTrainingLoadChronicSample latestChronicLoadSample = getLatestTrainingLoadChronicSample(db, device, dayEnd.getTime()); + if (latestChronicLoadSample != null) { + latestChronicLoad = latestChronicLoadSample.getValue(); + } } } return new LoadsData(data, latestAcuteLoad, latestChronicLoad, thisWeekLoad, lastWeekLoad); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java index 02f6ab96d6..1c89acd472 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java @@ -1,5 +1,5 @@ -/* Copyright (C) 2019-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniel - Dakhno, José Rebelo, Petr Vaněk +/* Copyright (C) 2019-2026 Andreas Shimokawa, Carsten Pfeiffer, Daniel + Dakhno, José Rebelo, Petr Vaněk, Thomas Kuehne This file is part of Gadgetbridge. @@ -17,6 +17,10 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.model; +import static nodomain.freeyourgadget.gadgetbridge.devices.GenericMetricSampleProvider.getLatestMetricSample; +import static nodomain.freeyourgadget.gadgetbridge.devices.GenericMetricSampleProvider.supportsMetrics; +import static nodomain.freeyourgadget.gadgetbridge.model.MetricSample.Metric.GENERIC_RESTING_METABOLIC_RATE; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -159,8 +163,15 @@ public class DailyTotals implements Serializable { day.set(Calendar.MINUTE, 0); day.set(Calendar.SECOND, 0); day.add(Calendar.HOUR, 0); - RestingMetabolicRateSample metabolicRate = getRestingMetabolicRate(db, day, device); - if (metabolicRate == null) { + final Integer restingMetabolicRate; + if (GBApplication.getPrefs().experimentalMetrics() && supportsMetrics(device, GENERIC_RESTING_METABOLIC_RATE)) { + MetricSample sample = getLatestMetricSample(db, device, GENERIC_RESTING_METABOLIC_RATE, day); + restingMetabolicRate = (sample == null) ? null : (int) sample.getMetricScore(); + } else { + RestingMetabolicRateSample sample = getRestingMetabolicRate(db, day, device); + restingMetabolicRate = (sample == null) ? null : sample.getRestingMetabolicRate(); + } + if (restingMetabolicRate == null) { // should never happen return 0; } @@ -170,7 +181,7 @@ public class DailyTotals implements Serializable { if (sameDay) { passedDayProportion = (double) (calendar.getTimeInMillis() - day.getTimeInMillis()) / (24L * 60 * 60 * 1000); } - return (int) (metabolicRate.getRestingMetabolicRate() * passedDayProportion); + return (int) (restingMetabolicRate * passedDayProportion); } public static List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java index b137df6b47..610d77f85e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java @@ -238,4 +238,8 @@ public class GBPrefs extends Prefs { public boolean experimentalSettings() { return getBoolean("experimental_settings", false); } + + public boolean experimentalMetrics() { + return getBoolean("experimental_metrics", false); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1bcba8525..c6326f8458 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -773,6 +773,8 @@ Cache weather information Experimental settings Experimental settings might be unstable or not work as expected. Proceed at your own risk. + Generic Metrics + Enable experimental UI logic for generic metrics. Currently only relevant for training load and calories recorded by Garmin devices. Weather information will be cached across application restarts. File logging initialization failed, writing log files is currently not available. Restart the application to attempt to initialize the log files again. Initializing diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 42ab196a6f..e9e450f91a 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -540,6 +540,14 @@ android:summary="@string/pref_experimental_settings_summary" android:title="@string/pref_experimental_settings_title" app:iconSpaceReserved="false" /> + +