From 622d37ed38ecc96084dfd6fe481b263299461d7f Mon Sep 17 00:00:00 2001 From: a0z Date: Sun, 27 Oct 2024 10:47:41 +0100 Subject: [PATCH] Calories: add fragment --- .../gadgetbridge/GBApplication.java | 40 ++- .../activities/DashboardFragment.java | 57 ++++ .../charts/AbstractChartsActivity.java | 1 + .../activities/charts/ActivityAnalysis.java | 5 + .../charts/ActivityChartsActivity.java | 10 + .../charts/CaloriesDailyFragment.java | 251 ++++++++++++++++++ .../activities/charts/StepsDailyFragment.java | 59 +--- .../dashboard/AbstractDashboardWidget.java | 3 +- .../dashboard/AbstractGaugeWidget.java | 8 +- .../DashboardCaloriesActiveGoalWidget.java | 58 ++++ .../DashboardCaloriesGoalWidget.java | 58 ++++ ...DashboardCaloriesTotalSegmentedWidget.java | 78 ++++++ .../activities/dashboard/GaugeDrawer.java | 127 +++++++++ .../devices/AbstractDeviceCoordinator.java | 10 + .../devices/DeviceCoordinator.java | 2 + .../devices/garmin/GarminCoordinator.java | 11 +- .../gadgetbridge/model/ActivityAmount.java | 9 + .../gadgetbridge/model/ActivityUser.java | 12 + .../gadgetbridge/model/DailyTotals.java | 69 +++-- .../gadgetbridge/util/DashboardUtils.java | 48 ++++ app/src/main/res/layout/fragment_calories.xml | 125 +++++++++ app/src/main/res/values/arrays.xml | 12 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 11 + app/src/main/res/xml/about_user.xml | 9 + 25 files changed, 999 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesDailyFragment.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesActiveGoalWidget.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesGoalWidget.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesTotalSegmentedWidget.java create mode 100644 app/src/main/res/layout/fragment_calories.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 6d5d3e12d..c84ac92f1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -127,7 +127,7 @@ public class GBApplication extends Application { private static SharedPreferences sharedPrefs; private static final String PREFS_VERSION = "shared_preferences_version"; //if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version - private static final int CURRENT_PREFS_VERSION = 42; + private static final int CURRENT_PREFS_VERSION = 44; private static final LimitedQueue mIDSenderLookup = new LimitedQueue<>(16); private static GBPrefs prefs; @@ -1838,6 +1838,44 @@ public class GBApplication extends Application { } } + if (oldVersion < 43) { + // Add the new calories tab to all devices. + try (DBHandler db = acquireDB()) { + final DaoSession daoSession = db.getDaoSession(); + final List activeDevices = DBHelper.getActiveDevices(daoSession); + + for (final Device dbDevice : activeDevices) { + final SharedPreferences deviceSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier()); + + final String chartsTabsValue = deviceSharedPrefs.getString("charts_tabs", null); + if (chartsTabsValue == null) { + continue; + } + + final String newPrefValue; + if (!StringUtils.isBlank(chartsTabsValue)) { + newPrefValue = chartsTabsValue + ",calories"; + } else { + newPrefValue = "calories"; + } + + final SharedPreferences.Editor deviceSharedPrefsEdit = deviceSharedPrefs.edit(); + deviceSharedPrefsEdit.putString("charts_tabs", newPrefValue); + deviceSharedPrefsEdit.apply(); + } + } catch (Exception e) { + Log.e(TAG, "Failed to migrate prefs to version 43", e); + } + } + + if (oldVersion < 44) { + // Add new dashboard calories widgets. + final String dashboardWidgetsOrder = sharedPrefs.getString("pref_dashboard_widgets_order", null); + if (!StringUtils.isBlank(dashboardWidgetsOrder) && !dashboardWidgetsOrder.contains("calories")) { + editor.putString("pref_dashboard_widgets_order", dashboardWidgetsOrder + ",calories,calories_active,calories_segmented"); + } + } + editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION)); editor.apply(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java index 64aeeaf87..bb1404734 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java @@ -63,9 +63,12 @@ import java.util.function.Supplier; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.AbstractDashboardWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCaloriesActiveGoalWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardActiveTimeWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardBodyEnergyWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCalendarActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCaloriesTotalSegmentedWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCaloriesGoalWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardDistanceWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardGoalsWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardHrvWidget; @@ -310,6 +313,15 @@ public class DashboardFragment extends Fragment implements MenuProvider { case "vo2max": widget = DashboardVO2MaxAnyWidget.newInstance(dashboardData); break; + case "calories": + widget = DashboardCaloriesGoalWidget.newInstance(dashboardData); + break; + case "calories_active": + widget = DashboardCaloriesActiveGoalWidget.newInstance(dashboardData); + break; + case "calories_segmented": + widget = DashboardCaloriesTotalSegmentedWidget.newInstance(dashboardData); + break; default: LOG.error("Unknown dashboard widget {}", widgetName); continue; @@ -372,6 +384,11 @@ public class DashboardFragment extends Fragment implements MenuProvider { public final List generalizedActivities = Collections.synchronizedList(new ArrayList<>()); private int stepsTotal; private float stepsGoalFactor; + private int restingCaloriesTotal; + private int activeCaloriesTotal; + private float activeCaloriesGoalFactor; + private int caloriesTotal; + private float caloriesGoalFactor; private long sleepTotalMinutes; private float sleepGoalFactor; private float distanceTotalMeters; @@ -381,6 +398,11 @@ public class DashboardFragment extends Fragment implements MenuProvider { private final Map genericData = new ConcurrentHashMap<>(); public void clear() { + restingCaloriesTotal = 0; + activeCaloriesTotal = 0; + activeCaloriesGoalFactor = 0; + caloriesTotal = 0; + caloriesGoalFactor = 0; stepsTotal = 0; stepsGoalFactor = 0; sleepTotalMinutes = 0; @@ -396,6 +418,11 @@ public class DashboardFragment extends Fragment implements MenuProvider { public boolean isEmpty() { return (stepsTotal == 0 && stepsGoalFactor == 0 && + restingCaloriesTotal == 0 && + activeCaloriesTotal == 0 && + activeCaloriesGoalFactor == 0 && + caloriesTotal == 0 && + caloriesGoalFactor == 0 && sleepTotalMinutes == 0 && sleepGoalFactor == 0 && distanceTotalMeters == 0 && @@ -454,6 +481,36 @@ public class DashboardFragment extends Fragment implements MenuProvider { return sleepGoalFactor; } + public synchronized int getActiveCaloriesTotal() { + if (activeCaloriesTotal == 0) + activeCaloriesTotal = DashboardUtils.getActiveCaloriesTotal(this); + return activeCaloriesTotal; + } + + public synchronized int getRestingCaloriesTotal() { + if (restingCaloriesTotal == 0) + restingCaloriesTotal = DashboardUtils.getRestingCaloriesTotal(this); + return restingCaloriesTotal; + } + + public synchronized float getActiveCaloriesGoalFactor() { + if (activeCaloriesGoalFactor == 0) + activeCaloriesGoalFactor = DashboardUtils.getActiveCaloriesGoalFactor(this); + return activeCaloriesGoalFactor; + } + + public synchronized int getCaloriesTotal() { + if (caloriesTotal == 0) + caloriesTotal = getRestingCaloriesTotal() + getActiveCaloriesTotal(); + return caloriesTotal; + } + + public synchronized float getCaloriesGoalFactor() { + if (caloriesGoalFactor == 0) + caloriesGoalFactor = DashboardUtils.getCaloriesGoalFactor(this); + return caloriesGoalFactor; + } + public void put(final String key, final Serializable value) { genericData.put(key, value); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartsActivity.java index 445e8fc1b..d97909222 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartsActivity.java @@ -70,6 +70,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity public static final String EXTRA_SINGLE_FRAGMENT_NAME = "singleFragmentName"; public static final String EXTRA_ACTIONBAR_TITLE = "actionbarTitle"; public static final String EXTRA_TIMESTAMP = "timestamp"; + public static final String EXTRA_MODE = "mode"; private TextView mDateControl; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java index 000177ee8..a8798f1a6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java @@ -80,6 +80,11 @@ public class ActivityAnalysis { amount.addDistance(distance); } + final int activeCalories = sample.getActiveCalories(); + if (activeCalories > 0) { + amount.addActiveCalories(activeCalories); + } + if (previousSample != null) { long timeDifference = sample.getTimestamp() - previousSample.getTimestamp(); if (previousSample.getRawKind() == sample.getRawKind()) { 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 4c5eccf7d..15c8ba0ce 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 @@ -17,6 +17,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -133,6 +134,9 @@ public class ActivityChartsActivity extends AbstractChartsActivity { if (!coordinator.supportsVO2Max()) { tabList.remove("vo2max"); } + if (!coordinator.supportsActiveCalories() && !coordinator.supportsRestingCalories()) { + tabList.remove("calories"); + } return tabList; } @@ -187,6 +191,10 @@ public class ActivityChartsActivity extends AbstractChartsActivity { return new CyclingChartFragment(); case "weight": return new WeightChartFragment(); + case "calories": + Intent intent = getIntent(); + String mode = intent.getStringExtra(ActivityChartsActivity.EXTRA_MODE); + return CaloriesDailyFragment.newInstance(mode); } return new UnknownFragment(); @@ -232,6 +240,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity { return getString(R.string.title_cycling); case "weight": return getString(R.string.menuitem_weight); + case "calories": + return getString(R.string.calories); } return String.format(Locale.getDefault(), "Unknown %d", position); 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 new file mode 100644 index 000000000..3a22b7766 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesDailyFragment.java @@ -0,0 +1,251 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; + +import com.github.mikephil.charting.charts.Chart; + +import org.apache.commons.lang3.EnumUtils; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.GaugeDrawer; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminRestingMetabolicRateSample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.RestingMetabolicRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.TimeSample; + +public class CaloriesDailyFragment extends AbstractChartFragment { + + private ImageView caloriesGauge; + private TextView dateView; + private TextView caloriesResting; + private LinearLayout caloriesRestingWrapper; + private TextView caloriesActive; + private TextView caloriesActiveGoal; + private TextView caloriesTotalGoal; + private LinearLayout caloriesTotalGoalWrapper; + protected int CALORIES_GOAL; + public enum GaugeViewMode { + ACTIVE_CALORIES_GOAL, + TOTAL_CALORIES_GOAL, + TOTAL_CALORIES_SEGMENT + } + private GaugeViewMode gaugeViewMode; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + String mode = getArguments().getString(ActivityChartsActivity.EXTRA_MODE, ""); + if (EnumUtils.isValidEnum(GaugeViewMode.class, mode)) { + gaugeViewMode = GaugeViewMode.valueOf(mode); + } + } + } + + public static CaloriesDailyFragment newInstance(final String mode) { + final CaloriesDailyFragment fragment = new CaloriesDailyFragment(); + final Bundle args = new Bundle(); + args.putString(ActivityChartsActivity.EXTRA_MODE, mode); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_calories, container, false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + getChartsHost().enableSwipeRefresh(scrollY == 0); + }); + } + + caloriesGauge = rootView.findViewById(R.id.calories_gauge); + dateView = rootView.findViewById(R.id.date_view); + caloriesResting = rootView.findViewById(R.id.calories_resting); + caloriesRestingWrapper = rootView.findViewById(R.id.calories_resting_wrapper); + caloriesActive = rootView.findViewById(R.id.calories_active); + caloriesActiveGoal = rootView.findViewById(R.id.calories_active_goal); + caloriesTotalGoal = rootView.findViewById(R.id.calories_total_goal); + caloriesTotalGoalWrapper = rootView.findViewById(R.id.calories_total_goal_wrapper); + ActivityUser activityUser = new ActivityUser(); + int TOTAL_CALORIES_GOAL = activityUser.getCaloriesBurntGoal(); + caloriesTotalGoal.setText(String.valueOf(TOTAL_CALORIES_GOAL)); + int ACTIVE_CALORIES_GOAL = activityUser.getActiveCaloriesBurntGoal(); + caloriesActiveGoal.setText(String.valueOf(ACTIVE_CALORIES_GOAL)); + + refresh(); + if (!supportsActiveCalories()) { + caloriesActive.setVisibility(View.GONE); + } + + if (gaugeViewMode == null) { + gaugeViewMode = GaugeViewMode.TOTAL_CALORIES_SEGMENT; + } + + if (gaugeViewMode.equals(GaugeViewMode.ACTIVE_CALORIES_GOAL)) { + CALORIES_GOAL = ACTIVE_CALORIES_GOAL; + } else if (gaugeViewMode.equals(GaugeViewMode.TOTAL_CALORIES_GOAL)) { + CALORIES_GOAL = TOTAL_CALORIES_GOAL; + } + + return rootView; + } + + public boolean supportsActiveCalories() { + final GBDevice device = getChartsHost().getDevice(); + return device.getDeviceCoordinator().supportsActiveCalories(); + } + + protected TimeSample getRestingMetabolicRate(DBHandler db, GBDevice device) { + TimeSampleProvider provider = device.getDeviceCoordinator().getRestingMetabolicRateProvider(device, db.getDaoSession()); + return provider.getLatestSample(); + } + + protected List getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + SampleProvider provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession()); + return provider.getAllActivitySamples(tsFrom, tsTo); + } + + @Override + public String getTitle() { + return getString(R.string.calories); + } + + @Override + protected void init() {} + + @Override + protected CaloriesDailyFragment.CaloriesData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { + Calendar calendar = Calendar.getInstance(); + Calendar day = Calendar.getInstance(); + day.setTime(chartsHost.getEndDate()); + day.add(Calendar.DATE, 0); + day.set(Calendar.HOUR_OF_DAY, 0); + day.set(Calendar.MINUTE, 0); + day.set(Calendar.SECOND, 0); + day.add(Calendar.HOUR, 0); + int startTs = (int) (day.getTimeInMillis() / 1000); + int endTs = startTs + 24 * 60 * 60 - 1; + Date date = new Date((long) endTs * 1000); + String formattedDate = new SimpleDateFormat("E, MMM dd").format(date); + dateView.setText(formattedDate); + List samples = getActivitySamples(db, device, startTs, endTs); + TimeSample metabolicRate = getRestingMetabolicRate(db, device); + int totalBurnt; + int activeBurnt = 0; + boolean sameDay = calendar.get(Calendar.DAY_OF_YEAR) == day.get(Calendar.DAY_OF_YEAR) && + calendar.get(Calendar.YEAR) == day.get(Calendar.YEAR); + double passedDayProportion = 1; + if (sameDay) { + passedDayProportion = (double) (calendar.getTimeInMillis() - day.getTimeInMillis()) / (24L * 60 * 60 * 1000); + } + int restingBurnt = (int) ((double) ((GarminRestingMetabolicRateSample) metabolicRate).getRestingMetabolicRate() * passedDayProportion); + + for (int i = 0; i <= samples.size() - 1; i++) { + ActivitySample sample = samples.get(i); + if (sample.getActiveCalories() > 0) { + activeBurnt += sample.getActiveCalories(); + } + } + totalBurnt = restingBurnt + activeBurnt; + + return new CaloriesData(totalBurnt, activeBurnt, restingBurnt); + } + + @Override + protected void updateChartsnUIThread(CaloriesDailyFragment.CaloriesData data) { + int restingCalories = data.restingBurnt; + int activeCalories = data.activeBurnt; + int totalCalories = activeCalories + restingCalories; + caloriesActive.setText(String.valueOf(activeCalories)); + caloriesResting.setText(String.valueOf(restingCalories)); + + if (gaugeViewMode.equals(GaugeViewMode.TOTAL_CALORIES_SEGMENT)) { + int[] colors = new int[] { + ContextCompat.getColor(GBApplication.getContext(), R.color.calories_resting_color), + ContextCompat.getColor(GBApplication.getContext(), R.color.calories_color) + }; + float[] segments = new float[] { + restingCalories > 0 ? (float) restingCalories / totalCalories : 0, + activeCalories > 0 ? (float) activeCalories / totalCalories : 0 + }; + final int width = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 300, + GBApplication.getContext().getResources().getDisplayMetrics() + ); + caloriesGauge.setImageBitmap(GaugeDrawer.drawCircleGaugeSegmented( + width, + width / 15, + colors, + segments, + true, + String.valueOf(totalCalories), + getContext().getString(R.string.total_burnt), + getContext() + )); + } else { + int value = 0; + if (gaugeViewMode.equals(GaugeViewMode.ACTIVE_CALORIES_GOAL)) { + value = activeCalories; + } else if (gaugeViewMode.equals(GaugeViewMode.TOTAL_CALORIES_GOAL)) { + value = totalCalories; + } + final int width = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 300, + GBApplication.getContext().getResources().getDisplayMetrics() + ); + caloriesGauge.setImageBitmap(GaugeDrawer.drawCircleGauge( + width, + width / 15, + getResources().getColor(R.color.calories_color), + value, + CALORIES_GOAL, + getContext() + )); + } + } + + @Override + protected void renderCharts() {} + + @Override + protected void setupLegend(Chart chart) {} + + protected static class CaloriesData extends ChartsData { + public int activeBurnt; + public int restingBurnt; + public int totalBurnt; + + protected CaloriesData(int totalBurnt, int activeBurnt, int restingBurnt) { + this.totalBurnt = totalBurnt; + this.activeBurnt = activeBurnt; + this.restingBurnt = restingBurnt; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepsDailyFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepsDailyFragment.java index 6bffca93d..f20b04830 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepsDailyFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepsDailyFragment.java @@ -37,6 +37,7 @@ import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.GaugeDrawer; import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -125,12 +126,13 @@ public class StepsDailyFragment extends StepsFragment samples; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractDashboardWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractDashboardWidget.java index 566dd8b37..eebbfe04d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractDashboardWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractDashboardWidget.java @@ -87,7 +87,7 @@ public abstract class AbstractDashboardWidget extends Fragment { .collect(Collectors.toList()); } - protected void onClickOpenChart(final View view, final String chart, final int label) { + protected void onClickOpenChart(final View view, final String chart, final int label, final String mode) { view.setOnClickListener(v -> { chooseDevice(dashboardData, device -> { final Intent startIntent; @@ -96,6 +96,7 @@ public abstract class AbstractDashboardWidget extends Fragment { startIntent.putExtra(ActivityChartsActivity.EXTRA_SINGLE_FRAGMENT_NAME, chart); startIntent.putExtra(ActivityChartsActivity.EXTRA_ACTIONBAR_TITLE, label); startIntent.putExtra(ActivityChartsActivity.EXTRA_TIMESTAMP, dashboardData.timeTo); + startIntent.putExtra(ActivityChartsActivity.EXTRA_MODE, mode); requireContext().startActivity(startIntent); }); }); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java index 233659c2c..4fd321def 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java @@ -42,18 +42,24 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget { private final int label; private final String targetActivityTab; + private String mode = ""; public AbstractGaugeWidget(@StringRes final int label, @Nullable final String targetActivityTab) { this.label = label; this.targetActivityTab = targetActivityTab; } + public AbstractGaugeWidget(@StringRes final int label, @Nullable final String targetActivityTab, final String mode) { + this(label, targetActivityTab); + this.mode = mode; + } + @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { final View fragmentView = inflater.inflate(R.layout.dashboard_widget_generic_gauge, container, false); if (targetActivityTab != null) { - onClickOpenChart(fragmentView, targetActivityTab, label); + onClickOpenChart(fragmentView, targetActivityTab, label, mode); } gaugeValue = fragmentView.findViewById(R.id.gauge_value); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesActiveGoalWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesActiveGoalWidget.java new file mode 100644 index 000000000..3adce8ea2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesActiveGoalWidget.java @@ -0,0 +1,58 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; + +import android.os.Bundle; + +import androidx.core.content.ContextCompat; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.CaloriesDailyFragment; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +/** + * A simple {@link AbstractDashboardWidget} subclass. + * Use the {@link DashboardCaloriesActiveGoalWidget#newInstance} factory method to + * create an instance of this fragment. + */ +public class DashboardCaloriesActiveGoalWidget extends AbstractGaugeWidget { + public DashboardCaloriesActiveGoalWidget() { + super(R.string.active_calories, "calories", CaloriesDailyFragment.GaugeViewMode.ACTIVE_CALORIES_GOAL.toString()); + } + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param dashboardData An instance of DashboardFragment.DashboardData. + * @return A new instance of fragment DashboardStepsWidget. + */ + public static DashboardCaloriesActiveGoalWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardCaloriesActiveGoalWidget fragment = new DashboardCaloriesActiveGoalWidget(); + final Bundle args = new Bundle(); + args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); + fragment.setArguments(args); + return fragment; + } + + @Override + protected boolean isSupportedBy(final GBDevice device) { + return device.getDeviceCoordinator().supportsActiveCalories(); + } + + @Override + protected void populateData(final DashboardFragment.DashboardData dashboardData) { + dashboardData.getActiveCaloriesTotal(); + dashboardData.getActiveCaloriesGoalFactor(); + } + + @Override + protected void draw(final DashboardFragment.DashboardData dashboardData) { + setText(String.valueOf(dashboardData.getActiveCaloriesTotal())); + final int colorCalories = ContextCompat.getColor(GBApplication.getContext(), R.color.calories_color); + drawSimpleGauge( + colorCalories, + dashboardData.getActiveCaloriesGoalFactor() + ); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesGoalWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesGoalWidget.java new file mode 100644 index 000000000..11b60709e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesGoalWidget.java @@ -0,0 +1,58 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; + +import android.os.Bundle; + +import androidx.core.content.ContextCompat; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.CaloriesDailyFragment; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +/** + * A simple {@link AbstractDashboardWidget} subclass. + * Use the {@link DashboardCaloriesGoalWidget#newInstance} factory method to + * create an instance of this fragment. + */ +public class DashboardCaloriesGoalWidget extends AbstractGaugeWidget { + public DashboardCaloriesGoalWidget() { + super(R.string.calories, "calories", CaloriesDailyFragment.GaugeViewMode.TOTAL_CALORIES_GOAL.toString()); + } + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param dashboardData An instance of DashboardFragment.DashboardData. + * @return A new instance of fragment DashboardStepsWidget. + */ + public static DashboardCaloriesGoalWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardCaloriesGoalWidget fragment = new DashboardCaloriesGoalWidget(); + final Bundle args = new Bundle(); + args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); + fragment.setArguments(args); + return fragment; + } + + @Override + protected boolean isSupportedBy(final GBDevice device) { + return device.getDeviceCoordinator().supportsActiveCalories(); + } + + @Override + protected void populateData(final DashboardFragment.DashboardData dashboardData) { + dashboardData.getCaloriesTotal(); + dashboardData.getCaloriesGoalFactor(); + } + + @Override + protected void draw(final DashboardFragment.DashboardData dashboardData) { + setText(String.valueOf(dashboardData.getCaloriesTotal())); + final int colorCalories = ContextCompat.getColor(GBApplication.getContext(), R.color.calories_color); + drawSimpleGauge( + colorCalories, + dashboardData.getCaloriesGoalFactor() + ); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesTotalSegmentedWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesTotalSegmentedWidget.java new file mode 100644 index 000000000..a6c5e428a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardCaloriesTotalSegmentedWidget.java @@ -0,0 +1,78 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; + +import android.graphics.Color; +import android.os.Bundle; + +import androidx.core.content.ContextCompat; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.CaloriesDailyFragment; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +/** + * A simple {@link AbstractDashboardWidget} subclass. + * Use the {@link DashboardCaloriesTotalSegmentedWidget#newInstance} factory method to + * create an instance of this fragment. + */ +public class DashboardCaloriesTotalSegmentedWidget extends AbstractGaugeWidget { + public DashboardCaloriesTotalSegmentedWidget() { + super(R.string.calories, "calories", CaloriesDailyFragment.GaugeViewMode.TOTAL_CALORIES_SEGMENT.toString()); + } + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param dashboardData An instance of DashboardFragment.DashboardData. + * @return A new instance of fragment DashboardStepsWidget. + */ + public static DashboardCaloriesTotalSegmentedWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardCaloriesTotalSegmentedWidget fragment = new DashboardCaloriesTotalSegmentedWidget(); + final Bundle args = new Bundle(); + args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); + fragment.setArguments(args); + return fragment; + } + + @Override + protected void populateData(final DashboardFragment.DashboardData dashboardData) { + dashboardData.getActiveCaloriesTotal(); + dashboardData.getRestingCaloriesTotal(); + } + + @Override + protected void draw(final DashboardFragment.DashboardData dashboardData) { + int activeCalories = dashboardData.getActiveCaloriesTotal(); + int restingCalories = dashboardData.getRestingCaloriesTotal(); + int totalCalories = activeCalories + restingCalories; + setText(String.valueOf(totalCalories)); + final int[] colors; + final float[] segments; + if (totalCalories != 0) { + colors = new int[] { + ContextCompat.getColor(GBApplication.getContext(), R.color.calories_resting_color), + ContextCompat.getColor(GBApplication.getContext(), R.color.calories_color) + }; + segments = new float[] { + restingCalories > 0 ? (float) restingCalories / totalCalories : 0, + activeCalories > 0 ? (float) activeCalories / totalCalories : 0 + }; + } else { + colors = new int[]{ + Color.argb(25, 128, 128, 128) + }; + segments = new float[] { + 1f + }; + } + drawSegmentedGauge( + colors, + segments, + -1, + false, + false + ); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/GaugeDrawer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/GaugeDrawer.java index 283ea678d..91aefd888 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/GaugeDrawer.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/GaugeDrawer.java @@ -1,5 +1,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; @@ -15,6 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; public class GaugeDrawer { private static final Logger LOG = LoggerFactory.getLogger(GaugeDrawer.class); @@ -194,6 +196,131 @@ public class GaugeDrawer { gaugeBar.setImageBitmap(bitmap); } + public static Bitmap drawCircleGaugeSegmented(int width, int barWidth, final int[] colors, final float[] segments, final boolean gapBetweenSegments, String text, String lowerText, Context context) { + int TEXT_COLOR = GBApplication.getTextColor(context); + int height = width; + int barMargin = (int) Math.ceil(barWidth / 2f); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.BUTT); + paint.setStrokeWidth(barWidth); + paint.setColor(context.getResources().getColor(R.color.gauge_line_color)); + canvas.drawArc( + barMargin, + barMargin, + width - barMargin, + width - barMargin, + 90, + 360, + false, + paint); + paint.setStrokeWidth(barWidth); + + float angleSum = 0; + for (int i = 0; i < segments.length; i++) { + if (segments[i] == 0) { + continue; + } + + paint.setColor(colors[i]); + paint.setStrokeWidth(barWidth); + + float startAngleDegrees = 270 + angleSum * 360; + float sweepAngleDegrees = segments[i] * 360; + + if (gapBetweenSegments) { + sweepAngleDegrees -= 2; + } + + canvas.drawArc( + barMargin, + barMargin, + width - barMargin, + height - barMargin, + startAngleDegrees, + sweepAngleDegrees, + false, + paint + ); + angleSum += segments[i]; + } + + Paint textPaint = new Paint(); + textPaint.setColor(TEXT_COLOR); + float textPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, width * 0.06f, context.getResources().getDisplayMetrics()); + textPaint.setTextSize(textPixels); + textPaint.setTextAlign(Paint.Align.CENTER); + int yPos = (int) ((float) height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2)) ; + canvas.drawText(String.valueOf(text), width / 2f, yPos, textPaint); + Paint textLowerPaint = new Paint(); + textLowerPaint.setColor(TEXT_COLOR); + textLowerPaint.setTextAlign(Paint.Align.CENTER); + float textLowerPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, width * 0.025f, context.getResources().getDisplayMetrics()); + textLowerPaint.setTextSize(textLowerPixels); + int yPosLowerText = (int) ((float) height / 2 - textPaint.ascent()) ; + canvas.drawText(String.valueOf(lowerText), width / 2f, yPosLowerText, textLowerPaint); + + return bitmap; + } + + public static Bitmap drawCircleGauge(int width, int barWidth, @ColorInt int filledColor, int value, int maxValue, Context context) { + int TEXT_COLOR = GBApplication.getTextColor(context); + int height = width; + int barMargin = (int) Math.ceil(barWidth / 2f); + float filledFactor = (float) value / maxValue; + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(barWidth); + paint.setColor(context.getResources().getColor(R.color.gauge_line_color)); + canvas.drawArc( + barMargin, + barMargin, + width - barMargin, + width - barMargin, + 90, + 360, + false, + paint); + paint.setStrokeWidth(barWidth); + paint.setColor(filledColor); + canvas.drawArc( + barMargin, + barMargin, + width - barMargin, + height - barMargin, + 270, + 360 * filledFactor, + false, + paint + ); + + Paint textPaint = new Paint(); + textPaint.setColor(TEXT_COLOR); + float textPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, width * 0.06f, context.getResources().getDisplayMetrics()); + textPaint.setTextSize(textPixels); + textPaint.setTextAlign(Paint.Align.CENTER); + int yPos = (int) ((float) height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2)) ; + canvas.drawText(String.valueOf(value), width / 2f, yPos, textPaint); + Paint textLowerPaint = new Paint(); + textLowerPaint.setColor(TEXT_COLOR); + textLowerPaint.setTextAlign(Paint.Align.CENTER); + float textLowerPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, width * 0.025f, context.getResources().getDisplayMetrics()); + textLowerPaint.setTextSize(textLowerPixels); + int yPosLowerText = (int) ((float) height / 2 - textPaint.ascent()) ; + canvas.drawText(String.valueOf(maxValue), width / 2f, yPosLowerText, textLowerPaint); + + return bitmap; + } + public static double normalize(final double value, final double min, final double max) { return normalize(value, min, max, 0, 1); } 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 8458f89a3..7c2d78b6a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -496,6 +496,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @Override + public boolean supportsActiveCalories() { + return false; + } + + @Override + public boolean supportsRestingCalories() { + return false; + } + @Override public boolean supportsActivityTabs() { return supportsActivityTracking(); 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 813bd8527..fc2ff72c2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -227,6 +227,8 @@ public interface DeviceCoordinator { boolean supportsStepCounter(); boolean supportsSpeedzones(); boolean supportsActivityTabs(); + boolean supportsRestingCalories(); + boolean supportsActiveCalories(); /** * Returns true if measurement and fetching of body temperature is supported by the device diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java index 8de544a70..c0e9082a5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java @@ -20,7 +20,6 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; -import nodomain.freeyourgadget.gadgetbridge.devices.DefaultRestingMetabolicRateProvider; import nodomain.freeyourgadget.gadgetbridge.devices.WorkoutVo2MaxSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; @@ -251,6 +250,16 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return true; } + @Override + public boolean supportsActiveCalories() { + return true; + } + + @Override + public boolean supportsRestingCalories() { + return true; + } + @Override public int[] getStressRanges() { // 1-25 = relaxed diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityAmount.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityAmount.java index 30da7d0c0..e89909f6a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityAmount.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityAmount.java @@ -24,6 +24,7 @@ public class ActivityAmount { private long totalSeconds; private long totalSteps; private long totalDistance; + private long totalActiveCalories; private Date startDate = null; private Date endDate = null; @@ -43,6 +44,10 @@ public class ActivityAmount { totalDistance += distance; } + public void addActiveCalories(long activeCalories) { + totalActiveCalories += activeCalories; + } + public long getTotalSeconds() { return totalSeconds; } @@ -55,6 +60,10 @@ public class ActivityAmount { return totalDistance; } + public long getTotalActiveCalories() { + return totalActiveCalories; + } + public ActivityKind getActivityKind() { return activityKind; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java index e9f218a68..00c4c6c12 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityUser.java @@ -44,6 +44,7 @@ public class ActivityUser { private int activityUserSleepDurationGoal; private int activityUserStepsGoal; private int activityUserCaloriesBurntGoal; + private int activityUserActiveCaloriesBurntGoal; private int activityUserDistanceGoalMeters; private int activityUserActiveTimeGoalMinutes; private int activityUserStandingTimeGoalHours; @@ -58,6 +59,7 @@ public class ActivityUser { public static final int defaultUserSleepDurationGoal = 7; public static final int defaultUserStepsGoal = 8000; public static final int defaultUserCaloriesBurntGoal = 2000; + public static final int defaultUserActiveCaloriesBurntGoal = 350; public static final int defaultUserDistanceGoalMeters = 5000; public static final int defaultUserActiveTimeGoalMinutes = 60; public static final int defaultUserStepLengthCm = 0; @@ -73,6 +75,7 @@ public class ActivityUser { public static final String PREF_USER_SLEEP_DURATION = "activity_user_sleep_duration"; public static final String PREF_USER_STEPS_GOAL = "fitness_goal"; // FIXME: for compatibility public static final String PREF_USER_CALORIES_BURNT = "activity_user_calories_burnt"; + public static final String PREF_USER_ACTIVE_CALORIES_BURNT = "activity_user_active_calories_burnt"; public static final String PREF_USER_DISTANCE_METERS = "activity_user_distance_meters"; public static final String PREF_USER_ACTIVETIME_MINUTES = "activity_user_activetime_minutes"; public static final String PREF_USER_STEP_LENGTH_CM = "activity_user_step_length_cm"; @@ -160,6 +163,7 @@ public class ActivityUser { activityUserSleepDurationGoal = prefs.getInt(PREF_USER_SLEEP_DURATION, defaultUserSleepDurationGoal); activityUserStepsGoal = prefs.getInt(PREF_USER_STEPS_GOAL, defaultUserStepsGoal); activityUserCaloriesBurntGoal = prefs.getInt(PREF_USER_CALORIES_BURNT, defaultUserCaloriesBurntGoal); + activityUserActiveCaloriesBurntGoal = prefs.getInt(PREF_USER_ACTIVE_CALORIES_BURNT, defaultUserActiveCaloriesBurntGoal); activityUserDistanceGoalMeters = prefs.getInt(PREF_USER_DISTANCE_METERS, defaultUserDistanceGoalMeters); activityUserActiveTimeGoalMinutes = prefs.getInt(PREF_USER_ACTIVETIME_MINUTES, defaultUserActiveTimeGoalMinutes); activityUserStandingTimeGoalHours = prefs.getInt(PREF_USER_GOAL_STANDING_TIME_HOURS, defaultUserGoalStandingTimeHours); @@ -187,6 +191,14 @@ public class ActivityUser { return activityUserCaloriesBurntGoal; } + public int getActiveCaloriesBurntGoal() + { + if (activityUserActiveCaloriesBurntGoal < 1) { + activityUserActiveCaloriesBurntGoal = defaultUserActiveCaloriesBurntGoal; + } + return activityUserActiveCaloriesBurntGoal; + } + public int getDistanceGoalMeters() { if (activityUserDistanceGoalMeters < 1) { 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 38df1981a..59a9804e6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java @@ -33,7 +33,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityAnalysis; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.GarminRestingMetabolicRateSample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -42,22 +44,34 @@ public class DailyTotals implements Serializable { private final long steps; private final long distance; + private final long activeCalories; + private final long restingCalories; private final long[] sleep; // light deep rem awake public DailyTotals() { - this(0, 0, new long[]{0, 0, 0 ,0}); + this(0, 0, new long[]{0, 0, 0 ,0}, 0, 0); } - public DailyTotals(final long steps, final long distance, final long[] sleep) { + public DailyTotals(final long steps, final long distance, final long[] sleep, final long activeCalories, final long restingCalories) { this.steps = steps; this.distance = distance; this.sleep = sleep; + this.activeCalories = activeCalories; + this.restingCalories = restingCalories; } public long getSteps() { return steps; } + public long getActiveCalories() { + return activeCalories; + } + + public long getRestingCalories() { + return restingCalories; + } + public long getDistance() { return distance; } @@ -79,17 +93,27 @@ public class DailyTotals implements Serializable { public static DailyTotals getDailyTotalsForDevice(GBDevice device, Calendar day, DBHandler handler) { ActivityAnalysis analysis = new ActivityAnalysis(); - ActivityAmounts amountsSteps; + ActivityAmounts totalAmounts; ActivityAmounts amountsSleep; - amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device)); + totalAmounts = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device)); amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device)); long[] sleep = getTotalsSleepForActivityAmounts(amountsSleep); - Pair stepsDistance = getTotalsStepsForActivityAmounts(amountsSteps); + + long totalSteps = 0; + long totalDistance = 0; + long totalActiveCalories = 0; + long totalRestingCalories = 0; + for (ActivityAmount amount : totalAmounts.getAmounts()) { + totalSteps += amount.getTotalSteps(); + totalDistance += amount.getTotalDistance(); + totalActiveCalories += amount.getTotalActiveCalories(); + } + totalRestingCalories = getRestingCaloriesOfDay(handler, day, device); // Purposely not including awake sleep - return new DailyTotals(stepsDistance.getLeft(), stepsDistance.getRight(), sleep); + return new DailyTotals(totalSteps, totalDistance, sleep, totalActiveCalories, totalRestingCalories); } private static long[] getTotalsSleepForActivityAmounts(ActivityAmounts activityAmounts) { @@ -115,17 +139,6 @@ public class DailyTotals implements Serializable { return new long[]{totalMinutesLightSleep, totalMinutesDeepSleep, totalMinutesRemSleep, totalMinutesAwakeSleep}; } - public static Pair getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) { - long totalSteps = 0; - long totalDistance = 0; - - for (ActivityAmount amount : activityAmounts.getAmounts()) { - totalSteps += amount.getTotalSteps(); - totalDistance += amount.getTotalDistance(); - } - return Pair.of(totalSteps, totalDistance); - } - private static List getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) { int startTs; int endTs; @@ -142,10 +155,32 @@ public class DailyTotals implements Serializable { return getSamples(db, device, startTs, endTs); } + private static int getRestingCaloriesOfDay(DBHandler db, Calendar day, GBDevice device) { + Calendar calendar = Calendar.getInstance(); + day.add(Calendar.DATE, 0); + day.set(Calendar.HOUR_OF_DAY, 0); + day.set(Calendar.MINUTE, 0); + day.set(Calendar.SECOND, 0); + day.add(Calendar.HOUR, 0); + TimeSample metabolicRate = getRestingMetabolicRate(db, device); + double passedDayProportion = 1; + boolean sameDay = calendar.get(Calendar.DAY_OF_YEAR) == day.get(Calendar.DAY_OF_YEAR) && + calendar.get(Calendar.YEAR) == day.get(Calendar.YEAR); + if (sameDay) { + passedDayProportion = (double) (calendar.getTimeInMillis() - day.getTimeInMillis()) / (24L * 60 * 60 * 1000); + } + return (int) ((double) ((GarminRestingMetabolicRateSample) metabolicRate).getRestingMetabolicRate() * passedDayProportion); + } + public static List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { return getAllSamples(db, device, tsFrom, tsTo); } + protected static TimeSample getRestingMetabolicRate(DBHandler db, GBDevice device) { + TimeSampleProvider provider = device.getDeviceCoordinator().getRestingMetabolicRateProvider(device, db.getDaoSession()); + return provider.getLatestSample(); + } + protected static SampleProvider getProvider(DBHandler db, GBDevice device) { DeviceCoordinator coordinator = device.getDeviceCoordinator(); return coordinator.getSampleProvider(device, db.getDaoSession()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DashboardUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DashboardUtils.java index 2cf478ca2..bf721aa66 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DashboardUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DashboardUtils.java @@ -63,6 +63,36 @@ public class DashboardUtils { return totalSteps; } + public static int getActiveCaloriesTotal(DashboardFragment.DashboardData dashboardData) { + List devices = GBApplication.app().getDeviceManager().getDevices(); + int totalActiveCalories = 0; + try (DBHandler dbHandler = GBApplication.acquireDB()) { + for (GBDevice dev : devices) { + if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsActivityTracking()) { + totalActiveCalories += (int) getDailyTotals(dev, dbHandler, dashboardData.timeTo).getActiveCalories(); + } + } + } catch (Exception e) { + LOG.warn("Could not calculate total amount of active calories: ", e); + } + return totalActiveCalories; + } + + public static int getRestingCaloriesTotal(DashboardFragment.DashboardData dashboardData) { + List devices = GBApplication.app().getDeviceManager().getDevices(); + int totalRestingCalories = 0; + try (DBHandler dbHandler = GBApplication.acquireDB()) { + for (GBDevice dev : devices) { + if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsActivityTracking()) { + totalRestingCalories += (int) getDailyTotals(dev, dbHandler, dashboardData.timeTo).getRestingCalories(); + } + } + } catch (Exception e) { + LOG.warn("Could not calculate total amount of resting calories: ", e); + } + return totalRestingCalories; + } + public static float getStepsGoalFactor(DashboardFragment.DashboardData dashboardData) { ActivityUser activityUser = new ActivityUser(); float stepsGoal = activityUser.getStepsGoal(); @@ -134,6 +164,24 @@ public class DashboardUtils { return goalFactor; } + public static float getActiveCaloriesGoalFactor(DashboardFragment.DashboardData dashboardData) { + ActivityUser activityUser = new ActivityUser(); + int caloriesGoal = activityUser.getActiveCaloriesBurntGoal(); + float goalFactor = (float) getActiveCaloriesTotal(dashboardData) / caloriesGoal; + if (goalFactor > 1) goalFactor = 1; + + return goalFactor; + } + + public static float getCaloriesGoalFactor(DashboardFragment.DashboardData dashboardData) { + ActivityUser activityUser = new ActivityUser(); + int caloriesGoal = activityUser.getCaloriesBurntGoal(); + float goalFactor = (float) (getRestingCaloriesTotal(dashboardData) + getActiveCaloriesTotal(dashboardData)) / caloriesGoal; + if (goalFactor > 1) goalFactor = 1; + + return goalFactor; + } + public static long getActiveMinutesTotal(DashboardFragment.DashboardData dashboardData) { List devices = GBApplication.app().getDeviceManager().getDevices(); long totalActiveMinutes = 0; diff --git a/app/src/main/res/layout/fragment_calories.xml b/app/src/main/res/layout/fragment_calories.xml new file mode 100644 index 000000000..c58fa802a --- /dev/null +++ b/app/src/main/res/layout/fragment_calories.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 4c0cb8cef..554e16cac 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3113,6 +3113,7 @@ @string/pref_header_spo2 @string/menuitem_temperature @string/menuitem_weight + @string/menuitem_calories @@ -3131,6 +3132,7 @@ @string/p_spo2 @string/p_temperature @string/p_weight + @string/p_calories @@ -3150,6 +3152,7 @@ @string/p_spo2 @string/p_temperature @string/p_weight + @string/p_calories @@ -4276,6 +4279,9 @@ @string/menuitem_vo2_max @string/vo2max_running @string/vo2max_cycling + @string/menuitem_calories_goal + @string/menuitem_calories_active_goal + @string/menuitem_calories_segmented @@ -4293,6 +4299,9 @@ vo2max vo2max_running vo2max_cycling + calories + calories_active + calories_segmented @@ -4306,5 +4315,8 @@ stress_segmented hrv vo2max + calories + calories_active + calories_segmented diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d8e9409db..cfe69639d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -61,6 +61,8 @@ #5ac234 #ff6c43 #00c9bf + #fa4502 + #1f49f2 #858585 #ffe2e2e5 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b9d960c2..b8d13efb0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -928,6 +928,7 @@ Current MTU of %1$d is too low, please enable high MTU in the device settings and disconnect/re-connect the device. Steps Calories + Active calories Distance Clock Heart rate @@ -935,6 +936,11 @@ Maximum Minimum Average + Active + Active goal + Total goal + Total burnt + Goal Blood pressure Measuring Measurement results @@ -1230,6 +1236,7 @@ Daily target: active time in minutes Daily target: standing time in minutes Daily target: fat burn time in minutes + Daily target: active calories burnt Active time Standing time Store raw record in the database @@ -1939,6 +1946,9 @@ Stress (simple) Stress (segmented) Stress (breakdown) + Calories(segmented) + Calories goal(active) + Calories goal(total) PAI Heart Rate SpO2 @@ -1959,6 +1969,7 @@ Widgets Temperature Weight + Calories Barometer Flashlight E-mail diff --git a/app/src/main/res/xml/about_user.xml b/app/src/main/res/xml/about_user.xml index 08d3f626a..0564ad35a 100644 --- a/app/src/main/res/xml/about_user.xml +++ b/app/src/main/res/xml/about_user.xml @@ -93,6 +93,15 @@ android:title="@string/activity_prefs_calories_burnt" app:useSimpleSummaryProvider="true" /> + +