mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-27 04:16:49 +01:00
device-cycling-sensor (#3705)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3705 Co-authored-by: Daniel Dakhno <dakhnod@gmail.com> Co-committed-by: Daniel Dakhno <dakhnod@gmail.com>
This commit is contained in:
parent
25b1f8f3fa
commit
3b01422a45
@ -129,6 +129,7 @@ public class GBDaoGenerator {
|
||||
addWorldClocks(schema, user, device);
|
||||
addContacts(schema, user, device);
|
||||
addAppSpecificNotificationSettings(schema, device);
|
||||
addCyclingSample(schema, user, device);
|
||||
|
||||
Entity notificationFilter = addNotificationFilters(schema);
|
||||
|
||||
@ -624,6 +625,18 @@ public class GBDaoGenerator {
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addCyclingSample(Schema schema, Entity user, Entity device){
|
||||
Entity cyclingSample = addEntity(schema, "CyclingSample");
|
||||
addCommonTimeSampleProperties("AbstractTimeSample", cyclingSample, user, device);
|
||||
|
||||
cyclingSample.implementsSerializable();
|
||||
|
||||
cyclingSample.addIntProperty("RevolutionCount");
|
||||
cyclingSample.addFloatProperty("Distance");
|
||||
cyclingSample.addFloatProperty("Speed");
|
||||
return cyclingSample;
|
||||
}
|
||||
|
||||
private static Entity addVivomoveHrActivitySample(Schema schema, Entity user, Entity device) {
|
||||
final Entity activitySample = addEntity(schema, "VivomoveHrActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
|
@ -108,6 +108,9 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
||||
if (!coordinator.supportsTemperatureMeasurement()) {
|
||||
tabList.remove("temperature");
|
||||
}
|
||||
if(!coordinator.supportsCyclingData()) {
|
||||
tabList.remove("cycling");
|
||||
}
|
||||
return tabList;
|
||||
}
|
||||
|
||||
@ -151,6 +154,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
||||
return new Spo2ChartFragment();
|
||||
case "temperature":
|
||||
return new TemperatureChartFragment();
|
||||
case "cycling":
|
||||
return new CyclingChartFragment();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -201,6 +206,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
||||
return getString(R.string.pref_header_spo2);
|
||||
case "temperature":
|
||||
return getString(R.string.menuitem_temperature);
|
||||
case "cycling":
|
||||
return getString(R.string.title_cycling);
|
||||
}
|
||||
return super.getPageTitle(position);
|
||||
}
|
||||
|
@ -0,0 +1,288 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.github.mikephil.charting.animation.Easing;
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.charts.LineChart;
|
||||
import com.github.mikephil.charting.components.LegendEntry;
|
||||
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.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 java.text.DateFormat;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
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.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class CyclingChartFragment extends AbstractChartFragment<CyclingChartFragment.CyclingChartsData>{
|
||||
private LineChart cyclingHistoryChart;
|
||||
|
||||
private int BACKGROUND_COLOR;
|
||||
private int DESCRIPTION_COLOR;
|
||||
private int CHART_TEXT_COLOR;
|
||||
private int LEGEND_TEXT_COLOR;
|
||||
private int CHART_LINE_COLOR_DISTANCE;
|
||||
private int CHART_LINE_COLOR_SPEED;
|
||||
private final Prefs prefs = GBApplication.getPrefs();
|
||||
|
||||
protected static class CyclingChartsData extends DefaultChartsData<LineData> {
|
||||
public CyclingChartsData(LineData lineData) {
|
||||
super(lineData, new TimeFormatter());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return "Cycling data";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext());
|
||||
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext());
|
||||
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
|
||||
|
||||
if (prefs.getBoolean("chart_heartrate_color", false)) {
|
||||
CHART_LINE_COLOR_DISTANCE = ContextCompat.getColor(getContext(), R.color.chart_activity_dark);
|
||||
CHART_LINE_COLOR_SPEED = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
|
||||
} else {
|
||||
CHART_LINE_COLOR_DISTANCE = ContextCompat.getColor(getContext(), R.color.chart_activity_light);
|
||||
CHART_LINE_COLOR_SPEED = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CyclingChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
List<CyclingSample> samples = getSamples(db, device);
|
||||
|
||||
return new CyclingChartsDataBuilder(samples).build();
|
||||
}
|
||||
|
||||
protected class CyclingChartsDataBuilder {
|
||||
private final List<CyclingSample> samples;
|
||||
|
||||
private final List<Entry> lineEntries = new ArrayList<>();
|
||||
|
||||
long averageSum;
|
||||
long averageNumSamples;
|
||||
|
||||
public CyclingChartsDataBuilder(final List<CyclingSample> samples) {
|
||||
this.samples = samples;
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
lineEntries.clear();
|
||||
|
||||
averageSum = 0;
|
||||
averageNumSamples = 0;
|
||||
}
|
||||
|
||||
public CyclingChartsData build() {
|
||||
List<Entry> distanceEntries = new ArrayList<>();
|
||||
List<Entry> speedEntries = new ArrayList<>();
|
||||
|
||||
Float dayStart = 0f;
|
||||
|
||||
if(!samples.isEmpty()){
|
||||
dayStart = samples.get(0).getDistance() / 1000f;
|
||||
}
|
||||
|
||||
int nextIndex = 0;
|
||||
for (CyclingSample sample : samples) {
|
||||
// add distance in Km
|
||||
distanceEntries.add(new Entry(sample.getTimestamp(), (sample.getDistance() / 1000f) - dayStart));
|
||||
Float speed = sample.getSpeed();
|
||||
speedEntries.add(new Entry(sample.getTimestamp(), (speed != null) ? (sample.getSpeed() * 3.6f) : 0));
|
||||
|
||||
if(nextIndex < samples.size()){
|
||||
CyclingSample nextSample = samples.get(nextIndex);
|
||||
if(nextSample.getSpeed() == null){
|
||||
// sensor is off, doesn't report zero speed. So let's inject it outselves
|
||||
speedEntries.add(new Entry(sample.getTimestamp() + 30_000, 0));
|
||||
}
|
||||
}
|
||||
|
||||
nextIndex++;
|
||||
}
|
||||
|
||||
LineDataSet distanceSet = new LineDataSet(distanceEntries, "Cycling");
|
||||
distanceSet.setLineWidth(2.2f);
|
||||
distanceSet.setColor(CHART_LINE_COLOR_DISTANCE);
|
||||
distanceSet.setDrawCircles(false);
|
||||
distanceSet.setDrawCircleHole(false);
|
||||
distanceSet.setDrawValues(true);
|
||||
distanceSet.setValueTextSize(10f);
|
||||
distanceSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||
distanceSet.setHighlightEnabled(false);
|
||||
distanceSet.setValueFormatter(new CyclingDistanceFormatter(CyclingChartFragment.this, dayStart));
|
||||
distanceSet.setAxisDependency(cyclingHistoryChart.getAxisLeft().getAxisDependency());
|
||||
LineData lineData = new LineData(distanceSet);
|
||||
|
||||
LineDataSet speedSet = new LineDataSet(speedEntries, "Speed");
|
||||
speedSet.setLineWidth(2.2f);
|
||||
speedSet.setColor(CHART_LINE_COLOR_SPEED);
|
||||
speedSet.setDrawCircles(false);
|
||||
speedSet.setDrawCircleHole(false);
|
||||
speedSet.setDrawValues(true);
|
||||
speedSet.setValueTextSize(10f);
|
||||
speedSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||
speedSet.setHighlightEnabled(true);
|
||||
speedSet.setValueFormatter(new CyclingSpeedFormatter(CyclingChartFragment.this));
|
||||
speedSet.setAxisDependency(cyclingHistoryChart.getAxisRight().getAxisDependency());
|
||||
|
||||
lineData.addDataSet(speedSet);
|
||||
|
||||
return new CyclingChartsData(lineData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
cyclingHistoryChart.animateX(ANIM_TIME, Easing.EaseInOutQuart);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(final Chart<?> chart) {
|
||||
final List<LegendEntry> legendEntries = new ArrayList<>(2);
|
||||
|
||||
final LegendEntry distanceEntry = new LegendEntry();
|
||||
distanceEntry.label = getString(R.string.activity_list_summary_distance);
|
||||
distanceEntry.formColor = CHART_LINE_COLOR_DISTANCE;
|
||||
legendEntries.add(distanceEntry);
|
||||
|
||||
final LegendEntry speedEntry = new LegendEntry();
|
||||
speedEntry.label = getString(R.string.Speed);
|
||||
speedEntry.formColor = CHART_LINE_COLOR_SPEED;
|
||||
legendEntries.add(speedEntry);
|
||||
|
||||
chart.getLegend().setCustom(legendEntries);
|
||||
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(CyclingChartsData cyclingData) {
|
||||
cyclingHistoryChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
|
||||
cyclingHistoryChart.getXAxis().setValueFormatter(cyclingData.getXValueFormatter());
|
||||
cyclingHistoryChart.getXAxis().setAvoidFirstLastClipping(true);
|
||||
|
||||
cyclingHistoryChart.setData(cyclingData.getData());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_cycling, container, false);
|
||||
|
||||
cyclingHistoryChart = rootView.findViewById(R.id.chart_cycling_history);
|
||||
|
||||
cyclingHistoryChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
cyclingHistoryChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
configureBarLineChartDefaults(cyclingHistoryChart);
|
||||
|
||||
final XAxis x = cyclingHistoryChart.getXAxis();
|
||||
x.setDrawLabels(true);
|
||||
x.setDrawGridLines(true);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
|
||||
final YAxis yAxisLeft = cyclingHistoryChart.getAxisLeft();
|
||||
yAxisLeft.setDrawGridLines(true);
|
||||
yAxisLeft.setTextColor(CHART_LINE_COLOR_DISTANCE);
|
||||
yAxisLeft.setEnabled(true);
|
||||
yAxisLeft.setGridColor(CHART_LINE_COLOR_DISTANCE);
|
||||
|
||||
final YAxis yAxisRight = cyclingHistoryChart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(true);
|
||||
yAxisRight.setTextColor(CHART_LINE_COLOR_SPEED);
|
||||
yAxisRight.setEnabled(true);
|
||||
yAxisRight.setGridColor(CHART_LINE_COLOR_SPEED);
|
||||
|
||||
refresh();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private List<CyclingSample> getSamples(final DBHandler db, final GBDevice device) {
|
||||
final int tsStart = getTSStart();
|
||||
final int tsEnd = getTSEnd();
|
||||
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
final TimeSampleProvider<CyclingSample> sampleProvider = coordinator.getCyclingSampleProvider(device, db.getDaoSession());
|
||||
return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected static class CyclingDistanceFormatter extends ValueFormatter {
|
||||
// private final DecimalFormat formatter = new DecimalFormat("0.00 km");
|
||||
Float dayStartDistance;
|
||||
CyclingChartFragment fragment;
|
||||
|
||||
public CyclingDistanceFormatter(CyclingChartFragment fragment, Float dayStartDistance) {
|
||||
this.dayStartDistance = dayStartDistance;
|
||||
this.fragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPointLabel(Entry entry) {
|
||||
return fragment.getString(R.string.chart_cycling_point_label_distance, entry.getY(), entry.getY() + dayStartDistance);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class CyclingSpeedFormatter extends ValueFormatter {
|
||||
CyclingChartFragment fragment;
|
||||
|
||||
public CyclingSpeedFormatter(CyclingChartFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPointLabel(Entry entry) {
|
||||
return fragment.getString(R.string.chart_cycling_point_label_speed, entry.getY());
|
||||
}
|
||||
}
|
||||
|
||||
protected static class TimeFormatter extends ValueFormatter {
|
||||
DateFormat annotationDateFormat = SimpleDateFormat.getTimeInstance(DateFormat.SHORT);
|
||||
Calendar cal = GregorianCalendar.getInstance();
|
||||
|
||||
public TimeFormatter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormattedValue(float value) {
|
||||
cal.clear();
|
||||
cal.setTimeInMillis((long)(value));
|
||||
Date date = cal.getTime();
|
||||
return annotationDateFormat.format(date);
|
||||
}
|
||||
}
|
||||
}
|
@ -460,4 +460,7 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_AUTO_REPLY_INCOMING_CALL = "pref_auto_reply_phonecall";
|
||||
public static final String PREF_AUTO_REPLY_INCOMING_CALL_DELAY = "pref_auto_reply_phonecall_delay";
|
||||
public static final String PREF_SPEAK_NOTIFICATIONS_ALOUD = "pref_speak_notifications_aloud";
|
||||
|
||||
public static final String PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL = "pref_cycling_persistence_interval";
|
||||
public static final String PREF_CYCLING_SENSOR_WHEEL_DIAMETER = "pref_cycling_wheel_diameter";
|
||||
}
|
||||
|
@ -643,6 +643,9 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
addPreferenceHandlerFor(PREF_HEARTRATE_AUTOMATIC_ENABLE);
|
||||
addPreferenceHandlerFor(PREF_SPO_AUTOMATIC_ENABLE);
|
||||
|
||||
addPreferenceHandlerFor(PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL);
|
||||
addPreferenceHandlerFor(PREF_CYCLING_SENSOR_WHEEL_DIAMETER);
|
||||
|
||||
addPreferenceHandlerFor("lock");
|
||||
|
||||
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
||||
|
@ -61,6 +61,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevelDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
|
||||
@ -226,6 +227,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<CyclingSample> getCyclingSampleProvider(GBDevice device, DaoSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends HeartRateSample> getHeartRateMaxSampleProvider(GBDevice device, DaoSession session) {
|
||||
return null;
|
||||
@ -567,6 +573,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCyclingData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRemSleep() {
|
||||
return false;
|
||||
|
@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
@ -56,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TimeSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
|
||||
@ -189,6 +191,15 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
boolean supportsActivityTracking();
|
||||
|
||||
/**
|
||||
* Returns true if cycling data is supported by the device
|
||||
* (with this coordinator).
|
||||
* This enables the ChartsActivity.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean supportsCyclingData();
|
||||
|
||||
/**
|
||||
* Indicates whether the device supports recording dedicated activity tracks, like
|
||||
* walking, hiking, running, swimming, etc. and retrieving the recorded
|
||||
@ -287,6 +298,11 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session);
|
||||
|
||||
/**
|
||||
* Returns the sample provider for Cycling data, for the device being supported.
|
||||
*/
|
||||
TimeSampleProvider<CyclingSample> getCyclingSampleProvider(GBDevice device, DaoSession session);
|
||||
|
||||
/**
|
||||
* Returns the sample provider for max HR data, for the device being supported.
|
||||
*/
|
||||
|
@ -0,0 +1,113 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.coordinator;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.db.CyclingSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.cycling_sensor.support.CyclingSensorSupport;
|
||||
|
||||
public class CyclingSensorCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
final Long deviceId = device.getId();
|
||||
|
||||
session.getCyclingSampleDao().queryBuilder()
|
||||
.where(CyclingSampleDao.Properties.DeviceId.eq(deviceId))
|
||||
.buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
return candidate.supportsService(CyclingSensorSupport.UUID_CYCLING_SENSOR_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCyclingData() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<CyclingSample> getCyclingSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new CyclingSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSleepMeasurement() {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean supportsStepCounter() {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean supportsSpeedzones() {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean supportsActivityTabs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_cycling_sensor
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return CyclingSensorSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_cycling_sensor;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.db;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class CyclingSampleProvider extends AbstractTimeSampleProvider<CyclingSample> {
|
||||
public CyclingSampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<CyclingSample, ?> getSampleDao() {
|
||||
return getSession().getCyclingSampleDao();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return CyclingSampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return CyclingSampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CyclingSample createSample() {
|
||||
return new CyclingSample();
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.casio.gbx100.CasioGBX100Devi
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGMWB5000DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.casio.gwb5600.CasioGWB5600DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.CmfWatchProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.coordinator.CyclingSensorCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.divoom.PixooCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator;
|
||||
@ -388,6 +389,7 @@ public enum DeviceType {
|
||||
FEMOMETER_VINCA2(FemometerVinca2DeviceCoordinator.class),
|
||||
PIXOO(PixooCoordinator.class),
|
||||
SCANNABLE(ScannableDeviceCoordinator.class),
|
||||
CYCLING_SENSOR(CyclingSensorCoordinator.class),
|
||||
TEST(TestDeviceCoordinator.class);
|
||||
|
||||
private DeviceCoordinator coordinator;
|
||||
|
@ -0,0 +1,16 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.cycling_sensor.support;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
|
||||
public class CyclingSensorBaseSupport extends AbstractBTLEDeviceSupport {
|
||||
public CyclingSensorBaseSupport(Logger logger) {
|
||||
super(logger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,267 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.cycling_sensor.support;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityKind.TYPE_CYCLING;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.db.CyclingSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.CyclingSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class CyclingSensorSupport extends CyclingSensorBaseSupport {
|
||||
static class CyclingSpeedCadenceMeasurement {
|
||||
private static final int FLAG_REVOLUTION_DATA_PRESENT = 1 << 0;
|
||||
private static final int FLAG_CADENCE_DATA_PRESENT = 1 << 1;
|
||||
public boolean revolutionDataPresent = false;
|
||||
public boolean cadenceDataPresent = false;
|
||||
public int revolutionCount;
|
||||
public int lastRevolutionTimeTicks;
|
||||
private int crankRevolutionCount;
|
||||
private int lastCrankRevolutionTimeTicks;
|
||||
|
||||
public static CyclingSpeedCadenceMeasurement fromPayload(byte[] payload) throws RuntimeException {
|
||||
if(payload.length < 7){
|
||||
throw new RuntimeException("wrong payload length");
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer
|
||||
.wrap(payload)
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
byte flags = buffer.get();
|
||||
|
||||
boolean revolutionDataPresent = (flags | FLAG_REVOLUTION_DATA_PRESENT) == FLAG_REVOLUTION_DATA_PRESENT;
|
||||
boolean cadenceDataPresent = (flags | FLAG_CADENCE_DATA_PRESENT) == FLAG_CADENCE_DATA_PRESENT;
|
||||
CyclingSpeedCadenceMeasurement result = new CyclingSpeedCadenceMeasurement();
|
||||
|
||||
if(revolutionDataPresent){
|
||||
result.revolutionDataPresent = true;
|
||||
result.revolutionCount = buffer.getInt() & 0xFFFFFFFF; // remove sign
|
||||
result.lastRevolutionTimeTicks = buffer.getShort() & 0xFFFF;
|
||||
}
|
||||
|
||||
if(cadenceDataPresent){
|
||||
result.cadenceDataPresent = true;
|
||||
result.crankRevolutionCount = buffer.getInt();
|
||||
result.lastCrankRevolutionTimeTicks = buffer.getShort();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Measurement revolutions: %d, time ticks %d", revolutionCount, lastRevolutionTimeTicks);
|
||||
}
|
||||
}
|
||||
|
||||
public final static UUID UUID_CYCLING_SENSOR_SERVICE =
|
||||
UUID.fromString("00001816-0000-1000-8000-00805f9b34fb");
|
||||
public final static UUID UUID_CYCLING_SENSOR_CSC_MEASUREMENT =
|
||||
UUID.fromString("00002a5b-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CyclingSensorSupport.class);
|
||||
|
||||
private long persistenceInterval;
|
||||
private long nextPersistenceTimestamp = 0;
|
||||
|
||||
private float wheelCircumference;
|
||||
|
||||
private CyclingSpeedCadenceMeasurement lastReportedMeasurement = null;
|
||||
private long lastMeasurementTime = 0;
|
||||
|
||||
private BluetoothGattCharacteristic batteryCharacteristic = null;
|
||||
|
||||
public CyclingSensorSupport() {
|
||||
super(logger);
|
||||
|
||||
addSupportedService(UUID_CYCLING_SENSOR_SERVICE);
|
||||
addSupportedService(BatteryInfoProfile.SERVICE_UUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
switch (config){
|
||||
case DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL:
|
||||
case DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_WHEEL_DIAMETER:
|
||||
loadConfiguration();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadConfiguration(){
|
||||
Prefs deviceSpecificPrefs = new Prefs(
|
||||
GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())
|
||||
);
|
||||
persistenceInterval = deviceSpecificPrefs.getInt(DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL, 60) * 1000;
|
||||
nextPersistenceTimestamp = 0;
|
||||
|
||||
float wheelDiameter = deviceSpecificPrefs.getFloat(DeviceSettingsPreferenceConst.PREF_CYCLING_SENSOR_WHEEL_DIAMETER, 29);
|
||||
wheelCircumference = (float)(wheelDiameter * 2.54 * Math.PI) / 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
|
||||
BluetoothGattCharacteristic measurementCharacteristic =
|
||||
getCharacteristic(UUID_CYCLING_SENSOR_CSC_MEASUREMENT);
|
||||
|
||||
builder.add(new NotifyAction(measurementCharacteristic, true));
|
||||
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
batteryCharacteristic = getCharacteristic(BatteryInfoProfile.UUID_CHARACTERISTIC_BATTERY_LEVEL);
|
||||
|
||||
if(batteryCharacteristic != null){
|
||||
builder.add(new ReadAction(batteryCharacteristic));
|
||||
}
|
||||
|
||||
loadConfiguration();
|
||||
|
||||
gbDevice.setFirmwareVersion("1.0.0");
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void handleMeasurementCharacteristic(BluetoothGattCharacteristic characteristic){
|
||||
byte[] value = characteristic.getValue();
|
||||
if(value == null || value.length < 7){
|
||||
logger.error("Measurement characteristic value length smaller than 7");
|
||||
return;
|
||||
}
|
||||
|
||||
CyclingSpeedCadenceMeasurement measurement = null;
|
||||
|
||||
try {
|
||||
measurement = CyclingSpeedCadenceMeasurement.fromPayload(value);
|
||||
}catch (RuntimeException e){
|
||||
// do nothing, measurement stays null
|
||||
}
|
||||
|
||||
if(measurement == null){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!measurement.revolutionDataPresent){
|
||||
return;
|
||||
}
|
||||
|
||||
handleCyclingSpeedMeasurement(measurement);
|
||||
}
|
||||
|
||||
private void handleCyclingSpeedMeasurement(CyclingSpeedCadenceMeasurement currentMeasurement) {
|
||||
logger.debug("Measurement " + currentMeasurement);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
Float speed = null;
|
||||
|
||||
long lastMeasurementDelta = (now - lastMeasurementTime);
|
||||
|
||||
if(lastMeasurementDelta <= 30_000){
|
||||
int ticksPassed = currentMeasurement.lastRevolutionTimeTicks - lastReportedMeasurement.lastRevolutionTimeTicks;
|
||||
// every second is subdivided in 1024 ticks
|
||||
int millisDelta = (int)(ticksPassed * (1000f / 1024f));
|
||||
|
||||
if(millisDelta > 0) {
|
||||
int revolutionsDelta = currentMeasurement.revolutionCount - lastReportedMeasurement.revolutionCount;
|
||||
|
||||
float revolutionsPerSecond = revolutionsDelta * (1000f / millisDelta);
|
||||
|
||||
speed = revolutionsPerSecond * wheelCircumference;
|
||||
}
|
||||
}
|
||||
|
||||
lastReportedMeasurement = currentMeasurement;
|
||||
lastMeasurementTime = now;
|
||||
|
||||
if(now < nextPersistenceTimestamp){
|
||||
// too early
|
||||
return;
|
||||
}
|
||||
|
||||
nextPersistenceTimestamp = now + persistenceInterval;
|
||||
|
||||
CyclingSample sample = new CyclingSample();
|
||||
|
||||
if (currentMeasurement.revolutionDataPresent) {
|
||||
sample.setRevolutionCount(currentMeasurement.revolutionCount);
|
||||
sample.setSpeed(speed);
|
||||
sample.setDistance(currentMeasurement.revolutionCount * wheelCircumference);
|
||||
}
|
||||
|
||||
sample.setTimestamp(now);
|
||||
|
||||
Intent liveIntent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES);
|
||||
liveIntent.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
|
||||
LocalBroadcastManager.getInstance(getContext())
|
||||
.sendBroadcast(liveIntent);
|
||||
|
||||
try(DBHandler handler = GBApplication.acquireDB()) {
|
||||
DaoSession session = handler.getDaoSession();
|
||||
|
||||
CyclingSampleProvider sampleProvider =
|
||||
new CyclingSampleProvider(getDevice(), session);
|
||||
|
||||
Device databaseDevice = DBHelper.getDevice(getDevice(), session);
|
||||
User databaseUser = DBHelper.getUser(session);
|
||||
sample.setDevice(databaseDevice);
|
||||
sample.setUser(databaseUser);
|
||||
|
||||
sampleProvider.addSample(sample);
|
||||
} catch (Exception e) {
|
||||
// throw new RuntimeException(e);
|
||||
logger.error("failed adding DB cycling sample");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
byte[] value = characteristic.getValue();
|
||||
|
||||
if(characteristic.equals(batteryCharacteristic) && value != null && value.length == 1){
|
||||
GBDeviceEventBatteryInfo info = new GBDeviceEventBatteryInfo();
|
||||
info.level = characteristic.getValue()[0];
|
||||
handleGBDeviceEvent(info);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
if(characteristic.getUuid().equals(UUID_CYCLING_SENSOR_CSC_MEASUREMENT)){
|
||||
handleMeasurementCharacteristic(characteristic);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
16
app/src/main/res/layout/fragment_cycling.xml
Normal file
16
app/src/main/res/layout/fragment_cycling.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.gridlayout.widget.GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:grid="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
grid:columnCount="2">
|
||||
|
||||
<com.github.mikephil.charting.charts.LineChart
|
||||
android:id="@+id/chart_cycling_history"
|
||||
grid:layout_columnSpan="2"
|
||||
grid:layout_columnWeight="1"
|
||||
grid:layout_rowWeight="1">
|
||||
</com.github.mikephil.charting.charts.LineChart>
|
||||
|
||||
|
||||
</androidx.gridlayout.widget.GridLayout>
|
@ -3014,6 +3014,7 @@
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_charts_tabs_items_default">
|
||||
<item>@string/p_cycling</item>
|
||||
<item>@string/p_activity</item>
|
||||
<item>@string/p_activity_list</item>
|
||||
<item>@string/p_sleep</item>
|
||||
|
@ -2832,4 +2832,14 @@
|
||||
<string name="pref_title_huawei_account">Huawei Account</string>
|
||||
<string name="pref_summary_huawei_account">Huawei account used in pairing process. Setting it allows to pair without factory reset.</string>
|
||||
<string name="watchface_resolution_doesnt_match">Watchface resolution doesnt match device screen. Watchface is %1$s device screen is %2$s</string>
|
||||
<string name="device_name_bicycle_sensor">Bicycle sensor</string>
|
||||
<string name="device_name_cycling_sensor">Cycling sensor</string>
|
||||
<string name="devicetype_cycling_sensor">Cycling speed sensor</string>
|
||||
<string name="title_cycling">Cycling</string>
|
||||
<string name="pref_summary_wheel_diameter">Wheel diameter in inches. Typically 29, 27,5 or 26.</string>
|
||||
<string name="pref_title_wheel_diameter">Wheel diameter</string>
|
||||
<string name="pref_summary_cycling_persistence_interval">Interval in seconds when the current cycling data should be written to the database</string>
|
||||
<string name="pref_title_cycling_persistence_interval">Persistence interval</string>
|
||||
<string name="chart_cycling_point_label_distance">Today: %.1f km\nTotal: %.1f km</string>
|
||||
<string name="chart_cycling_point_label_speed">%.1f km/h</string>
|
||||
</resources>
|
||||
|
@ -96,6 +96,7 @@
|
||||
<item name="p_call_privacy_mode_number" type="string">number</item>
|
||||
<item name="p_call_privacy_mode_complete" type="string">complete</item>
|
||||
|
||||
<item name="p_cycling" type="string">cycling</item>
|
||||
<item name="p_activity" type="string">activity</item>
|
||||
<item name="p_activity_list" type="string">activitylist</item>
|
||||
<item name="p_sleep" type="string">sleep</item>
|
||||
|
21
app/src/main/res/xml/devicesettings_cycling_sensor.xml
Normal file
21
app/src/main/res/xml/devicesettings_cycling_sensor.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue="29"
|
||||
android:inputType="number"
|
||||
android:title="@string/pref_title_wheel_diameter"
|
||||
android:summary="@string/pref_summary_wheel_diameter"
|
||||
android:icon="@drawable/ic_activity_biking"
|
||||
android:key="pref_cycling_wheel_diameter" />
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue="60"
|
||||
android:inputType="number"
|
||||
android:title="@string/pref_title_cycling_persistence_interval"
|
||||
android:summary="@string/pref_summary_cycling_persistence_interval"
|
||||
android:icon="@drawable/ic_activity_biking"
|
||||
android:key="pref_cycling_persistence_interval" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user