mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-27 02:55:50 +01:00
VO2Max: initalize activity
This commit is contained in:
parent
e2be851097
commit
d440ec1e36
@ -127,7 +127,7 @@ public class GBApplication extends Application {
|
|||||||
private static SharedPreferences sharedPrefs;
|
private static SharedPreferences sharedPrefs;
|
||||||
private static final String PREFS_VERSION = "shared_preferences_version";
|
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
|
//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 = 39;
|
private static final int CURRENT_PREFS_VERSION = 40;
|
||||||
|
|
||||||
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
|
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
|
||||||
private static GBPrefs prefs;
|
private static GBPrefs prefs;
|
||||||
@ -1793,6 +1793,36 @@ public class GBApplication extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 40) {
|
||||||
|
// Add the new VO2Max tab to all devices
|
||||||
|
try (DBHandler db = acquireDB()) {
|
||||||
|
final DaoSession daoSession = db.getDaoSession();
|
||||||
|
final List<Device> 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 + ",vo2max";
|
||||||
|
} else {
|
||||||
|
newPrefValue = "vo2max";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 40", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
||||||
editor.apply();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
@ -186,7 +187,7 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
|||||||
|
|
||||||
if (samples.isEmpty()) {
|
if (samples.isEmpty()) {
|
||||||
lineData = new LineData();
|
lineData = new LineData();
|
||||||
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
|
||||||
return new DefaultChartsData<>(lineData, xValueFormatter);
|
return new DefaultChartsData<>(lineData, xValueFormatter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +199,7 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
|||||||
for (int i = 0; i < 6; i++) {
|
for (int i = 0; i < 6; i++) {
|
||||||
entries.add(new ArrayList<>());
|
entries.add(new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hr = supportsHeartrate(gbDevice);
|
boolean hr = supportsHeartrate(gbDevice);
|
||||||
List<Entry> heartrateEntries = hr ? new ArrayList<Entry>(numEntries) : null;
|
List<Entry> heartrateEntries = hr ? new ArrayList<Entry>(numEntries) : null;
|
||||||
|
|
||||||
@ -270,7 +271,7 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
|||||||
|
|
||||||
lineData = new LineData(lineDataSets);
|
lineData = new LineData(lineDataSets);
|
||||||
|
|
||||||
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
|
||||||
return new DefaultChartsData<>(lineData, xValueFormatter);
|
return new DefaultChartsData<>(lineData, xValueFormatter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +130,9 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
if (!coordinator.supportsBodyEnergy()) {
|
if (!coordinator.supportsBodyEnergy()) {
|
||||||
tabList.remove("bodyenergy");
|
tabList.remove("bodyenergy");
|
||||||
}
|
}
|
||||||
|
if (!coordinator.supportsVO2Max()) {
|
||||||
|
tabList.remove("vo2max");
|
||||||
|
}
|
||||||
return tabList;
|
return tabList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +167,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
return new HRVStatusFragment();
|
return new HRVStatusFragment();
|
||||||
case "bodyenergy":
|
case "bodyenergy":
|
||||||
return new BodyEnergyFragment();
|
return new BodyEnergyFragment();
|
||||||
|
case "vo2max":
|
||||||
|
return new VO2MaxFragment();
|
||||||
case "stress":
|
case "stress":
|
||||||
return new StressChartFragment();
|
return new StressChartFragment();
|
||||||
case "pai":
|
case "pai":
|
||||||
@ -207,6 +212,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
return getString(R.string.pref_header_hrv_status);
|
return getString(R.string.pref_header_hrv_status);
|
||||||
case "bodyenergy":
|
case "bodyenergy":
|
||||||
return getString(R.string.body_energy);
|
return getString(R.string.body_energy);
|
||||||
|
case "vo2max":
|
||||||
|
return getString(R.string.vo2max);
|
||||||
case "stress":
|
case "stress":
|
||||||
return getString(R.string.menuitem_stress);
|
return getString(R.string.menuitem_stress);
|
||||||
case "pai":
|
case "pai":
|
||||||
|
@ -47,6 +47,7 @@ import com.github.mikephil.charting.utils.Utils;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@ -465,7 +466,7 @@ public class LiveActivityFragment extends AbstractActivityChartFragment<ChartsDa
|
|||||||
x.setDrawGridLines(false);
|
x.setDrawGridLines(false);
|
||||||
x.setEnabled(true);
|
x.setEnabled(true);
|
||||||
x.setTextColor(CHART_TEXT_COLOR);
|
x.setTextColor(CHART_TEXT_COLOR);
|
||||||
x.setValueFormatter(new SampleXLabelFormatter(tsTranslation));
|
x.setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
|
||||||
x.setDrawLimitLinesBehindData(true);
|
x.setDrawLimitLinesBehindData(true);
|
||||||
|
|
||||||
YAxis y = chart.getAxisLeft();
|
YAxis y = chart.getAxisLeft();
|
||||||
|
@ -28,12 +28,13 @@ import java.util.GregorianCalendar;
|
|||||||
class SampleXLabelFormatter extends ValueFormatter {
|
class SampleXLabelFormatter extends ValueFormatter {
|
||||||
private final TimestampTranslation tsTranslation;
|
private final TimestampTranslation tsTranslation;
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("SimpleDateFormat")
|
||||||
private final SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
|
private final SimpleDateFormat annotationDateFormat;
|
||||||
// SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
// SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
||||||
private final Calendar cal = GregorianCalendar.getInstance();
|
private final Calendar cal = GregorianCalendar.getInstance();
|
||||||
|
|
||||||
public SampleXLabelFormatter(final TimestampTranslation tsTranslation) {
|
public SampleXLabelFormatter(final TimestampTranslation tsTranslation, String simpleDateFormatPattern) {
|
||||||
this.tsTranslation = tsTranslation;
|
this.tsTranslation = tsTranslation;
|
||||||
|
this.annotationDateFormat = new SimpleDateFormat(simpleDateFormatPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this does not work. Cannot use precomputed labels
|
// TODO: this does not work. Cannot use precomputed labels
|
||||||
|
@ -41,6 +41,7 @@ import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@ -256,7 +257,7 @@ public class Spo2ChartFragment extends AbstractChartFragment<Spo2ChartFragment.S
|
|||||||
lineDataSets.add(createDataSet(lineEntries));
|
lineDataSets.add(createDataSet(lineEntries));
|
||||||
|
|
||||||
final LineData lineData = new LineData(lineDataSets);
|
final LineData lineData = new LineData(lineDataSets);
|
||||||
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
|
||||||
final DefaultChartsData<LineData> chartsData = new DefaultChartsData<>(lineData, xValueFormatter);
|
final DefaultChartsData<LineData> chartsData = new DefaultChartsData<>(lineData, xValueFormatter);
|
||||||
return new Spo2ChartsData(chartsData, Math.round((float) averageSum / averageNumSamples));
|
return new Spo2ChartsData(chartsData, Math.round((float) averageSum / averageNumSamples));
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsDa
|
|||||||
lineEntries.add(new Entry(tsTranslation.shorten(sample.getTimestamp()), sum));
|
lineEntries.add(new Entry(tsTranslation.shorten(sample.getTimestamp()), sum));
|
||||||
}
|
}
|
||||||
|
|
||||||
stepsChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation));
|
stepsChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
|
||||||
|
|
||||||
if (sum < STEPS_GOAL) {
|
if (sum < STEPS_GOAL) {
|
||||||
stepsChart.getAxisLeft().setAxisMaximum(STEPS_GOAL);
|
stepsChart.getAxisLeft().setAxisMaximum(STEPS_GOAL);
|
||||||
|
@ -484,7 +484,7 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
final PieData pieData = new PieData(pieDataSet);
|
final PieData pieData = new PieData(pieDataSet);
|
||||||
|
|
||||||
final LineData lineData = new LineData(lineDataSets);
|
final LineData lineData = new LineData(lineDataSets);
|
||||||
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
|
||||||
final DefaultChartsData<LineData> chartsData = new DefaultChartsData<>(lineData, xValueFormatter);
|
final DefaultChartsData<LineData> chartsData = new DefaultChartsData<>(lineData, xValueFormatter);
|
||||||
return new StressChartsData(pieData, chartsData, Math.round((float) averageSum / averageNumSamples), stressZoneTimes);
|
return new StressChartsData(pieData, chartsData, Math.round((float) averageSum / averageNumSamples), stressZoneTimes);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,351 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.GridLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.github.mikephil.charting.charts.Chart;
|
||||||
|
import com.github.mikephil.charting.charts.LineChart;
|
||||||
|
import com.github.mikephil.charting.components.XAxis;
|
||||||
|
import com.github.mikephil.charting.components.YAxis;
|
||||||
|
import com.github.mikephil.charting.data.Entry;
|
||||||
|
import com.github.mikephil.charting.data.LineData;
|
||||||
|
import com.github.mikephil.charting.data.LineDataSet;
|
||||||
|
import com.github.mikephil.charting.formatter.ValueFormatter;
|
||||||
|
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
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.DeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.Vo2MaxSampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
|
||||||
|
|
||||||
|
|
||||||
|
public class VO2MaxFragment extends AbstractChartFragment<VO2MaxFragment.VO2MaxData> {
|
||||||
|
protected static final Logger LOG = LoggerFactory.getLogger(VO2MaxFragment.class);
|
||||||
|
|
||||||
|
private TextView mDateView;
|
||||||
|
private TextView vo2MaxGeneralValue;
|
||||||
|
private TextView vo2MaxRunningValue;
|
||||||
|
private TextView vo2MaxCyclingValue;
|
||||||
|
private ImageView vo2MaxGeneralGauge;
|
||||||
|
private ImageView vo2MaxRunningGauge;
|
||||||
|
private ImageView vo2MaxCyclingGauge;
|
||||||
|
protected GaugeDrawer gaugeDrawer = new GaugeDrawer();
|
||||||
|
private LineChart vo2MaxChart;
|
||||||
|
private RelativeLayout vo2maxCyclingWrapper;
|
||||||
|
private RelativeLayout vo2maxRunningWrapper;
|
||||||
|
private RelativeLayout vo2maxGeneralWrapper;
|
||||||
|
private GridLayout tilesGridWrapper;
|
||||||
|
private int tsFrom;
|
||||||
|
GBDevice device;
|
||||||
|
|
||||||
|
protected int CHART_TEXT_COLOR;
|
||||||
|
protected int LEGEND_TEXT_COLOR;
|
||||||
|
protected int TEXT_COLOR;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
View rootView = inflater.inflate(R.layout.fragment_vo2max, container, false);
|
||||||
|
|
||||||
|
mDateView = rootView.findViewById(R.id.vo2max_date_view);
|
||||||
|
vo2MaxGeneralValue = rootView.findViewById(R.id.vo2max_general_gauge_value);
|
||||||
|
vo2MaxRunningValue = rootView.findViewById(R.id.vo2max_running_gauge_value);
|
||||||
|
vo2MaxCyclingValue = rootView.findViewById(R.id.vo2max_cycling_gauge_value);
|
||||||
|
vo2MaxGeneralGauge = rootView.findViewById(R.id.vo2max_general_gauge);
|
||||||
|
vo2MaxRunningGauge = rootView.findViewById(R.id.vo2max_running_gauge);
|
||||||
|
vo2MaxCyclingGauge = rootView.findViewById(R.id.vo2max_cycling_gauge);
|
||||||
|
vo2MaxChart = rootView.findViewById(R.id.vo2max_chart);
|
||||||
|
vo2maxCyclingWrapper = rootView.findViewById(R.id.vo2max_cycling_card_layout);
|
||||||
|
vo2maxGeneralWrapper = rootView.findViewById(R.id.vo2max_general_card_layout);
|
||||||
|
vo2maxRunningWrapper = rootView.findViewById(R.id.vo2max_running_card_layout);
|
||||||
|
tilesGridWrapper = rootView.findViewById(R.id.tiles_grid_wrapper);
|
||||||
|
device = getChartsHost().getDevice();
|
||||||
|
if (!supportsVO2MaxCycling(device)) {
|
||||||
|
tilesGridWrapper.removeView(vo2maxCyclingWrapper);
|
||||||
|
}
|
||||||
|
if (!supportsVO2MaxRunning(device)) {
|
||||||
|
tilesGridWrapper.removeView(vo2maxRunningWrapper);
|
||||||
|
}
|
||||||
|
if (!supportsVO2MaxGeneral(device)) {
|
||||||
|
tilesGridWrapper.removeView(vo2maxGeneralWrapper);
|
||||||
|
}
|
||||||
|
setupVO2MaxChart();
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
|
||||||
|
return rootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsVO2MaxCycling(GBDevice device) {
|
||||||
|
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||||
|
return coordinator != null && coordinator.supportsVO2MaxCycling();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsVO2MaxGeneral(GBDevice device) {
|
||||||
|
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||||
|
return coordinator != null && coordinator.supportsVO2MaxGeneral();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsVO2MaxRunning(GBDevice device) {
|
||||||
|
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||||
|
return coordinator != null && coordinator.supportsVO2MaxRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTitle() {
|
||||||
|
return getString(R.string.vo2max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
TEXT_COLOR = GBApplication.getTextColor(requireContext());
|
||||||
|
LEGEND_TEXT_COLOR = GBApplication.getTextColor(requireContext());
|
||||||
|
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected VO2MaxData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||||
|
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||||
|
mDateView.setText(formattedDate);
|
||||||
|
List<VO2MaxRecord> records = new ArrayList<>();
|
||||||
|
int tsEnd = getTSEnd();
|
||||||
|
Calendar day = Calendar.getInstance();
|
||||||
|
day.setTimeInMillis(tsEnd * 1000L); //we need today initially, which is the end of the time range
|
||||||
|
day.set(Calendar.HOUR_OF_DAY, 0); //and we set time for the start and end of the same day
|
||||||
|
day.set(Calendar.MINUTE, 0);
|
||||||
|
day.set(Calendar.SECOND, 0);
|
||||||
|
day.add(Calendar.DAY_OF_YEAR, -30);
|
||||||
|
tsFrom = (int) (day.getTimeInMillis() / 1000);
|
||||||
|
List<? extends Vo2MaxSample> samples = getAllSamples(db, device, tsFrom, tsEnd);
|
||||||
|
for (Vo2MaxSample sample : samples) {
|
||||||
|
records.add(new VO2MaxRecord(sample.getTimestamp() / 1000, sample.getValue(), sample.getType()));
|
||||||
|
}
|
||||||
|
Map<Vo2MaxSample.Type, VO2MaxRecord> latestValues = new HashMap<>();
|
||||||
|
for (Vo2MaxSample.Type type : Vo2MaxSample.Type.values()) {
|
||||||
|
Vo2MaxSample sample = getLatestVo2MaxSample(db, device, type);
|
||||||
|
if (sample != null) {
|
||||||
|
latestValues.put(type, new VO2MaxRecord(sample.getTimestamp() / 1000, sample.getValue(), type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new VO2MaxData(records, latestValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateChartsnUIThread(VO2MaxData vo2MaxData) {
|
||||||
|
TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||||
|
List<Entry> runningEntries = new ArrayList<>();
|
||||||
|
List<Entry> cyclingEntries = new ArrayList<>();
|
||||||
|
List<Entry> generalEntries = new ArrayList<>();
|
||||||
|
vo2MaxData.records.forEach((record) -> {
|
||||||
|
float nd = (float) (record.timestamp - this.tsFrom) / (60 * 60 * 24);
|
||||||
|
switch (record.type) {
|
||||||
|
case RUNNING:
|
||||||
|
runningEntries.add(new Entry(nd, record.value));
|
||||||
|
break;
|
||||||
|
case CYCLING:
|
||||||
|
cyclingEntries.add(new Entry(nd, record.value));
|
||||||
|
break;
|
||||||
|
case GENERAL:
|
||||||
|
generalEntries.add(new Entry(nd, record.value));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final int[] colors = {
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_poor_color),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_fair_color),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_good_color),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_excellent_color),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_superior_color),
|
||||||
|
};
|
||||||
|
final float[] segments = {
|
||||||
|
0.20F,
|
||||||
|
0.20F,
|
||||||
|
0.20F,
|
||||||
|
0.20F,
|
||||||
|
0.20F,
|
||||||
|
};
|
||||||
|
float[] vo2MaxRanges = {
|
||||||
|
55.4F,
|
||||||
|
51.1F,
|
||||||
|
45.4F,
|
||||||
|
41.7F,
|
||||||
|
0.0F,
|
||||||
|
};
|
||||||
|
|
||||||
|
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||||
|
if (supportsVO2MaxGeneral(device)) {
|
||||||
|
VO2MaxRecord latestGeneralRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.GENERAL);
|
||||||
|
float generalVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestGeneralRecord != null ? latestGeneralRecord.value : 0);
|
||||||
|
gaugeDrawer.drawSegmentedGauge(vo2MaxGeneralGauge, colors, segments, generalVO2MaxValue, false, true);
|
||||||
|
vo2MaxGeneralValue.setText(String.valueOf(latestGeneralRecord != null ? Math.round(latestGeneralRecord.value) : "-"));
|
||||||
|
lineDataSets.add(createDataSet(generalEntries, getResources().getColor(R.color.vo2max_general_char_line_color), getString(R.string.vo2_max_general)));
|
||||||
|
}
|
||||||
|
if (supportsVO2MaxRunning(device)) {
|
||||||
|
VO2MaxRecord latestRunningRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.RUNNING);
|
||||||
|
float runningVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestRunningRecord != null ? latestRunningRecord.value : 0);
|
||||||
|
vo2MaxRunningValue.setText(String.valueOf(latestRunningRecord != null ? Math.round(latestRunningRecord.value) : "-"));
|
||||||
|
gaugeDrawer.drawSegmentedGauge(vo2MaxRunningGauge, colors, segments, runningVO2MaxValue, false, true);
|
||||||
|
lineDataSets.add(createDataSet(runningEntries, getResources().getColor(R.color.vo2max_running_char_line_color), getString(R.string.vo2_max_running)));
|
||||||
|
}
|
||||||
|
if (supportsVO2MaxCycling(device)) {
|
||||||
|
VO2MaxRecord latestCyclingRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.CYCLING);
|
||||||
|
float cyclingVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestCyclingRecord != null ? latestCyclingRecord.value : 0);
|
||||||
|
gaugeDrawer.drawSegmentedGauge(vo2MaxCyclingGauge, colors, segments, cyclingVO2MaxValue, false, true);
|
||||||
|
vo2MaxCyclingValue.setText(String.valueOf(latestCyclingRecord != null ? Math.round(latestCyclingRecord.value) : "-"));
|
||||||
|
lineDataSets.add(createDataSet(cyclingEntries, getResources().getColor(R.color.vo2max_cycling_char_line_color), getString(R.string.vo2_max_cycling)));
|
||||||
|
}
|
||||||
|
final LineData lineData = new LineData(lineDataSets);
|
||||||
|
vo2MaxChart.getXAxis().setValueFormatter(getVO2MaxLineChartValueFormatter());
|
||||||
|
vo2MaxChart.setData(lineData);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueFormatter getVO2MaxLineChartValueFormatter() {
|
||||||
|
return new ValueFormatter() {
|
||||||
|
@Override
|
||||||
|
public String getFormattedValue(float value) {
|
||||||
|
Calendar day = Calendar.getInstance();
|
||||||
|
day.setTimeInMillis(tsFrom * 1000L);
|
||||||
|
day.add(Calendar.DAY_OF_YEAR, (int) value);
|
||||||
|
return new SimpleDateFormat("dd/MM").format(day.getTime());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private float calculateVO2maxGaugeValue(float[] vo2MaxRanges, float vo2MaxValue) {
|
||||||
|
float value = -1;
|
||||||
|
for (int i = 0; i < vo2MaxRanges.length; i++) {
|
||||||
|
if (vo2MaxValue - vo2MaxRanges[i] > 0) {
|
||||||
|
float rangeValue = i - 1 >= 0 ? vo2MaxRanges[i-1] : 60F;
|
||||||
|
float rangeDiff = rangeValue - vo2MaxRanges[i];
|
||||||
|
float valueDiff = vo2MaxValue - vo2MaxRanges[i];
|
||||||
|
float multiplayer = valueDiff / rangeDiff;
|
||||||
|
value = (4 - i) * 0.2F + 0.2F * (multiplayer > 1 ? 1 : multiplayer) ;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected LineDataSet createDataSet(final List<Entry> values, int color, String label) {
|
||||||
|
final LineDataSet lineDataSet = new LineDataSet(values, label);
|
||||||
|
lineDataSet.setColor(color);
|
||||||
|
lineDataSet.setDrawCircles(false);
|
||||||
|
lineDataSet.setLineWidth(2f);
|
||||||
|
lineDataSet.setFillAlpha(255);
|
||||||
|
lineDataSet.setCircleRadius(5f);
|
||||||
|
lineDataSet.setDrawCircles(true);
|
||||||
|
lineDataSet.setDrawCircleHole(true);
|
||||||
|
lineDataSet.setCircleColor(color);
|
||||||
|
lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
|
||||||
|
lineDataSet.setDrawValues(true);
|
||||||
|
lineDataSet.setValueTextSize(10f);
|
||||||
|
lineDataSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||||
|
lineDataSet.setValueFormatter(new ValueFormatter() {
|
||||||
|
@Override
|
||||||
|
public String getFormattedValue(float value) {
|
||||||
|
return String.format(Locale.ROOT, "%d", Math.round(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lineDataSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderCharts() {
|
||||||
|
vo2MaxChart.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<? extends Vo2MaxSample> getAllSamples(final DBHandler db, final GBDevice device, int tsFrom, int tsTo) {
|
||||||
|
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||||
|
final TimeSampleProvider<? extends Vo2MaxSample> sampleProvider = coordinator.getVo2MaxSampleProvider(device, db.getDaoSession());
|
||||||
|
return sampleProvider.getAllSamples(tsFrom * 1000L, tsTo * 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vo2MaxSample getLatestVo2MaxSample(final DBHandler db, final GBDevice device, Vo2MaxSample.Type type) {
|
||||||
|
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||||
|
final Vo2MaxSampleProvider sampleProvider = (Vo2MaxSampleProvider) coordinator.getVo2MaxSampleProvider(device, db.getDaoSession());
|
||||||
|
return sampleProvider.getLatestSample(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupVO2MaxChart() {
|
||||||
|
final XAxis xAxisBottom = vo2MaxChart.getXAxis();
|
||||||
|
xAxisBottom.setPosition(XAxis.XAxisPosition.BOTTOM);
|
||||||
|
xAxisBottom.setDrawLabels(true);
|
||||||
|
xAxisBottom.setDrawGridLines(false);
|
||||||
|
xAxisBottom.setEnabled(true);
|
||||||
|
xAxisBottom.setDrawLimitLinesBehindData(true);
|
||||||
|
xAxisBottom.setTextColor(CHART_TEXT_COLOR);
|
||||||
|
xAxisBottom.setAxisMinimum(0f);
|
||||||
|
xAxisBottom.setAxisMaximum(31f);
|
||||||
|
xAxisBottom.setGranularity(1f);
|
||||||
|
xAxisBottom.setGranularityEnabled(true);
|
||||||
|
|
||||||
|
final YAxis yAxisLeft = vo2MaxChart.getAxisLeft();
|
||||||
|
yAxisLeft.setDrawGridLines(true);
|
||||||
|
yAxisLeft.setAxisMaximum(100);
|
||||||
|
yAxisLeft.setAxisMinimum(0);
|
||||||
|
yAxisLeft.setDrawTopYLabelEntry(true);
|
||||||
|
yAxisLeft.setEnabled(true);
|
||||||
|
yAxisLeft.setTextColor(CHART_TEXT_COLOR);
|
||||||
|
|
||||||
|
final YAxis yAxisRight = vo2MaxChart.getAxisRight();
|
||||||
|
yAxisRight.setEnabled(true);
|
||||||
|
yAxisRight.setDrawLabels(false);
|
||||||
|
yAxisRight.setDrawGridLines(false);
|
||||||
|
yAxisRight.setDrawAxisLine(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setupLegend(Chart<?> chart) {}
|
||||||
|
|
||||||
|
protected static class VO2MaxRecord {
|
||||||
|
float value;
|
||||||
|
long timestamp;
|
||||||
|
Vo2MaxSample.Type type;
|
||||||
|
|
||||||
|
protected VO2MaxRecord(long timestamp, float value, Vo2MaxSample.Type type) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.value = value;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class VO2MaxData extends ChartsData {
|
||||||
|
private final List<? extends VO2MaxRecord> records;
|
||||||
|
private final Map<Vo2MaxSample.Type, VO2MaxRecord> latestValues;
|
||||||
|
|
||||||
|
protected VO2MaxData(List<? extends VO2MaxRecord> records, Map<Vo2MaxSample.Type, VO2MaxRecord> latestValues) {
|
||||||
|
this.records = records;
|
||||||
|
this.latestValues = latestValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public VO2MaxRecord getLatestValue(Vo2MaxSample.Type type) {
|
||||||
|
return this.latestValues.get(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -38,7 +38,6 @@ import androidx.annotation.StringRes;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
|
||||||
@ -47,6 +46,7 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
|||||||
|
|
||||||
private TextView gaugeValue;
|
private TextView gaugeValue;
|
||||||
private ImageView gaugeBar;
|
private ImageView gaugeBar;
|
||||||
|
protected GaugeDrawer gaugeDrawer;
|
||||||
|
|
||||||
private final int label;
|
private final int label;
|
||||||
private final String targetActivityTab;
|
private final String targetActivityTab;
|
||||||
@ -66,6 +66,7 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
|||||||
|
|
||||||
gaugeValue = fragmentView.findViewById(R.id.gauge_value);
|
gaugeValue = fragmentView.findViewById(R.id.gauge_value);
|
||||||
gaugeBar = fragmentView.findViewById(R.id.gauge_bar);
|
gaugeBar = fragmentView.findViewById(R.id.gauge_bar);
|
||||||
|
gaugeDrawer = new GaugeDrawer();
|
||||||
final TextView gaugeLabel = fragmentView.findViewById(R.id.gauge_label);
|
final TextView gaugeLabel = fragmentView.findViewById(R.id.gauge_label);
|
||||||
gaugeLabel.setText(label);
|
gaugeLabel.setText(label);
|
||||||
|
|
||||||
@ -143,50 +144,7 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
|||||||
*/
|
*/
|
||||||
protected void drawSimpleGauge(final int color,
|
protected void drawSimpleGauge(final int color,
|
||||||
final float value) {
|
final float value) {
|
||||||
|
gaugeDrawer.drawSimpleGauge(gaugeBar, color, value);
|
||||||
final int width = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
150,
|
|
||||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Draw gauge
|
|
||||||
gaugeBar.setImageBitmap(drawSimpleGaugeInternal(
|
|
||||||
width,
|
|
||||||
Math.round(width * 0.075f),
|
|
||||||
color,
|
|
||||||
value
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param width Bitmap width in pixels
|
|
||||||
* @param barWidth Gauge bar width in pixels
|
|
||||||
* @param filledColor Color of the filled part of the gauge
|
|
||||||
* @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled
|
|
||||||
* @return Bitmap containing the gauge
|
|
||||||
*/
|
|
||||||
private Bitmap drawSimpleGaugeInternal(final int width, final int barWidth, @ColorInt final int filledColor, final float filledFactor) {
|
|
||||||
final int height = width / 2;
|
|
||||||
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
|
||||||
|
|
||||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
||||||
final Canvas canvas = new Canvas(bitmap);
|
|
||||||
final Paint paint = new Paint();
|
|
||||||
paint.setAntiAlias(true);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
|
||||||
paint.setStrokeWidth(barWidth * 0.75f);
|
|
||||||
paint.setColor(color_unknown);
|
|
||||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint);
|
|
||||||
|
|
||||||
if (filledFactor >= 0) {
|
|
||||||
paint.setStrokeWidth(barWidth);
|
|
||||||
paint.setColor(filledColor);
|
|
||||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bitmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -203,116 +161,6 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
|||||||
final float value,
|
final float value,
|
||||||
final boolean fadeOutsideDot,
|
final boolean fadeOutsideDot,
|
||||||
final boolean gapBetweenSegments) {
|
final boolean gapBetweenSegments) {
|
||||||
if (colors.length != segments.length) {
|
gaugeDrawer.drawSegmentedGauge(gaugeBar, colors, segments, value, fadeOutsideDot, gapBetweenSegments);
|
||||||
LOG.error("Colors length {} differs from segments length {}", colors.length, segments.length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int width = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
150,
|
|
||||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
|
||||||
);
|
|
||||||
|
|
||||||
final int barWidth = Math.round(width * 0.075f);
|
|
||||||
|
|
||||||
final int height = width / 2;
|
|
||||||
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
|
||||||
|
|
||||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
||||||
final Canvas canvas = new Canvas(bitmap);
|
|
||||||
final Paint paint = new Paint();
|
|
||||||
paint.setAntiAlias(true);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
paint.setStrokeCap(Paint.Cap.BUTT);
|
|
||||||
paint.setStrokeWidth(barWidth);
|
|
||||||
|
|
||||||
final double cornersGapRadians = Math.asin((width * 0.055f) / (double) height);
|
|
||||||
final double cornersGapFactor = cornersGapRadians / Math.PI;
|
|
||||||
|
|
||||||
int dotColor = 0;
|
|
||||||
float angleSum = 0;
|
|
||||||
for (int i = 0; i < segments.length; i++) {
|
|
||||||
if (segments[i] == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
paint.setColor(colors[i]);
|
|
||||||
paint.setStrokeWidth(barWidth);
|
|
||||||
|
|
||||||
if (value < 0 || (value >= angleSum && value <= angleSum + segments[i])) {
|
|
||||||
dotColor = colors[i];
|
|
||||||
} else {
|
|
||||||
if (fadeOutsideDot) {
|
|
||||||
paint.setColor(colors[i] - 0xB0000000);
|
|
||||||
} else {
|
|
||||||
paint.setStrokeWidth(barWidth * 0.75f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float startAngleDegrees = 180 + angleSum * 180;
|
|
||||||
float sweepAngleDegrees = segments[i] * 180;
|
|
||||||
|
|
||||||
if (value >= 0) {
|
|
||||||
// Do not draw to the end if it will be overlapped by the dot
|
|
||||||
if (i == 0 && value <= cornersGapFactor) {
|
|
||||||
startAngleDegrees += (float) Math.toDegrees(cornersGapRadians);
|
|
||||||
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
|
||||||
} else if (i == segments.length - 1 && value >= 1 - cornersGapFactor) {
|
|
||||||
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gapBetweenSegments) {
|
|
||||||
if (i + 1 < segments.length) {
|
|
||||||
sweepAngleDegrees -= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawArc(
|
|
||||||
barMargin,
|
|
||||||
barMargin,
|
|
||||||
width - barMargin,
|
|
||||||
width - barMargin,
|
|
||||||
startAngleDegrees,
|
|
||||||
sweepAngleDegrees,
|
|
||||||
false,
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
angleSum += segments[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value >= 0) {
|
|
||||||
// Prevent the dot from going outside the widget in the extremities
|
|
||||||
final float angleRadians = (float) normalize(value, 0, 1, cornersGapRadians, Math.toRadians(180) - cornersGapRadians);
|
|
||||||
|
|
||||||
paint.setColor(Color.TRANSPARENT);
|
|
||||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
|
||||||
|
|
||||||
// In the corners the circle is slightly offset, so adjust it slightly
|
|
||||||
final float widthAdjustment = width * 0.04f * (float) normalize(Math.abs(value - 0.5d), 0, 0.5d);
|
|
||||||
|
|
||||||
final float x = ((width - (barWidth / 2f) - widthAdjustment) / 2f) * (float) Math.cos(angleRadians);
|
|
||||||
final float y = (height - (barWidth / 2f)) * (float) Math.sin(angleRadians);
|
|
||||||
|
|
||||||
// Draw hole
|
|
||||||
paint.setStyle(Paint.Style.FILL);
|
|
||||||
canvas.drawCircle((width / 2f) - x, height - y, barMargin * 1.6f, paint);
|
|
||||||
|
|
||||||
// Draw dot
|
|
||||||
paint.setColor(dotColor);
|
|
||||||
paint.setXfermode(null);
|
|
||||||
canvas.drawCircle((width / 2f) - x, height - y, barMargin, paint);
|
|
||||||
}
|
|
||||||
|
|
||||||
gaugeBar.setImageBitmap(bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static double normalize(final double value, final double min, final double max) {
|
|
||||||
return normalize(value, min, max, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static double normalize(final double value, final double minSource, final double maxSource, final double minTarget, final double maxTarget) {
|
|
||||||
return ((value - minSource) * (maxTarget - minTarget)) / (maxSource - minSource) + minTarget;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,13 +111,13 @@ public class DashboardHrvWidget extends AbstractGaugeWidget {
|
|||||||
valueText = getString(R.string.hrv_status_unit, hrvData.weeklyAverage);
|
valueText = getString(R.string.hrv_status_unit, hrvData.weeklyAverage);
|
||||||
|
|
||||||
if (hrvData.weeklyAverage < hrvData.baselineLowUpper) {
|
if (hrvData.weeklyAverage < hrvData.baselineLowUpper) {
|
||||||
value = 0.125f * (float) normalize(hrvData.weeklyAverage, 0f, hrvData.baselineLowUpper);
|
value = 0.125f * (float) GaugeDrawer.normalize(hrvData.weeklyAverage, 0f, hrvData.baselineLowUpper);
|
||||||
} else if (hrvData.weeklyAverage < hrvData.baselineBalancedLower) {
|
} else if (hrvData.weeklyAverage < hrvData.baselineBalancedLower) {
|
||||||
value = 0.125f + 0.125f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineLowUpper, hrvData.baselineBalancedLower);
|
value = 0.125f + 0.125f * (float) GaugeDrawer.normalize((float) hrvData.weeklyAverage, hrvData.baselineLowUpper, hrvData.baselineBalancedLower);
|
||||||
} else if (hrvData.weeklyAverage < hrvData.baselineBalancedUpper) {
|
} else if (hrvData.weeklyAverage < hrvData.baselineBalancedUpper) {
|
||||||
value = 0.125f + 0.125f + 0.5f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedLower, hrvData.baselineBalancedUpper);
|
value = 0.125f + 0.125f + 0.5f * (float) GaugeDrawer.normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedLower, hrvData.baselineBalancedUpper);
|
||||||
} else {
|
} else {
|
||||||
value = 0.125f + 0.125f + 0.5f + 0.125f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedUpper, 2 * hrvData.baselineBalancedUpper);
|
value = 0.125f + 0.125f + 0.5f + 0.125f * (float) GaugeDrawer.normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedUpper, 2 * hrvData.baselineBalancedUpper);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = -1;
|
value = -1;
|
||||||
|
@ -0,0 +1,205 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.PorterDuffXfermode;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
|
||||||
|
public class GaugeDrawer {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(GaugeDrawer.class);
|
||||||
|
protected @ColorInt int color_unknown = Color.argb(25, 128, 128, 128);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a simple gauge.
|
||||||
|
*
|
||||||
|
* @param color the gauge color
|
||||||
|
* @param value the gauge value. Range: [0, 1]
|
||||||
|
*/
|
||||||
|
public void drawSimpleGauge(ImageView gaugeBar, final int color,
|
||||||
|
final float value) {
|
||||||
|
|
||||||
|
final int width = (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
150,
|
||||||
|
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw gauge
|
||||||
|
gaugeBar.setImageBitmap(drawSimpleGaugeInternal(
|
||||||
|
width,
|
||||||
|
Math.round(width * 0.075f),
|
||||||
|
color,
|
||||||
|
value
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param width Bitmap width in pixels
|
||||||
|
* @param barWidth Gauge bar width in pixels
|
||||||
|
* @param filledColor Color of the filled part of the gauge
|
||||||
|
* @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled
|
||||||
|
* @return Bitmap containing the gauge
|
||||||
|
*/
|
||||||
|
private Bitmap drawSimpleGaugeInternal(final int width, final int barWidth, @ColorInt final int filledColor, final float filledFactor) {
|
||||||
|
final int height = width / 2;
|
||||||
|
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||||
|
|
||||||
|
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
final Canvas canvas = new Canvas(bitmap);
|
||||||
|
final Paint paint = new Paint();
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
paint.setStrokeWidth(barWidth * 0.75f);
|
||||||
|
paint.setColor(color_unknown);
|
||||||
|
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint);
|
||||||
|
|
||||||
|
if (filledFactor >= 0) {
|
||||||
|
paint.setStrokeWidth(barWidth);
|
||||||
|
paint.setColor(filledColor);
|
||||||
|
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a segmented gauge.
|
||||||
|
*
|
||||||
|
* @param colors the colors of each segment
|
||||||
|
* @param segments the size of each segment. The sum of all segments should be 1
|
||||||
|
* @param value the gauge value, in range [0, 1], or -1 for no value and only segments
|
||||||
|
* @param fadeOutsideDot whether to fade out colors outside the dot value
|
||||||
|
* @param gapBetweenSegments whether to introduce a small gap between the segments
|
||||||
|
*/
|
||||||
|
public void drawSegmentedGauge(ImageView gaugeBar,
|
||||||
|
final int[] colors,
|
||||||
|
final float[] segments,
|
||||||
|
final float value,
|
||||||
|
final boolean fadeOutsideDot,
|
||||||
|
final boolean gapBetweenSegments) {
|
||||||
|
if (colors.length != segments.length) {
|
||||||
|
LOG.error("Colors length {} differs from segments length {}", colors.length, segments.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int width = (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
150,
|
||||||
|
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||||
|
);
|
||||||
|
|
||||||
|
final int barWidth = Math.round(width * 0.075f);
|
||||||
|
|
||||||
|
final int height = width / 2;
|
||||||
|
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||||
|
|
||||||
|
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
final Canvas canvas = new Canvas(bitmap);
|
||||||
|
final Paint paint = new Paint();
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeCap(Paint.Cap.BUTT);
|
||||||
|
paint.setStrokeWidth(barWidth);
|
||||||
|
|
||||||
|
final double cornersGapRadians = Math.asin((width * 0.055f) / (double) height);
|
||||||
|
final double cornersGapFactor = cornersGapRadians / Math.PI;
|
||||||
|
|
||||||
|
int dotColor = 0;
|
||||||
|
float angleSum = 0;
|
||||||
|
for (int i = 0; i < segments.length; i++) {
|
||||||
|
if (segments[i] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.setColor(colors[i]);
|
||||||
|
paint.setStrokeWidth(barWidth);
|
||||||
|
|
||||||
|
if (value < 0 || (value >= angleSum && value <= angleSum + segments[i])) {
|
||||||
|
dotColor = colors[i];
|
||||||
|
} else {
|
||||||
|
if (fadeOutsideDot) {
|
||||||
|
paint.setColor(colors[i] - 0xB0000000);
|
||||||
|
} else {
|
||||||
|
paint.setStrokeWidth(barWidth * 0.75f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float startAngleDegrees = 180 + angleSum * 180;
|
||||||
|
float sweepAngleDegrees = segments[i] * 180;
|
||||||
|
|
||||||
|
if (value >= 0) {
|
||||||
|
// Do not draw to the end if it will be overlapped by the dot
|
||||||
|
if (i == 0 && value <= cornersGapFactor) {
|
||||||
|
startAngleDegrees += (float) Math.toDegrees(cornersGapRadians);
|
||||||
|
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
||||||
|
} else if (i == segments.length - 1 && value >= 1 - cornersGapFactor) {
|
||||||
|
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gapBetweenSegments) {
|
||||||
|
if (i + 1 < segments.length) {
|
||||||
|
sweepAngleDegrees -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawArc(
|
||||||
|
barMargin,
|
||||||
|
barMargin,
|
||||||
|
width - barMargin,
|
||||||
|
width - barMargin,
|
||||||
|
startAngleDegrees,
|
||||||
|
sweepAngleDegrees,
|
||||||
|
false,
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
angleSum += segments[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value >= 0) {
|
||||||
|
// Prevent the dot from going outside the widget in the extremities
|
||||||
|
final float angleRadians = (float) normalize(value, 0, 1, cornersGapRadians, Math.toRadians(180) - cornersGapRadians);
|
||||||
|
|
||||||
|
paint.setColor(Color.TRANSPARENT);
|
||||||
|
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||||
|
|
||||||
|
// In the corners the circle is slightly offset, so adjust it slightly
|
||||||
|
final float widthAdjustment = width * 0.04f * (float) normalize(Math.abs(value - 0.5d), 0, 0.5d);
|
||||||
|
|
||||||
|
final float x = ((width - (barWidth / 2f) - widthAdjustment) / 2f) * (float) Math.cos(angleRadians);
|
||||||
|
final float y = (height - (barWidth / 2f)) * (float) Math.sin(angleRadians);
|
||||||
|
|
||||||
|
// Draw hole
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
canvas.drawCircle((width / 2f) - x, height - y, barMargin * 1.6f, paint);
|
||||||
|
|
||||||
|
// Draw dot
|
||||||
|
paint.setColor(dotColor);
|
||||||
|
paint.setXfermode(null);
|
||||||
|
canvas.drawCircle((width / 2f) - x, height - y, barMargin, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
gaugeBar.setImageBitmap(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double normalize(final double value, final double min, final double max) {
|
||||||
|
return normalize(value, min, max, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double normalize(final double value, final double minSource, final double maxSource, final double minTarget, final double maxTarget) {
|
||||||
|
return ((value - minSource) * (maxTarget - minTarget)) / (maxSource - minSource) + minTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -476,7 +476,22 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsVo2Max() {
|
public boolean supportsVO2Max() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsVO2MaxCycling() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsVO2MaxRunning() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsVO2MaxGeneral() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,11 +218,11 @@ public interface DeviceCoordinator {
|
|||||||
boolean supportsStressMeasurement();
|
boolean supportsStressMeasurement();
|
||||||
|
|
||||||
boolean supportsBodyEnergy();
|
boolean supportsBodyEnergy();
|
||||||
|
|
||||||
boolean supportsHrvMeasurement();
|
boolean supportsHrvMeasurement();
|
||||||
|
boolean supportsVO2Max();
|
||||||
boolean supportsVo2Max();
|
boolean supportsVO2MaxCycling();
|
||||||
|
boolean supportsVO2MaxGeneral();
|
||||||
|
boolean supportsVO2MaxRunning();
|
||||||
boolean supportsSleepMeasurement();
|
boolean supportsSleepMeasurement();
|
||||||
boolean supportsStepCounter();
|
boolean supportsStepCounter();
|
||||||
boolean supportsSpeedzones();
|
boolean supportsSpeedzones();
|
||||||
|
@ -218,7 +218,17 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsVo2Max() {
|
public boolean supportsVO2Max() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsVO2MaxCycling() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsVO2MaxRunning() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
157
app/src/main/res/layout/fragment_vo2max.xml
Normal file
157
app/src/main/res/layout/fragment_vo2max.xml
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.VO2Max">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/vo2max_date_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GridLayout
|
||||||
|
android:id="@+id/tiles_grid_wrapper"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:columnCount="2"
|
||||||
|
>
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/vo2max_general_card_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="UselessParent"
|
||||||
|
android:layout_gravity="fill"
|
||||||
|
android:layout_columnWeight="1"
|
||||||
|
>
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/vo2max_general_gauge"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="75dp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:scaleType="fitStart" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/vo2max_general_gauge_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_marginTop="28dp"
|
||||||
|
android:text="@string/stats_empty_value"
|
||||||
|
android:textSize="30sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/vo2max_general_gauge_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/vo2max_general_gauge_value"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="@string/vo2max_general_gauge_label" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/vo2max_running_card_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="UselessParent"
|
||||||
|
android:layout_gravity="fill"
|
||||||
|
android:layout_columnWeight="1"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/vo2max_running_gauge"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="75dp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:scaleType="fitStart" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/vo2max_running_gauge_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_marginTop="28dp"
|
||||||
|
android:text="@string/stats_empty_value"
|
||||||
|
android:textSize="30sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/vo2max_running_gauge_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/vo2max_running_gauge_value"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="@string/vo2max_running_gauge_label" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/vo2max_cycling_card_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="UselessParent"
|
||||||
|
android:layout_gravity="fill"
|
||||||
|
android:layout_columnWeight="1"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/vo2max_cycling_gauge"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="75dp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:scaleType="fitStart" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/vo2max_cycling_gauge_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_marginTop="28dp"
|
||||||
|
android:text="@string/stats_empty_value"
|
||||||
|
android:textSize="30sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/vo2max_cycling_gauge_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/vo2max_cycling_gauge_value"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="@string/vo2max_cycling_gauge_label" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</GridLayout>
|
||||||
|
<TextView
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:layout_marginStart="25dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="start"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:text="@string/thirty_days_timeline"
|
||||||
|
/>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="250sp"
|
||||||
|
>
|
||||||
|
<com.github.mikephil.charting.charts.LineChart
|
||||||
|
android:id="@+id/vo2max_chart"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_weight="2" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@ -3041,6 +3041,7 @@
|
|||||||
<item>@string/weekstepschart_steps_a_week_or_month</item>
|
<item>@string/weekstepschart_steps_a_week_or_month</item>
|
||||||
<item>@string/pref_header_hrv_status</item>
|
<item>@string/pref_header_hrv_status</item>
|
||||||
<item>@string/body_energy</item>
|
<item>@string/body_energy</item>
|
||||||
|
<item>@string/vo2max</item>
|
||||||
<item>@string/menuitem_stress</item>
|
<item>@string/menuitem_stress</item>
|
||||||
<item>@string/menuitem_pai</item>
|
<item>@string/menuitem_pai</item>
|
||||||
<item>@string/stats_title</item>
|
<item>@string/stats_title</item>
|
||||||
@ -3057,6 +3058,7 @@
|
|||||||
<item>@string/p_steps_week</item>
|
<item>@string/p_steps_week</item>
|
||||||
<item>@string/p_hrv_status</item>
|
<item>@string/p_hrv_status</item>
|
||||||
<item>@string/p_body_energy</item>
|
<item>@string/p_body_energy</item>
|
||||||
|
<item>@string/p_vo2max</item>
|
||||||
<item>@string/p_stress</item>
|
<item>@string/p_stress</item>
|
||||||
<item>@string/p_pai</item>
|
<item>@string/p_pai</item>
|
||||||
<item>@string/p_speed_zones</item>
|
<item>@string/p_speed_zones</item>
|
||||||
@ -3073,6 +3075,7 @@
|
|||||||
<item>@string/p_sleep</item>
|
<item>@string/p_sleep</item>
|
||||||
<item>@string/p_hrv_status</item>
|
<item>@string/p_hrv_status</item>
|
||||||
<item>@string/p_body_energy</item>
|
<item>@string/p_body_energy</item>
|
||||||
|
<item>@string/p_vo2max</item>
|
||||||
<item>@string/p_steps_week</item>
|
<item>@string/p_steps_week</item>
|
||||||
<item>@string/p_stress</item>
|
<item>@string/p_stress</item>
|
||||||
<item>@string/p_pai</item>
|
<item>@string/p_pai</item>
|
||||||
@ -4174,6 +4177,7 @@
|
|||||||
<item>@string/active_time</item>
|
<item>@string/active_time</item>
|
||||||
<item>@string/menuitem_sleep</item>
|
<item>@string/menuitem_sleep</item>
|
||||||
<item>@string/body_energy</item>
|
<item>@string/body_energy</item>
|
||||||
|
<item>@string/vo2max</item>
|
||||||
<item>@string/menuitem_stress_simple</item>
|
<item>@string/menuitem_stress_simple</item>
|
||||||
<item>@string/menuitem_stress_segmented</item>
|
<item>@string/menuitem_stress_segmented</item>
|
||||||
<item>@string/menuitem_stress_breakdown</item>
|
<item>@string/menuitem_stress_breakdown</item>
|
||||||
@ -4188,6 +4192,7 @@
|
|||||||
<item>activetime</item>
|
<item>activetime</item>
|
||||||
<item>sleep</item>
|
<item>sleep</item>
|
||||||
<item>bodyenergy</item>
|
<item>bodyenergy</item>
|
||||||
|
<item>vo2max</item>
|
||||||
<item>stress_simple</item>
|
<item>stress_simple</item>
|
||||||
<item>stress_segmented</item>
|
<item>stress_segmented</item>
|
||||||
<item>stress_breakdown</item>
|
<item>stress_breakdown</item>
|
||||||
|
@ -51,6 +51,14 @@
|
|||||||
<color name="hrv_status_low" type="color">#fc5203</color>
|
<color name="hrv_status_low" type="color">#fc5203</color>
|
||||||
<color name="hrv_status_poor" type="color">#be03fc</color>
|
<color name="hrv_status_poor" type="color">#be03fc</color>
|
||||||
<color name="hrv_status_char_line_color" type="color">#d12a2a</color>
|
<color name="hrv_status_char_line_color" type="color">#d12a2a</color>
|
||||||
|
<color name="vo2max_running_char_line_color" type="color">#46acea</color>
|
||||||
|
<color name="vo2max_cycling_char_line_color" type="color">#59b22c</color>
|
||||||
|
<color name="vo2max_general_char_line_color" type="color">#824be3</color>
|
||||||
|
<color name="vo2max_value_poor_color" type="color">#d93832</color>
|
||||||
|
<color name="vo2max_value_fair_color" type="color">#ffa703</color>
|
||||||
|
<color name="vo2max_value_good_color" type="color">#04c79c</color>
|
||||||
|
<color name="vo2max_value_excellent_color" type="color">#02a8e6</color>
|
||||||
|
<color name="vo2max_value_superior_color" type="color">#824be3</color>
|
||||||
<color name="body_energy_level_color" type="color">#5ac234</color>
|
<color name="body_energy_level_color" type="color">#5ac234</color>
|
||||||
<color name="body_energy_lost_color" type="color">#ff6c43</color>
|
<color name="body_energy_lost_color" type="color">#ff6c43</color>
|
||||||
<color name="steps_color" type="color">#00c9bf</color>
|
<color name="steps_color" type="color">#00c9bf</color>
|
||||||
|
@ -1623,6 +1623,9 @@
|
|||||||
<string name="hrv_status_unit">%1$d ms</string>
|
<string name="hrv_status_unit">%1$d ms</string>
|
||||||
<string name="hrv_status_baseline">%1$d-%2$d ms</string>
|
<string name="hrv_status_baseline">%1$d-%2$d ms</string>
|
||||||
<string name="hrv_status_baseline_label">Baseline</string>
|
<string name="hrv_status_baseline_label">Baseline</string>
|
||||||
|
<string name="vo2_max_running">VO2Max Running</string>
|
||||||
|
<string name="vo2_max_cycling">VO2Max Cycling</string>
|
||||||
|
<string name="vo2_max_general">VO2Max General</string>
|
||||||
<string name="bpm_value_unit">%1$d bpm</string>
|
<string name="bpm_value_unit">%1$d bpm</string>
|
||||||
<string name="steps_distance_unit">%1$,.2f km</string>
|
<string name="steps_distance_unit">%1$,.2f km</string>
|
||||||
<string name="body_energy_gained">Gained</string>
|
<string name="body_energy_gained">Gained</string>
|
||||||
@ -1990,6 +1993,9 @@
|
|||||||
<string name="warning">Warning!</string>
|
<string name="warning">Warning!</string>
|
||||||
<string name="note">Note</string>
|
<string name="note">Note</string>
|
||||||
<string name="no_data">No data</string>
|
<string name="no_data">No data</string>
|
||||||
|
<string name="vo2max_general_gauge_label">VO2Max</string>
|
||||||
|
<string name="vo2max_running_gauge_label">Running VO2Max</string>
|
||||||
|
<string name="vo2max_cycling_gauge_label">Cycling VO2Max</string>
|
||||||
<!-- LED Color -->
|
<!-- LED Color -->
|
||||||
<string name="preferences_led_color">LED Color</string>
|
<string name="preferences_led_color">LED Color</string>
|
||||||
<!-- FM transmitters -->
|
<!-- FM transmitters -->
|
||||||
@ -2530,6 +2536,8 @@
|
|||||||
<string name="pref_header_spo2">Blood Oxygen</string>
|
<string name="pref_header_spo2">Blood Oxygen</string>
|
||||||
<string name="pref_header_hrv_status">HRV Status</string>
|
<string name="pref_header_hrv_status">HRV Status</string>
|
||||||
<string name="body_energy">Body Energy</string>
|
<string name="body_energy">Body Energy</string>
|
||||||
|
<string name="vo2max">VO2 Max</string>
|
||||||
|
<string name="thirty_days_timeline">30 Days Timeline</string>
|
||||||
<string name="pref_header_sony_ambient_sound_control">Ambient Sound Control</string>
|
<string name="pref_header_sony_ambient_sound_control">Ambient Sound Control</string>
|
||||||
<string name="pref_header_sony_sound_control">Sound Control</string>
|
<string name="pref_header_sony_sound_control">Sound Control</string>
|
||||||
<string name="pref_header_sony_device_info">Device Information</string>
|
<string name="pref_header_sony_device_info">Device Information</string>
|
||||||
|
@ -109,6 +109,7 @@
|
|||||||
<item name="p_speed_zones" type="string">speedzones</item>
|
<item name="p_speed_zones" type="string">speedzones</item>
|
||||||
<item name="p_hrv_status" type="string">hrvstatus</item>
|
<item name="p_hrv_status" type="string">hrvstatus</item>
|
||||||
<item name="p_body_energy" type="string">bodyenergy</item>
|
<item name="p_body_energy" type="string">bodyenergy</item>
|
||||||
|
<item name="p_vo2max" type="string">vo2max</item>
|
||||||
<item name="p_live_stats" type="string">livestats</item>
|
<item name="p_live_stats" type="string">livestats</item>
|
||||||
<item name="p_spo2" type="string">spo2</item>
|
<item name="p_spo2" type="string">spo2</item>
|
||||||
<item name="p_temperature" type="string">temperature</item>
|
<item name="p_temperature" type="string">temperature</item>
|
||||||
|
Loading…
Reference in New Issue
Block a user