Storing data from sensors without GPS
This commit is contained in:
Dennis Guse
2021-12-28 14:24:57 +01:00
committed by GitHub
23 changed files with 418 additions and 227 deletions
+3 -1
View File
@@ -78,7 +78,7 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* __Photos and Markers:__ mark interesting locations while tracking
* __Export:__
* export tracks either as KMZ (incl. photos), KML, or GPX
* export automatically after each recording (e.g., to sync)
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
* __Altitude:__
* gain/loss via barometric sensor (if present)
@@ -89,6 +89,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* cycling: cadence
* cycling: power meter
* running: speed and cadence
* support for BLE sensor training only (i.e., without GPS) for indoor training
An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md)
### Gadgetbridge integration
@@ -15,6 +15,13 @@
*/
package de.dennisguse.opentracks.content.provider;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -57,13 +64,6 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
/**
* A unit test for {@link ContentProviderUtils}.
*
@@ -220,6 +220,8 @@ public class CustomContentProviderUtilsTest {
assertEquals(0, tracksPointsCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, markerCursor.getCount());
//TODO Close all cursors
}
/**
@@ -55,6 +55,7 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceTest;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
@@ -98,24 +99,29 @@ public class ExportImportTest {
private TrackImporter trackImporter;
@Before
public void fileSetup() throws IOException {
public void fileSetup() throws IOException, TimeoutException {
TrackRecordingServiceTest.resetService(mServiceRule, context);
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
tmpFileUri = Uri.fromFile(tmpFile);
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
TrackRecordingServiceTest.resetService(mServiceRule, context);
}
@After
public void FileTearDown() throws TimeoutException {
public void tearDown() throws TimeoutException {
tmpFile.deleteOnExit();
tmpFileUri = null;
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getTrackPointCreator().setClock(Clock.systemUTC());
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
TrackRecordingServiceTest.resetService(mServiceRule, context);
}
public void setUp(boolean hasSensorDistance) throws TimeoutException {
public void setUp() throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
@@ -123,13 +129,21 @@ public class ExportImportTest {
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
trackId = service.startNewTrack();
service.stopUpdateRecordingData();
Distance sensorDistance = hasSensorDistance ? Distance.of(10) : null; // recording distance interval
Distance sensorDistance = Distance.of(10); // recording distance interval
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:03Z"), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance);
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:03Z"), 3, 14, 10, 15, 10, 1);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:04Z"), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:05Z"), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
// A sensor-only TrackPoint
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
mockAltitudeChange(trackPointCreator, 1);
sendSensor(trackPointCreator, Instant.parse("2020-02-02T02:02:04Z"));
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:05Z"), 3, 14.002, 10, 15, 10, 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:06Z"), ZoneId.of("CET")));
@@ -138,12 +152,17 @@ public class ExportImportTest {
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:20Z"), ZoneId.of("CET")));
service.resumeCurrentTrack();
service.stopUpdateRecordingData();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:21Z"), ZoneId.of("CET")));
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:21Z"), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:21Z"), 3, 14.003, 10, 15, 10, 0);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:22Z"), 3, 16, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:23Z"), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:22Z"), 3, 16, 10, 15, 10, 0);
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:23Z"), 3, 16.001, 10, 15, 10, 0);
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:24Z"), ZoneId.of("CET")));
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context));
@@ -162,21 +181,11 @@ public class ExportImportTest {
assertEquals(2, markers.size());
}
@After
public void tearDown() {
if (trackId != null) {
contentProviderUtils.deleteTrack(context, trackId);
}
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
}
//TODO Does not test images
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp(true);
setUp();
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -213,23 +222,40 @@ public class ExportImportTest {
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:24Z"), importedTrackStatistics.getStopTime());
assertEquals(track.getTrackStatistics().getTotalTime(), importedTrackStatistics.getTotalTime());
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(8), importedTrackStatistics.getTotalTime());
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
assertEquals(Distance.of(60), importedTrackStatistics.getTotalDistance());
// Speed
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(Speed.of(15), importedTrackStatistics.getMaxSpeed());
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(7.5), importedTrackStatistics.getAverageSpeed());
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(Speed.of(15), importedTrackStatistics.getAverageMovingSpeed());
// Altitude
assertEquals(originalTrackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(originalTrackStatistics.getMaxAltitude(), importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(10, importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(originalTrackStatistics.getTotalAltitudeGain(), importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(originalTrackStatistics.getTotalAltitudeLoss(), importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
// 4. markers
assertMarkers();
@@ -238,7 +264,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false);
setUp();
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
@@ -264,7 +290,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void gpx() throws TimeoutException, IOException {
setUp(true);
setUp();
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -297,7 +323,10 @@ public class ExportImportTest {
// Therefore, the track segmentation is changes.
List<TrackPoint> trackPointsWithCoordinates = trackPoints.stream().filter(it -> TrackPoint.Type.SEGMENT_START_AUTOMATIC.equals(it.getType()) || TrackPoint.Type.TRACKPOINT.equals(it.getType())).collect(Collectors.toList());
trackPointsWithCoordinates.get(0).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackPointsWithCoordinates.get(3).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackPointsWithCoordinates.get(1).setSensorDistance(Distance.of(20));
trackPointsWithCoordinates.get(1).setAltitudeGain(1f);
trackPointsWithCoordinates.get(1).setAltitudeLoss(1f);
trackPointsWithCoordinates.get(2).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
TrackPointAssert a = new TrackPointAssert()
.setDelta(0.05)
@@ -326,8 +355,8 @@ public class ExportImportTest {
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(10, importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
// 4. markers
assertMarkers();
@@ -336,7 +365,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false);
setUp();
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
@@ -380,22 +409,7 @@ public class ExportImportTest {
}
}
private void sendLocation(TrackPointCreator trackPointCreator, Instant time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
location.setAltitude(altitude);
trackPointCreator.setAltitudeSumManager(new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(altitudeGain);
trackPoint.setAltitudeLoss(altitudeGain);
}
});
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, Float heartRate, Float cyclingCadence, Float power) {
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
@Override
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
@@ -421,8 +435,30 @@ public class ExportImportTest {
return sensorDataSet;
}
});
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
AltitudeSumManager altitudeSumManager = trackPointCreator.getAltitudeSumManager();
altitudeSumManager.setAltitudeGain_m(altitudeGain);
altitudeSumManager.setAltitudeLoss_m(altitudeGain);
}
private void sendSensor(TrackPointCreator trackPointCreator, Instant time) {
trackPointCreator.setClock(Clock.fixed(time, ZoneId.of("CET")));
trackPointCreator.onNewTrackPointWithoutGPS();
}
private void sendLocation(TrackPointCreator trackPointCreator, Instant time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
location.setAltitude(altitude);
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(Clock.fixed(time, ZoneId.of("CET")));
trackPointCreator.getLocationHandler().onLocationChanged(location);
trackPointCreator.getGpsHandler().onLocationChanged(location);
}
}
@@ -22,7 +22,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
@@ -60,7 +59,6 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
@@ -110,30 +108,15 @@ public class TrackRecordingServiceTest {
};
@Before
public void setUp() {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
public void setUp() throws TimeoutException {
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
PreferencesUtils.clear();
// Ensure that the database is empty before every test
contentProviderUtils.deleteAllTracks(context);
tearDown();
}
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
service.getTrackPointCreator().setClock(Clock.systemUTC());
TrackRecordingServiceTest.resetService(mServiceRule, context);
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
@@ -211,6 +194,7 @@ public class TrackRecordingServiceTest {
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
@@ -241,12 +225,14 @@ public class TrackRecordingServiceTest {
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.pauseCurrentTrack();
service.stopUpdateRecordingData();
// then
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
@@ -254,6 +240,7 @@ public class TrackRecordingServiceTest {
//when
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.resumeTrack(trackId);
service.stopUpdateRecordingData();
// then
assertTrue(service.isRecording());
@@ -280,6 +267,7 @@ public class TrackRecordingServiceTest {
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
@@ -322,6 +310,7 @@ public class TrackRecordingServiceTest {
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
@@ -352,10 +341,13 @@ public class TrackRecordingServiceTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
assertTrue(service.isRecording());
// when
Track.Id newTrackId = service.startNewTrack();
service.stopUpdateRecordingData();
// then
assertNotNull(trackId);
@@ -405,6 +397,7 @@ public class TrackRecordingServiceTest {
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
assertTrue(service.isRecording());
trackPointCreator.onNewTrackPoint(
@@ -461,4 +454,19 @@ public class TrackRecordingServiceTest {
trackRecordingService.getTrackPointCreator().onNewTrackPoint(trackPoint, Distance.of(50));
}
//TODO Workaround as service is not stopped on API23; thus sharedpreferences are not reset between tests.
//TODO Anyhow, the service should re-create all it's resources if a recording starts and makes sure that there is no leftovers from previous recordings.
@Deprecated
public static void resetService(ServiceTestRule mServiceRule, Context context) throws TimeoutException {
// Let's use default values.
PreferencesUtils.clear();
// Reset service (if some previous test failed)
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.endCurrentTrack();
service.getTrackPointCreator().setClock(Clock.systemUTC());
service.sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
}
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertFalse;
import android.content.ContentProvider;
import android.content.Context;
import android.os.Looper;
@@ -31,14 +30,12 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Tests insert location.
@@ -79,24 +76,17 @@ public class TrackRecordingServiceTestLocation {
@Before
public void setUp() throws TimeoutException {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
tearDown();
// Let's use default values.
PreferencesUtils.clear();
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
service.getTrackPointCreator().stopGPS();
}
@After
public void tearDown() {
public void tearDown() throws TimeoutException {
TrackRecordingServiceTest.resetService(mServiceRule, context);
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@@ -106,6 +96,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_movingAccurate() {
// given
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when
@@ -179,6 +170,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_slowMovingAccurate() {
// given
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when
@@ -224,6 +216,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_idle() {
// given
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when
@@ -269,6 +262,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_idle_withMovement() {
// given
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
service.getTrackPointCreator().stopGPS();
@@ -329,6 +323,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_idle_withSensorData() {
// given
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
@@ -403,6 +398,7 @@ public class TrackRecordingServiceTestLocation {
// given
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
service.getTrackPointCreator().setRemoteSensorManager(remoteSensorManager);
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
altitudeSumManager.stop(service);
@@ -474,6 +470,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_segment() {
// given
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when
@@ -1,6 +1,10 @@
package de.dennisguse.opentracks.services;
import android.content.ContentProvider;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Looper;
@@ -30,20 +34,15 @@ import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests for the track recording service, which require a {@link Looper}.
*
* @author Bartlomiej Niechwiej
*/
//TODO Check that those tests are really testing something!
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestLooper {
@@ -66,29 +65,15 @@ public class TrackRecordingServiceTestLooper {
}
@Before
public void setUp() {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
public void setUp() throws TimeoutException {
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
PreferencesUtils.clear();
// Ensure that the database is empty before every test
contentProviderUtils.deleteAllTracks(context);
tearDown();
}
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
TrackRecordingServiceTest.resetService(mServiceRule, context);
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
@@ -195,6 +180,7 @@ public class TrackRecordingServiceTestLooper {
// Start a track.
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
assertNotNull(trackId);
assertTrue(service.isRecording());
Track track = contentProviderUtils.getTrack(trackId);
@@ -3,10 +3,7 @@ package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Looper;
import androidx.annotation.NonNull;
@@ -18,7 +15,6 @@ import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -38,9 +34,7 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
@@ -81,27 +75,15 @@ public class TrackRecordingServiceTestStatistics {
@Before
public void setUp() throws TimeoutException {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
tearDown();
// Let's use default values.
PreferencesUtils.clear();
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
service.getTrackPointCreator().stopGPS();
}
@After
public void tearDown() throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getTrackPointCreator().setClock(Clock.systemUTC());
TrackRecordingServiceTest.resetService(mServiceRule, context);
contentProviderUtils.deleteAllTracks(context);
}
/**
@@ -109,15 +91,15 @@ public class TrackRecordingServiceTestStatistics {
*/
@MediumTest
@Test
public void movingtime_with_pauses() {
Assume.assumeTrue(
"Test fails on API23; reproducible on CI and some machines.",
Build.VERSION.SDK_INT > 23
);
public void movingtime_with_pauses() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
service.getTrackPointCreator().stopGPS();
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()));
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
Function<Integer, Void> assertMovingTime = expected -> {
@@ -26,7 +26,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(MockitoJUnitRunner.class)
public class LocationHandlerTest {
public class GPSHandlerTest {
private final Context context = ApplicationProvider.getApplicationContext();
@@ -34,7 +34,7 @@ public class LocationHandlerTest {
private TrackPointCreator trackPointCreator;
@InjectMocks
private LocationHandler locationHandler;
private GPSHandler locationHandler;
@BeforeClass
public static void preSetUp() {
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.services.handlers;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.content.SharedPreferences;
import org.junit.After;
import org.junit.Before;
@@ -26,10 +25,7 @@ public class TrackPointCreatorTest {
private TrackPointCreator.Callback server;
@Mock
private LocationHandler locationHandler;
@Mock
private SharedPreferences sharedPreferences;
private GPSHandler locationHandler;
private TrackPointCreator subject;
@@ -63,7 +63,8 @@ public class TrackPoint {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
TRACKPOINT(0), //Just GPS data.
TRACKPOINT(0), //Just GPS data and may contain BLE sensor data
SENSORPOINT(2), //Just BLE sensor data
SEGMENT_END_MANUAL(1); //End of a segment
@@ -310,7 +311,7 @@ public class TrackPoint {
}
@Nullable
public Distance distanceToPrevious(TrackPoint previous) {
public Distance distanceToPrevious(@Nullable TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
@@ -77,7 +77,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ SENSOR_POWER + " FLOAT, "
+ ALTITUDE_GAIN + " FLOAT, "
+ ALTITUDE_LOSS + " FLOAT, "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1)), "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1, 2)), "
+ SENSOR_DISTANCE + " FLOAT, "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
+ ")";
@@ -26,7 +26,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 31;
private static final int DATABASE_VERSION = 32;
public CustomSQLiteOpenHelper(Context context) {
this(context, ((Startup) context.getApplicationContext()).getDatabaseName());
@@ -83,6 +83,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 31:
upgradeFrom30to31(db);
break;
case 32:
upgradeFrom31to32(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -119,6 +122,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 30:
downgradeFrom31to30(db);
break;
case 31:
downgradeFrom32to31(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
@@ -425,4 +431,35 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.setTransactionSuccessful();
db.endTransaction();
}
/**
* Add TrackPoint type SENSORPOINT (2).
*/
private void upgradeFrom31to32(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1, 2)), sensor_distance FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom32to31(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1)), sensor_distance FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
}
@@ -182,7 +182,9 @@ public class IntervalsFragment extends Fragment {
}
private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) {
boolean update = metricUnits != this.metricUnits || !selectedInterval.sameMultiplier(this.selectedInterval);
boolean update = metricUnits != this.metricUnits
|| selectedInterval == null
|| !selectedInterval.sameMultiplier(this.selectedInterval);
this.metricUnits = metricUnits;
this.selectedInterval = selectedInterval;
@@ -24,13 +24,19 @@ import androidx.annotation.NonNull;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -122,6 +128,8 @@ public class GPXTrackExporter implements TrackExporter {
boolean wroteTrack = false;
boolean wroteSegment = false;
LinkedList<TrackPoint> sensorPoints = new LinkedList<>();
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) throw new InterruptedException();
@@ -149,7 +157,12 @@ public class GPXTrackExporter implements TrackExporter {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
wroteSegment = true;
writeTrackPoint(trackPoint);
writeTrackPoint(trackPoint, sensorPoints);
sensorPoints.clear();
break;
case SENSORPOINT:
sensorPoints.add(trackPoint);
break;
case TRACKPOINT:
if (!wroteSegment) {
@@ -157,11 +170,21 @@ public class GPXTrackExporter implements TrackExporter {
writeOpenSegment();
wroteSegment = true;
}
writeTrackPoint(trackPoint);
writeTrackPoint(trackPoint, sensorPoints);
sensorPoints.clear();
break;
default:
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
}
if (!sensorPoints.isEmpty()) {
//TODO We might miss to export data; this happens if there are SENSORPOINTs after the final TRACKPOINT of a track.
//For segments the data is added to the next segment.
Log.d(TAG, "SENSORPOINTs after final TRACKPOINT; this data is not exported.");
}
if (wroteSegment) {
// Should not be necessary as tracks should end with SEGMENT_END_MANUAL.
// Anyhow, make sure that the last segment is closed.
@@ -200,6 +223,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
printWriter.println("xmlns:gpxtrkx=\"http://www.garmin.com/xmlschemas/TrackStatsExtension/v1\"");
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
printWriter.println("xsi:schemaLocation=" +
"\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
@@ -256,6 +280,18 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<extensions>");
printWriter.println("<topografix:color>c0c0c0</topografix:color>");
printWriter.println("<opentracks:trackid>" + track.getUuid() + "</opentracks:trackid>");
TrackStatistics trackStatistics = track.getTrackStatistics();
printWriter.println("<gpxtrkx:TrackStatsExtension>");
printWriter.println("<gpxtrkx:Distance>" + trackStatistics.getTotalDistance().toM() + "</gpxtrkx:Distance>");
printWriter.println("<gpxtrkx:TimerTime>" + trackStatistics.getTotalTime().getSeconds() + "</gpxtrkx:TimerTime>");
printWriter.println("<gpxtrkx:MovingTime>" + trackStatistics.getMovingTime().getSeconds() + "</gpxtrkx:MovingTime>");
printWriter.println("<gpxtrkx:StoppedTime>" + trackStatistics.getStoppedTime().getSeconds() + "</gpxtrkx:StoppedTime>");
printWriter.println("<gpxtrkx:MaxSpeed>" + trackStatistics.getMaxSpeed().toMPS() + "</gpxtrkx:MaxSpeed>");
printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.getTotalAltitudeGain() + "</gpxtrkx:Ascent>");
printWriter.println("<gpxtrkx:Descent>" + trackStatistics.getTotalAltitudeLoss() + "</gpxtrkx:Descent>");
printWriter.println("</gpxtrkx:TrackStatsExtension>");
printWriter.println("</extensions>");
}
}
@@ -274,7 +310,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("</trkseg>");
}
public void writeTrackPoint(TrackPoint trackPoint) {
public void writeTrackPoint(TrackPoint trackPoint, List<TrackPoint> sensorPoints) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
@@ -285,44 +321,59 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
if (trackPoint.hasSpeed() || trackPoint.hasHeartRate() || trackPoint.hasCadence() || trackPoint.hasAltitudeGain() || trackPoint.hasAltitudeLoss()) {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
{
String trackPointExtensionContent = "";
if (trackPoint.hasSpeed()) {
printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>");
trackPointExtensionContent += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>\n";
}
if (trackPoint.hasHeartRate()) {
printWriter.println("<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate_bpm()) + "</gpxtpx:hr>");
trackPointExtensionContent += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate_bpm()) + "</gpxtpx:hr>\n";
}
if (trackPoint.hasCadence()) {
printWriter.println("<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence_rpm()) + "</gpxtpx:cad>");
trackPointExtensionContent += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence_rpm()) + "</gpxtpx:cad>\n";
}
if (trackPoint.hasPower()) {
printWriter.println("<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower()) + "</pwr:PowerInWatts>");
trackPointExtensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower()) + "</pwr:PowerInWatts>\n";
}
if (trackPoint.hasAltitudeGain()) {
printWriter.println("<opentracks:gain>" + ALTITUDE_FORMAT.format(trackPoint.getAltitudeGain()) + "</opentracks:gain>");
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGain() ? (double) tp.getAltitudeGain() : null);
if (cumulativeGain != null) {
trackPointExtensionContent += ("<opentracks:gain>" + ALTITUDE_FORMAT.format(cumulativeGain) + "</opentracks:gain>\n");
}
if (trackPoint.hasAltitudeLoss()) {
printWriter.println("<opentracks:loss>" + ALTITUDE_FORMAT.format(trackPoint.getAltitudeLoss()) + "</opentracks:loss>");
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeLoss() ? (double) tp.getAltitudeLoss() : null);
if (cumulativeLoss != null) {
trackPointExtensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
}
if (trackPoint.hasSensorDistance()) {
printWriter.println("<opentracks:distance>" + DISTANCE_FORMAT.format(trackPoint.getSensorDistance().toM()) + "</opentracks:distance>");
Double cumulativeDistance = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null);
if (cumulativeDistance != null) {
trackPointExtensionContent += ("<opentracks:distance>" + ALTITUDE_FORMAT.format(cumulativeDistance) + "</opentracks:distance>\n");
}
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
if (!trackPointExtensionContent.isEmpty()) {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
printWriter.print(trackPointExtensionContent);
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
}
}
printWriter.println("</trkpt>");
}
}
private Double cumulateSensorData(TrackPoint trackPoint, List<TrackPoint> sensorPoints, Function<TrackPoint, Double> map) {
return Stream.concat(sensorPoints.stream(), Stream.of(trackPoint))
.map(map)
.filter(Objects::nonNull)
.reduce((gain, next) -> gain + next)
.orElse(null);
}
private String formatLocation(double latitude, double longitude) {
return "lat=\"" + COORDINATE_FORMAT.format(latitude) + "\" lon=\"" + COORDINATE_FORMAT.format(longitude) + "\"";
}
@@ -181,6 +181,7 @@ public class KMLTrackExporter implements TrackExporter {
writeCloseSegment();
wroteSegment = false;
break;
case SENSORPOINT:
case TRACKPOINT:
if (!wroteSegment) {
// Might happen for older data (pre v3.15.0)
@@ -189,6 +190,8 @@ public class KMLTrackExporter implements TrackExporter {
}
writeTrackPoint(trackPoint);
break;
default:
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
}
@@ -243,7 +243,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private void onTrackSegmentEnd() {
if (currentSegment.isEmpty()) {
Log.w(TAG, "No locations in current segment.");
Log.w(TAG, "No TrackPoints in current segment.");
return;
}
@@ -256,7 +256,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private TrackPoint createTrackPoint() throws ParsingException {
Instant parsedTime = null;
Instant parsedTime;
try {
parsedTime = StringUtils.parseTime(time);
} catch (Exception e) {
@@ -295,15 +295,21 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
TrackPoint trackPoint;
if (i == 0) {
//first
if (location == null) {
trackPoint = TrackPoint.createSegmentStartManualWithTime(time);
} else {
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, location, time);
}
} else if (i == locationList.size() - 1 && location == null) {
//last
trackPoint = TrackPoint.createSegmentEndWithTime(time);
} else {
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, location, time);
if (location == null) {
trackPoint = new TrackPoint(TrackPoint.Type.SENSORPOINT, time);
} else {
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, location, time);
}
}
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
@@ -36,9 +36,11 @@ class TrackRecordingManager {
private Track.Id trackId;
private TrackStatisticsUpdater trackStatisticsUpdater;
//TDOO use lastStoredTrackPoint?
private boolean currentSegmentHasTrackPoint;
private TrackPoint lastTrackPoint;
private TrackPoint lastStoredTrackPoint;
private TrackPoint lastStoredTrackPointWithLocation;
TrackRecordingManager(Context context) {
this.context = context;
@@ -80,15 +82,20 @@ class TrackRecordingManager {
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(trackId, segmentStartTrackPoint);
currentSegmentHasTrackPoint = false;
lastTrackPoint = null;
lastStoredTrackPoint = null; }
void pause(TrackPointCreator trackPointCreator) {
insertTrackPoint(trackId, trackPointCreator.createSegmentEnd());
currentSegmentHasTrackPoint = false;
lastTrackPoint = null;
lastStoredTrackPoint = null;
lastStoredTrackPointWithLocation = null;
}
void pause(TrackPointCreator trackPointCreator) {
insertTrackPoint(trackId, trackPointCreator.createSegmentEnd());
currentSegmentHasTrackPoint = false;
lastTrackPoint = null;
lastStoredTrackPoint = null;
lastStoredTrackPointWithLocation = null;
}
void end(TrackPointCreator trackPointCreator) {
@@ -97,9 +104,11 @@ class TrackRecordingManager {
trackId = null;
trackStatisticsUpdater = null;
currentSegmentHasTrackPoint = false;
lastTrackPoint = null;
lastStoredTrackPoint = null;
currentSegmentHasTrackPoint = false;
lastStoredTrackPointWithLocation = null;
}
Pair<Track, Pair<TrackPoint, SensorDataSet>> get(TrackPointCreator trackPointCreator) {
@@ -131,7 +140,7 @@ class TrackRecordingManager {
name = context.getString(R.string.marker_name_format, nextMarkerNumber + 1);
}
if (lastStoredTrackPoint == null) {
if (lastStoredTrackPointWithLocation == null) {
Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
return null;
}
@@ -142,11 +151,14 @@ class TrackRecordingManager {
photoUrl = photoUrl != null ? photoUrl : "";
// Insert marker
Marker marker = new Marker(name, description, category, icon, trackId, getTrackStatistics(), lastStoredTrackPoint, photoUrl);
Marker marker = new Marker(name, description, category, icon, trackId, getTrackStatistics(), lastStoredTrackPointWithLocation, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri));
}
/**
* @return TrackPoint was stored?
*/
boolean onNewTrackPoint(TrackPoint trackPoint) {
//Storing trackPoint
@@ -157,18 +169,31 @@ class TrackRecordingManager {
return true;
}
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastStoredTrackPoint);
if (distanceToLastTrackLocation != null) {
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
Distance distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
if (distanceToLastStoredTrackPoint != null) {
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(trackId, trackPoint);
return true;
}
if (distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval) && trackPoint.isMoving()) {
if (distanceToLastStoredTrackPoint.greaterOrEqualThan(recordingDistanceInterval) && trackPoint.isMoving()) {
insertTrackPoint(trackId, trackPoint);
return true;
}
if (trackPoint.hasLocation()) {
if (lastStoredTrackPointWithLocation == null) {
insertTrackPoint(trackId, trackPoint);
return true;
}
Distance distanceToLastStoredTrackPointWithLocation = trackPoint.distanceToPrevious(lastStoredTrackPointWithLocation);
if (distanceToLastStoredTrackPointWithLocation != null && distanceToLastStoredTrackPointWithLocation.greaterOrEqualThan(recordingDistanceInterval)) {
insertTrackPoint(trackId, trackPoint);
return true;
}
}
}
if (lastStoredTrackPoint != null && trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) {
@@ -209,6 +234,9 @@ class TrackRecordingManager {
contentProviderUtils.updateTrackStatistics(trackId, trackStatisticsUpdater.getTrackStatistics());
lastStoredTrackPoint = trackPoint;
if (trackPoint.hasLocation()) {
lastStoredTrackPointWithLocation = lastStoredTrackPoint;
}
} catch (SQLiteException e) {
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.services;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
@@ -50,11 +49,11 @@ import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.tasks.VoiceAnnouncementManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
/**
@@ -73,6 +72,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
private final Binder binder = new Binder();
// The following variables are setFrequency in onCreate:
private VoiceAnnouncementManager voiceAnnouncementManager;
private TrackRecordingServiceNotificationManager notificationManager;
@@ -86,6 +87,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
@Override
public void run() {
updateRecordingDataWhileRecording();
trackPointCreator.onNewTrackPointWithoutGPS(); //TODO Should not be called every second, right? + with do some duplicate computation with updateRecordingDataWhileRecording().
Handler localHandler = TrackRecordingService.this.handler;
if (localHandler == null) {
// when this happens, no recording is running and we should not send any notifications.
@@ -96,7 +99,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
};
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Deprecated
//TODO Workaround as service is not stopped on API23; thus sharedpreferences are not reset between tests.
@VisibleForTesting
final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
@@ -115,11 +121,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
};
// The following variables are setFrequency when recording:
// The following variables are set when recording:
private WakeLock wakeLock;
private final Binder binder = new Binder();
private TrackPointCreator trackPointCreator; //TODO Move to TrackRecordingManager?
private RecordingStatus recordingStatus;
@@ -148,27 +152,20 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public Binder onBind(Intent intent) {
return binder;
}
@Override
public void onDestroy() {
handler.removeCallbacksAndMessages(null); //Some tests do not finish the recording completely
handler = null;
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackPointCreator.stop();
trackPointCreator = null;
trackRecordingManager = null;
// Reverse order from onCreate
showNotification(false); //TODO Why?
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
notificationManager = null;
try {
voiceAnnouncementManager.shutdown();
@@ -179,6 +176,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
// This should be the next to last operation
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
updateRecordingStatus(STATUS_DEFAULT);
recordingStatusObservable = null;
gpsStatusObservable = null;
recordingDataObservable = null;
@@ -186,6 +184,16 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public Binder onBind(Intent intent) {
return binder;
}
public boolean isRecording() {
return recordingStatus.isRecording();
}
@@ -317,7 +325,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
* @param trackStopped true if track is stopped, false if track is paused
*/
private void endRecording(boolean trackStopped) {
handler.removeCallbacks(updateRecordingData);
stopUpdateRecordingData();
if (!trackStopped) {
updateRecordingDataWhileRecording();
} else {
@@ -360,8 +368,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return false;
}
boolean stored = trackRecordingManager.onNewTrackPoint(trackPoint);
notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy);
return trackRecordingManager.onNewTrackPoint(trackPoint);
return stored;
}
@Override
@@ -419,6 +428,12 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return trackPointCreator;
}
@Deprecated
@VisibleForTesting
public TrackRecordingManager getTrackRecordingManager() {
return trackRecordingManager;
}
public LiveData<GpsStatusValue> getGpsStatusObservable() {
return gpsStatusObservable;
}
@@ -456,6 +471,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second));
}
@VisibleForTesting
public void stopUpdateRecordingData() {
handler.removeCallbacks(updateRecordingData);
}
public LiveData<RecordingStatus> getRecordingStatusObservable() {
return recordingStatusObservable;
}
@@ -15,13 +15,13 @@ import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.LocationUtils;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class LocationHandler implements LocationListener, GpsStatus.GpsStatusListener {
public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener {
private final String TAG = LocationHandler.class.getSimpleName();
private final String TAG = GPSHandler.class.getSimpleName();
private LocationManager locationManager;
private final TrackPointCreator trackPointCreator;
@@ -30,7 +30,7 @@ public class LocationHandler implements LocationListener, GpsStatus.GpsStatusLis
private Distance thresholdHorizontalAccuracy;
private TrackPoint lastTrackPoint;
public LocationHandler(TrackPointCreator trackPointCreator) {
public GPSHandler(TrackPointCreator trackPointCreator) {
this.trackPointCreator = trackPointCreator;
}
@@ -158,6 +158,10 @@ public class LocationHandler implements LocationListener, GpsStatus.GpsStatusLis
return lastTrackPoint;
}
Distance getThresholdHorizontalAccuracy() {
return thresholdHorizontalAccuracy;
}
/**
* Called from {@link GpsStatus} to inform that GPS status has changed from prevStatus to currentStatus.
*
@@ -31,25 +31,25 @@ public class TrackPointCreator {
@NonNull
private Clock clock = Clock.systemUTC();
private final LocationHandler locationHandler;
private final GPSHandler gpsHandler;
private BluetoothRemoteSensorManager remoteSensorManager;
private AltitudeSumManager altitudeSumManager;
public TrackPointCreator(Callback service) {
this.service = service;
this.locationHandler = new LocationHandler(this);
this.gpsHandler = new GPSHandler(this);
}
@VisibleForTesting
TrackPointCreator(LocationHandler locationHandler, Callback service) {
TrackPointCreator(GPSHandler gpsHandler, Callback service) {
this.service = service;
this.locationHandler = locationHandler;
this.gpsHandler = gpsHandler;
}
public void start(@NonNull Context context) {
this.context = context;
locationHandler.onStart(context);
gpsHandler.onStart(context);
remoteSensorManager = new BluetoothRemoteSensorManager(context);
remoteSensorManager.start();
@@ -58,11 +58,15 @@ public class TrackPointCreator {
altitudeSumManager.start(context);
}
private boolean isStarted() {
return context != null;
}
@Deprecated
//There should be a cooler way to do this; we want to send fake locations without getting affected by real GPS data.
@VisibleForTesting
public void stopGPS() {
locationHandler.onStop();
gpsHandler.onStop();
}
public void resetSensorData() {
@@ -75,6 +79,10 @@ public class TrackPointCreator {
}
private SensorDataSet fill(TrackPoint trackPoint) {
if (!isStarted()) {
Log.w(TAG, "Not started, should not be called.");
return null;
}
SensorDataSet sensorDataSet = remoteSensorManager.fill(trackPoint);
altitudeSumManager.fill(trackPoint);
@@ -82,7 +90,7 @@ public class TrackPointCreator {
}
public void stop() {
locationHandler.onStop();
gpsHandler.onStop();
if (remoteSensorManager != null) {
remoteSensorManager.stop();
@@ -98,10 +106,10 @@ public class TrackPointCreator {
}
public void onSharedPreferenceChanged(String key) {
locationHandler.onSharedPreferenceChanged(key);
gpsHandler.onSharedPreferenceChanged(key);
}
public void onNewTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) {
public synchronized void onNewTrackPoint(@NonNull TrackPoint trackPoint, @NonNull Distance thresholdHorizontalAccuracy) {
fill(trackPoint);
boolean stored = service.newTrackPoint(trackPoint, thresholdHorizontalAccuracy);
@@ -110,6 +118,10 @@ public class TrackPointCreator {
}
}
public void onNewTrackPointWithoutGPS() {
onNewTrackPoint(new TrackPoint(TrackPoint.Type.SENSORPOINT, createNow()), gpsHandler.getThresholdHorizontalAccuracy());
}
public TrackPoint createSegmentStartManual() {
return TrackPoint.createSegmentStartManualWithTime(createNow());
}
@@ -123,7 +135,7 @@ public class TrackPointCreator {
public Pair<TrackPoint, SensorDataSet> createCurrentTrackPoint(@Nullable TrackPoint lastValidTrackPoint) {
TrackPoint currentTrackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, createNow());
TrackPoint lastTrackPoint = locationHandler.getLastTrackPoint();
TrackPoint lastTrackPoint = gpsHandler.getLastTrackPoint();
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
currentTrackPoint.setSpeed(lastTrackPoint.getSpeed());
@@ -148,6 +160,11 @@ public class TrackPointCreator {
return Instant.now(clock);
}
@VisibleForTesting
public AltitudeSumManager getAltitudeSumManager() {
return altitudeSumManager;
}
@Deprecated
@VisibleForTesting
public void setAltitudeSumManager(AltitudeSumManager altitudeSumManager) {
@@ -166,8 +183,8 @@ public class TrackPointCreator {
}
@VisibleForTesting
public LocationHandler getLocationHandler() {
return locationHandler;
public GPSHandler getGpsHandler() {
return gpsHandler;
}
void sendGpsStatus(GpsStatusValue gpsStatusValue) {
@@ -65,29 +65,38 @@ public class AltitudeSumManager implements SensorEventListener {
trackPoint.setAltitudeLoss(altitudeLoss_m);
}
public @Nullable
Float getAltitudeGain_m() {
@Nullable
public Float getAltitudeGain_m() {
return isConnected ? altitudeGain_m : null;
}
@VisibleForTesting
public void setAltitudeGain_m(float altitudeGain_m) {
this.altitudeGain_m = altitudeGain_m;
}
@VisibleForTesting
public void addAltitudeGain_m(float altitudeGain_m) {
this.altitudeGain_m = this.altitudeGain_m == null ? 0f : this.altitudeGain_m;
this.altitudeGain_m += altitudeGain_m ;
this.altitudeGain_m += altitudeGain_m;
}
@VisibleForTesting
public void addAltitudeLoss_m(Float altitudeLoss_m) {
this.altitudeLoss_m = this.altitudeLoss_m == null ? 0f : this.altitudeLoss_m;
this.altitudeLoss_m += altitudeLoss_m ;
this.altitudeLoss_m += altitudeLoss_m;
}
public @Nullable
Float getAltitudeLoss_m() {
@Nullable
public Float getAltitudeLoss_m() {
return isConnected ? altitudeLoss_m : null;
}
@VisibleForTesting
public void setAltitudeLoss_m(float altitudeLoss_m) {
this.altitudeLoss_m = altitudeLoss_m;
}
public void reset() {
Log.d(TAG, "Reset");
altitudeGain_m = null;
@@ -203,6 +203,10 @@ public class TrackStatistics {
movingTime = movingTime.plus(time);
}
public Duration getStoppedTime() {
return totalTime.minus(movingTime);
}
/**
* Gets the average speed.
* This calculation only takes into account the displacement until the last point that was accounted for in statistics.