Charts: Allow devices to provide their own charts

This commit is contained in:
José Rebelo
2026-01-16 22:40:59 +00:00
parent 78bd8ca55d
commit eeff5dba54
5 changed files with 190 additions and 163 deletions
@@ -50,12 +50,7 @@ import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
@@ -67,7 +62,6 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
@@ -75,11 +69,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class ActivityChartsActivity extends AbstractGBActivity implements ChartsHost {
private static final Logger LOG = LoggerFactory.getLogger(ActivityChartsActivity.class);
public static final String STATE_START_DATE = "stateStartDate";
public static final String STATE_END_DATE = "stateEndDate";
@@ -261,81 +252,17 @@ public class ActivityChartsActivity extends AbstractGBActivity implements Charts
return new SectionsStateAdapter(this);
}
protected List<String> fillChartsTabsList() {
return fillChartsTabsList(getDevice(), this);
return fillChartsTabsList(getDevice());
}
private static List<String> fillChartsTabsList(final GBDevice device, final Context context) {
final List<String> tabList;
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
final String myTabs = prefs.getString(DeviceSettingsPreferenceConst.PREFS_DEVICE_CHARTS_TABS, null);
if (myTabs == null) {
//make list mutable to be able to remove items later
tabList = new ArrayList<>(Arrays.asList(context.getResources().getStringArray(R.array.pref_charts_tabs_items_default)));
} else {
tabList = new ArrayList<>(Arrays.asList(myTabs.split(",")));
}
private static List<String> fillChartsTabsList(final GBDevice device) {
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!coordinator.supportsActivityTabs(device)) {
tabList.remove("activity");
tabList.remove("activitylist");
}
if (!coordinator.supportsSleepMeasurement(device)) {
tabList.remove("sleep");
}
if (!coordinator.supportsStressMeasurement(device)) {
tabList.remove("stress");
}
if (!coordinator.supportsPai(device)) {
tabList.remove("pai");
}
if (!coordinator.supportsSpo2(device)) {
tabList.remove("spo2");
}
if (!coordinator.supportsStepCounter(device)) {
tabList.remove("stepsweek");
}
if (!coordinator.supportsSpeedzones(device)) {
tabList.remove("speedzones");
}
if (!coordinator.supportsRealtimeData(device)) {
tabList.remove("livestats");
}
if (!coordinator.supportsTemperatureMeasurement(device)) {
tabList.remove("temperature");
}
if (!coordinator.supportsCyclingData(device)) {
tabList.remove("cycling");
}
if (!coordinator.supportsWeightMeasurement(device)) {
tabList.remove("weight");
}
if (!coordinator.supportsHrvMeasurement(device)) {
tabList.remove("hrvstatus");
}
if (!coordinator.supportsHeartRateMeasurement(device)) {
tabList.remove("heartrate");
}
if (!coordinator.supportsBodyEnergy(device)) {
tabList.remove("bodyenergy");
}
if (!coordinator.supportsVO2Max(device)) {
tabList.remove("vo2max");
}
if (!coordinator.supportsTrainingLoad(device)) {
tabList.remove("load");
}
if (!coordinator.supportsActiveCalories(device)) {
tabList.remove("calories");
}
if (!coordinator.supportsRespiratoryRate(device)) {
tabList.remove("respiratoryrate");
}
return tabList;
return coordinator.getChartsProvider().getSupportedCharts(device);
}
public static int getChartsTabIndex(final String tab, final GBDevice device, final Context context) {
final List<String> enabledTabsList = fillChartsTabsList(device, context);
final List<String> enabledTabsList = fillChartsTabsList(device);
return enabledTabsList.indexOf(tab);
}
@@ -466,48 +393,13 @@ public class ActivityChartsActivity extends AbstractGBActivity implements Charts
}
public CharSequence getPageTitle(int position) {
switch (enabledTabsList.get(position)) {
case "activity":
return getString(R.string.activity_sleepchart_activity_and_sleep);
case "activitylist":
return getString(R.string.charts_activity_list);
case "sleep":
return getString(R.string.sleepchart_your_sleep);
case "heartrate":
return getString(R.string.menuitem_hr);
case "hrvstatus":
return getString(R.string.pref_header_hrv_status);
case "bodyenergy":
return getString(R.string.body_energy);
case "vo2max":
return getString(R.string.menuitem_vo2_max);
case "stress":
return getString(R.string.menuitem_stress);
case "pai":
return getString(getDevice().getDeviceCoordinator().getPaiName());
case "stepsweek":
return getString(R.string.steps);
case "speedzones":
return getString(R.string.stats_title);
case "livestats":
return getString(R.string.liveactivity_live_activity);
case "spo2":
return getString(R.string.pref_header_spo2);
case "temperature":
return getString(R.string.menuitem_temperature);
case "cycling":
return getString(R.string.title_cycling);
case "weight":
return getString(R.string.menuitem_weight);
case "calories":
return getString(R.string.calories);
case "respiratoryrate":
return getString(R.string.respiratoryrate);
case "load":
return getString(R.string.pref_header_training_load);
}
return String.format(Locale.getDefault(), "Unknown %d", position);
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
final DeviceChartsProvider chartsProvider = coordinator.getChartsProvider();
return chartsProvider.getChartLabel(
this,
getDevice(),
enabledTabsList.get(position)
);
}
/**
@@ -523,49 +415,12 @@ public class ActivityChartsActivity extends AbstractGBActivity implements Charts
@Override
public Fragment createFragment(int position) {
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
// getItem is called to instantiate the fragment for the given page.
switch (enabledTabsList.get(position)) {
case "activity":
return new ActivitySleepChartFragment();
case "activitylist":
return new ActivityListingChartFragment();
case "sleep":
return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "heartrate":
return HeartRateCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "hrvstatus":
return new HRVStatusFragment();
case "bodyenergy":
return BodyEnergyCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "vo2max":
return new VO2MaxFragment();
case "load":
return new LoadFragment();
case "stress":
return StressCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "pai":
return new PaiChartFragment();
case "stepsweek":
return StepsCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "speedzones":
return new SpeedZonesFragment();
case "livestats":
return new LiveActivityFragment();
case "spo2":
return Spo2CollectionFragment.newInstance(enabledTabsList.size() == 1);
case "temperature":
return coordinator.supportsContinuousTemperature(getDevice())? new TemperatureDailyFragment(): new TemperatureChartFragment();
case "cycling":
return new CyclingChartFragment();
case "weight":
return new WeightChartFragment();
case "calories":
return CaloriesCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "respiratoryrate":
return RespiratoryRateCollectionFragment.newInstance(enabledTabsList.size() == 1);
}
return new ActivityChartsActivity.UnknownFragment();
final DeviceChartsProvider chartsProvider = coordinator.getChartsProvider();
return chartsProvider.getChartFragment(
getDevice(),
enabledTabsList.get(position),
enabledTabsList.size() == 1
);
}
@Override
@@ -0,0 +1,147 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts
import android.content.Context
import androidx.fragment.app.Fragment
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity.UnknownFragment
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import java.util.Locale
object DefaultChartsProvider : DeviceChartsProvider {
override fun getSupportedCharts(device: GBDevice): List<String> {
val coordinator = device.deviceCoordinator
val supportedCharts = mutableListOf<String>()
if (coordinator.supportsCyclingData(device)) {
supportedCharts.add("cycling")
}
if (coordinator.supportsActivityTabs(device)) {
supportedCharts.add("activity")
supportedCharts.add("activitylist")
}
if (coordinator.supportsSleepMeasurement(device)) {
supportedCharts.add("sleep")
}
if (coordinator.supportsHrvMeasurement(device)) {
supportedCharts.add("hrvstatus")
}
if (coordinator.supportsBodyEnergy(device)) {
supportedCharts.add("bodyenergy")
}
if (coordinator.supportsVO2Max(device)) {
supportedCharts.add("vo2max")
}
if (coordinator.supportsTrainingLoad(device)) {
supportedCharts.add("load")
}
if (coordinator.supportsHeartRateMeasurement(device)) {
supportedCharts.add("heartrate")
}
if (coordinator.supportsStepCounter(device)) {
supportedCharts.add("stepsweek")
}
if (coordinator.supportsStressMeasurement(device)) {
supportedCharts.add("stress")
}
if (coordinator.supportsPai(device)) {
supportedCharts.add("pai")
}
if (coordinator.supportsSpeedzones(device)) {
supportedCharts.add("speedzones")
}
if (coordinator.supportsRealtimeData(device)) {
supportedCharts.add("livestats")
}
if (coordinator.supportsSpo2(device)) {
supportedCharts.add("spo2")
}
if (coordinator.supportsTemperatureMeasurement(device)) {
supportedCharts.add("temperature")
}
if (coordinator.supportsWeightMeasurement(device)) {
supportedCharts.add("weight")
}
if (coordinator.supportsActiveCalories(device)) {
supportedCharts.add("calories")
}
if (coordinator.supportsRespiratoryRate(device)) {
supportedCharts.add("respiratoryrate")
}
return supportedCharts
}
override fun getEnabledCharts(device: GBDevice): List<String> {
val prefs = GBApplication.getDevicePrefs(device)
val enabledCharts = prefs.getString(DeviceSettingsPreferenceConst.PREFS_DEVICE_CHARTS_TABS, null)
return if (!enabledCharts.isNullOrBlank()) {
enabledCharts.split(",").intersect(getSupportedCharts(device).toSet()).toList()
} else {
getSupportedCharts(device)
}
}
override fun getChartLabel(
context: Context,
device: GBDevice,
chartName: String
): String {
return when (chartName) {
"activity" -> context.getString(R.string.activity_sleepchart_activity_and_sleep)
"activitylist" -> context.getString(R.string.charts_activity_list)
"sleep" -> context.getString(R.string.sleepchart_your_sleep)
"heartrate" -> context.getString(R.string.menuitem_hr)
"hrvstatus" -> context.getString(R.string.pref_header_hrv_status)
"bodyenergy" -> context.getString(R.string.body_energy)
"vo2max" -> context.getString(R.string.menuitem_vo2_max)
"stress" -> context.getString(R.string.menuitem_stress)
"pai" -> context.getString(device.deviceCoordinator.getPaiName())
"stepsweek" -> context.getString(R.string.steps)
"speedzones" -> context.getString(R.string.stats_title)
"livestats" -> context.getString(R.string.liveactivity_live_activity)
"spo2" -> context.getString(R.string.pref_header_spo2)
"temperature" -> context.getString(R.string.menuitem_temperature)
"cycling" -> context.getString(R.string.title_cycling)
"weight" -> context.getString(R.string.menuitem_weight)
"calories" -> context.getString(R.string.calories)
"respiratoryrate" -> context.getString(R.string.respiratoryrate)
"load" -> context.getString(R.string.pref_header_training_load)
else -> String.format(Locale.getDefault(), "Unknown %s", chartName)
}
}
override fun getChartFragment(
device: GBDevice,
chartName: String,
allowSwipe: Boolean
): Fragment {
return when (chartName) {
"activity" -> ActivitySleepChartFragment()
"activitylist" -> ActivityListingChartFragment()
"sleep" -> SleepCollectionFragment.newInstance(allowSwipe)
"heartrate" -> HeartRateCollectionFragment.newInstance(allowSwipe)
"hrvstatus" -> HRVStatusFragment()
"bodyenergy" -> BodyEnergyCollectionFragment.newInstance(allowSwipe)
"vo2max" -> VO2MaxFragment()
"load" -> LoadFragment()
"stress" -> StressCollectionFragment.newInstance(allowSwipe)
"pai" -> PaiChartFragment()
"stepsweek" -> StepsCollectionFragment.newInstance(allowSwipe)
"speedzones" -> SpeedZonesFragment()
"livestats" -> LiveActivityFragment()
"spo2" -> Spo2CollectionFragment.newInstance(allowSwipe)
"temperature" -> {
if (device.deviceCoordinator.supportsContinuousTemperature(device))
TemperatureDailyFragment() else TemperatureChartFragment()
}
"cycling" -> CyclingChartFragment()
"weight" -> WeightChartFragment()
"calories" -> CaloriesCollectionFragment.newInstance(allowSwipe)
"respiratoryrate" -> RespiratoryRateCollectionFragment.newInstance(allowSwipe)
else -> UnknownFragment()
}
}
}
@@ -0,0 +1,15 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts
import android.content.Context
import androidx.fragment.app.Fragment
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
interface DeviceChartsProvider {
fun getSupportedCharts(device: GBDevice): List<String>
fun getEnabledCharts(device: GBDevice): List<String>
fun getChartLabel(context: Context, device: GBDevice, chartName: String): String
fun getChartFragment(device: GBDevice, chartName: String, allowSwipe: Boolean): Fragment
}
@@ -58,6 +58,8 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DefaultChartsProvider;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DeviceChartsProvider;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
@@ -701,6 +703,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false;
}
@Override
public DeviceChartsProvider getChartsProvider() {
return DefaultChartsProvider.INSTANCE;
}
@Override
public boolean supportsTemperatureMeasurement(@NonNull final GBDevice device) {
return false;
@@ -37,6 +37,7 @@ import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DeviceChartsProvider;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
@@ -288,6 +289,8 @@ public interface DeviceCoordinator {
boolean supportsWorkoutLoad(@NonNull GBDevice device);
boolean supportsGlucoseMeasurement(@NonNull GBDevice device);
DeviceChartsProvider getChartsProvider();
/**
* Returns true if measurement and fetching of body temperature is supported by the device
* (with this coordinator).