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 ff5f1993bf..05f66dc8d8 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,7 +17,6 @@
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;
@@ -196,9 +195,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
case "weight":
return new WeightChartFragment();
case "calories":
- Intent intent = getIntent();
- String mode = intent.getStringExtra(ActivityChartsActivity.EXTRA_MODE);
- return CaloriesDailyFragment.newInstance(mode);
+ return CaloriesCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "respiratoryrate":
return RespiratoryRateCollectionFragment.newInstance(enabledTabsList.size() == 1);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesCollectionFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesCollectionFragment.java
new file mode 100644
index 0000000000..3e0498f419
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesCollectionFragment.java
@@ -0,0 +1,45 @@
+/* Copyright (C) 2024 a0z, 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.activities.charts;
+
+import android.os.Bundle;
+
+import androidx.fragment.app.FragmentManager;
+
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
+import nodomain.freeyourgadget.gadgetbridge.adapter.CaloriesFragmentAdapter;
+import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
+
+public class CaloriesCollectionFragment extends AbstractCollectionFragment {
+ public CaloriesCollectionFragment() {
+
+ }
+
+ public static CaloriesCollectionFragment newInstance(final boolean allowSwipe) {
+ final CaloriesCollectionFragment fragment = new CaloriesCollectionFragment();
+ final Bundle args = new Bundle();
+ args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
+ return new CaloriesFragmentAdapter(this, getChildFragmentManager());
+ }
+}
+
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesFragment.java
new file mode 100644
index 0000000000..e6d2fa33da
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesFragment.java
@@ -0,0 +1,138 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.charts;
+
+import android.app.Activity;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.DefaultRestingMetabolicRateProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
+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.RestingMetabolicRateSample;
+import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
+
+abstract class CaloriesFragment extends AbstractChartFragment {
+ protected static final Logger LOG = LoggerFactory.getLogger(CaloriesFragment.class);
+
+ protected int CHART_TEXT_COLOR;
+ protected int TEXT_COLOR;
+
+ protected int BACKGROUND_COLOR;
+ protected int DESCRIPTION_COLOR;
+ protected int TOTAL_DAYS = 1;
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.steps);
+ }
+
+ @Override
+ protected void init() {
+ TEXT_COLOR = GBApplication.getTextColor(requireContext());
+ CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
+ BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext());
+ DESCRIPTION_COLOR = GBApplication.getTextColor(getContext());
+ CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(getContext());
+ }
+
+ protected List getMyCaloriesDaysData(DBHandler db, Calendar day, GBDevice device) {
+ RestingMetabolicRateSample metabolicRate = getRestingMetabolicRate(db, device);
+ int restingCalories = metabolicRate.getRestingMetabolicRate();
+
+ day = (Calendar) day.clone(); // do not modify the caller's argument
+ day.add(Calendar.DATE, -TOTAL_DAYS + 1);
+
+ List daysData = new ArrayList<>();
+ for (int counter = 0; counter < TOTAL_DAYS; counter++) {
+ long activeCalories = 0;
+ ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
+ for (ActivityAmount amount : amounts.getAmounts()) {
+ if (amount.getTotalActiveCalories() > 0) {
+ activeCalories += amount.getTotalActiveCalories() / 1000;
+ }
+ }
+ Calendar d = (Calendar) day.clone();
+ daysData.add(new CaloriesDay(d, activeCalories, restingCalories));
+ day.add(Calendar.DATE, 1);
+ }
+ return daysData;
+ }
+
+ protected RestingMetabolicRateSample getRestingMetabolicRate(DBHandler db, GBDevice device) {
+ TimeSampleProvider extends RestingMetabolicRateSample> provider = device.getDeviceCoordinator().getRestingMetabolicRateProvider(device, db.getDaoSession());
+ RestingMetabolicRateSample latestSample = provider.getLatestSample();
+ if (latestSample != null) {
+ return latestSample;
+ }
+ DefaultRestingMetabolicRateProvider defaultProvider = new DefaultRestingMetabolicRateProvider(device, db.getDaoSession());
+ return defaultProvider.getLatestSample();
+ }
+
+ protected ActivityAmounts getActivityAmountsForDay(DBHandler db, Calendar day, GBDevice device) {
+ LimitedQueue activityAmountCache = null;
+ ActivityAmounts amounts = null;
+
+ Activity activity = getActivity();
+ int key = (int) (day.getTimeInMillis() / 1000);
+ if (activity != null) {
+ activityAmountCache = ((ActivityChartsActivity) activity).mActivityAmountCache;
+ amounts = activityAmountCache.lookup(key);
+ }
+
+ if (amounts == null) {
+ ActivityAnalysis analysis = new ActivityAnalysis();
+ amounts = analysis.calculateActivityAmounts(getSamplesOfDay(db, day, 0, device));
+ if (activityAmountCache != null) {
+ activityAmountCache.add(key, amounts);
+ }
+ }
+
+ return amounts;
+ }
+
+ protected List extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) {
+ int startTs;
+ int endTs;
+
+ day = (Calendar) day.clone(); // do not modify the caller's argument
+ day.set(Calendar.HOUR_OF_DAY, 0);
+ day.set(Calendar.MINUTE, 0);
+ day.set(Calendar.SECOND, 0);
+ day.add(Calendar.HOUR, offsetHours);
+
+ startTs = (int) (day.getTimeInMillis() / 1000);
+ endTs = startTs + 24 * 60 * 60 - 1;
+
+ return getSamples(db, device, startTs, endTs);
+ }
+
+ protected List extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
+ SampleProvider extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
+ return provider.getAllActivitySamples(tsFrom, tsTo);
+ }
+
+ protected static class CaloriesDay {
+ public long activeCalories;
+ public long restingCalories;
+ public Calendar day;
+
+ protected CaloriesDay(Calendar day, long activeCalories, long restingCalories) {
+ this.activeCalories = activeCalories;
+ this.restingCalories = restingCalories;
+ this.day = day;
+ }
+ }
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesPeriodFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesPeriodFragment.java
new file mode 100644
index 0000000000..38989e9475
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/CaloriesPeriodFragment.java
@@ -0,0 +1,244 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.charts;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.github.mikephil.charting.charts.BarChart;
+import com.github.mikephil.charting.charts.Chart;
+import com.github.mikephil.charting.components.LimitLine;
+import com.github.mikephil.charting.components.XAxis;
+import com.github.mikephil.charting.components.YAxis;
+import com.github.mikephil.charting.data.BarData;
+import com.github.mikephil.charting.data.BarDataSet;
+import com.github.mikephil.charting.data.BarEntry;
+import com.github.mikephil.charting.formatter.ValueFormatter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+
+public class CaloriesPeriodFragment extends CaloriesFragment {
+ protected static final Logger LOG = LoggerFactory.getLogger(CaloriesPeriodFragment.class);
+
+ private TextView mDateView;
+ private TextView activeCaloriesAvg;
+ private TextView activeCaloriesTotal;
+ private TextView restingCaloriesAvg;
+ private TextView restingCaloriesTotal;
+ private BarChart caloriesChart;
+
+ private TextView mBalanceView;
+
+ protected int CHART_TEXT_COLOR;
+ protected int TEXT_COLOR;
+ protected int CALORIES_GOAL;
+
+ protected int BACKGROUND_COLOR;
+ protected int DESCRIPTION_COLOR;
+
+ public static CaloriesPeriodFragment newInstance(int totalDays) {
+ CaloriesPeriodFragment fragmentFirst = new CaloriesPeriodFragment();
+ Bundle args = new Bundle();
+ args.putInt("totalDays", totalDays);
+ fragmentFirst.setArguments(args);
+ return fragmentFirst;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ TOTAL_DAYS = getArguments() != null ? getArguments().getInt("totalDays") : 0;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_calories_period, container, false);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+ getChartsHost().enableSwipeRefresh(scrollY == 0);
+ });
+ }
+
+ mDateView = rootView.findViewById(R.id.calories_date_view);
+ caloriesChart = rootView.findViewById(R.id.calories_chart);
+ activeCaloriesAvg = rootView.findViewById(R.id.active_calories_avg);
+ activeCaloriesTotal = rootView.findViewById(R.id.active_calories_total);
+ restingCaloriesAvg = rootView.findViewById(R.id.resting_calories_avg);
+ restingCaloriesTotal = rootView.findViewById(R.id.resting_calories_total);
+ CALORIES_GOAL = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_CALORIES_BURNT, ActivityUser.defaultUserCaloriesBurntGoal);
+
+ mBalanceView = rootView.findViewById(R.id.balance);
+
+ setupCaloriesChart();
+ refresh();
+
+ return rootView;
+ }
+
+ protected void setupCaloriesChart() {
+ caloriesChart.getDescription().setEnabled(false);
+ if (TOTAL_DAYS <= 7) {
+ caloriesChart.setTouchEnabled(false);
+ caloriesChart.setPinchZoom(false);
+ }
+ caloriesChart.setDoubleTapToZoomEnabled(false);
+ caloriesChart.getLegend().setEnabled(false);
+
+ final XAxis xAxisBottom = caloriesChart.getXAxis();
+ xAxisBottom.setPosition(XAxis.XAxisPosition.BOTTOM);
+ xAxisBottom.setDrawLabels(true);
+ xAxisBottom.setDrawGridLines(false);
+ xAxisBottom.setEnabled(true);
+ xAxisBottom.setDrawLimitLinesBehindData(true);
+ xAxisBottom.setTextColor(CHART_TEXT_COLOR);
+
+ final YAxis yAxisLeft = caloriesChart.getAxisLeft();
+ yAxisLeft.setDrawGridLines(true);
+ yAxisLeft.setDrawTopYLabelEntry(true);
+ yAxisLeft.setEnabled(true);
+ yAxisLeft.setTextColor(CHART_TEXT_COLOR);
+ yAxisLeft.setAxisMinimum(0f);
+ final LimitLine goalLine = new LimitLine(CALORIES_GOAL);
+ goalLine.setLineColor(getResources().getColor(R.color.calories_color));
+ goalLine.setLineWidth(1.5f);
+ goalLine.enableDashedLine(15f, 10f, 0f);
+ yAxisLeft.addLimitLine(goalLine);
+
+ final YAxis yAxisRight = caloriesChart.getAxisRight();
+ yAxisRight.setEnabled(true);
+ yAxisRight.setDrawLabels(false);
+ yAxisRight.setDrawGridLines(false);
+ yAxisRight.setDrawAxisLine(true);
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.calories);
+ }
+
+ @Override
+ protected void init() {
+ TEXT_COLOR = GBApplication.getTextColor(requireContext());
+ CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
+ BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext());
+ DESCRIPTION_COLOR = GBApplication.getTextColor(getContext());
+ CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(getContext());
+ }
+
+ @Override
+ protected CaloriesData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
+ Calendar day = Calendar.getInstance();
+ day.setTime(getEndDate());
+ List caloriesDaysData = getMyCaloriesDaysData(db, day, device);
+ return new CaloriesData(caloriesDaysData);
+ }
+
+ @Override
+ protected void updateChartsnUIThread(CaloriesData caloriesData) {
+ mDateView.setText(DateTimeUtils.formatDaysUntil(TOTAL_DAYS, getTSEnd()));
+ caloriesChart.setData(null);
+
+ List entries = new ArrayList<>();
+ int counter = 0;
+ for (CaloriesDay day : caloriesData.days) {
+ entries.add(new BarEntry(counter, day.activeCalories));
+ counter++;
+ }
+ BarDataSet set = new BarDataSet(entries, "Calories");
+ set.setDrawValues(true);
+ set.setColors(getResources().getColor(R.color.calories_color));
+ final XAxis x = caloriesChart.getXAxis();
+ x.setValueFormatter(getCaloriesChartDayValueFormatter(caloriesData));
+ caloriesChart.getAxisLeft().setAxisMaximum((float) Math.max(set.getYMax() * 1.1, CALORIES_GOAL));
+
+ BarData barData = new BarData(set);
+ barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false);
+ barData.setValueTextSize(10f);
+ if (TOTAL_DAYS > 7) {
+ caloriesChart.setRenderer(new AngledLabelsChartRenderer(caloriesChart, caloriesChart.getAnimator(), caloriesChart.getViewPortHandler()));
+ }
+ caloriesChart.setData(barData);
+ activeCaloriesAvg.setText(String.format(String.valueOf(caloriesData.activeCaloriesDailyAvg)));
+ activeCaloriesTotal.setText(String.format(String.valueOf(caloriesData.totalActiveCalories)));
+ restingCaloriesAvg.setText(String.format(String.valueOf(caloriesData.restingCaloriesDailyAvg)));
+ restingCaloriesTotal.setText(String.format(String.valueOf(caloriesData.totalRestingCalories)));
+
+ mBalanceView.setText(caloriesData.getBalanceMessage(getContext(), CALORIES_GOAL));
+ }
+
+ ValueFormatter getCaloriesChartDayValueFormatter(CaloriesPeriodFragment.CaloriesData caloriesData) {
+ return new ValueFormatter() {
+ @Override
+ public String getFormattedValue(float value) {
+ CaloriesPeriodFragment.CaloriesDay day = caloriesData.days.get((int) value);
+ String pattern = TOTAL_DAYS > 7 ? "dd" : "EEE";
+ SimpleDateFormat formatLetterDay = new SimpleDateFormat(pattern, Locale.getDefault());
+ return formatLetterDay.format(new Date(day.day.getTimeInMillis()));
+ }
+ };
+ }
+
+ @Override
+ protected void renderCharts() {
+ caloriesChart.invalidate();
+ }
+
+ protected void setupLegend(Chart> chart) {
+ }
+
+ protected static class CaloriesData extends ChartsData {
+ List days;
+ long activeCaloriesDailyAvg = 0;
+ long totalActiveCalories = 0;
+ long restingCaloriesDailyAvg = 0;
+ long totalRestingCalories = 0;
+ CaloriesDay todayCaloriesDay;
+
+ protected CaloriesData(List days) {
+ this.days = days;
+ int daysCounter = days.size();
+ for (CaloriesDay day : days) {
+ this.totalActiveCalories += day.activeCalories;
+ this.totalRestingCalories += day.restingCalories;
+ }
+ if (daysCounter > 0) {
+ this.activeCaloriesDailyAvg = this.totalActiveCalories / daysCounter;
+ this.restingCaloriesDailyAvg = this.totalRestingCalories / daysCounter;
+ }
+ this.todayCaloriesDay = days.get(days.size() - 1);
+ }
+
+ protected String getBalanceMessage(final Context context, final int targetValue) {
+ if (totalActiveCalories == 0) {
+ return context.getString(R.string.no_data);
+ }
+
+ final long totalBalance = totalActiveCalories - ((long) targetValue * days.size());
+ if (totalBalance > 0) {
+ return context.getString(R.string.calorie_over_goal, Math.abs(totalBalance));
+ } else {
+ return context.getString(R.string.calorie_under_goal, Math.abs(totalBalance));
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/CaloriesFragmentAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/CaloriesFragmentAdapter.java
new file mode 100644
index 0000000000..fbc56f5f08
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/CaloriesFragmentAdapter.java
@@ -0,0 +1,32 @@
+package nodomain.freeyourgadget.gadgetbridge.adapter;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
+import nodomain.freeyourgadget.gadgetbridge.activities.charts.CaloriesDailyFragment;
+import nodomain.freeyourgadget.gadgetbridge.activities.charts.CaloriesPeriodFragment;
+
+public class CaloriesFragmentAdapter extends NestedFragmentAdapter {
+ protected FragmentManager fragmentManager;
+
+ public CaloriesFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
+ super(fragment, childFragmentManager);
+ fragmentManager = childFragmentManager;
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ switch (position) {
+ case 0:
+ return new CaloriesDailyFragment();
+ case 1:
+ return CaloriesPeriodFragment.newInstance(7);
+ case 2:
+ return CaloriesPeriodFragment.newInstance(30);
+ }
+ return new CaloriesDailyFragment();
+ }
+}
diff --git a/app/src/main/res/layout/fragment_calories_period.xml b/app/src/main/res/layout/fragment_calories_period.xml
new file mode 100644
index 0000000000..4a72a6aff9
--- /dev/null
+++ b/app/src/main/res/layout/fragment_calories_period.xml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 01222ffb80..453ec4c3f6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2393,6 +2393,11 @@
Calories
Resting Calories
Metabolic Rate
+ Active AVG
+ Active Total
+ Resting Total
+ Above goal: %1$d kcal
+ Below goal: %1$d kcal
Maximum
Minimum
Slowest Pace