forked from upstream-mirrors/OpenTracks
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b9711d82e | |||
| 114fc8255f | |||
| 26e655d58f | |||
| 815b6a3cf1 | |||
| 3dc8a724d8 | |||
| 70f571cd09 | |||
| 3f5c3a5c08 | |||
| a13465391f | |||
| 97454b5369 | |||
| f6a47c4594 | |||
| fce76230e1 | |||
| fdf4336a9b | |||
| 844c6e9609 | |||
| 784942ffd2 | |||
| edf4ccc941 |
@@ -18,4 +18,6 @@ jobs:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
arch: x86_64
|
||||
profile: Nexus 6
|
||||
script: ./gradlew connectedCheck
|
||||
|
||||
|
||||
+2
-2
@@ -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());
|
||||
}
|
||||
}
|
||||
+337
@@ -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);
|
||||
}
|
||||
|
||||
+106
-84
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-16
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user