Compare commits

..

15 Commits

Author SHA1 Message Date
Dennis Guse 3b9711d82e Release: v3.15.3 2021-03-08 18:16:18 +01:00
Dennis Guse 114fc8255f KML export: remove #start (was broken due to #316) and #end (included track statistics) placemarks.
Fixes #629.
2021-03-08 18:11:36 +01:00
Dennis Guse 26e655d58f Cleanup. 2021-03-08 18:11:36 +01:00
Dennis Guse 815b6a3cf1 StatisticsRecordingFragment: set speed=0 when TrackPoint is outdated. 2021-03-07 22:28:53 +01:00
Dennis Guse 3dc8a724d8 Bug: StatisticsRecordingFragment: speed and location where sometimes 0.
Fixes #628.
2021-03-07 22:28:53 +01:00
Dennis Guse 70f571cd09 CI: avoid OutOfMemoryError.
Fixes #599.
2021-03-07 19:46:37 +01:00
Dennis Guse 3f5c3a5c08 Bugfix: Close cursor. 2021-03-06 18:27:24 +01:00
Dennis Guse a13465391f Import: replaced SAXParserException as it only shows the message of it's cause. 2021-03-06 18:13:06 +01:00
Dennis Guse 97454b5369 Statistics*Fragment now updates the units on track category change. 2021-03-04 19:18:31 +01:00
Dennis Guse f6a47c4594 Statistics*Fragement now uses SharedPreferenceListener instead of accessing preferences repeatedly. 2021-03-04 19:18:31 +01:00
Dennis Guse fce76230e1 TrackDataListener now passes most recent TrackPoint to onNewTrackPointsDone(). 2021-03-04 19:18:31 +01:00
Dennis Guse fdf4336a9b Cleanup. 2021-03-04 19:18:31 +01:00
Dennis Guse 844c6e9609 TrackPoint: moved methods into class. 2021-03-04 19:18:31 +01:00
Manuel Zangl 784942ffd2 Fixed longitudeText in StatisticsRecordingFragment 2021-03-04 17:38:42 +01:00
Román Martínez edf4ccc941 It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence. 2021-03-02 19:55:18 +01:00
36 changed files with 871 additions and 349 deletions
+2
View File
@@ -18,4 +18,6 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
profile: Nexus 6
script: ./gradlew connectedCheck
+2 -2
View File
@@ -51,8 +51,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3875
versionName "v3.15.2"
versionCode 3890
versionName "v3.15.3"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -0,0 +1,8 @@
v3.15.3: OpenTracks
Changes:
- Computation avg/max: heart rate, cadence, and power
Bugfix:
- KML/KMZ do not export #start/#end placemarks anymore
- While recording: Speed/location was sometimes falsly shown as 0.
@@ -0,0 +1,92 @@
package de.dennisguse.opentracks.content.data;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class TestSensorDataUtil {
private List<TrackPoint> trackPointList = new ArrayList<>();
private List<SensorData> sensorDataList = new ArrayList<>();
public void add(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
TrackPoint tp = new TrackPoint(type);
int i = trackPointList.size() + 1;
tp.setLatitude(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0);
tp.setLongitude(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0);
tp.setHeartRate_bpm(hr);
tp.setCyclingCadence_rpm(cadence);
tp.setPower(power);
tp.setAccuracy(1f);
tp.setAltitude(1f);
tp.setTime(time);
tp.setSpeed(5f + (i / 10f));
tp.setElevationGain(3f);
tp.setElevationLoss(3f);
trackPointList.add(tp);
}
public List<TrackPoint> getTrackPointList() {
return this.trackPointList;
}
public SensorDataStats computeStats() {
if (sensorDataList == null || sensorDataList.size() <= 1) {
return null;
}
SensorDataStats stats = new SensorDataStats();
long timeElapsed;
long movingTime = 0;
SensorData dataPrev = sensorDataList.get(0);
stats.maxHr = dataPrev.hr;
stats.maxCadence = dataPrev.cadence;
SensorData dataCurrent;
for (int i = 1; i < sensorDataList.size(); i++) {
dataCurrent = sensorDataList.get(i);
if (dataPrev.type != TrackPoint.Type.SEGMENT_START_MANUAL) {
timeElapsed = dataCurrent.type != TrackPoint.Type.SEGMENT_START_MANUAL ? dataCurrent.time.getEpochSecond() - dataPrev.time.getEpochSecond() : 0;
stats.avgHr += (dataPrev.hr * timeElapsed);
stats.maxHr = dataPrev.hr > stats.maxHr ? dataPrev.hr : stats.maxHr;
stats.avgCadence += (dataPrev.cadence * timeElapsed);
stats.maxCadence = dataPrev.cadence > stats.maxCadence ? dataPrev.cadence : stats.maxCadence;
stats.avgPower += (dataPrev.power * timeElapsed);
movingTime += timeElapsed;
}
dataPrev = dataCurrent;
}
stats.avgHr /= movingTime;
stats.avgCadence /= movingTime;
stats.avgPower /= movingTime;
return stats;
}
private static class SensorData {
Instant time;
float hr;
float cadence;
float power;
TrackPoint.Type type;
public SensorData(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
this.time = time;
this.hr = hr == null ? 0 : hr;
this.cadence = cadence == null ? 0 : cadence;
this.power = power == null ? 0 : power;
this.type = type;
}
}
public static class SensorDataStats {
public float avgHr = 0f;
public float maxHr = 0f;
public float avgCadence = 0f;
public float maxCadence = 0f;
public float avgPower = 0f;
}
}
@@ -0,0 +1,28 @@
package de.dennisguse.opentracks.content.data;
import org.junit.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TrackPointTest {
@Test
public void isRecent_true() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
tp.setTime(Instant.now());
assertTrue(tp.isRecent());
}
@Test
public void isRecent_false() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
tp.setTime(Instant.now().minus(2, ChronoUnit.MINUTES));
assertFalse(tp.isRecent());
}
}
@@ -35,18 +35,22 @@ import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
@@ -927,4 +931,337 @@ public class CustomContentProviderUtilsTest {
assertEquals("12", ContentProviderUtils.formatIdListForUri(new Track.Id(12)));
assertEquals("42,43,44", ContentProviderUtils.formatIdListForUri(new Track.Id(42), new Track.Id(43), new Track.Id(44)));
}
@Test
public void testGetSensorStats_noSensorData() {
// given
List<TrackPoint> trackPointList = new ArrayList<>();
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
trackPoint.setPower(null);
trackPoint.setCyclingCadence_rpm(null);
trackPoint.setHeartRate_bpm(null);
trackPointList.add(trackPoint);
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, trackPointList);
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_needAtLeastTwoTrackPointsFalse() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 90 300 0
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.TRACKPOINT);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_needAtLeastTwoTrackPointsTrue() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 90 300 -1
* 1 140 90 300 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, 90f, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertTrue(sensorStatistics.hasHeartRate());
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertTrue(sensorStatistics.hasCadence());
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertTrue(sensorStatistics.hasPower());
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_onlyHr() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 NULL NULL -1
* 1 140 NULL NULL 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, null, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, null, null, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertTrue(sensorStatistics.hasHeartRate());
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_onlyCadence() {
// given
/*
* time elapsed hr cadence power track type
* 0 NULL 90 NULL -1
* 1 NULL 90 NULL 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, null, 90f, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, 90f, null, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertFalse(sensorStatistics.hasHeartRate());
assertTrue(sensorStatistics.hasCadence());
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_onlyPower() {
// given
/*
* time elapsed hr cadence power track type
* 0 NULL NULL 300 -1
* 1 NULL NULL 300 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, null, null, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, null, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertTrue(sensorStatistics.hasPower());
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 7 160 90 275 0
* 4 155 85 280 0
* 1 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(10, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(14, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(15, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_withManualResume() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 3 174 88 400 0
* 20 127 54 175 -2
* 3 160 90 275 0
* 7 155 85 280 0
* 3 150 90 267 0
* 3 170 90 240 0
* 2 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_MANUAL);
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_withStartAutomatic() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 3 174 88 400 0
* 20 127 54 175 -1
* 3 160 90 275 0
* 7 155 85 280 0
* 3 150 90 267 0
* 3 170 90 240 0
* 2 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
private void testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
// given
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
Random random = new Random();
for (int i = 0; i < totalPoints; i++) {
int randomNum = withStartSegments ? random.nextInt(50) - 2 : 0;
TrackPoint.Type type = randomNum >= 0 ? TrackPoint.Type.TRACKPOINT : TrackPoint.Type.getById(randomNum);
float randomHr = random.nextFloat() * (200f - 90f) + 90f;
float randomCadence = random.nextFloat() * (110f - 40f) + 40f;
float randomPower = random.nextFloat() * (500f - 100f) + 100f;
sensorDataUtil.add(start.plus(i, ChronoUnit.SECONDS), randomHr, randomCadence, randomPower, type);
}
sensorDataUtil.add(start.plus(totalPoints, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0.01f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0.01f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0.01f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0.01f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0.01f);
}
@Test
public void testGetSensorStats_veryLongActivity12h() {
testGetSensorStats_randomData(43200, false);
}
@Test
public void testGetSensorStats_withSeveralRandomStartSegments() {
testGetSensorStats_randomData(5000, true);
}
}
@@ -94,18 +94,12 @@ public class StringUtilsTest {
assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there"));
}
/**
* Tests {@link StringUtils#parseTime(String)} with fractional seconds.
*/
@Test
public void testGetTime_fractional() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 352);
}
/**
* Tests {@link StringUtils#parseTime(String)} with time zone.
*/
@Test
public void testGetTime_timezone() {
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
@@ -118,9 +112,6 @@ public class StringUtilsTest {
assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0);
}
/**
* Tests {@link StringUtils#parseTime(String)} with fractional seconds and time zone.
*/
@Test
public void testGetTime_fractionalAndTimezone() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
@@ -84,11 +84,9 @@ public class MarkerEditActivity extends AbstractActivity {
// Setup UI elements
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.markerEditMarkerType.setAdapter(adapter);
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> {
viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
});
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString()));
viewBinding.markerEditCancel.setOnClickListener(v -> {
viewModel.onCancel();
@@ -14,8 +14,8 @@ import java.util.concurrent.Future;
*/
class ExecutorListViewService<T> {
private List<T> enqueueObjects = new ArrayList<>();
private ExecutorService executorService;
private final List<T> enqueueObjects = new ArrayList<>();
private final ExecutorService executorService;
public ExecutorListViewService(int numThreads) {
executorService = Executors.newFixedThreadPool(numThreads);
@@ -23,7 +23,6 @@ class ExecutorListViewService<T> {
public void shutdown() {
enqueueObjects.clear();
enqueueObjects = null;
executorService.shutdown();
}
@@ -12,7 +12,7 @@ public class ScrollVisibleViews implements AbsListView.OnScrollListener {
private int from = -1;
private int to = -1;
private VisibleViewsListener visibleViewsListener;
private final VisibleViewsListener visibleViewsListener;
public ScrollVisibleViews(@NonNull VisibleViewsListener visibleViewsListener) {
this.visibleViewsListener = visibleViewsListener;
@@ -365,10 +365,11 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
}
TrackPoint trackPoint = null;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
while (trackPointIterator.hasNext()) {
TrackPoint trackPoint = trackPointIterator.next();
trackPoint = trackPointIterator.next();
TrackPoint.Id trackPointId = trackPoint.getId();
// Stop if past the last wanted point
@@ -398,18 +399,23 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
}
localNumLoadedTrackPoints++;
localLastSeenTrackPointIdId = trackPointId;
}
}
if (trackPoint != null) {
localLastSeenTrackPointIdId = trackPoint.getId();
}
if (updateSamplingState) {
numLoadedPoints = localNumLoadedTrackPoints;
firstSeenTrackPointId = localFirstSeenTrackPointId;
lastSeenTrackPointId = localLastSeenTrackPointIdId;
}
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone();
if (trackPoint != null) {
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone(trackPoint);
}
}
}
@@ -16,6 +16,8 @@
package de.dennisguse.opentracks.content;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -32,6 +34,7 @@ public interface TrackDataListener {
*
* @param track the track
*/
//TODO Could be @NonNull
void onTrackUpdated(Track track);
/**
@@ -44,20 +47,20 @@ public interface TrackDataListener {
*
* @param trackPoint the trackPoint
*/
void onSampledInTrackPoint(TrackPoint trackPoint);
void onSampledInTrackPoint(@NonNull TrackPoint trackPoint);
/**
* Called when a sampled out track point is read.
*
* @param trackPoint the trackPoint
*/
void onSampledOutTrackPoint(TrackPoint trackPoint);
void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint);
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint)} and {@link #onSampledOutTrackPoint(TrackPoint)}.
*/
void onNewTrackPointsDone();
void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint);
/**
* Called to clear previously sent markers.
@@ -21,6 +21,7 @@ import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
@@ -42,6 +43,11 @@ import java.util.Objects;
*/
public class TrackPoint {
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private TrackPoint.Id id;
private Instant time;
@@ -246,6 +252,12 @@ public class TrackPoint {
this.time = time;
}
public boolean isRecent() {
return Instant.now()
.isBefore(time.plus(MAX_LOCATION_AGE));
}
public boolean hasAltitude() {
return altitude_m != null;
}
@@ -270,6 +282,10 @@ public class TrackPoint {
this.speed_mps = speed;
}
public boolean isMoving() {
return hasSpeed() && getSpeed() >= MAX_NO_MOVEMENT_SPEED;
}
public boolean hasBearing() {
return bearing != null;
}
@@ -298,6 +314,10 @@ public class TrackPoint {
return getLocation().distanceTo(dest.getLocation());
}
public boolean fulfillsAccuracy(int poorAccuracy) {
return hasAccuracy() && accuracy < poorAccuracy;
}
public float bearingTo(@NonNull TrackPoint dest) {
return getLocation().bearingTo(dest.getLocation());
}
@@ -54,6 +54,13 @@ public interface TrackPointsColumns extends BaseColumns {
String ELEVATION_GAIN = "elevation_gain";
String ELEVATION_LOSS = "elevation_loss";
// Alias for sensor statistics
String ALIAS_AVG_HR = "avg_hr";
String ALIAS_MAX_HR = "max_hr";
String ALIAS_AVG_CADENCE = "avg_cadence";
String ALIAS_MAX_CADENCE = "max_cadence";
String ALIAS_AVG_POWER = "avg_power";
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TRACKID + " INTEGER NOT NULL, "
@@ -30,6 +30,7 @@ public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
Uri CONTENT_URI_SENSOR_STATS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
String DEFAULT_SORT_ORDER = _ID;
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.content.provider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
@@ -578,24 +579,6 @@ public class ContentProviderUtils {
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
/**
* Gets the first location id for a track.
* Returns null if it doesn't exist.
*
* @param trackId the track id
*/
@Deprecated
public Track.Id getFirstTrackPointId(Track.Id trackId) {
String selection = TrackPointsColumns._ID + "=(SELECT MIN(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
return new Track.Id(cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)));
}
}
return null;
}
/**
* Gets the last location id for a track.
* Returns -1L if it doesn't exist.
@@ -837,30 +820,19 @@ public class ContentProviderUtils {
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
}
public SensorStatistics getSensorStats(Track.Id trackId) {
if (trackId == null) {
return null;
}
String[] projection = {
"MAX(" + TrackPointsColumns.SENSOR_HEARTRATE + ") max_hr",
"AVG(" + TrackPointsColumns.SENSOR_HEARTRATE + ") avg_hr",
"MAX(" + TrackPointsColumns.SENSOR_CADENCE + ") max_cadence",
"AVG(" + TrackPointsColumns.SENSOR_CADENCE + ") avg_cadence",
"AVG(" + TrackPointsColumns.SENSOR_POWER + ") avg_power"
};
String selection = TrackPointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
public SensorStatistics getSensorStats(@NonNull Track.Id trackId) {
SensorStatistics sensorStatistics = null;
try (Cursor cursor = getTrackPointCursor(projection, selection, selectionArgs, null)) {
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.getId()), null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
sensorStatistics = new SensorStatistics(
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("max_hr")) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_hr")) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("max_cadence")) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_cadence")) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_power")) : null
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_HR)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_HR)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_CADENCE)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_CADENCE)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_POWER)) : null
);
}
}
return sensorStatistics;
}
@@ -36,6 +36,7 @@ import androidx.annotation.VisibleForTesting;
import java.util.Arrays;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
@@ -56,6 +57,38 @@ public class CustomContentProvider extends ContentProvider {
private SQLiteDatabase db;
/**
* The string representing the query that compute sensor stats from trackpoints table.
* It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence.
* Finally, it ignores manual pause (SEGMENT_START_MANUAL).
*/
private final String SENSOR_STATS_QUERY =
"WITH time_select as " +
"(SELECT t1." + TrackPointsColumns.TIME + " * (t1." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")) time_value " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t1 " +
"WHERE t1." + TrackPointsColumns._ID + " > t." + TrackPointsColumns._ID + " AND t1." + TrackPointsColumns.TRACKID + " = ? ORDER BY _id LIMIT 1) " +
"SELECT " +
"SUM(t." + TrackPointsColumns.SENSOR_HEARTRATE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_HR + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_HEARTRATE + ") " + TrackPointsColumns.ALIAS_MAX_HR + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_CADENCE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_CADENCE + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_CADENCE + ") " + TrackPointsColumns.ALIAS_MAX_CADENCE + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_POWER + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + " " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
@@ -63,6 +96,7 @@ public class CustomContentProvider extends ContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
@@ -223,6 +257,10 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
case TRACKS_SENSOR_STATS:
long trackId = ContentUris.parseId(url);
Cursor cursor = db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)});
return cursor;
case MARKERS:
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
@@ -363,6 +401,7 @@ public class CustomContentProvider extends ContentProvider {
TRACKPOINTS_BY_TRACKID,
TRACKS,
TRACKS_BY_ID,
TRACKS_SENSOR_STATS,
MARKERS,
MARKERS_BY_ID,
MARKERS_BY_TRACKID
@@ -8,14 +8,14 @@ import java.time.Instant;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
public class SensorData<T> {
public abstract class SensorData<T> {
protected T value;
private final String sensorAddress;
private final String sensorName;
private final Instant timestamp_ms;
private final Instant time;
SensorData(String sensorAddress) {
this(sensorAddress, null);
@@ -26,10 +26,10 @@ public class SensorData<T> {
}
@VisibleForTesting
SensorData(String sensorAddress, String sensorName, Instant timestamp_ms) {
SensorData(String sensorAddress, String sensorName, Instant time) {
this.sensorAddress = sensorAddress;
this.sensorName = sensorName;
this.timestamp_ms = timestamp_ms;
this.time = time;
}
@NonNull
@@ -58,6 +58,7 @@ public class SensorData<T> {
* Is the data recent considering the current time.
*/
public boolean isRecent() {
return Instant.now().isBefore(timestamp_ms.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
return Instant.now()
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
}
}
@@ -33,11 +33,11 @@ public final class SensorDataSet {
return cyclingPower;
}
public void set(SensorData data) {
public void set(SensorData<?> data) {
set(data, data);
}
public void remove(SensorData type) {
public void remove(SensorData<?> type) {
set(type, null);
}
@@ -75,7 +75,7 @@ public final class SensorDataSet {
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
}
private void set(@NonNull SensorData type, SensorData data) {
private void set(@NonNull SensorData<?> type, SensorData<?> data) {
if (type instanceof SensorDataHeartRate) {
this.heartRate = (SensorDataHeartRate) data;
return;
@@ -224,14 +224,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
pendingPoints.add(createPendingPoint(trackPoint));
}
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
if (trackStatisticsUpdater != null) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
@@ -240,7 +240,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
if (isResumed()) {
chartView.addChartPoints(pendingPoints);
pendingPoints.clear();
@@ -198,21 +198,21 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
viewModel.add(trackPoint);
}
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
viewModel.add(trackPoint);
}
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
if (isResumed()) {
runOnUiThread(this::loadIntervals);
}
@@ -51,32 +51,10 @@ import de.dennisguse.opentracks.viewmodels.SensorDataModel;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
public class StatisticsRecordedFragment extends Fragment {
private static final String TRACK_ID_KEY = "trackId";
private TrackStatistics trackStatistics;
private SensorStatistics sensorStatistics;
private SensorsAdapter sensorsAdapter;
private String category = "";
private Track.Id trackId;
private ContentProviderUtils contentProviderUtils;
private StatisticsRecordedBinding viewBinding;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
}
};
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
Bundle bundle = new Bundle();
bundle.putParcelable(TRACK_ID_KEY, trackId);
@@ -86,21 +64,41 @@ public class StatisticsRecordedFragment extends Fragment {
return fragment;
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
private TrackStatistics trackStatistics;
private SensorStatistics sensorStatistics;
private SensorsAdapter sensorsAdapter;
private String category = "";
private Track.Id trackId;
private ContentProviderUtils contentProviderUtils;
sensorsAdapter = new SensorsAdapter(getContext());
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
}
private StatisticsRecordedBinding viewBinding;
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
}
if (key != null && updateUInecessary && isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -108,12 +106,29 @@ public class StatisticsRecordedFragment extends Fragment {
trackId = getArguments().getParcelable(TRACK_ID_KEY);
contentProviderUtils = new ContentProviderUtils(getContext());
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sensorsAdapter = new SensorsAdapter(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
loadStatistics();
}
@@ -123,7 +138,13 @@ public class StatisticsRecordedFragment extends Fragment {
super.onDestroyView();
viewBinding = null;
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
public void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
public void loadStatistics() {
@@ -133,7 +154,13 @@ public class StatisticsRecordedFragment extends Fragment {
Track track = contentProviderUtils.getTrack(trackId);
trackStatistics = track != null ? track.getTrackStatistics() : null;
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
category = track != null ? track.getCategory() : "";
String newCategory = track != null ? track.getCategory() : "";
if (!category.equals(newCategory)) {
category = newCategory;
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
updateUI();
updateSensorUI();
}
@@ -142,13 +169,10 @@ public class StatisticsRecordedFragment extends Fragment {
}
private void updateUI() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
// Set total distance
{
double totalDistance = trackStatistics == null ? Double.NaN : trackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -169,9 +193,9 @@ public class StatisticsRecordedFragment extends Fragment {
// Set average speed/pace
{
double speed = trackStatistics != null ? trackStatistics.getAverageSpeed() : Double.NaN;
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
@@ -180,9 +204,9 @@ public class StatisticsRecordedFragment extends Fragment {
{
double speed = trackStatistics == null ? Double.NaN : trackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
@@ -191,9 +215,9 @@ public class StatisticsRecordedFragment extends Fragment {
{
double speed = trackStatistics != null ? trackStatistics.getAverageMovingSpeed() : Double.NaN;
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
@@ -209,11 +233,11 @@ public class StatisticsRecordedFragment extends Fragment {
Pair<String, String> parts;
parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits);
parts = StringUtils.formatElevation(getContext(), elevationGain_m, preferenceMetricUnits);
viewBinding.statsElevationGainValue.setText(parts.first);
viewBinding.statsElevationGainUnit.setText(parts.second);
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, metricUnits);
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, preferenceMetricUnits);
viewBinding.statsElevationLossValue.setText(parts.first);
viewBinding.statsElevationLossUnit.setText(parts.second);
}
@@ -10,6 +10,7 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -32,7 +33,6 @@ import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -45,54 +45,78 @@ import de.dennisguse.opentracks.viewmodels.SensorDataModel;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
//TODO isRecording should not be relevant anymore as we now have StatisticRecordedFragment.
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
public class StatisticsRecordingFragment extends Fragment implements TrackDataListener {
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND_MS;
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private TrackDataHub trackDataHub;
private Handler handlerUpdateUI;
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private TrackPoint lastTrackPoint;
private TrackStatistics lastTrackStatistics;
private String category = "";
private StatisticsRecordingBinding viewBinding;
private SensorsAdapter sensorsAdapter;
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private boolean preferenceShowElevation;
private boolean preferenceShowCoordinate;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_elevation_key, key)) {
updateUInecessary = true;
preferenceShowElevation = PreferencesUtils.isShowStatsElevation(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_coordinate_key, key)) {
updateUInecessary = true;
preferenceShowCoordinate = PreferencesUtils.isStatsShowCoordinate(sharedPreferences, getContext());
}
if (key != null && updateUInecessary && isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTrackStatistics != null) {
synchronized (this) {
if (lastTrackPoint != null && lastTrackPoint.hasLocation() && !lastTrackPoint.isRecent()) {
lastTrackPoint = null;
setLocationValues();
}
}
updateTotalTime();
updateSensorDataUI();
}
@@ -103,27 +127,38 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
};
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handlerUpdateUI = new Handler();
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sensorsAdapter = new SensorsAdapter(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(getContext());
updateUIeachSecond.run();
handlerUpdateUI.post(updateUIeachSecond);
}
@@ -131,7 +166,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
public void onPause() {
super.onPause();
pauseTrackDataHub();
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
}
@@ -152,6 +188,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection = null;
sharedPreferences = null;
}
@Override
@@ -160,7 +197,11 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
getActivity().runOnUiThread(() -> {
if (isResumed()) {
lastTrackStatistics = track != null ? track.getTrackStatistics() : null;
category = track != null ? track.getCategory() : "";
String newCategory = track != null ? track.getCategory() : "";
if (!category.equals(newCategory)) {
category = newCategory;
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
updateUI();
}
});
@@ -173,33 +214,32 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
// We don't care.
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint newLastTrackPoint) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastTrackPoint = null;
}
synchronized (this) {
this.lastTrackPoint = newLastTrackPoint;
TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now.
if (trackPoint != null && trackPoint.hasLocation()) {
boolean hasFix = !LocationUtils.isTrackPointOld(trackPoint);
if (!hasFix) {
lastTrackPoint = null;
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
this.lastTrackPoint = null;
}
if (this.lastTrackPoint != null && this.lastTrackPoint.hasLocation() && !this.lastTrackPoint.isRecent()) {
this.lastTrackPoint = null;
}
setLocationValues();
}
setLocationValues();
}
});
}
@@ -279,7 +319,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
if (sensorDataSet.getCyclingCadence() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
}
if(sensorDataSet.getCyclingPower() != null) {
if (sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
}
sensorsAdapter.swapData(sensorDataList);
@@ -293,8 +333,6 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set elevation gain
private void setTotalElevationGain(Float elevationGain_m) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
Float totalElevationGain = elevationGain_m;
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationGain()) {
@@ -305,15 +343,13 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, preferenceMetricUnits);
viewBinding.statsElevationGainValue.setText(parts.first);
viewBinding.statsElevationGainUnit.setText(parts.second);
}
// Set elevation loss
private void setTotalElevationLoss(Float elevationLoss_m) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
Float totalElevationLoss = elevationLoss_m;
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationLoss()) {
@@ -324,7 +360,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, preferenceMetricUnits);
viewBinding.statsElevationLossValue.setText(parts.first);
viewBinding.statsElevationLossUnit.setText(parts.second);
}
@@ -342,14 +378,10 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private void updateStats() {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
boolean isRecording = isSelectedTrackRecording();
// Set total distance
{
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -357,7 +389,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set activity type
{
viewBinding.statsActivityTypeIcon.setEnabled(isRecording);
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
}
@@ -370,9 +402,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set average speed/pace
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
@@ -381,9 +413,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
{
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
@@ -392,18 +424,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
// Set elevation gain and loss
{
// Make elevation visible?
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
}
}
@@ -420,34 +450,29 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
private void setLocationValues() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
// Set speed/pace
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
setSpeed(speed);
// Set elevation
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
if (showElevation) {
if (preferenceShowElevation) {
// Current elevation
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsElevationCurrentValue.setText(parts.first);
viewBinding.statsElevationCurrentUnit.setText(parts.second);
}
// Set coordinate
boolean showCoordinate = PreferencesUtils.isStatsShowCoordinate(getContext());
viewBinding.statsCoordinateGroup.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
if (showCoordinate) {
viewBinding.statsCoordinateGroup.setVisibility(preferenceShowCoordinate ? View.VISIBLE : View.GONE);
if (preferenceShowCoordinate) {
String latitudeText = getContext().getString(R.string.value_unknown);
String longitudeText = getContext().getString(R.string.value_unknown);
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLongitude());
}
viewBinding.statsLatitudeValue.setText(latitudeText);
viewBinding.statsLongitudeValue.setText(longitudeText);
@@ -455,12 +480,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
private void setSpeed(double speed) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
viewBinding.statsSpeedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsSpeedValue.setText(parts.first);
viewBinding.statsSpeedUnit.setText(parts.second);
}
@@ -123,7 +123,7 @@ public class GPXTrackExporter implements TrackExporter {
TrackPoint trackPoint = trackPointIterator.next();
if (!wroteTrack) {
writeBeginTrack(track, trackPoint);
writeBeginTrack(track);
wroteTrack = true;
}
@@ -162,14 +162,12 @@ public class GPXTrackExporter implements TrackExporter {
writeCloseSegment();
}
if (wroteTrack) {
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
writeEndTrack(track, lastValidTrackPoint);
} else {
if (!wroteTrack) {
// Write an empty track
writeBeginTrack(track, null);
writeEndTrack(track, null);
writeBeginTrack(track);
}
writeEndTrack();
}
}
@@ -247,7 +245,7 @@ public class GPXTrackExporter implements TrackExporter {
}
}
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
public void writeBeginTrack(Track track) {
if (printWriter != null) {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
@@ -261,7 +259,7 @@ public class GPXTrackExporter implements TrackExporter {
}
}
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
public void writeEndTrack() {
if (printWriter != null) {
printWriter.println("</trk>");
}
@@ -33,7 +33,6 @@ import java.util.List;
import java.util.Objects;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.DescriptionGenerator;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -54,8 +53,6 @@ public class KMLTrackExporter implements TrackExporter {
private static final String TAG = KMLTrackExporter.class.getSimpleName();
public static final String MARKER_STYLE = "waypoint";
private static final String START_STYLE = "start";
private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
@@ -67,15 +64,12 @@ public class KMLTrackExporter implements TrackExporter {
public static final String EXTENDED_DATA_TYPE_ELEVATION_LOSS = "elevation_loss";
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context;
private final boolean exportPhotos;
private final boolean exportTrackDetail;
private final boolean exportSensorData;
private final DescriptionGenerator descriptionGenerator;
private final ContentProviderUtils contentProviderUtils;
private PrintWriter printWriter;
@@ -86,6 +80,7 @@ public class KMLTrackExporter implements TrackExporter {
private final List<Float> elevationGainList = new ArrayList<>();
private final List<Float> elevationLossList = new ArrayList<>();
@Deprecated // Figure out a better way to do this! (if needed)
private TrackPoint startTrackPoint;
public KMLTrackExporter(Context context, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) {
@@ -93,7 +88,6 @@ public class KMLTrackExporter implements TrackExporter {
this.exportTrackDetail = exportTrackDetail;
this.exportSensorData = exportSensorData;
this.exportPhotos = exportPhotos;
this.descriptionGenerator = new DescriptionGenerator(context);
this.contentProviderUtils = new ContentProviderUtils(context);
}
@@ -165,9 +159,12 @@ public class KMLTrackExporter implements TrackExporter {
TrackPoint trackPoint = trackPointIterator.next();
setLocationTime(trackPoint, offset);
if (startTrackPoint == null) {
startTrackPoint = trackPoint;
}
if (!wroteTrack) {
writeBeginTrack(track, trackPoint);
writeBeginTrack(track);
wroteTrack = true;
}
@@ -202,15 +199,14 @@ public class KMLTrackExporter implements TrackExporter {
writeCloseSegment();
}
if (wroteTrack) {
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
writeEndTrack(track, lastValidTrackPoint);
} else {
if (!wroteTrack) {
// Write an empty track
writeBeginTrack(track, null);
writeEndTrack(track, null);
writeBeginTrack(track);
}
writeEndTrack();
startTrackPoint = null;
}
}
@@ -247,8 +243,6 @@ public class KMLTrackExporter implements TrackExporter {
}
writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(MARKER_STYLE, MARKER_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
@@ -312,12 +306,8 @@ public class KMLTrackExporter implements TrackExporter {
}
}
private void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
this.startTrackPoint = startTrackPoint;
private void writeBeginTrack(Track track) {
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
Location location = startTrackPoint != null ? startTrackPoint.getLocation() : null;
writePlacemark(name, "", "", START_STYLE, location);
printWriter.println("<Placemark>");
if (exportTrackDetail) {
@@ -336,17 +326,10 @@ public class KMLTrackExporter implements TrackExporter {
}
private void writeEndTrack(Track track, TrackPoint endTrackPoint) {
private void writeEndTrack() {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
if (exportTrackDetail) {
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, false);
Location location = endTrackPoint != null ? endTrackPoint.getLocation() : null;
writePlacemark(name, "", description, END_STYLE, location);
}
}
}
@@ -21,6 +21,8 @@ import android.database.sqlite.SQLiteConstraintException;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
@@ -122,7 +124,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, this);
return trackIds.get(0);
} catch (IOException | SAXException | ParserConfigurationException e) {
} catch (IOException | SAXException | ParserConfigurationException | ParsingException e) {
Log.e(TAG, "Unable to import file", e);
if (trackIds.size() > 0) {
cleanImport();
@@ -304,7 +306,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
}
protected void addMarker() throws SAXException {
protected void addMarker() throws ParsingException {
// Markers must have a time, else cannot match to the track points
if (time == null) {
Log.w(TAG, "Marker without time ignored.");
@@ -335,7 +337,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
markers.add(marker);
}
protected TrackPoint getTrackPoint() throws SAXException {
protected TrackPoint getTrackPoint() throws ParsingException {
TrackPoint trackPoint = createTrackPoint();
if (trackPoint.hasLocation()) {
@@ -350,7 +352,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackPoint.setTime(time);
} else if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
throw new ParsingException(createErrorMessage("Invalid location detected: " + trackPoint));
}
}
@@ -424,16 +426,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return null;
}
/**
* Creates a location.
*/
private TrackPoint createTrackPoint() throws SAXException {
private TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
try {
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
if (latitude == null || longitude == null) {
@@ -444,7 +443,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackPoint.setLatitude(Double.parseDouble(latitude));
trackPoint.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
@@ -452,7 +451,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
trackPoint.setAltitude(Double.parseDouble(altitude));
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
@@ -460,14 +459,14 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
trackPoint.setSpeed(Float.parseFloat(speed));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
}
if (heartrate != null) {
try {
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
@@ -475,7 +474,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
@@ -483,14 +482,14 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
trackPoint.setElevationGain(Float.parseFloat(gain));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation gain: %s", gain)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation gain: %s", gain)), e);
}
}
if (loss != null) {
try {
trackPoint.setElevationLoss(Float.parseFloat(loss));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation loss: %s", loss)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation loss: %s", loss)), e);
}
}
@@ -546,4 +545,21 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// The buffered locations
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
}
public static class ParsingException extends RuntimeException {
private ParsingException(@NonNull String message) {
super(message);
}
private ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@NonNull
@Override
public String toString() {
return "" + getMessage();
}
}
}
@@ -57,33 +57,33 @@ public class ImportService extends JobIntentService {
trackImporter = new KmzTrackImporter(this, file.getUri());
} else {
Log.d(TAG, "Unsupported file format.");
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unsupported_format));
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
return;
}
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri())) {
Track.Id trackId = trackImporter.importFile(inputStream);
if (trackId != null) {
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackId, file.getName(), getString(R.string.import_file_imported, file.getName()));
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackId, file, getString(R.string.import_file_imported, file.getName()));
} else {
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackId, file.getName(), getString(R.string.import_unable_to_import_file, file.getName()));
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackId, file, getString(R.string.import_unable_to_import_file, file.getName()));
}
} catch (IOException e) {
Log.d(TAG, "Unable to import file", e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unable_to_import_file, e.getMessage()));
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unable_to_import_file, e.getMessage()));
} catch (ImportParserException e) {
Log.d(TAG, "Parser error: " + e.getMessage(), e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_parser_error, e.getMessage()));
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_parser_error, e.getMessage()));
} catch (ImportAlreadyExistsException e) {
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file.getName(), e.getMessage());
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file, e.getMessage());
}
}
private void sendResult(int resultCode, Track.Id trackId, String fileName, String message) {
private void sendResult(int resultCode, Track.Id trackId, DocumentFile file, String message) {
Bundle bundle = new Bundle();
bundle.putParcelable(ImportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, fileName);
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, file.getName());
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE, message);
resultReceiver.send(resultCode, bundle);
}
@@ -95,7 +95,7 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
private int successCount;
private int existsCount;
private int errorCount;
private ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
private final ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
private final ArrayList<String> fileErrors = new ArrayList<>();
public int getTotalCount() {
@@ -66,7 +66,6 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackNameUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
/**
* A background service that registers a location listener and records track points.
@@ -594,7 +593,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
if (!isIdle && !TrackPointUtils.isMoving(trackPoint)) {
if (!isIdle && !trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
@@ -605,7 +604,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
if (isIdle && TrackPointUtils.isMoving(trackPoint)) {
if (isIdle && trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
@@ -16,7 +16,6 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
class LocationHandler implements HandlerServer.Handler, LocationListener, GpsStatus.GpsStatusListener {
@@ -92,7 +91,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
@Override
public void onLocationChanged(@NonNull Location location) {
TrackPoint trackPoint = new TrackPoint(location);
boolean isAccurate = TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy);
boolean isAccurate = trackPoint.fulfillsAccuracy(recordingGpsAccuracy);
boolean isValid = LocationUtils.isValidLocation(location);
if (gpsStatus != null) {
@@ -1,11 +1,11 @@
package de.dennisguse.opentracks.stats;
public class SensorStatistics {
private Float maxHr;
private Float avgHr;
private Float maxCadence;
private Float avgCadence;
private Float avgPower;
private final Float maxHr;
private final Float avgHr;
private final Float maxCadence;
private final Float avgCadence;
private final Float avgPower;
public SensorStatistics(Float maxHr, Float avgHr, Float maxCadence, Float avgCadence, Float avgPower) {
this.maxHr = maxHr;
@@ -16,7 +16,7 @@ public class SensorStatistics {
}
public boolean hasHeartRate() {
return maxHr != null;
return avgHr != null && maxHr != null;
}
public float getMaxHeartRate() {
@@ -28,7 +28,7 @@ public class SensorStatistics {
}
public boolean hasCadence() {
return maxCadence != null;
return avgCadence != null && maxCadence != null;
}
public float getMaxCadence() {
@@ -25,7 +25,6 @@ import java.time.Duration;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.TrackPointUtils;
/**
* Updater for {@link TrackStatistics}.
@@ -161,7 +160,7 @@ public class TrackStatisticsUpdater {
}
double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint);
if (movingDistance < minRecordingDistance && !TrackPointUtils.isMoving(trackPoint)) {
if (movingDistance < minRecordingDistance && !trackPoint.isMoving()) {
speedBuffer_ms.reset();
lastTrackPoint = trackPoint;
return;
@@ -211,7 +210,7 @@ public class TrackStatisticsUpdater {
*/
@VisibleForTesting
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
if (!TrackPointUtils.isMoving(trackPoint)) {
if (!trackPoint.isMoving()) {
speedBuffer_ms.reset();
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_ms.setNext(trackPoint.getSpeed());
@@ -17,11 +17,6 @@ package de.dennisguse.opentracks.util;
import android.location.Location;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* Utility class for decimating tracks at a given level of precision.
*
@@ -31,8 +26,6 @@ public class LocationUtils {
private static final String TAG = LocationUtils.class.getSimpleName();
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
private LocationUtils() {
}
@@ -47,11 +40,4 @@ public class LocationUtils {
&& Math.abs(location.getLatitude()) <= 90
&& Math.abs(location.getLongitude()) <= 180;
}
public static boolean isTrackPointOld(TrackPoint trackPoint) {
Instant now = Instant.now();
return !(Duration.between(trackPoint.getTime(), now)
.minus(MAX_LOCATION_AGE)
.isNegative());
}
}
@@ -46,8 +46,10 @@ public class PreferencesUtils {
return PreferenceManager.getDefaultSharedPreferences(context);
}
public static void register(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
getSharedPreferences(context).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
public static SharedPreferences register(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
return sharedPreferences;
}
public static void unregister(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
@@ -106,6 +108,10 @@ public class PreferencesUtils {
return sharedPreferences.getBoolean(getKey(context, keyId), defaultValue);
}
private static boolean getBoolean(Context context, SharedPreferences sharedPreferences, int keyId, boolean defaultValue) {
return sharedPreferences.getBoolean(getKey(context, keyId), defaultValue);
}
/**
* Sets a boolean preference value.
*
@@ -179,18 +185,16 @@ public class PreferencesUtils {
editor.apply();
}
/**
* Gets a string preference value.
*
* @param context the context
* @param keyId the key id
* @param defaultValue default value
*/
@Deprecated
public static String getString(Context context, int keyId, String defaultValue) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
return sharedPreferences.getString(getKey(context, keyId), defaultValue);
}
public static String getString(Context context, SharedPreferences sharedPreferences, int keyId, String defaultValue) {
return sharedPreferences.getString(getKey(context, keyId), defaultValue);
}
/**
* Sets a string preference value.
*
@@ -206,15 +210,25 @@ public class PreferencesUtils {
editor.apply();
}
@Deprecated
public static boolean isMetricUnits(Context context) {
final String STATS_UNIT = context.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(context, R.string.stats_units_key, STATS_UNIT));
return isMetricUnits(getSharedPreferences(context), context);
}
public static boolean isMetricUnits(SharedPreferences sharedPreferences, Context context) {
final String STATS_UNIT = context.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(context, sharedPreferences, R.string.stats_units_key, STATS_UNIT));
}
@Deprecated
public static boolean isReportSpeed(Context context, String category) {
return isReportSpeed(getSharedPreferences(context), context, category);
}
public static boolean isReportSpeed(SharedPreferences sharedPreferences, Context context, String category) {
final String STATS_RATE_DEFAULT = context.getString(R.string.stats_rate_default);
String currentStatsRate = getString(context, R.string.stats_rate_key, STATS_RATE_DEFAULT);
if (currentStatsRate.equals(getString(context, R.string.stats_rate_speed_or_pace_default, STATS_RATE_DEFAULT))) {
String currentStatsRate = getString(context, sharedPreferences, R.string.stats_rate_key, STATS_RATE_DEFAULT);
if (currentStatsRate.equals(getString(context, sharedPreferences, R.string.stats_rate_speed_or_pace_default, STATS_RATE_DEFAULT))) {
return TrackIconUtils.isSpeedIcon(context, category);
}
@@ -286,12 +300,17 @@ public class PreferencesUtils {
return getBoolean(context, R.string.stats_fullscreen_while_recording_key, DEFAULT);
}
@Deprecated
public static boolean isShowStatsElevation(Context context) {
final boolean STATS_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_elevation_default);
return getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
return isShowStatsElevation(getSharedPreferences(context), context);
}
public static boolean isStatsShowCoordinate(Context context) {
public static boolean isShowStatsElevation(SharedPreferences sharedPreferences, Context context) {
final boolean STATS_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_elevation_default);
return getBoolean(context, sharedPreferences, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
}
public static boolean isStatsShowCoordinate(SharedPreferences sharedPreferences, Context context) {
final boolean STATS_SHOW_COORDINATE = context.getResources().getBoolean(R.bool.stats_show_coordinate_default);
return getBoolean(context, R.string.stats_show_coordinate_key, STATS_SHOW_COORDINATE);
}
@@ -1,27 +0,0 @@
package de.dennisguse.opentracks.util;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.TrackPoint;
public class TrackPointUtils {
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private final static String TAG = TrackPointUtils.class.getSimpleName();
private TrackPointUtils() {
}
public static boolean isMoving(@NonNull TrackPoint trackPoint) {
return trackPoint.hasSpeed() && trackPoint.getSpeed() >= MAX_NO_MOVEMENT_SPEED;
}
/**
* Is accuracy better than threshold?
*/
public static boolean fulfillsAccuracy(@NonNull TrackPoint trackPoint, int poorAccuracy) {
return trackPoint.hasAccuracy() && trackPoint.getAccuracy() < poorAccuracy;
}
}
@@ -9,7 +9,7 @@ import de.dennisguse.opentracks.util.StringUtils;
public class SensorDataModel {
private int labelId;
private String sensorValue;
private final String sensorValue;
private String sensorName;
private int unitId;