Compare commits

...

39 Commits

Author SHA1 Message Date
Dennis Guse 2ec82af8f1 Release: v4.26.0 2026-02-15 18:39:39 +01:00
Dennis Guse 447d1bb7a3 Bugfix: speed and altitude were set to 0m repeatedly (UI only. Data is okay).
Fixes #2258.
2026-02-15 09:08:48 +01:00
Dennis Guse 2f6765e45f Bugfix: UI didn't show altitude in EGM2008 (but rather WGS84).
Introduced in 015352c989

Fixes #2279.
2026-02-15 09:08:48 +01:00
Dennis Guse 90bf2e6ab0 Cleanup: test is now working.
Part of #1279.
2026-02-15 09:08:48 +01:00
Dennis Guse d12423522e Cleanup. 2026-02-15 09:08:48 +01:00
Dennis Guse 24f052491b Cleanup: Move current time handling into SensorManager. 2026-02-15 09:08:48 +01:00
CI/CD Bot by pstorch 6b4e7d7d9b Update dependency com.android.tools.build:gradle to v9.0.1 2026-02-13 17:35:00 +00:00
Dennis Guse 00fcbb070a Cleanup. 2026-02-10 22:04:47 +01:00
Dennis Guse 708cbf0b2a Refactored Sensor subsystem 2026-02-10 18:30:15 +01:00
Dennis Guse 60e2fe57f9 Bugfix: SensorHandler may receive a connectionLost from BluetoothDriver without being connected first. 2026-02-10 18:13:13 +01:00
Dennis Guse 14cebf3fec Aggregator: there are no default values anymore. 2026-02-10 11:13:57 +01:00
Dennis Guse b651a8ca6a GPS is now optional: data source (internal, none) be selected in settings.
Migrated to GPSHandler.

Fixes #1279.
2026-02-10 10:07:41 +01:00
Dennis Guse d75472ef11 BluetoothLeSensorPreference was not adhering to configured default value. 2026-02-09 18:15:37 +01:00
Dennis Guse 0b849c3d55 CyclingDistanceSpeedHandler: uses wheel size (again).
Skipped while to SensorHandler: a5dcbec3961957dfc651cf4ffd62ec075324c0ba
2026-02-09 18:15:37 +01:00
Dennis Guse 1c767c41a7 DriverObserver: renamed methods and all drivers properly use onConnected, onConnectionLost and onDisconnected. 2026-02-09 18:15:37 +01:00
Dennis Guse dfa80d61d7 Restored functionality: sensor name/address is shown AFTER connecting. 2026-02-09 18:15:37 +01:00
Dennis Guse fa558c9ab7 Cleanup: SensorManager avoids instanceof for testing. 2026-02-09 18:15:37 +01:00
Dennis Guse a1a363698f SensorHandler now interacts with Aggregator. 2026-02-09 18:15:37 +01:00
Dennis Guse b79acfb942 Cleanup: SensorManager provides context and handler. 2026-02-09 18:15:37 +01:00
Dennis Guse 24ea718ff2 Cleanup: renamed GainManager to AltitudeChangeHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 6d187fa66d Cleanup: removed DriverObserver0 and moved sensor drivers to separate package. 2026-02-09 18:15:37 +01:00
Dennis Guse ea27db13fa Created RunningSpeedCadenceDistanceHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 29d2e7c78c Created CyclingPowerHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 4b7badbecc Created CyclingDistanceSpeedHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 0dcb8010e2 Created CyclingCadenceHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 0a9d4b207c Created TemperatureHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse c6cb388ce4 Created HeartRateHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse e6e009ba14 GainManager should only re-connect if sensor should be changed. 2026-02-09 18:15:37 +01:00
Dennis Guse f8aa456549 Extract SensorHandler from GainManager for re-use. 2026-02-09 18:15:37 +01:00
Dennis Guse 167d90332e Extract Driver for internal GPS to GpsInternal. 2026-02-09 18:15:37 +01:00
Dennis Guse 719a052f34 Cleanup: merged BluetoothRemoteSensorManager into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 244ca26210 Cleanup: created new DriverObserver interface. 2026-02-09 18:15:37 +01:00
Dennis Guse 476f3a517b Cleanup: GainManager now processes AggregatorBarometer directly (instead of SensorManager). 2026-02-09 18:15:37 +01:00
Dennis Guse 289e93eb5d Cleanup: moved AggregatorGPS to GPSManager. 2026-02-09 18:15:37 +01:00
Dennis Guse 18ff96733e Cleanup: moved Raw into Aggregator.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 0f2c5adf26 Cleanup: SensorManager.SensorDataChangedObserver inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 74954df600 Cleanup: SensorManager.SensorDataChangedObserver inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 1a14fed287 Cleanup: SensorDataAggregator inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 744853ee91 Cleanup: removed SensorConnector interface.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
74 changed files with 1540 additions and 1371 deletions
+3 -3
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:9.0.0'
classpath 'com.android.tools.build:gradle:9.0.1'
}
}
@@ -47,8 +47,8 @@ android {
defaultConfig {
applicationId 'de.dennisguse.opentracks'
versionCode 6564
versionName "v4.25.2"
versionCode 6603
versionName "v4.26.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -0,0 +1,12 @@
v4.26.0: OpenTracks
Changes:
- GPS is optional: configurable in the settings
- BLE sensor address/name is only shown after successful connection
Bugfix:
- Altitude is now shown again in EGM2008 (introduced in v4.25.0)
- Altitude and speed were shown as 0.0 briefly when data was stored
Developer:
- Refactored sensor subsystem
@@ -66,7 +66,8 @@ import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
@@ -74,10 +75,9 @@ import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpee
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataAggregator;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
@@ -94,17 +94,19 @@ public class ExportImportTest {
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
//For csv_export_only() as we the timezone is hardcoded in the expectation.
//For csv_export_only() as the timezone is hardcoded in the expectation.
@Rule
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
private static final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
private final Context context = ApplicationProvider.getApplicationContext();
PreferencesUtils.resetPreferences(context, true);
}
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -162,7 +164,7 @@ public class ExportImportTest {
"Marker 1 desc",
"Marker 1 typeLocalized",
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
Instant.parse("2020-02-02T02:02:03Z"),
3.1234567, 14.0014567,
Distance.of(10),
Altitude.WGS84.of(1020.25),
@@ -197,7 +199,7 @@ public class ExportImportTest {
));
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.getSensorManager().sensorDataAggregator = new SensorDataAggregator();
trackPointCreator.getSensorManager().clear();
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -212,7 +214,7 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3.1234567, 16.001, 10, 27, 15, 999.123, 0f);
trackPointCreator.getSensorManager().sensorDataAggregator = new SensorDataAggregator();
trackPointCreator.getSensorManager().clear();
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
@@ -660,17 +662,17 @@ public class ExportImportTest {
}
}
private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain, float temperature) {
SensorDataAggregator sensorDataAggregator = trackPointCreator.getSensorManager().sensorDataAggregator;
private static void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain, float temperature) {
SensorManager sensorManager = trackPointCreator.getSensorManager();
AggregatorCyclingPower cyclingPower = new AggregatorCyclingPower("", "");
cyclingPower.add(new Raw<>(trackPointCreator.createNow(), new BluetoothHandlerCyclingPower.Data(Power.of(power), null)));
sensorDataAggregator.add(cyclingPower);
cyclingPower.add(trackPointCreator.getNow(), new CyclingPowerBluetooth.Data(Power.of(power), null));
sensorManager.setAggregator(cyclingPower);
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
avgHeartRate.add(new Raw<>(trackPointCreator.createNow(), HeartRate.of(heartRate)));
sensorDataAggregator.add(avgHeartRate);
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(heartRate));
sensorManager.setAggregator(avgHeartRate);
AggregatorCyclingCadence cyclingCadence = new AggregatorCyclingCadence("", "") {
@NonNull
@@ -684,7 +686,7 @@ public class ExportImportTest {
return true;
}
};
sensorDataAggregator.add(cyclingCadence);
sensorManager.setAggregator(cyclingCadence);
if (distance != null && speed != null) {
AggregatorCyclingDistanceSpeed aggregatorCyclingDistanceSpeed = new AggregatorCyclingDistanceSpeed("", "") {
@@ -699,14 +701,14 @@ public class ExportImportTest {
return true;
}
};
sensorDataAggregator.add(aggregatorCyclingDistanceSpeed);
sensorManager.setAggregator(aggregatorCyclingDistanceSpeed);
} else {
sensorDataAggregator.add(new AggregatorCyclingDistanceSpeed("", ""));
sensorManager.setAggregator(new AggregatorCyclingDistanceSpeed("", ""));
}
mockAltitudeChange(trackPointCreator, altitudeGain);
sensorDataAggregator.add(new AggregatorTemperature("", "'") {
sensorManager.setAggregator(new AggregatorTemperature("", "'") {
@NonNull
@Override
public Temperature getAggregatedValue(Instant now) {
@@ -722,11 +724,11 @@ public class ExportImportTest {
trackPointCreator.onChange();
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
SensorDataAggregator sensorDataAggregator = trackPointCreator.getSensorManager().sensorDataAggregator;
private static void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
SensorManager sensorManager = trackPointCreator.getSensorManager();
if (altitudeGain == null) {
sensorDataAggregator.add(new AggregatorBarometer("test", null));
sensorManager.getAltitudeChangeHandler().setAggregator(new AggregatorBarometer("test", null));
return;
}
@@ -742,10 +744,10 @@ public class ExportImportTest {
return true;
}
};
sensorDataAggregator.add(aggregatorBarometer);
sensorManager.getAltitudeChangeHandler().setAggregator(aggregatorBarometer);
}
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
public static void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
@@ -757,6 +759,6 @@ public class ExportImportTest {
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
}
}
@@ -26,6 +26,7 @@ public class TrackPointAssert {
Assert.assertEquals("has altitude", expected.position().hasAltitude(), actual.position().hasAltitude());
if (expected.position().hasAltitude()) {
Assert.assertEquals("altitude", expected.position().altitude().getClass(), actual.position().altitude().getClass());
Assert.assertEquals("altitude", expected.position().altitude().toM(), actual.position().altitude().toM(), delta);
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -13,11 +13,11 @@ public class BluetoothHandlerBarometricPressureTest {
@Test
public void parseEnvironmentalSensing_Pa() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BarometerBluetooth.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
// when
AtmosphericPressure pressure = BluetoothHandlerBarometricPressure.parseEnvironmentalSensing(characteristic);
AtmosphericPressure pressure = BarometerBluetooth.parseEnvironmentalSensing(characteristic);
// then
assertEquals(AtmosphericPressure.ofHPA(1001.65f), pressure);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -13,11 +13,11 @@ public class BluetoothHandlerHeartRateTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(HeartRateBluetooth.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
HeartRate heartRate = BluetoothHandlerHeartRate.parseHeartRate(characteristic);
HeartRate heartRate = HeartRateBluetooth.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(60), heartRate);
@@ -26,11 +26,11 @@ public class BluetoothHandlerHeartRateTest {
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(HeartRateBluetooth.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
HeartRate heartRate = BluetoothHandlerHeartRate.parseHeartRate(characteristic);
HeartRate heartRate = HeartRateBluetooth.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(257), heartRate);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -8,14 +8,14 @@ import android.util.Pair;
import org.junit.Test;
public class BluetoothHandlerCyclingDistanceSpeedTest {
public class CyclingDistanceSpeedBluetoothTest {
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertNull(sensor.first);
@@ -24,11 +24,11 @@ public class BluetoothHandlerCyclingDistanceSpeedTest {
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertEquals(65535 + 16777216, sensor.first.wheelRevolutionsCount());
@@ -37,11 +37,11 @@ public class BluetoothHandlerCyclingDistanceSpeedTest {
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertEquals(200 + 16777216, sensor.first.wheelRevolutionsCount());
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -6,15 +6,15 @@ import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
public class BluetoothHandlerCyclingPowerTest {
public class CyclingPowerBluetoothTest {
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingPowerBluetooth.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
BluetoothHandlerCyclingPower.Data powerCadence = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
CyclingPowerBluetooth.Data powerCadence = CyclingPowerBluetooth.parseCyclingPower(characteristic);
// then
assertEquals(40, powerCadence.power().getW(), 0.01);
@@ -22,11 +22,11 @@ public class BluetoothHandlerCyclingPowerTest {
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingPowerBluetooth.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
BluetoothHandlerCyclingPower.Data powerCadence = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
CyclingPowerBluetooth.Data powerCadence = CyclingPowerBluetooth.parseCyclingPower(characteristic);
// then
assertEquals(0, powerCadence.power().getW(), 0.01);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -10,15 +10,15 @@ import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
public class BluetoothHandlerRunningSpeedAndCadenceTest {
public class RunningSpeedAndCadenceBluetoothTest {
@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(RunningSpeedAndCadenceBluetooth.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
// when
BluetoothHandlerRunningSpeedAndCadence.Data sensor = BluetoothHandlerRunningSpeedAndCadence.parseRunningSpeedAndCadence("sensorName", characteristic);
RunningSpeedAndCadenceBluetooth.Data sensor = RunningSpeedAndCadenceBluetooth.parseRunningSpeedAndCadence("sensorName", characteristic);
// then
assertEquals(Speed.of(5), sensor.speed());
@@ -11,7 +11,7 @@ public class AggregatorBarometerTest {
private static void addSensorValue(AggregatorBarometer aggregatorBarometer, float[] values) {
for (float f : values) {
aggregatorBarometer.add(new Raw<>(Instant.MIN, AtmosphericPressure.ofHPA(f)));
aggregatorBarometer.add(Instant.MIN, AtmosphericPressure.ofHPA(f));
}
}
@@ -24,7 +24,7 @@ public class AggregatorBarometerTest {
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
// then
Assert.assertEquals(0f, subject.aggregatedValue.gain_m(), 0.01);
Assert.assertEquals(15f, subject.aggregatedValue.loss_m(), 0.01);
Assert.assertEquals(0f, subject.output.gain_m(), 0.01);
Assert.assertEquals(15f, subject.output.loss_m(), 0.01);
}
}
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.sensors.sensorData;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -12,10 +13,9 @@ import java.time.Instant;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.UintUtils;
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
@RunWith(AndroidJUnit4.class)
public class SensorDataCyclingTest {
@@ -25,8 +25,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 2048));
// then
assertEquals(60, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -37,8 +37,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 6184)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 8016)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 6184));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 8016));
// then
assertEquals(33.53, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -49,8 +49,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 2048));
// then
assertEquals(Cadence.of(0), current.getAggregatedValue(Instant.MIN));
@@ -62,8 +62,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 1024)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 1024));
// then
assertFalse(current.hasReceivedData()); //TODO Cadence should be 0?
@@ -74,8 +74,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 0)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, UintUtils.UINT16_MAX - 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 0));
// then
assertEquals(60, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -87,13 +87,13 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(0, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(UintUtils.UINT32_MAX - 1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(0, 2048));
// then
// TODO See #953
// assertEquals(60, current.getValue().getRPM(), 0.01);
assertEquals(Cadence.of(0), current.getAggregatedValue(Instant.MIN));
assertNull(current.getAggregatedValue(Instant.MIN));
}
@Test
@@ -102,8 +102,8 @@ public class SensorDataCyclingTest {
current.setWheelCircumference(Distance.ofMM(2150));
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016)));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(1, 6184));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(2, 8016));
// then
assertEquals(2.15, current.getAggregatedValue(Instant.MIN).distance().toM(), 0.01);
@@ -117,14 +117,13 @@ public class SensorDataCyclingTest {
current.setWheelCircumference(Distance.ofMM(2000));
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(0, 2048)));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(UintUtils.UINT32_MAX - 1, 1024));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(0, 2048));
// then
// TODO See #953
// assertEquals(2, current.getValue().getDistance().toM(), 0.01);
// assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
assertEquals(new AggregatorCyclingDistanceSpeed.Data(Distance.ZERO, Distance.ZERO, Speed.ZERO), current.getAggregatedValue(Instant.MIN));
assertNull(current.getAggregatedValue(Instant.MIN));
}
}
@@ -6,7 +6,6 @@ import static org.junit.Assert.assertFalse;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationManager;
import android.os.Looper;
import androidx.annotation.NonNull;
@@ -44,12 +43,12 @@ import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -154,7 +153,8 @@ public class TrackRecordingServiceRecordingTest {
Track.Id trackId = service.startNewTrack();
//We do not want the real GPS to interfere.
LocationManagerCompat.removeUpdates((LocationManager) context.getSystemService(Context.LOCATION_SERVICE), trackPointCreator.getSensorManager().getGpsManager());
GpsInternal gpsInternal = (GpsInternal)trackPointCreator.getSensorManager().getGpsHandler().driver;
LocationManagerCompat.removeUpdates(gpsInternal.locationManager, gpsInternal.locationListenerCompat);
String gps1 = "2020-02-02T02:02:03Z";
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
@@ -339,16 +339,21 @@ public class TrackRecordingServiceRecordingTest {
mockAltitudeChange(trackPointCreator, 0);
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataAggregator.add(new AggregatorHeartRate("", ""));
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(sensor1, HeartRate.of(5))); //Should be ignored
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
sensorManager.setAggregator(avgHeartRate);
//Should be ignored
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(5));
sensorManager.onChange();
String sensor3 = "2020-02-02T02:02:13Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(sensor3, HeartRate.of(7)));
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(7));
sensorManager.onChange();
String stopTime = "2020-02-02T02:02:15Z";
trackPointCreator.setClock(stopTime);
@@ -808,19 +813,22 @@ public class TrackRecordingServiceRecordingTest {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
AggregatorRunning aggregatorRunning = new AggregatorRunning("", "");
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataAggregator.add(new AggregatorRunning("", ""));
sensorManager.sensorDataAggregator.barometer = null;
sensorManager.setAggregator(aggregatorRunning);
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(sensor1, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(1), Distance.ZERO))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(1), Distance.ZERO));
sensorManager.onChange();
// when
String sensor2 = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(sensor2);
sensorManager.onChanged(new Raw<>(sensor2, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(2), Distance.of(2))));
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(2), Distance.of(2)));
sensorManager.onChange();
// when
String gps1 = "2020-02-02T02:02:05Z";
@@ -829,12 +837,15 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor3 = "2020-02-02T02:02:06Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(sensor3, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(3), Distance.of(12))));
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(3), Distance.of(12)));
sensorManager.onChange();
// when
String sensor4 = "2020-02-02T02:02:07Z";
trackPointCreator.setClock(sensor4);
sensorManager.onChanged(new Raw<>(sensor4, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(4), Distance.of(14)))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(4), Distance.of(14)));
sensorManager.onChange();
// when
String gps2 = "2020-02-02T02:02:08Z";
@@ -843,7 +854,9 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor5 = "2020-02-02T02:02:10Z";
trackPointCreator.setClock(sensor5);
sensorManager.onChanged(new Raw<>(sensor5, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(5), Distance.of(16)))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(5), Distance.of(16)));
sensorManager.onChange();
// when
String gps3 = "2020-02-02T02:02:12Z";
@@ -962,7 +975,7 @@ public class TrackRecordingServiceRecordingTest {
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
trackPointCreator.getSensorManager().sensorDataAggregator.barometer = new AggregatorBarometer("", "") {
trackPointCreator.getSensorManager().getAltitudeChangeHandler().setAggregator(new AggregatorBarometer("", "") {
@Override
public boolean hasReceivedData() {
return true;
@@ -973,7 +986,7 @@ public class TrackRecordingServiceRecordingTest {
public AltitudeGainLoss getAggregatedValue(Instant now) {
return new AltitudeGainLoss(altitudeGain, altitudeGain);
}
};
});
}
private static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
@@ -985,6 +998,6 @@ public class TrackRecordingServiceRecordingTest {
location.setSpeed(speed);
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
}
}
@@ -135,7 +135,7 @@ public class TrackRecordingServiceStateMachineTest {
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
// assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); TODO BUG: GPS can only be stopped if a service is recording.
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
}
@MediumTest
@@ -0,0 +1,112 @@
package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static de.dennisguse.opentracks.services.TrackRecordingService.RECORDING_DATA_UPDATE_INTERVAL;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import androidx.core.location.LocationManagerCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.ExportImportTest;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceUITest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
private static final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@Test
public void getDataForUI_gps() throws TimeoutException, InterruptedException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
RecordingData recordingData;
// given
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock("2020-02-02T02:02:02Z");
// when
service.startNewTrack();
//We do not want the real GPS to interfere.
GpsInternal gpsInternal = (GpsInternal)trackPointCreator.getSensorManager().getGpsHandler().driver;
LocationManagerCompat.removeUpdates(gpsInternal.locationManager, gpsInternal.locationListenerCompat);
Thread.sleep(RECORDING_DATA_UPDATE_INTERVAL.toMillis());
// then
assertEquals(new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of( Instant.parse("2020-02-02T02:02:02Z")),
null,
null,
null,
null,
null,
null
),
service.getRecordingDataObservable().getValue().latestTrackPoint());
// when
// In this step, the data is saved (and Aggregator.resetAggregated called), but data should still be shown in the UI.
trackPointCreator.setClock("2020-02-02T02:02:03Z");
ExportImportTest.sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
// then
Thread.sleep(RECORDING_DATA_UPDATE_INTERVAL.toMillis());
recordingData = service.getRecordingDataObservable().getValue();
new TrackPointAssert().assertEquals(new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.1234567, 14.0014567, null,
Altitude.EGM2008.of(1013.05), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
recordingData.latestTrackPoint());
}
}
@@ -8,7 +8,7 @@ import androidx.annotation.Nullable;
import java.time.Instant;
public record Position(
@Nullable Instant time,
@Nullable Instant time, //TODO It may be a good idea to move time out of Position
@Nullable Double latitude,
@Nullable Double longitude,
@Nullable Distance horizontalAccuracy,
@@ -17,10 +17,6 @@ public record Position(
@Nullable Float bearing,
@Nullable Speed speed
) {
@Deprecated
public static Position empty() {
return of(null);
}
public static Position of(@NonNull Instant time) {
return new Position(
@@ -0,0 +1,50 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.driver.BarometerBluetooth;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
/**
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/
//TODO Rename class
public class AltitudeChangeHandler extends SensorHandler<AtmosphericPressure, AltitudeGainLoss> {
public AltitudeChangeHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverInternal() {
return new BarometerInternal(this);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new BarometerBluetooth()
);
}
@NonNull
@Override
public AggregatorBarometer createAggregator(String sensorAddress, String sensorName) {
return new AggregatorBarometer(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_pressure_key;
}
}
@@ -1,57 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import android.util.Pair;
import java.util.List;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
private static final String TAG = BluetoothHandlerCyclingCadence.class.getSimpleName();
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
BluetoothHandlerCyclingPower.CYCLING_POWER,
BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return CYCLING_CADENCE;
}
@Override
public AggregatorCyclingCadence createEmptySensorData(String address, String name) {
return new AggregatorCyclingCadence(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingPower.CYCLING_POWER)) {
BluetoothHandlerCyclingPower.Data data = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
if (data != null && data.crank() != null) {
observer.onChange(new Raw<>(observer.getNow(), data.crank()));
}
return;
}
if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE)) {
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, CrankData> data = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (data != null && data.second != null) {
observer.onChange(new Raw<>(observer.getNow(), data.second));
}
return;
}
Log.e(TAG, "Don't know how to decode this payload.");
}
public record CrankData(
long crankRevolutionsCount, // UINT32
int crankRevolutionsTime // UINT16; 1/1024s
) {}
}
@@ -1,53 +0,0 @@
package de.dennisguse.opentracks.sensors;
import static de.dennisguse.opentracks.sensors.BluetoothHandlerBarometricPressure.ENVIRONMENTAL_SENSING_SERVICE;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerTemperature implements SensorHandlerInterface {
public static final ServiceMeasurementUUID TEMPERATURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6E00001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(TEMPERATURE);
}
@Override
public Aggregator<?, ?> createEmptySensorData(String address, String name) {
return new AggregatorTemperature(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Temperature value = parseEnvironmentalSensing(characteristic);
if (value == null) return;
observer.onChange(new Raw<>(observer.getNow(), value));
}
/**
* Decoding: org.bluetooth.characteristic.temperature.xml
*/
public static Temperature parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
if (raw.length < 2) {
return null;
}
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 0);
return Temperature.ofCentiCelsius(pressure);
}
}
@@ -1,126 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
* <p>
* Note: should only be instantiated once.
* TODO: listen for Bluetooth enabled/disabled events.
* @author Sandor Dornbush
*/
public class BluetoothRemoteSensorManager implements SensorConnector, SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
private final Context context;
private final Handler handler;
private boolean started = false;
private final BluetoothConnectionManager heartRate;
private final BluetoothConnectionManager temperature;
private final BluetoothConnectionManager cyclingCadence;
private final BluetoothConnectionManager cyclingSpeed;
private final BluetoothConnectionManager cyclingPower;
private final BluetoothConnectionManager runningSpeedAndCadence;
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) {
this.context = context;
this.handler = handler;
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerHeartRate());
this.temperature = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerTemperature());
this.cyclingCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingCadence());
this.cyclingSpeed = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingPower());
this.runningSpeedAndCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerRunningSpeedAndCadence());
}
@Override
public void start(Context context, Handler handler) {
started = true;
// Triggers connection startup
onSharedPreferenceChanged(null, null);
}
@Override
public synchronized void stop(Context context) {
heartRate.disconnect();
temperature.disconnect();
cyclingCadence.disconnect();
cyclingSpeed.disconnect();
cyclingPower.disconnect();
runningSpeedAndCadence.disconnect();
started = false;
}
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
connectionManager.connect(context, handler, address);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences unused, @Nullable String key) {
if (!started) return;
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress();
connect(heartRate, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_temperature_key, key)) {
String address = PreferencesUtils.getBluetoothTemperatureSensorAddress();
connect(temperature, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress();
connect(cyclingCadence, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_speed_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingSpeedSensorAddress();
connect(cyclingSpeed, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress();
connect(cyclingPower, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_running_speed_and_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothRunningSpeedAndCadenceAddress();
connect(runningSpeedAndCadence, address);
}
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
public class CyclingCadenceHandler extends SensorHandler<CyclingCadenceBluetooth.CrankData, Cadence> {
protected CyclingCadenceHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new CyclingCadenceBluetooth()
);
}
@NonNull
@Override
protected AggregatorCyclingCadence createAggregator(String sensorAddress, String sensorName) {
return new AggregatorCyclingCadence(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_cycling_cadence_key;
}
}
@@ -0,0 +1,46 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class CyclingDistanceSpeedHandler extends SensorHandler<CyclingDistanceSpeedBluetooth.WheelData, AggregatorCyclingDistanceSpeed.Data> {
protected CyclingDistanceSpeedHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new CyclingDistanceSpeedBluetooth()
);
}
@NonNull
@Override
protected AggregatorCyclingDistanceSpeed createAggregator(String sensorAddress, String sensorName) {
return new AggregatorCyclingDistanceSpeed(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_cycling_speed_key;
}
@Override
public void onDataReceived(CyclingDistanceSpeedBluetooth.WheelData value) {
//TODO Fetch once and then listen for changes.
((AggregatorCyclingDistanceSpeed) aggregator).setWheelCircumference(PreferencesUtils.getWheelCircumference());
super.onDataReceived(value);
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
public class CyclingPowerHandler extends SensorHandler<CyclingPowerBluetooth.Data, Power> {
protected CyclingPowerHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new CyclingPowerBluetooth()
);
}
@NonNull
@Override
protected AggregatorCyclingPower createAggregator(String sensorAddress, String sensorName) {
return new AggregatorCyclingPower(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_cycling_power_key;
}
}
@@ -1,77 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/
public class GainManager implements SensorConnector {
private static final String TAG = GainManager.class.getSimpleName();
private final SensorManager.SensorDataChangedObserver listener;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> connect();
private Context context;
private Handler handler;
private Driver driver;
public GainManager(SensorManager.SensorDataChangedObserver listener) {
this.listener = listener;
}
public void start(Context context, Handler handler) {
this.context = context;
this.handler = handler;
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
public void stop(Context context) {
Log.d(TAG, "Stop");
this.context = null;
this.handler = null;
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
onDisconnect();
}
private void connect() {
onDisconnect();
String address = PreferencesUtils.getBarometerSensorAddress();
switch (PreferencesUtils.getSensorType(address)) {
case NONE -> {
driver = null;
listener.onRemove(new AggregatorBarometer(null, null));
return;
}
case INTERNAL -> driver = new BarometerInternal(listener);
case REMOTE -> driver =
new BluetoothConnectionManager(
BluetoothUtils.getAdapter(context),
listener,
new BluetoothHandlerBarometricPressure()
);
default -> throw new RuntimeException("Not implemented");
}
driver.connect(context, handler, address);
}
private void onDisconnect() {
if (driver == null) return;
driver.disconnect();
listener.onDisconnect(new AggregatorBarometer("GainManager", null));
}
}
@@ -0,0 +1,120 @@
package de.dennisguse.opentracks.sensors;
import android.content.SharedPreferences;
import android.location.Location;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.sensors.driver.SensorType;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GpsHandler extends SensorHandler<Location, Position> {
private final String TAG = GpsHandler.class.getSimpleName();
//TODO Refactor to just pass information via SensorManager.
private TrackPointCreator trackPointCreator;
private GpsStatusManager gpsStatusManager;
private Distance thresholdHorizontalAccuracy;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
if (gpsStatusManager != null) {
Distance gpsMinDistance = PreferencesUtils.getRecordingDistanceInterval();
gpsStatusManager.onRecordingDistanceChanged(gpsMinDistance);
}
}
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
}
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
if (gpsStatusManager != null) {
gpsStatusManager.onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval());
}
//TODO
connect();
}
};
public GpsHandler(SensorManager sensorManager, TrackPointCreator trackPointCreator) {
super(sensorManager);
this.trackPointCreator = trackPointCreator;
gpsStatusManager = new GpsStatusManager(sensorManager.getContext(), trackPointCreator::sendGpsStatus, sensorManager.getHandler());
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
@Override
public void onConnected(String sensorAddress, String sensorName) {
super.onConnected(sensorAddress, sensorName);
gpsStatusManager.onGpsEnabled();
gpsStatusManager.start();
}
@Override
public void onDisconnected() {
super.onDisconnected();
gpsStatusManager.stop();
}
@Override
public void onSensorDeactivated() {
super.onSensorDeactivated();
gpsStatusManager.onGpsDisabled();
}
@Override
public void onDataReceived(Location location) {
// Send each update to the status; please note that this TrackPoint is not stored.
Position position = Position.of(location, trackPointCreator.getNow());
gpsStatusManager.onNewTrackPoint(position);
//TODO We could move the following check into the AggregatorGPS
if (!position.hasValidLocation()) {
Log.w(TAG, "Ignore newTrackPoint. Location is invalid.");
return;
}
if (!position.fulfillsAccuracy(thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
super.onDataReceived(location);
}
@NonNull
@Override
protected Driver createDriverInternal() {
return new GpsInternal(this);
}
@NonNull
@Override
protected AggregatorGPS createAggregator(String sensorAddress, String sensorName) {
return new AggregatorGPS(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_gps_key;
}
@Override
protected String getSensorPreferenceDefaultValue() {
return SensorType.INTERNAL.getPreferenceValue();
}
}
@@ -1,181 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.location.LocationListenerCompat;
import androidx.core.location.LocationManagerCompat;
import androidx.core.location.LocationRequestCompat;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GpsManager implements SensorConnector, LocationListenerCompat, GpsStatusManager.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
private final String TAG = GpsManager.class.getSimpleName();
private static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
private TrackPointCreator trackPointCreator;
private SensorManager.SensorDataChangedObserver observer;
private Context context;
private Handler handler;
private LocationManager locationManager;
private GpsStatusManager gpsStatusManager;
private Duration gpsInterval;
private Distance thresholdHorizontalAccuracy;
public GpsManager(TrackPointCreator trackPointCreator, SensorManager.SensorDataChangedObserver observer) {
this.trackPointCreator = trackPointCreator;
this.observer = observer;
}
public void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context;
this.handler = handler;
onSharedPreferenceChanged(null, null);
gpsStatusManager = new GpsStatusManager(context, this, handler);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
observer.onConnect(new AggregatorGPS("internal"));
registerLocationListener();
gpsStatusManager.start();
}
private boolean isStarted() {
return locationManager != null;
}
@SuppressWarnings({"MissingPermission"})
public void stop(Context context) {
if (isStarted()) {
LocationManagerCompat.removeUpdates(locationManager, this);
}
locationManager = null;
this.context = null;
handler = null;
gpsStatusManager.stop();
gpsStatusManager = null;
observer.onDisconnect(new AggregatorGPS("internal"));
observer = null;
trackPointCreator = null;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
boolean registerListener = false;
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
registerListener = true;
gpsInterval = PreferencesUtils.getMinSamplingInterval();
if (gpsStatusManager != null) {
gpsStatusManager.onMinSamplingIntervalChanged(gpsInterval);
}
}
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
}
if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
registerListener = true;
if (gpsStatusManager != null) {
Distance gpsMinDistance = PreferencesUtils.getRecordingDistanceInterval();
gpsStatusManager.onRecordingDistanceChanged(gpsMinDistance);
}
}
if (registerListener && isStarted()) {
registerLocationListener();
}
}
/**
* Checks if location is valid and builds a track point that will be send through TrackPointCreator.
*
* @param location {@link Location} object.
*/
@Override
public void onLocationChanged(@NonNull Location location) {
// Send each update to the status; please note that this TrackPoint is not stored.
Position position = Position.of(location, trackPointCreator.createNow());
gpsStatusManager.onNewTrackPoint(position);
if (!position.hasValidLocation()) {
Log.w(TAG, "Ignore newTrackPoint. Location is invalid.");
return;
}
if (!position.fulfillsAccuracy(thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
observer.onChange(new Raw<>(observer.getNow(), position));
}
@Override
public void onStatusChanged(@NonNull String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(@NonNull String provider) {
gpsStatusManager.onGpsEnabled();
}
@Override
public void onProviderDisabled(@NonNull String provider) {
gpsStatusManager.onGpsDisabled();
}
private void registerLocationListener() {
if (!LocationManagerCompat.hasProvider(locationManager, LOCATION_PROVIDER)) {
Log.e(TAG, "Device doesn't have GPS.");
return;
}
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(gpsInterval.toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
if (PermissionRequester.GPS.hasPermission(context)) {
try {
Log.i(TAG, "Register for location updates " + context);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, this);
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
}
}
}
@Override
public void onGpsStatusChanged(GpsStatusValue currentStatus) {
trackPointCreator.sendGpsStatus(currentStatus);
}
}
@@ -31,7 +31,7 @@ class GpsStatusManager {
private Duration signalLostThreshold;
private GpsStatusValue gpsStatus = GpsStatusValue.GPS_NONE;
private GpsStatusListener client;
private GpsStatusListener observer;
private final Context context;
@Nullable
@@ -44,8 +44,8 @@ class GpsStatusManager {
};
public GpsStatusManager(Context context, GpsStatusListener client, Handler handler) {
this.client = client;
public GpsStatusManager(Context context, GpsStatusListener observer, Handler handler) {
this.observer = observer;
this.context = context;
this.handler = handler;
@@ -54,7 +54,7 @@ class GpsStatusManager {
}
public void start() {
client.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
observer.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
}
/**
@@ -62,9 +62,7 @@ class GpsStatusManager {
*/
public void stop() {
stopTimer();
client.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
client = null;
handler = null;
observer.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
}
/**
@@ -159,8 +157,8 @@ class GpsStatusManager {
private void setGpsStatus(GpsStatusValue current) {
gpsStatus = current;
if (client != null) {
client.onGpsStatusChanged(current);
if (observer != null) {
observer.onGpsStatusChanged(current);
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.HeartRateBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
public class HeartRateHandler extends SensorHandler<HeartRate, HeartRate> {
protected HeartRateHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new HeartRateBluetooth()
);
}
@NonNull
@Override
protected AggregatorHeartRate createAggregator(String sensorAddress, String sensorName) {
return new AggregatorHeartRate(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_heart_rate_key;
}
}
@@ -0,0 +1,37 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
public class RunningSpeedCadenceDistanceHandler extends SensorHandler<RunningSpeedAndCadenceBluetooth.Data, AggregatorRunning.Data> {
protected RunningSpeedCadenceDistanceHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new RunningSpeedAndCadenceBluetooth()
);
}
@NonNull
@Override
protected AggregatorRunning createAggregator(String sensorAddress, String sensorName) {
return new AggregatorRunning("", null);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_running_speed_and_cadence_key;
}
}
@@ -1,11 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.os.Handler;
public interface SensorConnector {
void start(Context context, Handler handler);
void stop(Context context);
}
@@ -0,0 +1,134 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
import de.dennisguse.opentracks.sensors.driver.SensorType;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implements DriverObserver<AggregatorInput> {
private static final String TAG = SensorHandler.class.getSimpleName();
private final SensorManager sensorManager;
@VisibleForTesting
public Driver driver;
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
protected SensorHandler(SensorManager sensorManager) {
this.sensorManager = sensorManager;
}
void connect() {
disconnect();
String address = PreferencesUtils.getString(getSensorPreferenceKey(), getSensorPreferenceDefaultValue());
switch (PreferencesUtils.getSensorType(address)) {
case NONE -> {
onDisconnected();
return;
}
case INTERNAL -> {
driver = createDriverInternal();
}
case REMOTE -> {
driver = createDriverBluetooth(address);
}
default -> throw new UnsupportedOperationException();
}
driver.connect(sensorManager.getContext(), sensorManager.getHandler(), address);
}
void disconnect() {
if (driver == null) return;
driver.disconnect();
driver = null;
}
@NonNull
protected Driver createDriverInternal() {
throw new RuntimeException("Not implemented");
}
@NonNull
protected Driver createDriverBluetooth(String address) {
throw new RuntimeException("Not implemented");
}
@NonNull
protected abstract Aggregator<AggregatorInput, AggregatorOutput> createAggregator(String sensorAddress, String sensorName);
protected abstract int getSensorPreferenceKey();
protected String getSensorPreferenceDefaultValue() {
return SensorType.NONE.getPreferenceValue();
}
protected Context getContext() {
return sensorManager.getContext();
}
void resetAggregated() {
if (aggregator != null) {
aggregator.resetAggregated();
}
}
@Nullable
SensorData<AggregatorOutput> getSensorData(Instant now) {
if (aggregator == null) return null;
if (!aggregator.hasReceivedData()) return null;
return aggregator.getAggregatedValueWithSensorName(now);
}
//TODO REMOVE
@VisibleForTesting
public void setAggregator(Aggregator<AggregatorInput, AggregatorOutput> aggregator) {
this.aggregator = aggregator;
}
public void onConnected(String sensorAddress, String sensorName) {
SensorHandler.this.aggregator = createAggregator(sensorAddress, sensorName);
}
@Override
public void onConnectionLost() {
if (SensorHandler.this.aggregator == null) {
Log.d(TAG, "Connection lost, but where not connected.");
return;
}
SensorHandler.this.aggregator.resetAggregated();
}
@Override
public void onDataReceived(AggregatorInput value) {
if (aggregator == null) {
throw new RuntimeException("Received data while being disconnected.");
}
SensorHandler.this.aggregator.add(sensorManager.getNow(), value);
sensorManager.onChange();
}
@Override
public void onDisconnected() {
SensorHandler.this.aggregator = null;
}
}
@@ -6,153 +6,333 @@ import android.os.Handler;
import android.os.PowerManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataAggregator;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
public class SensorManager implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = SensorManager.class.getSimpleName();
//TODO Should be final and not be visible for testing
@VisibleForTesting
public SensorDataAggregator sensorDataAggregator;
private Context context;
private Handler handler;
private PowerManager.WakeLock wakeLock;
private final TrackPointCreator observer;
private final SensorDataChangedObserver listener = new SensorDataChangedObserver() {
private HeartRateHandler heartRateHandler;
private TemperatureHandler temperatureHandler;
private CyclingCadenceHandler cyclingCadenceHandler;
private CyclingDistanceSpeedHandler cyclingDistanceSpeedHandler;
private CyclingPowerHandler cyclingPowerHandler;
private RunningSpeedCadenceDistanceHandler runningSpeedCadenceDistanceHandler;
@Override
public void onConnect(Aggregator<?, ?> aggregator) {
sensorDataAggregator.add(aggregator);
}
private AltitudeChangeHandler altitudeChangeHandler;
@Override
public void onChange(Raw<?> data) {
sensorDataAggregator.update(data);
observer.onChange();
}
@Override
public void onDisconnect(Aggregator<?, ?> aggregator) {
sensorDataAggregator.add(aggregator);
}
@Override
public void onRemove(Aggregator<?, ?> aggregator) {
sensorDataAggregator.remove(aggregator);
}
@Override
public Instant getNow() {
return observer.createNow();
}
};
private BluetoothRemoteSensorManager bluetoothSensorManager;
private GainManager altitudeSumManager;
private GpsManager gpsManager;
private GpsHandler gpsHandler;
public SensorManager(TrackPointCreator observer) {
this.observer = observer;
this.sensorDataAggregator = new SensorDataAggregator();
}
public void start(Context context, Handler handler) {
if (gpsManager != null) {
if (isStarted()) {
throw new RuntimeException("SensorManager cannot be started twice; stop first.");
}
wakeLock = SystemUtils.acquireWakeLock(context, wakeLock);
this.context = context;
this.handler = handler;
gpsManager = new GpsManager(observer, listener);
altitudeSumManager = new GainManager(listener);
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
gpsHandler = new GpsHandler(this, observer);
altitudeChangeHandler = new AltitudeChangeHandler(this);
heartRateHandler = new HeartRateHandler(this);
temperatureHandler = new TemperatureHandler(this);
cyclingCadenceHandler = new CyclingCadenceHandler(this);
cyclingDistanceSpeedHandler = new CyclingDistanceSpeedHandler(this);
cyclingPowerHandler = new CyclingPowerHandler(this);
runningSpeedCadenceDistanceHandler = new RunningSpeedCadenceDistanceHandler(this);
onSharedPreferenceChanged(null, null);
gpsManager.start(context, handler);
altitudeSumManager.start(context, handler);
bluetoothSensorManager.start(context, handler);
}
public void stop(Context context) {
public void stop() {
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
bluetoothSensorManager.stop(context);
bluetoothSensorManager = null;
heartRateHandler.disconnect();
heartRateHandler = null;
altitudeSumManager.stop(context);
altitudeSumManager = null;
temperatureHandler.disconnect();
temperatureHandler = null;
gpsManager.stop(context);
gpsManager = null;
cyclingCadenceHandler.disconnect();
cyclingCadenceHandler = null;
sensorDataAggregator.clear();
}
cyclingDistanceSpeedHandler.disconnect();
cyclingDistanceSpeedHandler = null;
public SensorDataSet getSensorDataSet(Instant now) {
return sensorDataAggregator.getSensorData(now);
cyclingPowerHandler.disconnect();
cyclingPowerHandler = null;
runningSpeedCadenceDistanceHandler.disconnect();
runningSpeedCadenceDistanceHandler = null;
altitudeChangeHandler.disconnect();
altitudeChangeHandler = null;
gpsHandler.disconnect();
gpsHandler = null;
context = null;
handler = null;
}
public void reset() {
if (bluetoothSensorManager == null || altitudeSumManager == null) {
if (gpsHandler == null || altitudeChangeHandler == null) {
Log.d(TAG, "No recording running and no reset necessary.");
return;
}
sensorDataAggregator.reset();
Log.i(TAG, "Resetting data");
heartRateHandler.resetAggregated();
temperatureHandler.resetAggregated();
cyclingCadenceHandler.resetAggregated();
cyclingDistanceSpeedHandler.resetAggregated();
cyclingPowerHandler.resetAggregated();
runningSpeedCadenceDistanceHandler.resetAggregated();
altitudeChangeHandler.resetAggregated();
gpsHandler.resetAggregated();
}
@VisibleForTesting
public void onChanged(Raw<?> data) {
listener.onChange(data);
}
public GpsManager getGpsManager() {
return gpsManager;
public GpsHandler getGpsHandler() {
return gpsHandler;
}
@Deprecated
@VisibleForTesting
public GainManager getAltitudeSumManager() {
return altitudeSumManager;
}
@Deprecated
@VisibleForTesting
public void setAltitudeSumManager(GainManager altitudeSumManager) {
this.altitudeSumManager = altitudeSumManager;
public AltitudeChangeHandler getAltitudeChangeHandler() {
return altitudeChangeHandler;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
if (gpsManager != null) {
gpsManager.onSharedPreferenceChanged(sharedPreferences, key);
bluetoothSensorManager.onSharedPreferenceChanged(sharedPreferences, key);
if (!isStarted()) return;
if (PreferencesUtils.isKey(gpsHandler.getSensorPreferenceKey(), key)) {
gpsHandler.connect();
}
if (PreferencesUtils.isKey(altitudeChangeHandler.getSensorPreferenceKey(), key)) {
altitudeChangeHandler.connect();
}
if (PreferencesUtils.isKey(heartRateHandler.getSensorPreferenceKey(), key)) {
heartRateHandler.connect();
}
if (PreferencesUtils.isKey(temperatureHandler.getSensorPreferenceKey(), key)) {
temperatureHandler.connect();
}
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
cyclingCadenceHandler.connect();
}
if (PreferencesUtils.isKey(cyclingDistanceSpeedHandler.getSensorPreferenceKey(), key)) {
cyclingDistanceSpeedHandler.connect();
}
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
cyclingPowerHandler.connect();
}
if (PreferencesUtils.isKey(runningSpeedCadenceDistanceHandler.getSensorPreferenceKey(), key)) {
runningSpeedCadenceDistanceHandler.connect();
}
}
public interface SensorDataChangedObserver {
public Instant getNow() {
return observer.getNow();
}
void onConnect(Aggregator<?, ?> sensorData);
void onChange(Raw<?> sensorData);
public Context getContext() {
return context;
}
void onDisconnect(Aggregator<?, ?> sensorData);
public Handler getHandler() {
return handler;
}
void onRemove(Aggregator<?, ?> sensorData);
private SensorData<HeartRate> getHeartRate(Instant now) {
SensorData<HeartRate> value = heartRateHandler.getSensorData(now);
if (value != null) {
Instant getNow();
//TODO Move this check into HeartRateHandler
if (value.data().isValid()) return value;
}
return null;
}
private SensorData<Cadence> getCadence(Instant now) {
{
SensorData<Cadence> value = cyclingCadenceHandler.getSensorData(now);
if (value != null) {
return cyclingCadenceHandler.getSensorData(now);
}
}
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
if (value != null && value.data().cadence() != null) {
return new SensorData<>(value.data().cadence(), value.sensorNameOrAddress());
}
return null;
}
private SensorData<Distance> getDistance(Instant now) {
{
SensorData<AggregatorCyclingDistanceSpeed.Data> value = cyclingDistanceSpeedHandler.getSensorData(now);
if (value != null) {
return new SensorData<>(value.data().distanceOverall(), value.sensorNameOrAddress());
}
}
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
if (value != null) {
return new SensorData<>(value.data().distance(), value.sensorNameOrAddress());
}
return null;
}
//TOOD simplify?
private SensorData<Speed> getSpeed(Instant now) {
{
SensorData<AggregatorCyclingDistanceSpeed.Data> value = cyclingDistanceSpeedHandler.getSensorData(now);
if (value != null && value.data() != null && value.data().speed() != null) {
return new SensorData<>(value.data().speed(), value.sensorNameOrAddress());
}
}
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
if (value != null && value.data() != null && value.data().speed() != null) {
return new SensorData<>(value.data().speed(), value.sensorNameOrAddress());
}
return null;
}
public void onChange() {
observer.onChange();
}
public SensorDataSet getSensorDataSet(Instant now) {
//We always need a Position with now
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
if (gpsSensorData != null && gpsSensorData.data() != null) {
gpsSensorData = new SensorData<>(gpsSensorData.data().with(now), gpsSensorData.sensorNameOrAddress());
} else {
gpsSensorData = new SensorData<>(Position.of(now), "");
}
return new SensorDataSet(
gpsSensorData,
getSpeed(now),
getDistance(now),
getHeartRate(now),
temperatureHandler.getSensorData(now),
getCadence(now),
cyclingPowerHandler.getSensorData(now),
altitudeChangeHandler.getSensorData(now)
);
}
@VisibleForTesting
public void clear() {
Log.i(TAG, "Removing all aggregators");
//TODO This is to ExportImportTest alive; refactor test.
if (this.altitudeChangeHandler != null) {
this.altitudeChangeHandler.setAggregator(null);
}
if (this.heartRateHandler != null) {
this.heartRateHandler.setAggregator(null);
}
if (this.temperatureHandler != null) {
this.temperatureHandler.setAggregator(null);
}
if (this.cyclingCadenceHandler != null) {
this.cyclingCadenceHandler.setAggregator(null);
}
if (cyclingDistanceSpeedHandler != null) {
cyclingDistanceSpeedHandler.setAggregator(null);
}
if (cyclingPowerHandler != null) {
cyclingPowerHandler.setAggregator(null);
}
if (runningSpeedCadenceDistanceHandler != null) {
runningSpeedCadenceDistanceHandler.setAggregator(null);
}
if (this.gpsHandler != null) {
this.gpsHandler.setAggregator(null);
}
}
private boolean isStarted() {
return wakeLock != null;
}
@VisibleForTesting
public void setAggregator(AggregatorHeartRate data) {
heartRateHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorTemperature data) {
temperatureHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorCyclingCadence data) {
cyclingCadenceHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorCyclingDistanceSpeed data) {
cyclingDistanceSpeedHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorCyclingPower data) {
cyclingPowerHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorRunning data) {
runningSpeedCadenceDistanceHandler.setAggregator(data);
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.TemperatureBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
public class TemperatureHandler extends SensorHandler<Temperature, Temperature> {
protected TemperatureHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new TemperatureBluetooth()
);
}
@NonNull
@Override
protected AggregatorTemperature createAggregator(String sensorAddress, String sensorName) {
return new AggregatorTemperature(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_temperature_key;
}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -6,12 +6,9 @@ import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
public class BarometerBluetooth implements BluetoothDriver.BluetoothParser<AtmosphericPressure> {
static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
@@ -24,16 +21,8 @@ public class BluetoothHandlerBarometricPressure implements SensorHandlerInterfac
}
@Override
public Aggregator<?, ?> createEmptySensorData(String address, String name) {
return new AggregatorBarometer(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
AtmosphericPressure value = parseEnvironmentalSensing(characteristic);
if (value == null) return;
observer.onChange(new Raw<>(observer.getNow(), value));
public AtmosphericPressure parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
return parseEnvironmentalSensing(characteristic);
}
/**
@@ -12,9 +12,6 @@ import androidx.annotation.NonNull;
import java.util.concurrent.TimeUnit;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
public class BarometerInternal implements Driver {
@@ -22,9 +19,9 @@ public class BarometerInternal implements Driver {
private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5);
private final SensorManager.SensorDataChangedObserver observer;
private final DriverObserver<AtmosphericPressure> observer;
private Context context;
private android.hardware.SensorManager sensorService;
private final SensorEventListener sensorEventListener = new SensorEventListener() {
@Override
@@ -34,7 +31,7 @@ public class BarometerInternal implements Driver {
return;
}
observer.onChange(new Raw<>(observer.getNow(), AtmosphericPressure.ofHPA(event.values[0])));
observer.onDataReceived(AtmosphericPressure.ofHPA(event.values[0]));
}
@Override
@@ -43,40 +40,34 @@ public class BarometerInternal implements Driver {
}
};
public BarometerInternal(@NonNull SensorManager.SensorDataChangedObserver observer) {
public BarometerInternal(@NonNull DriverObserver<AtmosphericPressure> observer) {
this.observer = observer;
}
@Override
public void connect(Context context, Handler handler, String addressIgnored) {
sensorService = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorService.getDefaultSensor(Sensor.TYPE_PRESSURE);
android.hardware.SensorManager sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
if (pressureSensor == null) {
Log.w(TAG, "No pressure sensor available.");
if (pressureSensor != null && sensorService.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
observer.onConnected(null, null);
return;
}
if (sensorManager.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
this.context = context;
observer.onConnect(new AggregatorBarometer("internal", null));
return;
}
disconnect();
Log.w(TAG, "No pressure sensor available.");
observer.onDisconnected();
}
@Override
public boolean isConnected() {
return context != null;
return sensorService != null;
}
@Override
public void disconnect() {
if (!isConnected()) return;
android.hardware.SensorManager sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(sensorEventListener);
this.context = null;
sensorService.unregisterListener(sensorEventListener);
sensorService = null;
}
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
@@ -31,23 +31,25 @@ import android.util.Log;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
/**
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
*/
//TODO Re-act to Bluetooth disabled/enabled
@SuppressLint("MissingPermission")
public class BluetoothConnectionManager implements Driver {
public class BluetoothDriver<T> implements Driver {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private static final String TAG = BluetoothDriver.class.getSimpleName();
private final SensorManager.SensorDataChangedObserver observer;
private final DriverObserver<T> observer;
private final SensorHandlerInterface sensorHandler;
private final BluetoothParser<T> sensorHandler;
private final BluetoothAdapter bluetoothAdapter;
private BluetoothGatt bluetoothGatt;
@@ -55,27 +57,29 @@ public class BluetoothConnectionManager implements Driver {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
switch (newState) {
case BluetoothProfile.STATE_CONNECTING ->
Log.i(TAG, gatt.getDevice() + ": connecting to sensor");
case BluetoothProfile.STATE_CONNECTING -> {
Log.i(TAG, gatt.getDevice() + ": connecting to sensor");
}
case BluetoothProfile.STATE_CONNECTED -> {
Log.i(TAG, gatt.getDevice() + ": connected to sensor; discovering services");
gatt.discoverServices();
}
case BluetoothProfile.STATE_DISCONNECTING ->
Log.i(TAG, gatt.getDevice() + ": disconnecting from sensor: ");
case BluetoothProfile.STATE_DISCONNECTING -> Log.i(TAG, gatt.getDevice() + ": disconnecting from sensor: ");
case BluetoothProfile.STATE_DISCONNECTED -> {
//This is also triggered, if no connection was established (ca. 30s)
Log.i(TAG, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
if (gatt.connect()) {
Log.e(TAG, gatt.getDevice() + ": could not trigger reconnect for sensor");
}
clearData();
observer.onConnectionLost();
}
}
}
@Override
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothDevice device = gatt.getDevice();
BluetoothGattService gattService = null;
ServiceMeasurementUUID serviceMeasurement = null;
for (ServiceMeasurementUUID s : sensorHandler.getServices()) {
@@ -87,13 +91,13 @@ public class BluetoothConnectionManager implements Driver {
}
if (gattService == null) {
Log.e(TAG, gatt.getDevice() + ": could not get gattService for serviceUUID=" + serviceMeasurement);
Log.e(TAG, device + ": could not get gattService for serviceUUID=" + serviceMeasurement);
return;
}
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(serviceMeasurement.measurementUUID());
if (characteristic == null) {
Log.e(TAG, gatt.getDevice() + ": could not get BluetoothCharacteristic for serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
Log.e(TAG, device + ": could not get BluetoothCharacteristic for serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
return;
}
gatt.setCharacteristicNotification(characteristic, true);
@@ -111,6 +115,8 @@ public class BluetoothConnectionManager implements Driver {
if (!gatt.writeDescriptor(descriptor)) {
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
}
observer.onConnected(device.getAddress(), device.getName());
}
@Override
@@ -127,11 +133,15 @@ public class BluetoothConnectionManager implements Driver {
return;
}
sensorHandler.handlePayload(observer, serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
T value = sensorHandler.parsePayload(serviceMeasurementUUID.get(), gatt.getDevice().getName(), characteristic);
if (value == null) {
return;
}
observer.onDataReceived(value);
}
};
BluetoothConnectionManager(BluetoothAdapter bluetoothAdapter, SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
public BluetoothDriver(BluetoothAdapter bluetoothAdapter, DriverObserver<T> observer, BluetoothParser<T> sensorHandler) {
this.bluetoothAdapter = bluetoothAdapter;
this.observer = observer;
this.sensorHandler = sensorHandler;
@@ -144,26 +154,11 @@ public class BluetoothConnectionManager implements Driver {
return;
}
if (SensorType.NONE.getPreferenceValue().equals(address)) {
Log.w(TAG, "NONE: going to disconnect");
if (isConnected()) {
disconnect();
observer.onRemove(sensorHandler.createEmptySensorData(null, null));
}
return;
}
if (isConnected()) {
Log.w(TAG, "Already connected; ignoring.");
Log.e(TAG, "Already connected; ignoring.");
return;
}
if (isSameBluetoothDevice(address)) {
return;
} else {
disconnect();
}
BluetoothDevice device;
try {
device = bluetoothAdapter.getRemoteDevice(address);
@@ -175,37 +170,23 @@ public class BluetoothConnectionManager implements Driver {
Log.d(TAG, device + ": trying to connect");
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
observer.onConnect(sensorHandler.createEmptySensorData(device.getAddress(), device.getName()));
}
private synchronized void clearData() {
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress(), bluetoothGatt.getDevice().getName()));
}
@Override
public synchronized void disconnect() {
if (!isConnected()) {
Log.w(TAG, "Not connected; no need to re-connect.");
Log.w(TAG, "Not connected; cannot disconnect.");
return;
}
Log.i(TAG, bluetoothGatt.getDevice() + ": start disconnect");
bluetoothGatt.disconnect();
bluetoothGatt.close();
clearData();
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
bluetoothGatt = null;
observer.onDisconnected();
}
private synchronized boolean isSameBluetoothDevice(String address) {
if (bluetoothGatt == null) {
return false;
}
return address.equals(bluetoothGatt.getDevice().getAddress());
}
private boolean isBluetoothEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
@@ -214,4 +195,11 @@ public class BluetoothConnectionManager implements Driver {
public boolean isConnected() {
return bluetoothGatt != null;
}
public interface BluetoothParser<T> {
List<ServiceMeasurementUUID> getServices();
T parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic);
}
}
@@ -0,0 +1,49 @@
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import android.util.Pair;
import java.util.List;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class CyclingCadenceBluetooth implements BluetoothDriver.BluetoothParser<CyclingCadenceBluetooth.CrankData> {
private static final String TAG = CyclingCadenceBluetooth.class.getSimpleName();
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
CyclingPowerBluetooth.CYCLING_POWER,
CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return CYCLING_CADENCE;
}
@Override
public CrankData parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
if (serviceMeasurementUUID.equals(CyclingPowerBluetooth.CYCLING_POWER)) {
CyclingPowerBluetooth.Data data = CyclingPowerBluetooth.parseCyclingPower(characteristic);
if (data != null && data.crank() != null) {
return data.crank();
}
}
if (serviceMeasurementUUID.equals(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE)) {
Pair<CyclingDistanceSpeedBluetooth.WheelData, CrankData> data = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
if (data != null && data.second != null) {
return data.second;
}
}
Log.e(TAG, "Don't know how to decode this payload.");
return null;
}
public record CrankData(
long crankRevolutionsCount, // UINT32
int crankRevolutionsTime // UINT16; 1/1024s
) {}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Pair;
@@ -9,11 +9,9 @@ import androidx.annotation.VisibleForTesting;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterface {
public class CyclingDistanceSpeedBluetooth implements BluetoothDriver.BluetoothParser<CyclingDistanceSpeedBluetooth.WheelData> {
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
new UUID(0x181600001000L, 0x800000805f9b34fbL),
@@ -26,21 +24,18 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
}
@Override
public AggregatorCyclingDistanceSpeed createEmptySensorData(String address, String name) {
return new AggregatorCyclingDistanceSpeed(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> data = parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (data.first != null) {
observer.onChange(new Raw<>(observer.getNow(), data.first));
public WheelData parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
Pair<WheelData, CyclingCadenceBluetooth.CrankData> data = parseCyclingCrankAndWheel(characteristic);
if (data != null && data.first != null) {
return data.first;
}
return null;
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
public static Pair<WheelData, CyclingCadenceBluetooth.CrankData> parseCyclingCrankAndWheel(@NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
@@ -61,13 +56,13 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
index += 2;
}
BluetoothHandlerCyclingCadence.CrankData crankData = null;
CyclingCadenceBluetooth.CrankData crankData = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
crankData = new CyclingCadenceBluetooth.CrankData(crankCount, crankTime);
}
return new Pair<>(wheelData, crankData);
@@ -78,5 +73,6 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
long wheelRevolutionsCount, // UINT32
int wheelRevolutionsTime // UINT16; 1/1024s
) {}
) {
}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -9,11 +9,9 @@ import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class BluetoothHandlerCyclingPower implements SensorHandlerInterface {
public class CyclingPowerBluetooth implements BluetoothDriver.BluetoothParser<CyclingPowerBluetooth.Data> {
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
new UUID(0x181800001000L, 0x800000805f9b34fbL),
@@ -26,17 +24,8 @@ public class BluetoothHandlerCyclingPower implements SensorHandlerInterface {
}
@Override
public AggregatorCyclingPower createEmptySensorData(String address, String name) {
return new AggregatorCyclingPower(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Data cyclingPower = parseCyclingPower(characteristic);
if (cyclingPower != null) {
observer.onChange(new Raw<>(observer.getNow(), cyclingPower));
}
public CyclingPowerBluetooth.Data parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
return parseCyclingPower(characteristic);
}
@@ -69,19 +58,22 @@ public class BluetoothHandlerCyclingPower implements SensorHandlerInterface {
index += 2 + 2;
}
BluetoothHandlerCyclingCadence.CrankData crankData = null;
CyclingCadenceBluetooth.CrankData crankData = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
crankData = new CyclingCadenceBluetooth.CrankData(crankCount, crankTime);
}
return new Data(Power.of(instantaneousPower), crankData);
}
public record Data(Power power, BluetoothHandlerCyclingCadence.CrankData crank) {}
public record Data(
Power power,
CyclingCadenceBluetooth.CrankData crank
) {
}
}
@@ -4,8 +4,11 @@ import android.content.Context;
import android.os.Handler;
public interface Driver {
//TODO Address is optional for internal drivers. Move address to driver instantiation.
void connect(Context context, Handler handler, String address);
//TODO Remove from interface?
boolean isConnected();
void disconnect();
@@ -0,0 +1,31 @@
package de.dennisguse.opentracks.sensors.driver;
public interface DriverObserver<T> {
/**
* The Driver could connect to the sensor.
*/
void onConnected(String sensorAddress, String sensorName);
/**
* Sensor got temporarily disconnected, Driver should try to re-connect.
*/
void onConnectionLost();
/**
* The Driver provided data from the sensor.
*/
void onDataReceived(T value);
/**
* The Driver was shutdown and no further data will be provided.
*/
//TODO check if really need this, because the driver was told to disconnect and then just signalizes that it finished this task.
void onDisconnected();
/**
* The Driver detected that the sensor got deactivated.
* For example, the user deactivated the internal GPS.
*/
default void onSensorDeactivated() {}
}
@@ -0,0 +1,95 @@
package de.dennisguse.opentracks.sensors.driver;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.location.LocationListenerCompat;
import androidx.core.location.LocationManagerCompat;
import androidx.core.location.LocationRequestCompat;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
public class GpsInternal implements Driver {
private static final String TAG = GpsInternal.class.getSimpleName();
public static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
@VisibleForTesting
public final LocationListenerCompat locationListenerCompat = new LocationListenerCompat() {
@Override
public void onLocationChanged(@NonNull Location location) {
driverObserver.onDataReceived(location);
}
@Override
public void onProviderEnabled(@NonNull String provider) {
driverObserver.onConnected(null, provider);
}
@Override
public void onProviderDisabled(@NonNull String provider) {
driverObserver.onSensorDeactivated();
}
};
private final DriverObserver<Location> driverObserver;
@VisibleForTesting
public LocationManager locationManager;
public GpsInternal(DriverObserver<Location> driverObserver) {
this.driverObserver = driverObserver;
}
@Override
public void connect(Context context, Handler handler, String address) {
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
if (!LocationManagerCompat.hasProvider(locationManager, LOCATION_PROVIDER)) {
Log.e(TAG, "Device doesn't have GPS.");
return;
}
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(PreferencesUtils.getMinSamplingInterval().toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
if (PermissionRequester.GPS.hasPermission(context)) {
try {
Log.i(TAG, "Register for location updates " + context);
driverObserver.onConnected(null, null);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, locationListenerCompat);
return;
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
}
}
driverObserver.onDisconnected();
}
@SuppressWarnings({"MissingPermission"})
@Override
public void disconnect() {
if (!isConnected()) return;
LocationManagerCompat.removeUpdates(locationManager, locationListenerCompat);
locationManager = null;
driverObserver.onDisconnected();
}
@Override
public boolean isConnected() {
return locationManager != null;
}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -9,11 +9,9 @@ import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class BluetoothHandlerHeartRate implements SensorHandlerInterface {
public class HeartRateBluetooth implements BluetoothDriver.BluetoothParser<HeartRate> {
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
@@ -36,17 +34,8 @@ public class BluetoothHandlerHeartRate implements SensorHandlerInterface {
}
@Override
public AggregatorHeartRate createEmptySensorData(String address, String name) {
return new AggregatorHeartRate(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
HeartRate heartRate = parseHeartRate(characteristic);
if (heartRate != null) {
observer.onChange(new Raw<>(observer.getNow(), heartRate));
}
public HeartRate parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
return parseHeartRate(characteristic);
}
@VisibleForTesting
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -11,11 +11,9 @@ import java.util.UUID;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInterface {
public class RunningSpeedAndCadenceBluetooth implements BluetoothDriver.BluetoothParser<RunningSpeedAndCadenceBluetooth.Data> {
public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID(
@@ -29,14 +27,8 @@ public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInte
}
@Override
public AggregatorRunning createEmptySensorData(String address, String name) {
return new AggregatorRunning(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Data data = parseRunningSpeedAndCadence(sensorName, characteristic);
observer.onChange(new Raw<>(observer.getNow(), data));
public Data parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
return parseRunningSpeedAndCadence(sensorName, characteristic);
}
@VisibleForTesting
@@ -1,8 +1,10 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
public enum SensorType {
NONE("NONE"),
INTERNAL("INTERNAL"),
NONE("NONE"), //TODO Use R.string.sensor_type_value_none
INTERNAL("INTERNAL"), //TODO Use R.string.sensor_type_value_none
//NOTE: preferenceValue of REMOTE should not be used anywhere.
REMOTE("*");
private final String preferenceValue;
@@ -0,0 +1,42 @@
package de.dennisguse.opentracks.sensors.driver;
import static de.dennisguse.opentracks.sensors.driver.BarometerBluetooth.ENVIRONMENTAL_SENSING_SERVICE;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class TemperatureBluetooth implements BluetoothDriver.BluetoothParser<Temperature> {
public static final ServiceMeasurementUUID TEMPERATURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6E00001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(TEMPERATURE);
}
@Override
public Temperature parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
return parseEnvironmentalSensing(characteristic);
}
/**
* Decoding: org.bluetooth.characteristic.temperature.xml
*/
public static Temperature parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
if (raw.length < 2) {
return null;
}
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 0);
return Temperature.ofCentiCelsius(pressure);
}
}
@@ -1,79 +1,74 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
public abstract class Aggregator<Input, Output> {
public abstract class Aggregator<InputType, OutputType> {
private static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
protected Raw<Input> previous;
@Nullable
protected Raw<InputType> previous;
protected Output aggregatedValue;
//TODO Make private
protected OutputType output;
private final String sensorAddress;
private final String sensorName;
Aggregator(String sensorAddress) {
this(sensorAddress, null);
}
Aggregator(String sensorAddress, String sensorName) {
this.sensorAddress = sensorAddress;
this.sensorName = sensorName;
}
public String getSensorNameOrAddress() {
private String getSensorNameOrAddress() {
return sensorName != null ? sensorName : sensorAddress;
}
public final void add(Raw<Input> current) {
computeValue(current);
previous = current;
public final void add(Instant now, InputType current) {
Raw<InputType> next = new Raw<>(now, current);
computeValue(next);
previous = next;
}
protected abstract void computeValue(Raw<Input> current);
protected abstract void computeValue(Raw<InputType> current);
/**
* @return did we process data from a sensor.
* NOTE: for some sensors this may require more than one measurement.
*/
public boolean hasReceivedData() {
return aggregatedValue != null;
return output != null;
}
@Deprecated //TODO Remove: for sensor there is no default value. Make getAggregatedValue() @Nullable
@NonNull
protected abstract Output getNoneValue();
@NonNull
public Output getAggregatedValue(Instant now) {
if (!hasReceivedData()) {
return getNoneValue();
}
@Nullable
protected OutputType getAggregatedValue(Instant now) {
if (isOutdated(now)) {
resetImmediate();
resetOutdated();
}
return aggregatedValue;
return output;
}
@NonNull
public SensorData<Output> getAggregatedValueWithSensorName(Instant now) {
public SensorData<OutputType> getAggregatedValueWithSensorName(Instant now) {
return new SensorData<>(getAggregatedValue(now), getSensorNameOrAddress());
}
/**
* Reset short-term (i.e., non-aggregated) values that were directly derived from sensor data.
*/
protected abstract void resetImmediate();
protected void resetOutdated() {
output = null;
};
/**
* Reset long-term (i.e., aggregated) values (more than derived from previous SensorData) like overall distance.
*/
public abstract void resetAggregated();
public void resetAggregated() {};
/**
* Is the data recent considering the current time.
@@ -97,6 +92,12 @@ public abstract class Aggregator<Input, Output> {
@NonNull
@Override
public String toString() {
return "sensorAddress=" + sensorAddress + " data=" + aggregatedValue;
return "sensorAddress=" + sensorAddress + " data=" + output;
}
protected record Raw<T>(
@NonNull Instant time,
@NonNull T value
) {
}
}
@@ -1,8 +1,5 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.PressureSensorUtils;
@@ -19,32 +16,27 @@ public class AggregatorBarometer extends Aggregator<AtmosphericPressure, Altitud
protected void computeValue(Raw<AtmosphericPressure> current) {
if (previous == null) {
lastAcceptedSensorValue = current.value();
aggregatedValue = getNoneValue();
output = new AltitudeGainLoss(0f, 0f);
return;
}
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, previous.value(), current.value());
if (altitudeChange != null) {
aggregatedValue = new AltitudeGainLoss(aggregatedValue.gain_m() + altitudeChange.getAltitudeGain_m(), aggregatedValue.loss_m() + altitudeChange.getAltitudeLoss_m());
if (output == null) {
output = new AltitudeGainLoss(0f, 0f);
}
output = new AltitudeGainLoss(output.gain_m() + altitudeChange.getAltitudeGain_m(), output.loss_m() + altitudeChange.getAltitudeLoss_m());
lastAcceptedSensorValue = altitudeChange.currentSensorValue();
}
}
@Override
protected void resetImmediate() {
protected void resetOutdated() {
}
@Override
public void resetAggregated() {
aggregatedValue = getNoneValue();
output = null;
}
@NonNull
@Override
protected AltitudeGainLoss getNoneValue() {
return new AltitudeGainLoss(0f, 0f);
}
public record Data(Altitude gain, Altitude loss) {}
}
@@ -2,15 +2,13 @@ package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.UintUtils;
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCyclingCadence.CrankData, Cadence> {
public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth.CrankData, Cadence> {
private final String TAG = AggregatorCyclingCadence.class.getSimpleName();
@@ -19,7 +17,7 @@ public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCycling
}
@Override
protected void computeValue(Raw<BluetoothHandlerCyclingCadence.CrankData> current) {
protected void computeValue(Raw<CyclingCadenceBluetooth.CrankData> current) {
if (previous == null) {
return;
}
@@ -32,32 +30,18 @@ public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCycling
}
if (timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
aggregatedValue = null;
output = null;
return;
}
// TODO We have to treat with overflow according to the documentation: read https://codeberg.org/OpenTracksApp/OpenTracks/pulls/953#issuecomment-6466930
if (current.value().crankRevolutionsCount() < previous.value().crankRevolutionsCount()) {
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
output = null;
return;
}
long crankDiff = UintUtils.diff(current.value().crankRevolutionsCount(), previous.value().crankRevolutionsCount(), UintUtils.UINT32_MAX);
aggregatedValue = Cadence.of(crankDiff, timeDiff);
}
@Override
protected void resetImmediate() {
aggregatedValue = getNoneValue();
}
@Override
public void resetAggregated() {
}
@NonNull
@Override
protected Cadence getNoneValue() {
return Cadence.of(0);
output = Cadence.of(crankDiff, timeDiff);
}
}
@@ -2,16 +2,14 @@ package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.UintUtils;
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerCyclingDistanceSpeed.WheelData, AggregatorCyclingDistanceSpeed.Data> {
public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSpeedBluetooth.WheelData, AggregatorCyclingDistanceSpeed.Data> {
private final String TAG = AggregatorCyclingDistanceSpeed.class.getSimpleName();
@@ -22,7 +20,7 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerC
}
@Override
protected void computeValue(Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData> current) {
protected void computeValue(Raw<CyclingDistanceSpeedBluetooth.WheelData> current) {
if (previous == null) {
return;
}
@@ -35,43 +33,39 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerC
}
if (timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
aggregatedValue = null;
output = null;
return;
}
if (current.value().wheelRevolutionsCount() < previous.value().wheelRevolutionsCount()) {
Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed.");
output = null;
return;
}
long wheelDiff = UintUtils.diff(current.value().wheelRevolutionsCount(), previous.value().wheelRevolutionsCount(), UintUtils.UINT32_MAX);
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
Distance distanceOverall = distance;
if (aggregatedValue != null) {
distanceOverall = distance.plus(aggregatedValue.distanceOverall);
if (output != null) {
distanceOverall = distance.plus(output.distanceOverall);
}
Speed speed_mps = Speed.of(distance, timeDiff);
aggregatedValue = new Data(distance, distanceOverall, speed_mps);
output = new Data(distance, distanceOverall, speed_mps);
}
@Override
protected void resetImmediate() {
aggregatedValue = new Data(Distance.ZERO, aggregatedValue.distanceOverall, Speed.ZERO);
protected void resetOutdated() {
Distance overallDistance = output != null ? output.distanceOverall : Distance.ZERO;
output = new Data(Distance.ZERO, overallDistance, Speed.ZERO);
}
@Override
public void resetAggregated() {
if (aggregatedValue != null) {
aggregatedValue = new Data(aggregatedValue.distance, Distance.ZERO, aggregatedValue.speed);
if (output != null) {
output = new Data(output.distance, Distance.ZERO, output.speed);
}
}
@NonNull
@Override
protected Data getNoneValue() {
return new Data(Distance.ZERO, Distance.ZERO, Speed.ZERO);
}
public void setWheelCircumference(Distance wheelCircumference) {
this.wheelCircumference = wheelCircumference;
}
@@ -1,33 +1,16 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
public class AggregatorCyclingPower extends Aggregator<BluetoothHandlerCyclingPower.Data, Power> {
public class AggregatorCyclingPower extends Aggregator<CyclingPowerBluetooth.Data, Power> {
public AggregatorCyclingPower(String name, String address) {
super(name, address);
}
@Override
public void computeValue(Raw<BluetoothHandlerCyclingPower.Data> current) {
this.aggregatedValue = current.value().power();
}
@Override
protected void resetImmediate() {
aggregatedValue = getNoneValue();
}
@Override
public void resetAggregated() {
}
@NonNull
@Override
protected Power getNoneValue() {
return Power.of(0f);
public void computeValue(Raw<CyclingPowerBluetooth.Data> current) {
this.output = current.value().power();
}
}
@@ -1,26 +1,18 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import java.time.Instant;
import android.location.Location;
import de.dennisguse.opentracks.data.models.Position;
public class AggregatorGPS extends Aggregator<Position, Position> {
public class AggregatorGPS extends Aggregator<Location, Position> {
public AggregatorGPS(String sensorAddress) {
super(sensorAddress);
public AggregatorGPS(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@Override
protected void computeValue(Raw<Position> current) {
aggregatedValue = current.value();
}
@Override
protected void resetImmediate() {
aggregatedValue = Position.empty();
protected void computeValue(Raw<Location> current) {
output = Position.of(current.value(), current.time());
}
@Override
@@ -28,21 +20,7 @@ public class AggregatorGPS extends Aggregator<Position, Position> {
/*
* GPS data is not an aggregated value, but for now we want to ensure to only save the data once.
* The data is too large to save it more often than needed (i.e., duplicated values).
* TODO: this behavior can be changed if TrackRecordingManager.insertTrackPoint() would strip GPS data if it was already saved. This would simplify TrackPointCreator.createCurrentTrackPoint()
*/
aggregatedValue = Position.empty();
}
@NonNull
@Override
protected Position getNoneValue() {
return Position.empty();
}
@NonNull
@Override
public Position getAggregatedValue(Instant now) {
return super.getAggregatedValue(now)
.with(now);
resetOutdated();
}
}
@@ -1,7 +1,5 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.HeartRate;
public class AggregatorHeartRate extends Aggregator<HeartRate, HeartRate> {
@@ -13,22 +11,7 @@ public class AggregatorHeartRate extends Aggregator<HeartRate, HeartRate> {
@Override
protected void computeValue(Raw<HeartRate> current) {
if (current.value().isValid()) {
this.aggregatedValue = current.value();
this.output = current.value();
}
}
@Override
protected void resetImmediate() {
aggregatedValue = getNoneValue();
}
@Override
public void resetAggregated() {
}
@NonNull
@Override
protected HeartRate getNoneValue() {
return HeartRate.of(0);
}
}
@@ -5,12 +5,12 @@ import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
/**
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors.
*/
public final class AggregatorRunning extends Aggregator<BluetoothHandlerRunningSpeedAndCadence.Data, AggregatorRunning.Data> {
public final class AggregatorRunning extends Aggregator<RunningSpeedAndCadenceBluetooth.Data, AggregatorRunning.Data> {
private static final String TAG = AggregatorRunning.class.getSimpleName();
@@ -19,7 +19,7 @@ public final class AggregatorRunning extends Aggregator<BluetoothHandlerRunningS
}
@Override
public void computeValue(Raw<BluetoothHandlerRunningSpeedAndCadence.Data> current) {
public void computeValue(Raw<RunningSpeedAndCadenceBluetooth.Data> current) {
if (previous == null) {
return;
}
@@ -27,33 +27,31 @@ public final class AggregatorRunning extends Aggregator<BluetoothHandlerRunningS
Distance distance = null;
if (previous.value().totalDistance() != null && current.value().totalDistance() != null) {
distance = current.value().totalDistance().minus(previous.value().totalDistance());
if (aggregatedValue != null) {
distance = distance.plus(aggregatedValue.distance);
if (output != null) {
distance = distance.plus(output.distance);
}
}
aggregatedValue = new Data(current.value().speed(), current.value().cadence(), distance);
output = new Data(current.value().speed(), current.value().cadence(), distance);
}
@Override
protected void resetImmediate() {
aggregatedValue = new Data(Speed.ZERO, Cadence.of(0f), aggregatedValue.distance);
protected void resetOutdated() {
Distance distance = output != null ? output.distance : Distance.ZERO;
output = new Data(Speed.ZERO, Cadence.of(0f), distance);
}
@Override
public void resetAggregated() {
if (aggregatedValue != null) {
aggregatedValue = new Data(aggregatedValue.speed, aggregatedValue.cadence, Distance.ZERO);
if (output != null) {
output = new Data(output.speed, output.cadence, Distance.ZERO);
}
}
@NonNull
@Override
protected Data getNoneValue() {
return new Data(Speed.ZERO, Cadence.of(0f), Distance.ZERO);
}
public record Data(Speed speed, Cadence cadence, @NonNull Distance distance) {
public record Data(
Speed speed,
Cadence cadence,
@NonNull Distance distance) {
}
}
@@ -1,7 +1,5 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Temperature;
@@ -13,20 +11,9 @@ public class AggregatorTemperature extends Aggregator<Temperature, Temperature>
}
protected void computeValue(Raw<Temperature> current) {
this.aggregatedValue = current.value();
this.output = current.value();
}
@NonNull
protected Temperature getNoneValue() {
return Temperature.ofCentiCelsius(0);
}
protected void resetImmediate() {
aggregatedValue = getNoneValue();
}
public void resetAggregated() {};
@Override
protected Duration getMaxSensorAge() {
return Duration.ofMinutes(1);
@@ -1,22 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Clock;
import java.time.Instant;
public record Raw<T>(
@NonNull Instant time,
@NonNull T value
) {
public Raw(@NonNull Clock clock, @NonNull T value) {
this(clock.instant(), value);
}
@VisibleForTesting
public Raw(@NonNull String time, @NonNull T value) {
this(Instant.parse(time), value);
}
}
@@ -1,7 +1,11 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.Nullable;
public record SensorData<T>(
@Nullable
T data,
String sensorNameOrAddress
@@ -1,229 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class SensorDataAggregator {
private static final String TAG = SensorDataAggregator.class.getSimpleName();
@VisibleForTesting
public AggregatorHeartRate heartRate;
@VisibleForTesting
public AggregatorTemperature temperature;
@VisibleForTesting
public AggregatorCyclingCadence cyclingCadence;
@VisibleForTesting
public AggregatorCyclingDistanceSpeed cyclingDistanceSpeed;
@VisibleForTesting
public AggregatorCyclingPower cyclingPower;
@VisibleForTesting
public AggregatorRunning runningDistanceSpeedCadence;
@VisibleForTesting
public AggregatorBarometer barometer;
public AggregatorGPS gps;
@VisibleForTesting
public SensorData<HeartRate> getHeartRate(Instant now) {
if (heartRate != null) {
SensorData<HeartRate> value = heartRate.getAggregatedValueWithSensorName(now);
if (value.data().isValid()) return value;
}
return null;
}
private SensorData<Cadence> getCadence(Instant now) {
if (cyclingCadence != null) {
return cyclingCadence.getAggregatedValueWithSensorName(now);
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.getAggregatedValue(now).cadence() != null) {
return new SensorData<>(runningDistanceSpeedCadence.aggregatedValue.cadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
private SensorData<Distance> getDistance(Instant now) {
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData()) {
return new SensorData<>(cyclingDistanceSpeed.getAggregatedValue(now).distanceOverall(), cyclingDistanceSpeed.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData()) {
return new SensorData<>(runningDistanceSpeedCadence.getAggregatedValue(now).distance(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
//TOOD simplify?
private SensorData<Speed> getSpeed(Instant now) {
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData() && cyclingDistanceSpeed.getAggregatedValue(now).speed() != null) {
return new SensorData<>(cyclingDistanceSpeed.getAggregatedValue(now).speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.getAggregatedValue(now).speed() != null) {
return new SensorData<>(runningDistanceSpeedCadence.aggregatedValue.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
public void add(@NonNull Aggregator<?, ?> data) {
set(data, data);
}
//TODO Refactor generic "fun" here: go explicit.
public void update(@NonNull Raw<?> data) {
Object value = data.value();
if (value instanceof HeartRate) {
this.heartRate.add((Raw<HeartRate>) data);
return;
}
if (value instanceof Temperature) {
this.temperature.add((Raw<Temperature>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingCadence.CrankData) {
this.cyclingCadence.add((Raw<BluetoothHandlerCyclingCadence.CrankData>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingDistanceSpeed.WheelData) {
this.cyclingDistanceSpeed.setWheelCircumference(PreferencesUtils.getWheelCircumference()); //TODO Fetch once and then listen for changes.
this.cyclingDistanceSpeed.add((Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData>) data);
return;
}
if (value instanceof BluetoothHandlerRunningSpeedAndCadence.Data) {
this.runningDistanceSpeedCadence.add((Raw<BluetoothHandlerRunningSpeedAndCadence.Data>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingPower.Data) {
this.cyclingPower.add((Raw<BluetoothHandlerCyclingPower.Data>) data);
return;
}
if (value instanceof AtmosphericPressure) {
this.barometer.add((Raw<AtmosphericPressure>) data);
return;
}
if (value instanceof Position) {
this.gps.add((Raw<Position>) data);
return;
}
throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName());
}
public void remove(@NonNull Aggregator<?, ?> type) {
set(type, null);
}
public void clear() {
Log.i(TAG, "Removing all aggregators");
this.heartRate = null;
this.temperature = null;
this.cyclingCadence = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
this.runningDistanceSpeedCadence = null;
this.barometer = null;
this.gps = null;
}
public SensorDataSet getSensorData(Instant now) {
return new SensorDataSet(
gps != null && gps.hasReceivedData() ? gps.getAggregatedValueWithSensorName(now) : new SensorData<>(Position.of(now), ""),
getSpeed(now),
getDistance(now),
getHeartRate(now),
temperature != null && temperature.hasReceivedData() ? temperature.getAggregatedValueWithSensorName(now) : null,
getCadence(now),
cyclingPower != null && cyclingPower.hasReceivedData() ? cyclingPower.getAggregatedValueWithSensorName(now) : null,
barometer != null && barometer.hasReceivedData() ? barometer.getAggregatedValueWithSensorName(now) : null
);
}
public void reset() {
Log.i(TAG, "Resetting data");
if (heartRate != null) heartRate.resetAggregated();
if (temperature != null) temperature.resetAggregated();
if (cyclingCadence != null) cyclingCadence.resetAggregated();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.resetAggregated();
if (cyclingPower != null) cyclingPower.resetAggregated();
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.resetAggregated();
if (barometer != null) barometer.resetAggregated();
if (gps != null) gps.resetAggregated();
}
//TODO Refactor generic "fun" here: go explicit.
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName() + " to " + sensorData);
if (type instanceof AggregatorHeartRate) {
heartRate = (AggregatorHeartRate) sensorData;
return;
}
if (type instanceof AggregatorTemperature) {
temperature = (AggregatorTemperature) sensorData;
return;
}
if (type instanceof AggregatorCyclingCadence) {
cyclingCadence = (AggregatorCyclingCadence) sensorData;
return;
}
if (type instanceof AggregatorCyclingDistanceSpeed) {
cyclingDistanceSpeed = (AggregatorCyclingDistanceSpeed) sensorData;
return;
}
if (type instanceof AggregatorCyclingPower) {
cyclingPower = (AggregatorCyclingPower) sensorData;
return;
}
if (type instanceof AggregatorRunning) {
runningDistanceSpeedCadence = (AggregatorRunning) sensorData;
return;
}
if (type instanceof AggregatorBarometer) {
barometer = (AggregatorBarometer) sensorData;
return;
}
if (type instanceof AggregatorGPS) {
gps = (AggregatorGPS) sensorData;
return;
}
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
}
}
@@ -1,17 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public interface SensorHandlerInterface {
List<ServiceMeasurementUUID> getServices();
Aggregator<?, ?> createEmptySensorData(String address, String name);
void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic);
}
@@ -10,6 +10,10 @@ import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
*/
public record RecordingData(
Track track,
/*
* The current view on the SensorData.
* May contain data from previous measurements that are not present in SensorDataSet.
*/
TrackPoint latestTrackPoint,
SensorDataSet sensorDataSet,
Statistics currentSegment) {
@@ -131,10 +131,10 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
RecordingData getDataForUI() {
Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastTrackPointUIWithSpeed, lastTrackPointUIWithAltitude, lastStoredTrackPointWithLocation);
TrackPoint trackPoint = current.first;
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater, trackPoint);
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater, current.first);
ALTITUDE_CORRECTION_MANAGER.correctAltitude(context, current.first);
TrackPoint TrackPointWithEGM2008 = ALTITUDE_CORRECTION_MANAGER.correctAltitude(context, trackPoint);
Track track = contentProviderUtils.getTrack(trackId); //Get copy TODO: Avoid re-loading data from DB
if (track == null) {
@@ -145,7 +145,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
TrackBuilder trackBuilder = new TrackBuilder(track);
trackBuilder.setStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
return new RecordingData(trackBuilder.getTrack(), current.first, current.second, tmpTrackStatisticsUpdater.getCurrentSegment());
return new RecordingData(trackBuilder.getTrack(), TrackPointWithEGM2008, current.second, tmpTrackStatisticsUpdater.getCurrentSegment());
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
@@ -48,7 +48,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private static final String TAG = TrackRecordingService.class.getSimpleName();
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
@VisibleForTesting
public static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
public static final RecordingStatus STATUS_DEFAULT = RecordingStatus.notRecording();
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
@@ -56,7 +56,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
}
public void stop() {
sensorManager.stop(context);
sensorManager.stop();
this.context = null;
}
@@ -73,7 +73,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
}
public synchronized TrackPoint createSegmentStartManual() {
return TrackPoint.createSegmentStartManualWithTime(createNow());
return TrackPoint.createSegmentStartManualWithTime(getNow());
}
public synchronized TrackPoint createSegmentEnd() {
@@ -89,7 +89,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
}
public Pair<TrackPoint, SensorDataSet> createCurrentTrackPoint(@Nullable TrackPoint lastTrackPointUISpeed, @Nullable TrackPoint lastTrackPointUIAltitude, @Nullable TrackPoint lastStoredTrackPointWithLocation) {
Instant now = createNow();
Instant now = getNow();
SensorDataSet sensorDataSet = sensorManager.getSensorDataSet(now);
Position position = sensorDataSet.position().data();
@@ -102,10 +102,10 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
}
if (lastTrackPointUISpeed != null)
position.with(lastTrackPointUISpeed.position().speed());
position = position.with(lastTrackPointUISpeed.position().speed());
if (lastTrackPointUIAltitude != null)
position.with(lastTrackPointUIAltitude.position().altitude());
position = position.with(lastTrackPointUIAltitude.position().altitude());
TrackPoint currentTrackPoint = new TrackPoint(
null,
@@ -123,7 +123,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
}
private TrackPoint createTrackPoint(TrackPoint.Type type) {
Instant now = createNow();
Instant now = getNow();
SensorDataSet sensorDataSet = sensorManager.getSensorDataSet(now);
@@ -146,7 +146,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
);
}
public Instant createNow() {
public Instant getNow() {
return Instant.now(clock);
}
@@ -48,7 +48,7 @@ import de.dennisguse.opentracks.data.models.HeartRateZones;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.sensors.SensorType;
import de.dennisguse.opentracks.sensors.driver.SensorType;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayoutIO;
@@ -253,10 +253,6 @@ public class PreferencesUtils {
return isReportSpeed(track.activityTypeLocalized());
}
private static String getBluetoothSensorAddressNone() {
return SensorType.NONE.getPreferenceValue();
}
public static SensorType getSensorType(String address) {
return Arrays.stream(SensorType.values())
.filter(it -> it.getPreferenceValue().equals(address))
@@ -264,39 +260,11 @@ public class PreferencesUtils {
.orElse(SensorType.REMOTE);
}
public static String getBarometerSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_pressure_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothHeartRateSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothTemperatureSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_temperature_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothCyclingCadenceSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothCyclingSpeedSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone());
}
public static Distance getWheelCircumference() {
final int DEFAULT = Integer.parseInt(resources.getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default));
return Distance.ofMM(getInt(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT));
}
public static String getBluetoothCyclingPowerSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_cycling_power_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothRunningSpeedAndCadenceAddress() {
return getString(R.string.settings_sensor_bluetooth_running_speed_and_cadence_key, getBluetoothSensorAddressNone());
}
public static boolean getBluetoothFilterEnabled() {
final boolean DEFAULT = resources.getBoolean(R.bool.settings_sensor_bluetooth_service_filter_enabled_default);
return getBoolean(R.string.settings_sensor_bluetooth_service_filter_enabled_key, DEFAULT);
@@ -579,6 +547,7 @@ public class PreferencesUtils {
return entries;
}
//TODO Rename to getGpsMinSamplingInterval
public static Duration getMinSamplingInterval() {
final Duration MIN_SAMPLING_INTERVAL = getMinSamplingIntervalDefault();
return Duration.ofSeconds(getInt(R.string.min_sampling_interval_key, (int) MIN_SAMPLING_INTERVAL.getSeconds()));
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPreference {
@@ -27,6 +27,6 @@ public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPrefer
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingCadence.CYCLING_CADENCE);
return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), CyclingCadenceBluetooth.CYCLING_CADENCE);
}
}
@@ -7,7 +7,7 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
@@ -29,6 +29,6 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerCyclingPower.CYCLING_POWER));
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(CyclingPowerBluetooth.CYCLING_POWER));
}
}
@@ -7,7 +7,7 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreference {
@@ -29,6 +29,6 @@ public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreferen
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE));
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE));
}
}
@@ -0,0 +1,41 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.content.Context;
import android.location.LocationManager;
import android.util.AttributeSet;
import androidx.core.location.LocationManagerCompat;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
public class BluetoothLeGpsPreference extends BluetoothLeSensorPreference {
public BluetoothLeGpsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLeGpsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLeGpsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLeGpsPreference(Context context) {
super(context);
}
@Override
public PreferenceDialogFragmentCompat createInstance() {
boolean hasInternalSensor = LocationManagerCompat.hasProvider((LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE), GpsInternal.LOCATION_PROVIDER);
return BluetoothLeSensorPreferenceDialog
.newInstance(getKey(), List.of(), hasInternalSensor);
}
}
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.sensors.BluetoothHandlerHeartRate;
import de.dennisguse.opentracks.sensors.driver.HeartRateBluetooth;
public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference {
@@ -27,6 +27,6 @@ public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerHeartRate.HEART_RATE_SUPPORTING_DEVICES);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), HeartRateBluetooth.HEART_RATE_SUPPORTING_DEVICES);
}
}
@@ -9,7 +9,7 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerBarometricPressure;
import de.dennisguse.opentracks.sensors.driver.BarometerBluetooth;
public class BluetoothLePressurePreference extends BluetoothLeSensorPreference {
@@ -36,6 +36,6 @@ public class BluetoothLePressurePreference extends BluetoothLeSensorPreference {
boolean hasInternalSensor = pressureSensor != null;
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog
.newInstance(getKey(), List.of(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE), hasInternalSensor);
.newInstance(getKey(), List.of(BarometerBluetooth.BAROMETRIC_PRESSURE), hasInternalSensor);
}
}
@@ -7,7 +7,7 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSensorPreference {
@@ -29,6 +29,6 @@ public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSens
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE));
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(RunningSpeedAndCadenceBluetooth.RUNNING_SPEED_CADENCE));
}
}
@@ -10,6 +10,7 @@ import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.text.TextUtils;
@@ -18,6 +19,7 @@ import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
@@ -28,8 +30,8 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.SensorType;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
import de.dennisguse.opentracks.sensors.driver.SensorType;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@@ -80,6 +82,12 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
}
}
@Nullable
@Override
protected Object onGetDefaultValue(@NonNull TypedArray a, int index) {
return a.getString(index);
}
@Override
protected void onSetInitialValue(Object defaultValue) {
setValue(getPersistedString((String) defaultValue));
@@ -7,7 +7,7 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerTemperature;
import de.dennisguse.opentracks.sensors.driver.TemperatureBluetooth;
public class BluetoothLeTemperaturePreference extends BluetoothLeSensorPreference {
@@ -30,6 +30,6 @@ public class BluetoothLeTemperaturePreference extends BluetoothLeSensorPreferenc
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreferenceDialog
.newInstance(getKey(), List.of(BluetoothHandlerTemperature.TEMPERATURE));
.newInstance(getKey(), List.of(TemperatureBluetooth.TEMPERATURE));
}
}
+1
View File
@@ -26,6 +26,7 @@
<string name="settings_sensor_bluetooth_cycling_power_key" translatable="false">bluetoothCyclingPowerSensor</string>
<string name="settings_sensor_bluetooth_running_speed_and_cadence_key" translatable="false">bluetoothRunningSpeedAndCadenceSensor</string>
<string name="settings_sensor_bluetooth_pressure_key" translatable="false">bluetoothPressureSensor</string>
<string name="settings_sensor_bluetooth_gps_key" translatable="false">bluetoothGPSSensor</string>
<string name="sensor_type_value_none" translatable="false">NONE</string>
<string name="sensor_type_value_internal" translatable="false">INTERNAL</string>
+6 -3
View File
@@ -5,12 +5,15 @@
android:title="@string/settings_sensors_title">
<PreferenceCategory android:title="@string/settings_gps_sensor">
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLeGpsPreference
android:defaultValue="@string/sensor_type_value_internal"
android:key="@string/settings_sensor_bluetooth_gps_key"
android:title="@string/settings_gps_sensor" />
<ListPreference
android:defaultValue="@string/min_sampling_interval_default"
android:entryValues="@array/min_sampling_interval_values"
android:key="@string/min_sampling_interval_key"
android:title="@string/settings_recording_min_sampling_interval_title" />
<ListPreference
android:defaultValue="@string/recording_gps_accuracy_default"
android:entryValues="@array/recording_gps_accuracy_values"
@@ -71,9 +74,9 @@
<SwitchPreferenceCompat
android:defaultValue="@bool/settings_sensor_bluetooth_service_filter_enabled_default"
android:key="@string/settings_sensor_bluetooth_service_filter_enabled_key"
android:title="@string/settings_sensor_bluetooth_service_filter_title"
android:summaryOff="@string/settings_sensor_bluetooth_service_filter_off"
android:summaryOn="@string/settings_sensor_bluetooth_service_filter_on"
android:summaryOff="@string/settings_sensor_bluetooth_service_filter_off" />
android:title="@string/settings_sensor_bluetooth_service_filter_title" />
</PreferenceCategory>
</PreferenceScreen>