Compare commits

...

49 Commits

Author SHA1 Message Date
Dennis Guse 8cab43d558 Release: v3.17.2 2021-04-19 19:38:31 +02:00
Dennis Guse 13f2a09652 Bugfix: AnnouncementPeriodicTask does not need to register as TrackRecordingService.Listener. 2021-04-19 19:29:51 +02:00
Dennis Guse 7d4072c0d8 Cleanup: ChartPoint uses null instead of NaN. 2021-04-18 17:50:16 +02:00
Dennis Guse 310b35713d ChartFragmentTest: added test for small changes. 2021-04-18 14:39:48 +02:00
Dennis Guse 7e6237de3a Release: v3.17.1 2021-04-17 22:40:05 +02:00
Román Martínez 772dad4837 announce called after getting recordingTrackId. Fixed a potencial race condition. 2021-04-17 22:26:23 +02:00
Román Martínez 99126fa357 Distance interval bugfixed: interval options are km/mi values. 2021-04-17 21:16:32 +02:00
Román Martínez b2c91d7592 registerListener for barometric sensor uses SENSOR_DELAY_FASTEST instead of nanoseconds. Fixes #714. 2021-04-17 19:34:05 +02:00
Rodrigo Oliveira 3c5a1be16b Fix #711: declare TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE on queries element
This enables the use of TextToSpeech engine on Android 11.
Ref.: https://developer.android.com/reference/android/speech/tts/TextToSpeech
2021-04-17 19:31:26 +02:00
Dennis Guse 01d83deefd Release: v3.17.0 2021-04-13 20:40:51 +02:00
Dennis Guse bc2a3ddee3 IntervalStatistics/Announcement: use Distance. 2021-04-13 19:18:18 +02:00
Dennis Guse da70fca389 Bugfix: TrackStatistics handles SEGMENT_START_AUTOMATIC properly. 2021-04-13 19:18:18 +02:00
Dennis Guse 67bbd9ca24 Bugfix: GPX exported altitude loss instead as distance. 2021-04-13 19:18:18 +02:00
Dennis Guse 5e1293fc82 For delays/timers: use Duration. 2021-04-13 19:18:18 +02:00
Dennis Guse 610da5a106 Statistics*Fragment: keep reference to track instead of TrackStatistics and category. 2021-04-13 19:18:18 +02:00
Dennis Guse 2b5744ccc1 Code cleanup. 2021-04-13 19:18:18 +02:00
Dennis Guse f0341e831f Github Actions: somehow the emulator has not enough memory.
Fixes #599.
2021-04-13 19:18:18 +02:00
Dennis Guse 99e5dfbe72 Github Actions: somehow the emulator has not enough memory.
Fixes #599.
2021-04-13 19:18:18 +02:00
Dennis Guse 0fb43f6530 Test: fix failing Export/Import tests.
While recording with the sensor is used while on import only the GPS distance is available.
Introduced in a5158a55d9

Fixes #670.
2021-04-13 19:18:18 +02:00
Dennis Guse 6f9c788965 Value objects distance and speed.
#679
2021-04-13 19:18:18 +02:00
Quang Trung f2b1d2a386 Translated using Weblate (Vietnamese)
Currently translated at 77.8% (412 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-04-13 04:33:26 +02:00
mondstern 18789a0729 Translated using Weblate (Polish)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-04-10 17:03:01 +02:00
mondstern d0f99be9d3 Translated using Weblate (French)
Currently translated at 86.5% (458 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-04-10 17:03:01 +02:00
Dennis Guse a899be4598 Use Duration for Markers (instead of long).
Now also tests.
2021-04-10 00:08:54 +02:00
Dennis Guse 1e518a4c35 Use a non-static Binder for TrackRecordingService.
Fixes #98.
2021-04-09 18:34:23 +02:00
Dennis Guse 1c6012596e Use Duration for Markers (instead of long). 2021-04-09 18:34:03 +02:00
Román Martínez 978206dee4 IntervalsFragment/IntervalStatisticsModel refactoring: better performance with AndroidViewModel (it doesn't recreate the model every track point done anymore).
Also, it doesn't jump anymore. Fixes #669.
2021-04-08 19:46:40 +02:00
Peter Storch 8713eb3d0c fix #698 added activity icon for kayaking 2021-04-07 13:30:14 +02:00
racuna d450e045b1 U: README_TESTED_SENSORS.md
A: Amazfit Neo
2021-04-06 18:13:21 +02:00
Dennis Guse 9f87f67153 While recording show coordinates by default. 2021-04-05 22:05:31 +02:00
Dennis Guse 3a073a3b0a Code cleanup. 2021-04-04 23:31:36 +02:00
Dennis Guse bc1b4e3cd2 Rename elevation to altitude (without translations).
Fixes #678.
2021-04-04 17:45:38 +02:00
Román Martínez e09b04308c Track recording service handles the status of recording: track's id, pause and GPS status.
Created an interface for all components that want to know about recording status through the service.
All interface's methods are optional so every component can implement what they need.
Fixes #388.
2021-04-04 13:15:42 +02:00
Román Martínez f3cf1a2571 Should instant export after workout if user set a directory and post_workout_export_enabled_key is True. Fixes #694. 2021-04-03 17:18:40 +02:00
Dennis Guse 7be4ac436c ChartView: missing (sensor) data does not draw until the end.
Fixes #42.
2021-04-03 15:27:11 +02:00
Mateusz Korniak 405120a78b Pl translation update.
Pl voice notification fixes.
2021-04-02 18:06:34 +02:00
Dennis Guse e86aeace79 Fix strings.xml for Ukrainian. 2021-03-31 21:56:59 +02:00
mondstern afe5c6ee92 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-03-31 21:54:11 +02:00
mondstern 055764b462 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-03-31 21:54:10 +02:00
mondstern 360738f4c8 Translated using Weblate (Dutch)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-03-31 21:54:10 +02:00
mondstern 3bd3e8fd9b Translated using Weblate (Italian)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-03-31 21:54:10 +02:00
mondstern 5cf421cf42 Translated using Weblate (Galician)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-03-31 21:54:10 +02:00
mondstern 455522a03d Translated using Weblate (Finnish)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-03-31 21:54:09 +02:00
mondstern b4c258c686 Translated using Weblate (Estonian)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-03-31 21:54:09 +02:00
mondstern 05ee54d58c Translated using Weblate (Spanish)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-03-31 21:54:09 +02:00
mondstern 28fbcdd2ca Translated using Weblate (Danish)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-03-31 21:54:08 +02:00
mondstern 0231343aa8 Translated using Weblate (Czech)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-03-31 21:54:08 +02:00
Max Schallert 852885cca1 Translated using Weblate (German)
Currently translated at 95.0% (503 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-03-30 17:33:21 +02:00
Dennis Guse d3c1247617 Bugfix: GPX TrackPointExtension/v2 broken.
Fixes #684.
2021-03-28 18:29:10 +02:00
154 changed files with 2966 additions and 2720 deletions
+1
View File
@@ -10,6 +10,7 @@
Has updatable firmware that requires a user account at polar.com
* Wahoo Tickr (Model: WFBTHR02)
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
* Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again)
## CSCP: Cycling Cadence and Speed Service
+2 -2
View File
@@ -51,8 +51,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3941
versionName "v3.16.0"
versionCode 3990
versionName "v3.17.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
+2 -2
View File
@@ -1,5 +1,5 @@
@startuml
SensorManager --> ElevationSumManager
SensorManager --> AltitudeSumManager
LocationManager --> TrackRecordingService
ElevationSumManager --> TrackRecordingService
@@ -14,6 +14,6 @@ TrackDataHub --> StatisticsRecordingFragment : TrackPoints, TrackStatistics, Way
TrackDataHub --> StatisticsRecordedFragment : TrackPoints, TrackStatistics, Waypoints (push)
TrackDataHub --> ChartFragment --> ChartView
TrackRecordingServiceConnection --> StatisticsRecordingFragment : Sensor data (heartrate, cadence, elevation gain; pull 1s)
TrackRecordingServiceConnection --> StatisticsRecordingFragment : Sensor data (heartrate, cadence, altitude gain; pull 1s)
TrackRecordingServiceConnection --> TrackController : Start time (pull, onStart)
@enduml
+1 -1
View File
@@ -18,7 +18,7 @@ class TrackStatistics {
- totalTime
- movingTime
- maxSpeed
- totalElevationGain
- totalGain
}
class TrackStatisticsUpdater {
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="22"
width="22"
fill="#000000"
version="1.0"
x="0px"
y="0px"
viewBox="0 0 21.84952 21.99934"
enable-background="new 0 0 99.316 99.997"
xml:space="preserve"
id="svg10"
sodipodi:docname="ic_activity_kayaking.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
id="metadata16"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs14" /><sodipodi:namedview
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1024"
id="namedview12"
showgrid="false"
inkscape:pagecheckerboard="false"
inkscape:zoom="22.472123"
inkscape:cx="11.932326"
inkscape:cy="8.4821218"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg10"
inkscape:document-rotation="0" /><path
id="path2"
style="fill:#ffffff;stroke-width:0.20758"
d="m 18.296044,13.516712 c -1.017125,-0.01824 -1.903328,0.770666 -2.945647,0.500454 -0.952298,-0.271819 -1.950516,-0.765534 -2.949277,-0.338881 -0.938402,0.492101 -2.078868,0.493759 -3.017025,0 C 8.3845167,13.251481 7.3852162,13.744815 6.4321693,14.017166 5.229901,14.31201 4.2005498,13.256398 2.9984219,13.558671 2.2076549,13.806805 1.0222411,14.11299 1.0222411,14.11299 v 1.764949 c 1.0226762,0.01947 1.9128198,-0.776355 2.9601398,-0.498166 0.4416703,0.216761 1.6682446,0.3639 1.6529583,0.601941 l -1.3879215,1.383539 c -0.7385797,-0.502111 -1.5118619,0.0527 -1.9901253,0.625328 -0.5087615,0.568011 -1.1635382,1.025896 -1.5505475,1.687065 -0.44607206,0.952772 0.6576964,2.056499 1.6104671,1.610525 0.7016688,-0.428027 1.2006595,-1.122907 1.8093513,-1.672458 0.6218606,-0.429609 0.7776032,-1.247655 0.6297837,-1.861615 l 2.0038313,-1.999046 c 0.9553792,-0.44042 2.0942104,-0.606317 3.0559286,-0.07975 0.9566571,0.429062 1.9815791,0.09313 2.8934291,-0.263824 1.038124,-0.339466 1.95525,0.382078 2.955963,0.461915 1.087184,0.105691 2.019821,-0.784126 3.121615,-0.493622 0.55673,-0.01509 1.943197,1.034046 1.977683,0.148315 v -1.471252 c -0.878739,0.05089 -1.592095,-0.588897 -2.468753,-0.540124 z"
sodipodi:nodetypes="sccccccccccccccccccccccss" /><path
id="path4"
style="fill:#ffffff;stroke-width:0.20758"
d="M 19.135498,4.6094374 C 19.777729,3.9239548 20.518106,3.3189058 21.090567,2.5760423 21.630775,1.6723294 20.631775,0.46345938 19.642408,0.81572972 18.952145,1.1177762 18.525406,1.7968856 17.966518,2.2833866 17.44088,2.7777653 16.748383,3.4079168 17.123672,4.2079927 17.316456,4.5791871 16.670933,4.8230987 16.479267,5.1359209 L 15.065885,6.5509368 C 13.459538,5.9569743 11.849312,5.3583867 10.246781,4.7652258 9.2045735,4.5310231 8.2546176,5.3827898 8.1060604,6.3824119 7.7002321,7.7689937 7.2645314,9.1479847 6.8776693,10.539386 h -5.851069 v 2.366209 c 1.0341353,-0.06583 1.9719234,-0.790016 3.0482038,-0.491433 0.8514673,0.182595 1.7032854,0.742921 2.5975093,0.377297 1.0893234,-0.553582 2.4097276,-0.562618 3.4983496,-0.0022 0.895986,0.368665 1.751474,-0.190407 2.60469,-0.37517 1.078334,-0.302717 2.015803,0.427862 3.051521,0.490909 0.985146,-0.07753 1.867505,-0.470806 2.884112,-0.579104 0.847652,-0.0903 2.163908,-1.785603 2.163908,-1.785603 0,0 -1.819193,-0.01538 -2.935123,0.0029 -1.990467,-2.45e-4 -3.980934,-0.0021 -5.9714,-0.0044 l 2.483907,-2.4814163 c 0.762424,0.5726687 1.957001,-0.3877847 1.20272,-1.1977389 l 2.032212,-2.039685 c 0.462782,0.2519942 1.079225,0.1706574 1.448288,-0.2104864 z m -8.060346,5.9299486 c -0.486522,0.101516 -0.612149,-0.06543 -0.374498,-0.502616 l 0.909432,-3.029364 2.189559,0.8099787 z"
sodipodi:nodetypes="cccccccccccccccccsccccccccccccc" /><path
id="path851"
style="fill:#ffffff;stroke-width:1.16861"
d="m 12.520186,2.6733336 c 0.05505,1.1807821 -1.406378,2.0132887 -2.394197,1.3656163 -1.0445314,-0.553724 -1.0179219,-2.235304 0.04332,-2.7559413 1.018447,-0.59197226 2.378758,0.20782 2.350878,1.390325 z" /></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,18 @@
v3.17.0: OpenTracks
Changes:
- Add icon for kayaking
- Charts draw missing sensor data properly
- Default: show coordinates while recording
Bugfixes:
- GPX schema corrected
- GPX exported altitude loss as sensor distance
- Statistics not computed correctly if using a speed/distance sensor
Developer:
- Distance/speed value objects
- Replaced communication via SharedPreferences
- Use non-static service binder
- Use altitude in the code base (instead of elevation)
@@ -0,0 +1,7 @@
v3.17.1: OpenTracks
Bugfix:
- Interval length in meters instead of kilometers (introduced in v3.17.0)
- Elevation gain/loss not recorded (introduced in v3.17.0)
- Announcements not working on Android 11
@@ -0,0 +1,8 @@
v3.17.2: OpenTracks
WARNING: The v3.17.0+ releases contain a memory leak.
This issues is NOT FIXED by this release.
It is recommended to not upgrade if you still use a release before v3.17.0
Bugfix:
- Crash while using voice announcements
@@ -44,16 +44,16 @@ public class ChartValueSeriesTest {
ApplicationProvider.getApplicationContext(),
Integer.MIN_VALUE,
Integer.MAX_VALUE,
new int[]{100, 1000},
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border,
new int[]{1, 100, 1000},
R.string.description_altitude_metric,
R.string.description_altitude_imperial,
R.color.chart_altitude_fill,
R.color.chart_altitude_border,
15,
18) {
@Override
double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getAltitude();
}
@Override
@@ -77,6 +77,17 @@ public class ChartValueSeriesTest {
assertFalse(series.isEnabled());
}
@Test
public void testVerySmallUpdates() {
series.update(new ChartPoint(1f));
series.update(new ChartPoint(2f));
series.update(new ChartPoint(3f));
series.updateDimension();
assertEquals(1, series.getInterval());
assertEquals(1, series.getMinMarkerValue());
assertEquals(6, series.getMaxMarkerValue());
}
@Test
public void testSmallUpdates() {
series.update(new ChartPoint(0));
@@ -29,6 +29,8 @@ import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -60,24 +62,24 @@ public class DescriptionGeneratorTest {
public void testGenerateTrackDescription() {
Track track = new Track();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
stats.setMaxSpeed(100);
stats.setMaxElevation(550);
stats.setMinElevation(-500);
stats.setTotalElevationGain(6000f);
stats.setTotalElevationLoss(6000f);
stats.setMaxSpeed(Speed.of(100));
stats.setMaxAltitude(550);
stats.setMinAltitude(-500);
stats.setTotalAltitudeGain(6000f);
stats.setTotalAltitudeLoss(6000f);
stats.setStartTime(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks (Debug)</a><p>"
+ "Name: -<br>"
+ "Activity type: hiking<br>"
+ "Description: -<br>"
+ "Total distance: 20.00 km (12.4 mi)<br>"
+ "Total time: 10:00<br>"
+ "Name: -<br>"
+ "Activity type: hiking<br>"
+ "Description: -<br>"
+ "Total distance: 20.00 km (12.4 mi)<br>"
+ "Total time: 10:00<br>"
+ "Moving time: 05:00<br>"
+ "Average speed: 120.00 km/h (74.6 mi/h)<br>"
+ "Average moving speed: 240.00 km/h (149.1 mi/h)<br>"
@@ -95,17 +97,17 @@ public class DescriptionGeneratorTest {
}
@Test
public void testGenerateTrackDescriptionWithoutMaxMinElevation() {
public void testGenerateTrackDescriptionWithoutMaxMinAltitude() {
Track track = new Track();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
stats.setMaxSpeed(100);
stats.setMaxElevation(Double.POSITIVE_INFINITY);
stats.setMinElevation(Double.NEGATIVE_INFINITY);
stats.setTotalElevationGain(6000f);
stats.setTotalElevationLoss(6000f);
stats.setMaxSpeed(Speed.of(100));
stats.setMaxAltitude(Double.POSITIVE_INFINITY);
stats.setMinAltitude(Double.NEGATIVE_INFINITY);
stats.setTotalAltitudeGain(6000f);
stats.setTotalAltitudeLoss(6000f);
stats.setStartTime(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
@@ -132,12 +134,12 @@ public class DescriptionGeneratorTest {
/**
* Tests {@link DescriptionGenerator#writeDistance(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeDistance(Distance, StringBuilder, int, String)}.
*/
@Test
public void testWriteDistance() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeDistance(1100, builder, R.string.description_total_distance, "<br>");
descriptionGenerator.writeDistance(Distance.of(1100), builder, R.string.description_total_distance, "<br>");
assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString());
}
@@ -152,32 +154,32 @@ public class DescriptionGeneratorTest {
}
/**
* Tests {@link DescriptionGenerator#writeSpeed(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeSpeed(Speed, StringBuilder, int, String)}.
*/
@Test
public void testWriteSpeed() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeSpeed(1.1, builder, R.string.description_average_speed, "\n");
descriptionGenerator.writeSpeed(Speed.of(1.1), builder, R.string.description_average_speed, "\n");
assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeElevation(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeAltitude(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteElevation() {
public void testWriteAltitude() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeElevation(4.2, builder, R.string.description_min_elevation, "<br>");
descriptionGenerator.writeAltitude(4.2, builder, R.string.description_min_altitude, "<br>");
assertEquals("Min elevation: 4 m (14 ft)<br>", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writePace(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writePace(Speed, StringBuilder, int, String)}.
*/
@Test
public void testWritePace() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n");
descriptionGenerator.writePace(Speed.of(1.1), builder, R.string.description_average_pace_in_minute, "\n");
assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
}
}
@@ -6,12 +6,14 @@ import android.util.Pair;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
public class TestDataUtil {
@@ -19,8 +21,8 @@ public class TestDataUtil {
public static final double INITIAL_LATITUDE = 37.0;
public static final double INITIAL_LONGITUDE = -57.0;
public static final double ALTITUDE_INTERVAL = 2.5;
public static final float ELEVATION_GAIN = 3;
public static final float ELEVATION_LOSS = 3;
public static final float ALTITUDE_GAIN = 3;
public static final float ALTITUDE_LOSS = 3;
/**
* Create a track without any trackPoints.
@@ -57,7 +59,7 @@ public class TestDataUtil {
int i = 0;
List<TrackPoint> trackPoints = List.of(
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochMilli(i++ + 1)),
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochSecond(i++)),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
@@ -65,19 +67,23 @@ public class TestDataUtil {
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1)),
createTrackPoint(i++, TrackPoint.Type.SEGMENT_END_MANUAL),
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochSecond(i++)),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1))
createTrackPoint(i, TrackPoint.Type.SEGMENT_END_MANUAL)
);
//TODO Use TrackStatisticsUpdater
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(0));
stats.setTotalTime(Duration.ofMillis(0));
List<Marker> markers = List.of(
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(1), null),
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(4), null),
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(5), null)
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, stats, trackPoints.get(1), null),
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, stats, trackPoints.get(4), null),
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, stats, trackPoints.get(5), null)
);
return new TrackData(track, trackPoints, markers);
@@ -104,11 +110,6 @@ public class TestDataUtil {
return pair.first;
}
/**
* Creates a location.
*
* @param i the index for the TrackPoint.
*/
public static TrackPoint createTrackPoint(int i) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
trackPoint.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
@@ -116,13 +117,13 @@ public class TestDataUtil {
trackPoint.setAccuracy((float) i / 100.0f);
trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
trackPoint.setTime(Instant.ofEpochSecond(i + 1));
trackPoint.setSpeed(5f + (i / 10f));
trackPoint.setSpeed(Speed.of(5f + (i / 10f)));
trackPoint.setHeartRate_bpm(100f + i);
trackPoint.setCyclingCadence_rpm(300f + i);
trackPoint.setPower(400f + i);
trackPoint.setElevationGain(ELEVATION_GAIN);
trackPoint.setElevationLoss(ELEVATION_LOSS);
trackPoint.setAltitudeGain(ALTITUDE_GAIN);
trackPoint.setAltitudeLoss(ALTITUDE_LOSS);
return trackPoint;
}
@@ -149,7 +150,12 @@ public class TestDataUtil {
Uri photoUri = FileUtils.getUriForFile(context, dstFile);
String photoUrl = photoUri.toString();
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, trackPoint, photoUrl);
//TODO Use TrackStatisticsUpdater
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(0));
stats.setTotalTime(Duration.ofMillis(0));
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, stats, trackPoint, photoUrl);
}
public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
@@ -22,9 +22,9 @@ public class TestSensorDataUtil {
tp.setAccuracy(1f);
tp.setAltitude(1f);
tp.setTime(time);
tp.setSpeed(5f + (i / 10f));
tp.setElevationGain(3f);
tp.setElevationLoss(3f);
tp.setSpeed(Speed.of(5f + (i / 10f)));
tp.setAltitudeGain(3f);
tp.setAltitudeLoss(3f);
trackPointList.add(tp);
}
@@ -42,8 +42,10 @@ import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
import de.dennisguse.opentracks.content.data.Track;
@@ -105,7 +107,7 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_largeTrack() {
testIterator(new Track.Id(1), 20000);
testIterator(new Track.Id(1), 20000 / 2);
}
private void testIterator(Track.Id trackId, int numPoints) {
@@ -428,11 +430,11 @@ public class CustomContentProviderUtilsTest {
statistics.setStopTime(Instant.ofEpochMilli(2500));
statistics.setTotalTime(Duration.ofMillis(1500));
statistics.setMovingTime(Duration.ofMillis(700));
statistics.setTotalDistance(750.0);
statistics.setTotalElevationGain(50.0f);
statistics.setMaxSpeed(60.0);
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0);
statistics.setTotalDistance(Distance.of(750.0));
statistics.setTotalAltitudeGain(50.0f);
statistics.setMaxSpeed(Speed.of(60.0));
statistics.setMaxAltitude(1250.0);
statistics.setMinAltitude(1200.0);
track.first.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track.first);
@@ -564,10 +566,10 @@ public class CustomContentProviderUtilsTest {
// statistics.setTotalTime(1500L);
// statistics.setMovingTime(700L);
// statistics.setTotalDistance(750.0);
// statistics.setTotalElevationGain(50.0);
// statistics.setTotalAltitudeGain(50.0);
// statistics.setMaxSpeed(60.0);
// statistics.setMaxElevation(1250.0);
// statistics.setMinElevation(1200.0);
// statistics.setMaxAltitude(1250.0);
// statistics.setMinAltitude(1200.0);
//
// track.setTrackStatistics(statistics);
// TestDataUtil.insertTrackWithLocations(contentProviderUtils, track);
@@ -606,7 +608,7 @@ public class CustomContentProviderUtilsTest {
contentProviderUtils.insertMarker(marker3);
contentProviderUtils.insertMarker(marker4);
assertEquals(4, contentProviderUtils.getNextMarkerNumber(trackId));
assertEquals(Integer.valueOf(4), contentProviderUtils.getNextMarkerNumber(trackId));
}
/**
@@ -807,7 +809,7 @@ public class CustomContentProviderUtilsTest {
assertEquals(longitude, trackPoint.getLongitude(), 0.01);
assertEquals(latitude, trackPoint.getLatitude(), 0.01);
assertEquals(time, trackPoint.getTime().toEpochMilli());
assertEquals(speed, trackPoint.getSpeed(), 0.01);
assertEquals(speed, trackPoint.getSpeed().toMPS(), 0.01);
assertFalse(trackPoint.hasHeartRate());
}
@@ -1250,7 +1252,7 @@ public class CustomContentProviderUtilsTest {
@Test
public void testGetSensorStats_veryLongActivity12h() {
testGetSensorStats_randomData(43200, false);
testGetSensorStats_randomData(43200 / 6, false);
}
@Test
@@ -103,8 +103,8 @@ public class SensorDataCyclingTest {
current.compute(previous, 2150);
// then
assertEquals(1.20, current.getValue().distance_m, 2150);
assertEquals(1.20, current.getValue().speed_mps, 0.01);
assertEquals(2.15, current.getValue().distance.toM(), 0.01);
assertEquals(1.20, current.getValue().speed.toMPS(), 0.01);
}
@Test
@@ -117,8 +117,8 @@ public class SensorDataCyclingTest {
current.compute(previous, 2000);
// then
assertEquals(1.20, current.getValue().distance_m, 2000);
assertEquals(2, current.getValue().speed_mps, 0.01);
assertEquals(2, current.getValue().distance.toM(), 0.01);
assertEquals(2, current.getValue().speed.toMPS(), 0.01);
}
@Test
@@ -33,10 +33,13 @@ import java.time.Instant;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.UnitConversions;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/**
* Tests {@link ChartFragment}.
@@ -66,6 +69,7 @@ public class ChartFragmentTest {
boolean chartByDistance = false;
chartFragment = ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setRecordingDistanceInterval(Distance.of(50));
}
/**
@@ -80,9 +84,9 @@ public class ChartFragmentTest {
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
// then
assertEquals(Float.NaN, point.getHeartRate(), 0.01);
assertEquals(Float.NaN, point.getCadence(), 0.01);
assertEquals(Float.NaN, point.getPower(), 0.01);
assertNull(point.getHeartRate());
assertNull(point.getCadence());
assertNull(point.getPower());
}
/**
@@ -211,18 +215,18 @@ public class ChartFragmentTest {
}
/**
* Tests the logic to get the value of elevation in {@link ChartFragment#createPendingPoint(TrackPoint)} by one and two points.
* Tests the logic to get the value of altitude in {@link ChartFragment#createPendingPoint(TrackPoint)} by one and two points.
*/
@Test
public void testCreatePendingPoint_elevation() {
public void testCreatePendingPoint_altitude() {
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
/*
* At first, clear old points of elevation, so give true to the second parameter.
* At first, clear old points of altitude, so give true to the second parameter.
* Then only one value INITIAL_ALTITUDE in buffer.
*/
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getElevation(), 0.01);
assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getAltitude(), 0.01);
/*
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
@@ -230,7 +234,7 @@ public class ChartFragmentTest {
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getElevation(), 0.01);
assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getAltitude(), 0.01);
}
/**
@@ -245,7 +249,7 @@ public class ChartFragmentTest {
* It will not be filled in to the speed buffer.
*/
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(128.5f);
trackPoint1.setSpeed(Speed.of(128.5f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
@@ -259,7 +263,7 @@ public class ChartFragmentTest {
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
*/
trackPoint2.setTime(trackPoint1.getTime().plusMillis(222));
trackPoint2.setSpeed(130f);
trackPoint2.setSpeed(Speed.of(130f));
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01);
}
@@ -273,7 +277,7 @@ public class ChartFragmentTest {
// First data point is not added to the speed buffer
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f);
trackPoint1.setSpeed(Speed.of(100.0f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
@@ -284,7 +288,7 @@ public class ChartFragmentTest {
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
trackPoint2.setSpeed(102f);
trackPoint2.setSpeed(Speed.of(102f));
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
}
@@ -298,7 +302,7 @@ public class ChartFragmentTest {
// First data point is not added to the speed buffer
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f);
trackPoint1.setSpeed(Speed.of(100.0f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
@@ -309,7 +313,7 @@ public class ChartFragmentTest {
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
trackPoint2.setSpeed(102f);
trackPoint2.setSpeed(Speed.of(102f));
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01);
}
@@ -321,7 +325,7 @@ public class ChartFragmentTest {
public void testCreatePendingPoint_pace_zeroSpeed() {
chartFragment.setReportSpeed(false);
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setSpeed(0f);
trackPoint.setSpeed(Speed.of(0f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
assertEquals(0.0, point.getPace(), 0.01);
}
@@ -6,15 +6,14 @@ import android.content.SharedPreferences;
import android.os.Looper;
import android.util.Log;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -31,7 +30,9 @@ import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -39,7 +40,6 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -86,23 +86,33 @@ public class ExportImportTest {
private Track.Id trackId;
private Track.Id importTrackId;
@Before
public void setUp() throws TimeoutException {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)));
public void setUp(boolean hasSensorDistance) throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
trackId = service.startNewTrack();
//TODO Workaround as those managers overwrite input data; We need to refactor TrackRecordingService to make it actually testable
service.setAltitudeSumManager(null);
service.setRemoteSensorManager(null);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50, 5), 0);
Distance sensorDistance = hasSensorDistance ? Distance.of(5) : null;
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance), 0);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
service.pauseCurrentTrack();
service.resumeCurrentTrack();
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, 5), 0);
//TODO Workaround as those managers overwrite input data; We need to refactor TrackRecordingService to make it actually testable
service.setAltitudeSumManager(null);
service.setRemoteSensorManager(null);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
service.endCurrentTrack();
track = contentProviderUtils.getTrack(trackId);
@@ -126,17 +136,11 @@ public class ExportImportTest {
}
}
@Ignore("Not implemented")
@LargeTest
@Test
public void kml_only_track() {
// TODO
Log.e(TAG, "Test not implemented.");
}
public void kml_with_trackdetail() throws TimeoutException {
setUp(false);
@LargeTest
@Test
public void kml_with_trackdetail() {
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -167,7 +171,7 @@ public class ExportImportTest {
assertTrackpoints(trackPoints, false, false, false, false, false, false);
// 3. trackstatistics
assertTrackStatistics(false, false, false);
assertTrackStatistics(false, false, true);
// 4. markers
assertMarkers();
@@ -175,7 +179,9 @@ public class ExportImportTest {
@LargeTest
@Test
public void kml_with_trackdetail_and_sensordata() {
public void kml_with_trackdetail_and_sensordata() throws TimeoutException {
setUp(true);
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -213,9 +219,13 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() {
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException {
setUp(false);
// given
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.import_prevent_reimport_key, true);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
editor.commit();
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
@@ -237,7 +247,9 @@ public class ExportImportTest {
@LargeTest
@Test
public void gpx() {
public void gpx() throws TimeoutException {
setUp(true);
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -283,9 +295,13 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() {
public void gpx_duplicate_trackUUID() throws TimeoutException {
setUp(false);
// given
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.import_prevent_reimport_key, true);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
editor.commit();
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
@@ -325,7 +341,7 @@ public class ExportImportTest {
}
}
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss, boolean verifyDistance) {
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyAltitudeGain, boolean verifyAltitudeLoss, boolean verifyDistance) {
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(trackPoints.size(), importedTrackPoints.size());
@@ -348,7 +364,7 @@ public class ExportImportTest {
}
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
if (trackPoint.hasSpeed()) {
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
assertEquals(trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
}
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
if (trackPoint.hasAltitude()) {
@@ -369,19 +385,19 @@ public class ExportImportTest {
if (verifyPower) {
assertEquals("" + trackPoint, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
}
if (verifyElevationGain) {
assertEquals(trackPoint.getElevationGain(), importedTrackPoint.getElevationGain(), 0.01);
if (verifyAltitudeGain) {
assertEquals(trackPoint.getAltitudeGain(), importedTrackPoint.getAltitudeGain(), 0.01);
}
if (verifyElevationLoss) {
assertEquals(trackPoint.getElevationLoss(), importedTrackPoint.getElevationLoss(), 0.01);
if (verifyAltitudeLoss) {
assertEquals(trackPoint.getAltitudeLoss(), importedTrackPoint.getAltitudeLoss(), 0.01);
}
if (verifyDistance) {
assertEquals(trackPoint.getSensorDistance(), importedTrackPoint.getSensorDistance(), 0.01);
assertEquals(trackPoint.getSensorDistance(), importedTrackPoint.getSensorDistance());
}
}
}
private void assertTrackStatistics(boolean isGpx, boolean verifyElevationGainAndLoss, boolean verifyDistance) {
private void assertTrackStatistics(boolean isGpx, boolean verifyAltitudeGainAndLoss, boolean verifyDistance) {
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack.getTrackStatistics());
@@ -400,35 +416,36 @@ public class ExportImportTest {
// Distance
if (verifyDistance) {
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01);
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
}
// Speed
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed(), 0.01);
assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed(), 0.01);
assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed(), 0.01);
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
}
// Elevation
assertEquals(trackStatistics.getMinElevation(), importedTrackStatistics.getMinElevation(), 0.01);
assertEquals(trackStatistics.getMaxElevation(), importedTrackStatistics.getMaxElevation(), 0.01);
if (verifyElevationGainAndLoss) {
assertEquals(trackStatistics.getTotalElevationGain(), importedTrackStatistics.getTotalElevationGain(), 0.01);
assertEquals(trackStatistics.getTotalElevationLoss(), importedTrackStatistics.getTotalElevationLoss(), 0.01);
// Altitude
assertEquals(trackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(trackStatistics.getMaxAltitude(), importedTrackStatistics.getMaxAltitude(), 0.01);
if (verifyAltitudeGainAndLoss) {
assertEquals(trackStatistics.getTotalAltitudeGain(), importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(trackStatistics.getTotalAltitudeLoss(), importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
} else {
assertFalse(importedTrackStatistics.hasTotalElevationGain());
assertFalse(importedTrackStatistics.hasTotalElevationLoss());
assertFalse(importedTrackStatistics.hasTotalAltitudeGain());
assertFalse(importedTrackStatistics.hasTotalAltitudeLoss());
}
}
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float elevationGain, float heartRate, float cyclingCadence, float power, float distance) {
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) {
TrackPoint tp = new TrackPoint(latitude, longitude, (double) altitude, Instant.ofEpochMilli(time));
tp.setAccuracy(accuracy);
tp.setSpeed(speed);
tp.setSpeed(Speed.of(speed));
tp.setHeartRate_bpm(heartRate);
tp.setCyclingCadence_rpm(cyclingCadence);
tp.setPower(power);
tp.setElevationGain(elevationGain);
tp.setAltitudeGain(altitudeGain);
tp.setAltitudeLoss(altitudeGain); //TODO
tp.setSensorDistance(distance);
return tp;
}
@@ -11,11 +11,12 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -41,8 +42,8 @@ public class TrackRecordingServiceNotificationManagerTest {
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
when(trackPointMock.hasAccuracy()).thenReturn(true);
when(trackPointMock.getAccuracy()).thenReturn(999f);
when(trackStatisticsMock.getTotalDistance()).thenReturn(0d);
when(notificationCompatBuilder.setContentText(anyString())).thenReturn(notificationCompatBuilder);
when(trackPointMock.getSpeed()).thenReturn(Speed.of(0));
when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean())).thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
@@ -33,11 +33,11 @@ import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@@ -113,7 +113,8 @@ public class TrackRecordingServiceTest {
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
@@ -146,36 +147,38 @@ public class TrackRecordingServiceTest {
// when
Intent startIntent = createStartIntent(context);
mServiceRule.startService(startIntent);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent))
.getService();
// then
// Test if we start in no-recording mode by default.
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_oldTracks() throws TimeoutException {
// given
createDummyTrack(trackId, false);
createDummyTrack(trackId);
// when
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
// then
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_serviceRestart_whileRecording() throws TimeoutException {
// given
createDummyTrack(trackId, true);
createDummyTrack(trackId);
//when
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
service.resumeTrack(trackId);
// then
assertTrue(service.isRecording());
@@ -185,7 +188,8 @@ public class TrackRecordingServiceTest {
@Test
public void testRecording_start() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
// when
Track.Id trackId = service.startNewTrack();
@@ -201,7 +205,8 @@ public class TrackRecordingServiceTest {
@Test
public void testRecording_stop() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
Track.Id trackId = service.startNewTrack();
// when
@@ -219,7 +224,8 @@ public class TrackRecordingServiceTest {
@Test
public void testRecording_pauseAndResume() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
Track.Id trackId = service.startNewTrack();
// when
@@ -233,7 +239,6 @@ public class TrackRecordingServiceTest {
// then
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(3, trackPoints.size());
@@ -246,7 +251,8 @@ public class TrackRecordingServiceTest {
@Test
public void testRecording_resumeStoppedTrack() throws TimeoutException, InterruptedException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
service.endCurrentTrack();
@@ -259,7 +265,6 @@ public class TrackRecordingServiceTest {
// then
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(4, trackPoints.size());
@@ -273,7 +278,8 @@ public class TrackRecordingServiceTest {
@Test
public void testRecording_stopPausedTrack() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
service.pauseCurrentTrack();
@@ -285,7 +291,6 @@ public class TrackRecordingServiceTest {
// then
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(2, trackPoints.size());
@@ -293,48 +298,29 @@ public class TrackRecordingServiceTest {
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
}
@Ignore("Sometimes fails on CI.")
@MediumTest
@Test
public void testRecording_orphanedRecordingTrack() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// when
// Just set recording track to a bogus value.
// Make sure that the service will not start recording and will clear the bogus track.
PreferencesUtils.setLong(sharedPreferences, context, R.string.recording_track_id_key, 123L);
// then
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
}
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
Track.Id trackId = service.getRecordingTrackId();
// when
Track.Id newTrackId = service.startNewTrack();
// then
assertNotNull(trackId);
assertNull(newTrackId);
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(sharedPreferences, context));
assertEquals(trackId, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testEndCurrentTrack_noRecording() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
assertFalse(service.isRecording());
// when
@@ -342,15 +328,15 @@ public class TrackRecordingServiceTest {
service.endCurrentTrack();
// then
assertFalse(PreferencesUtils.isRecording(sharedPreferences, context));
assertNull(service.getRecordingTrackId());
assertFalse(service.isRecording());
}
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
assertFalse(service.isRecording());
// when
@@ -364,57 +350,58 @@ public class TrackRecordingServiceTest {
@Test
public void testInsertWaypointMarker_validWaypoint() throws TimeoutException, InterruptedException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
newTrackPoint(service);
Track.Id trackId = service.getRecordingTrackId();
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
// then
assertNotEquals(-1L, markerId);
assertNotEquals(new Marker.Id(-1L), markerId);
Marker wpt = contentProviderUtils.getMarker(markerId);
assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
assertEquals(trackId, wpt.getTrackId());
assertEquals(0.0, wpt.getLength(), 0.01);
assertEquals(0.0, wpt.getLength().toM(), 0.01);
assertNotNull(wpt.getLocation());
service.endCurrentTrack();
}
private void addTrack(Track track, boolean isRecording) {
private void addTrack(Track track) {
assertNotNull(track.getId());
contentProviderUtils.insertTrack(track);
assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(sharedPreferences, context, R.string.recording_track_id_key, isRecording ? track.getId().getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.recording_track_paused_key, !isRecording);
}
// NOTE: Do not use to create a track that is currently recording.
private void createDummyTrack(Track.Id id, boolean isRecording) {
private void createDummyTrack(Track.Id id) {
Track dummyTrack = new Track();
dummyTrack.setId(id);
dummyTrack.setName("Dummy Track");
TrackStatistics trackStatistics = new TrackStatistics();
Instant now = Instant.now();
trackStatistics.setStartTime(now.minusSeconds(5L));
trackStatistics.setStopTime(now.minusSeconds(1L));
dummyTrack.setTrackStatistics(trackStatistics);
addTrack(dummyTrack, isRecording);
addTrack(dummyTrack);
}
private static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
private static void newTrackPoint(TrackRecordingService trackRecordingService) throws InterruptedException {
newTrackPoint(trackRecordingService, 45.0f, 35f, 5, 10, System.currentTimeMillis());
}
static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException {
static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException {
newTrackPoint(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis());
}
/**
* Inserts a location and waits for 200ms.
*/
private static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException {
private static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) {
Location location = new Location("");
location.setLongitude(longitude);
location.setLatitude(latitude);
@@ -55,7 +55,7 @@ public class TrackRecordingServiceTestLocation {
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private TrackRecordingServiceInterface service;
private TrackRecordingService service;
@BeforeClass
public static void preSetUp() {
@@ -77,7 +77,8 @@ public class TrackRecordingServiceTestLocation {
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().commit();
service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
}
@After
@@ -6,6 +6,7 @@ import android.content.SharedPreferences;
import android.location.Location;
import android.os.Looper;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
@@ -35,7 +36,6 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
@@ -87,7 +87,8 @@ public class TrackRecordingServiceTestLooper {
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
@@ -99,28 +100,36 @@ public class TrackRecordingServiceTestLooper {
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracy() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_gps_accuracy_key, 500);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.recording_gps_accuracy_key), 500);
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default)));
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.voice_frequency_key), Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default)));
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByDistance() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, -1);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.voice_frequency_key), -1);
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByTime() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, 1);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.voice_frequency_key), 1);
editor.commit();
fullRecordingSession();
}
@@ -128,29 +137,36 @@ public class TrackRecordingServiceTestLooper {
@MediumTest
@Test
public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.max_recording_distance_key, Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default)));
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.max_recording_distance_key), Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default)));
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_maxRecordingDistance() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.max_recording_distance_key, 50);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.max_recording_distance_key), 50);
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingDistanceDefault() throws TimeoutException {
int minRecordingIntervalDefault = Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_distance_interval_key, minRecordingIntervalDefault);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.recording_distance_interval_key), Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingDistance() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_distance_interval_key, 2);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.recording_distance_interval_key), 2);
editor.commit();
fullRecordingSession();
}
@@ -171,22 +187,27 @@ public class TrackRecordingServiceTestLooper {
@MediumTest
@Test
public void testWithProperties_minRecordingIntervalDefault() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.min_recording_interval_key, Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.min_recording_interval_key), Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingInterval() throws TimeoutException {
PreferencesUtils.setInt(sharedPreferences, context, R.string.min_recording_interval_key, 2);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.min_recording_interval_key), 2);
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracyDefault() throws TimeoutException {
int recordingGPSAccuracyDefault = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_gps_accuracy_key, recordingGPSAccuracyDefault);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.recording_gps_accuracy_key), Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default)));
editor.commit();
fullRecordingSession();
}
@@ -199,7 +220,8 @@ public class TrackRecordingServiceTestLooper {
}
private void fullRecordingSession() throws TimeoutException {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
assertFalse(service.isRecording());
// Start a track.
@@ -209,8 +231,6 @@ public class TrackRecordingServiceTestLooper {
Track track = contentProviderUtils.getTrack(trackId);
assertNotNull(track);
assertEquals(trackId, track.getId());
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(sharedPreferences, context));
assertEquals(trackId, service.getRecordingTrackId());
// Insert a few points, markers and statistics.
long startTime = System.currentTimeMillis();
@@ -234,7 +254,6 @@ public class TrackRecordingServiceTestLooper {
// Stop the track. Validate if it has correct data.
service.endCurrentTrack();
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
track = contentProviderUtils.getTrack(trackId);
assertNotNull(track);
assertEquals(trackId, track.getId());
@@ -0,0 +1,50 @@
package de.dennisguse.opentracks.services.sensors;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* NOTE: Test data is completely artificial.
*/
public class AltitudeSumManagerTest {
private final AltitudeSumManager subject = new AltitudeSumManager();
private static void addSensorValue(AltitudeSumManager altitudeSumManager, float[] values) {
for (float f : values) {
altitudeSumManager.onSensorValueChanged(f);
}
}
@Before
public void setUp() {
subject.reset();
}
@Test
public void getAltitudeGainLoss_downhill() {
// given
subject.setConnected(true);
// then
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1015.14f, 1015.15f});
// then
Assert.assertEquals(0f, subject.getAltitudeGain_m(), 0.01);
Assert.assertEquals(48.0, subject.getAltitudeLoss_m(), 0.01);
}
@Test
public void sensorUnavailable() {
// given
subject.setConnected(false);
// then
subject.onSensorValueChanged(999f);
// then
Assert.assertNull(subject.getAltitudeGain_m());
Assert.assertNull(subject.getAltitudeLoss_m());
}
}
@@ -1,50 +0,0 @@
package de.dennisguse.opentracks.services.sensors;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* NOTE: Test data is completely artificial.
*/
public class ElevationSumManagerTest {
private final ElevationSumManager elevationSumManager = new ElevationSumManager();
private static void addSensorValue(ElevationSumManager elevationSumManager, float[] values) {
for (float f : values) {
elevationSumManager.onSensorValueChanged(f);
}
}
@Before
public void setUp() {
elevationSumManager.reset();
}
@Test
public void getElevationGainLoss_downhill() {
// given
elevationSumManager.setConnected(true);
// then
addSensorValue(elevationSumManager, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1015.14f, 1015.15f});
// then
Assert.assertEquals(0f, elevationSumManager.getElevationGain_m(), 0.01);
Assert.assertEquals(48.0, elevationSumManager.getElevationLoss_m(), 0.01);
}
@Test
public void sensorUnavailable() {
// given
elevationSumManager.setConnected(false);
// then
elevationSumManager.onSensorValueChanged(999f);
// then
Assert.assertNull(elevationSumManager.getElevationGain_m());
Assert.assertNull(elevationSumManager.getElevationLoss_m());
}
}
@@ -15,14 +15,18 @@
*/
package de.dennisguse.opentracks.stats;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -32,7 +36,7 @@ import static org.junit.Assert.assertNull;
*
* @author Rodrigo Damazio
*/
@RunWith(JUnit4.class)
@RunWith(AndroidJUnit4.class)
public class TrackStatisticsTest {
private TrackStatistics statistics;
@@ -56,13 +60,13 @@ public class TrackStatisticsTest {
assertEquals(Duration.ofSeconds(0), statistics.getMovingTime());
assertEquals(Duration.ofSeconds(0), statistics.getTotalTime());
assertNull(statistics.getTotalElevationGain());
assertNull(statistics.getTotalElevationLoss());
assertEquals(Double.NEGATIVE_INFINITY, statistics.getMaxElevation(), 0.0);
assertEquals(Double.POSITIVE_INFINITY, statistics.getMinElevation(), 0.0);
assertEquals(0.0, statistics.getMaxSpeed(), 0.0);
assertEquals(0.0, statistics.getAverageSpeed(), 0.0);
assertEquals(0.0, statistics.getAverageMovingSpeed(), 0.0);
assertNull(statistics.getTotalAltitudeGain());
assertNull(statistics.getTotalAltitudeLoss());
assertEquals(Double.NEGATIVE_INFINITY, statistics.getMaxAltitude(), 0.0);
assertEquals(Double.POSITIVE_INFINITY, statistics.getMinAltitude(), 0.0);
assertEquals(0.0, statistics.getMaxSpeed().toMPS(), 0.0);
assertEquals(0.0, statistics.getAverageSpeed().toMPS(), 0.0);
assertEquals(0.0, statistics.getAverageMovingSpeed().toMPS(), 0.0);
}
@Test
@@ -77,16 +81,16 @@ public class TrackStatisticsTest {
statistics2.setTotalTime(Duration.ofMillis(1000)); // Result: 1500+1000
statistics.setMovingTime(Duration.ofMillis(700));
statistics2.setMovingTime(Duration.ofMillis(600)); // Result: 700+600
statistics.setTotalDistance(750.0);
statistics2.setTotalDistance(350.0); // Result: 750+350
statistics.setTotalElevationGain(50.0f);
statistics2.setTotalElevationGain(850.0f); // Result: 850+50
statistics.setMaxSpeed(60.0); // Resulting max speed
statistics2.setMaxSpeed(30.0);
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0); // Resulting min elevation
statistics2.setMaxElevation(3575.0); // Resulting max elevation
statistics2.setMinElevation(2800.0);
statistics.setTotalDistance(Distance.of(750.0));
statistics2.setTotalDistance(Distance.of(350.0)); // Result: 750+350
statistics.setTotalAltitudeGain(50.0f);
statistics2.setTotalAltitudeGain(850.0f); // Result: 850+50
statistics.setMaxSpeed(Speed.of(60.0)); // Resulting max speed
statistics2.setMaxSpeed(Speed.of(30.0));
statistics.setMaxAltitude(1250.0);
statistics.setMinAltitude(1200.0); // Resulting min altitude
statistics2.setMaxAltitude(3575.0); // Resulting max altitude
statistics2.setMinAltitude(2800.0);
// when
statistics.merge(statistics2);
@@ -96,24 +100,24 @@ public class TrackStatisticsTest {
assertEquals(Instant.ofEpochMilli(4000), statistics.getStopTime());
assertEquals(Duration.ofMillis(2500), statistics.getTotalTime());
assertEquals(Duration.ofMillis(1300), statistics.getMovingTime());
assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
assertEquals(900.0, statistics.getTotalElevationGain(), 0.001);
assertEquals(statistics.getTotalDistance() / statistics.getMovingTime().getSeconds(), statistics.getMaxSpeed(), 0.001);
assertEquals(1200.0, statistics.getMinElevation(), 0.001);
assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
assertEquals(1100.0, statistics.getTotalDistance().toM(), 0.001);
assertEquals(900.0, statistics.getTotalAltitudeGain(), 0.001);
assertEquals(Speed.of(statistics.getTotalDistance(), statistics.getMovingTime()).toMPS(), statistics.getMaxSpeed().toMPS(), 0.001);
assertEquals(1200.0, statistics.getMinAltitude(), 0.001);
assertEquals(3575.0, statistics.getMaxAltitude(), 0.001);
}
@Test
public void testGetAverageSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setTotalDistance(Distance.of(1000.0));
statistics.setTotalTime(Duration.ofMillis(50000));
assertEquals(20.0, statistics.getAverageSpeed(), 0.001);
assertEquals(20.0, statistics.getAverageSpeed().toMPS(), 0.001);
}
@Test
public void testGetAverageMovingSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setTotalDistance(Distance.of(1000.0));
statistics.setMovingTime(Duration.ofMillis(20000));
assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001);
assertEquals(50.0, statistics.getAverageMovingSpeed().toMPS(), 0.001);
}
}
@@ -9,6 +9,8 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -18,7 +20,7 @@ import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class TrackStatisticsUpdaterTest {
private static final int GPS_DISTANCE = 50;
private static final Distance GPS_DISTANCE = Distance.of(50);
@Test
public void addTrackPoint_TestingTrack() {
@@ -31,18 +33,18 @@ public class TrackStatisticsUpdaterTest {
// then
TrackStatistics statistics = subject.getTrackStatistics();
assertEquals(85.35, statistics.getTotalDistance(), 0.01);
assertEquals(Duration.ofMillis(13999), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(6), statistics.getMovingTime());
assertEquals(99.58, statistics.getTotalDistance().toM(), 0.01);
assertEquals(Duration.ofSeconds(14), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(7), statistics.getMovingTime());
assertEquals(2.5, statistics.getMinElevation(), 0.01);
assertEquals(27.5, statistics.getMaxElevation(), 0.01);
assertEquals(27, statistics.getTotalElevationGain(), 0.01);
assertEquals(27.0, statistics.getTotalElevationLoss(), 0.01);
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
assertEquals(27.5, statistics.getMaxAltitude(), 0.01);
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
assertEquals(14.226, statistics.getMaxSpeed(), 0.01);
assertEquals(14.226, statistics.getAverageMovingSpeed(), 0.01);
assertEquals(6.566, statistics.getAverageSpeed(), 0.01);
assertEquals(14.226, statistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(14.226, statistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(7.11, statistics.getAverageSpeed().toMPS(), 0.01);
}
@Test
@@ -60,7 +62,7 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01);
assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Test
@@ -71,7 +73,7 @@ public class TrackStatisticsUpdaterTest {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
tp3.setSpeed(5f);
tp3.setSpeed(Speed.of(5f));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -79,7 +81,7 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(1.10, subject.getTrackStatistics().getTotalDistance(), 0.01);
assertEquals(1.10, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Test
@@ -89,14 +91,14 @@ public class TrackStatisticsUpdaterTest {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
tp2.setSpeed(5f);
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(3000));
tp2.setSpeed(5f);
tp2.setSpeed(Speed.of(5f));
TrackPoint tp4 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(4000));
tp2.setSpeed(5f);
tp4.setSensorDistance(5f);
tp2.setSpeed(Speed.of(5f));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(10f);
tp5.setSensorDistance(Distance.of(10f));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -104,14 +106,14 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(110.57, subject.getTrackStatistics().getTotalDistance(), 0.01);
assertEquals(110.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
// when
subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE);
// then
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance(), 0.01);
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Test
@@ -123,9 +125,9 @@ public class TrackStatisticsUpdaterTest {
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000));
tp4.setSensorDistance(5f);
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(10f);
tp5.setSensorDistance(Distance.of(10f));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -133,14 +135,14 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01);
assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
// when
subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE);
// then
assertEquals(15, subject.getTrackStatistics().getTotalDistance(), 0.01);
assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@@ -11,6 +11,8 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -27,11 +29,11 @@ public class AnnouncementUtilsTest {
@Test
public void getAnnouncement_metric() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000f);
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, null);
@@ -43,14 +45,14 @@ public class AnnouncementUtilsTest {
@Test
public void getAnnouncement_withInterval_metric() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000f);
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000);
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
// when
@@ -3,11 +3,14 @@ package de.dennisguse.opentracks.util;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RunWith(JUnit4.class)
public class ExportUtilsTest extends TestCase {
@Test
@@ -9,7 +9,7 @@ public class PressureSensorUtilsTest {
// real data at 500Hz stationary
@Test
public void elevationChanges_none() {
public void altitudeChanges_none() {
// given
float[] sensorValues_hPa = new float[]{1015.6876f, 1015.699f, 1015.70905f, 1015.71075f, 1015.7224f, 1015.72f, 1015.7373f, 1015.7481f, 1015.74133f, 1015.73553f, 1015.7462f, 1015.74896f, 1015.752f, 1015.754f, 1015.7456f, 1015.73303f, 1015.7288f, 1015.7239f, 1015.7298f, 1015.7387f, 1015.72534f, 1015.72577f, 1015.72797f, 1015.71796f, 1015.7113f, 1015.7097f, 1015.7096f, 1015.7013f, 1015.7072f, 1015.71063f, 1015.7088f, 1015.7115f, 1015.713f, 1015.7121f, 1015.7062f, 1015.7062f, 1015.6994f, 1015.69617f, 1015.68945f, 1015.698f, 1015.6913f, 1015.69696f, 1015.69293f, 1015.6955f, 1015.6928f, 1015.6939f, 1015.68787f, 1015.6881f, 1015.68536f, 1015.68726f, 1015.6879f, 1015.6904f, 1015.6937f, 1015.69476f, 1015.7046f, 1015.7019f, 1015.7079f, 1015.71124f, 1015.71216f, 1015.71436f, 1015.7171f, 1015.7186f, 1015.7186f, 1015.7246f, 1015.72046f, 1015.7211f, 1015.72375f, 1015.7194f, 1015.72125f, 1015.7161f, 1015.71185f, 1015.7188f, 1015.721f, 1015.7252f, 1015.72266f, 1015.7236f, 1015.72614f, 1015.7266f, 1015.7344f, 1015.7376f, 1015.7337f, 1015.7253f, 1015.71875f, 1015.7219f, 1015.7219f, 1015.7317f, 1015.7284f, 1015.73444f, 1015.73584f, 1015.7369f, 1015.73254f, 1015.7369f, 1015.7336f, 1015.73254f, 1015.73676f, 1015.74176f, 1015.74506f, 1015.7478f, 1015.755f, 1015.7515f, 1015.7543f, 1015.7459f, 1015.7468f, 1015.7492f, 1015.74585f, 1015.74756f, 1015.74756f, 1015.75085f, 1015.754f, 1015.75336f, 1015.7568f, 1015.75165f, 1015.761f, 1015.7527f, 1015.75684f, 1015.7635f, 1015.7585f, 1015.7552f, 1015.7475f, 1015.74506f, 1015.7542f, 1015.75586f, 1015.74176f, 1015.74414f, 1015.7501f, 1015.751f, 1015.7478f, 1015.755f, 1015.75836f, 1015.7592f, 1015.75256f, 1015.7559f, 1015.76654f, 1015.76154f, 1015.766f, 1015.77014f, 1015.76904f, 1015.76013f, 1015.7674f, 1015.76514f, 1015.7674f, 1015.7635f, 1015.76404f, 1015.7607f, 1015.754f, 1015.7574f, 1015.7607f, 1015.7607f, 1015.7607f, 1015.754f, 1015.7567f, 1015.7616f, 1015.7643f, 1015.7666f, 1015.7609f, 1015.766f, 1015.76764f, 1015.755f, 1015.75757f, 1015.74927f, 1015.74927f, 1015.7426f, 1015.745f, 1015.745f, 1015.7409f, 1015.7376f, 1015.7326f, 1015.74164f, 1015.74664f, 1015.745f, 1015.7459f, 1015.7502f, 1015.7493f, 1015.7476f, 1015.7493f, 1015.751f, 1015.7601f, 1015.76434f, 1015.766f, 1015.756f, 1015.7617f, 1015.7623f, 1015.76404f, 1015.76733f, 1015.7607f, 1015.7623f, 1015.76337f, 1015.7584f, 1015.7573f, 1015.7502f, 1015.7534f, 1015.7534f, 1015.7601f, 1015.75684f, 1015.7518f, 1015.75574f, 1015.7524f, 1015.7507f, 1015.7484f, 1015.7502f, 1015.7534f, 1015.7623f, 1015.75903f, 1015.75574f, 1015.75574f, 1015.7573f, 1015.7524f, 1015.75574f, 1015.75574f, 1015.7607f, 1015.7573f, 1015.754f, 1015.74677f, 1015.754f, 1015.75574f, 1015.75903f, 1015.7573f, 1015.7551f, 1015.7573f, 1015.754f, 1015.7507f, 1015.7374f, 1015.739f, 1015.73566f, 1015.73566f, 1015.7367f, 1015.7417f, 1015.7417f, 1015.739f, 1015.74835f, 1015.74567f, 1015.7517f, 1015.74066f, 1015.7374f, 1015.739f, 1015.7417f, 1015.73065f, 1015.72894f, 1015.73505f, 1015.73334f, 1015.74005f, 1015.74005f, 1015.7367f, 1015.7434f, 1015.744f, 1015.7423f, 1015.7384f, 1015.739f, 1015.7374f, 1015.7374f, 1015.7374f, 1015.7341f, 1015.73505f, 1015.7341f, 1015.7267f, 1015.72833f, 1015.7234f, 1015.7167f, 1015.725f, 1015.7217f, 1015.7234f, 1015.73004f, 1015.7324f, 1015.7423f, 1015.74005f, 1015.73505f, 1015.73004f, 1015.739f, 1015.7417f, 1015.74567f, 1015.74066f, 1015.74146f, 1015.74146f, 1015.73816f, 1015.7274f, 1015.72906f, 1015.7274f, 1015.7341f, 1015.739f, 1015.744f, 1015.7417f, 1015.7374f, 1015.7324f, 1015.73816f, 1015.74146f, 1015.7374f, 1015.7341f, 1015.7365f, 1015.7365f, 1015.74146f, 1015.74414f, 1015.7475f, 1015.7448f, 1015.74146f, 1015.74585f, 1015.7475f, 1015.75146f, 1015.75146f, 1015.7591f, 1015.75476f, 1015.7614f, 1015.75476f, 1015.7492f, 1015.74817f, 1015.7498f, 1015.7515f, 1015.75586f, 1015.7509f, 1015.7448f, 1015.7525f, 1015.7581f, 1015.7592f};
float firstSensorValue = sensorValues_hPa[0];
@@ -20,7 +20,7 @@ public class PressureSensorUtilsTest {
}
}
// Simulate a sudden drop of elevation.
// Simulate a sudden drop of altitude.
// test data
@Test
public void computeChanges_downhill() {
@@ -28,22 +28,22 @@ public class PressureSensorUtilsTest {
float firstSensorValue = sensorValues_hPa[0];
// when
float elevationGain_m = 0;
float elevationLoss_m = 0;
float altitudeGain_m = 0;
float altitudeLoss = 0;
float lastUsedPressureValue_hPa = firstSensorValue;
for (float v : sensorValues_hPa) {
PressureSensorUtils.ElevationChange elevationChange = PressureSensorUtils.computeChanges_m(lastUsedPressureValue_hPa, v);
if (elevationChange != null) {
elevationGain_m += elevationChange.getElevationGain_m();
elevationLoss_m += elevationChange.getElevationLoss_m();
lastUsedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa();
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChanges_m(lastUsedPressureValue_hPa, v);
if (altitudeChange != null) {
altitudeGain_m += altitudeChange.getAltitudeGain_m();
altitudeLoss += altitudeChange.getAltitudeLoss_m();
lastUsedPressureValue_hPa = altitudeChange.getCurrentSensorValue_hPa();
}
}
// then
assertEquals(0f, elevationGain_m, 0.01);
assertEquals(15.0f, elevationLoss_m, 0.01);
assertEquals(0f, altitudeGain_m, 0.01);
assertEquals(15.0f, altitudeLoss, 0.01);
assertEquals(1016.80f, lastUsedPressureValue_hPa, 0.01); //Expect exponential smoothing
}
}
@@ -28,6 +28,9 @@ import java.time.Duration;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -70,19 +73,18 @@ public class StringUtilsTest {
}
/**
* Tests {@link StringUtils#formatDistance(android.content.Context, double,
* boolean)}.
* Tests {@link StringUtils#formatDistance(android.content.Context, Distance, boolean)}.
*/
@Test
public void testFormatDistance() {
// A large number in metric
assertEquals("5.00 km", StringUtils.formatDistance(context, 5000, true));
assertEquals("5.00 km", StringUtils.formatDistance(context, Distance.of(5000), true));
// A large number in imperial
assertEquals("3.11 mi", StringUtils.formatDistance(context, 5000, false));
assertEquals("3.11 mi", StringUtils.formatDistance(context, Distance.of(5000), false));
// A small number in metric
assertEquals("100.00 m", StringUtils.formatDistance(context, 100, true));
assertEquals("100.00 m", StringUtils.formatDistance(context, Distance.of(100), true));
// A small number in imperial
assertEquals("328.08 ft", StringUtils.formatDistance(context, 100, false));
assertEquals("328.08 ft", StringUtils.formatDistance(context, Distance.of(100), false));
}
/**
@@ -164,22 +166,22 @@ public class StringUtilsTest {
@Test
public void testGetSpeedParts() {
assertEquals("4:59", StringUtils.getSpeedParts(context, 3.34, true, false).first);
assertEquals("5:00", StringUtils.getSpeedParts(context, 3.33, true, false).first);
assertEquals("4:59", StringUtils.getSpeedParts(context, Speed.of(3.34), true, false).first);
assertEquals("5:00", StringUtils.getSpeedParts(context, Speed.of(3.33), true, false).first);
assertEquals("11.9", StringUtils.getSpeedParts(context, 3.31, true, true).first);
assertEquals("7.5", StringUtils.getSpeedParts(context, 3.34, false, true).first);
assertEquals("11.9", StringUtils.getSpeedParts(context, Speed.of(3.31), true, true).first);
assertEquals("7.5", StringUtils.getSpeedParts(context, Speed.of(3.34), false, true).first);
assertEquals("min/km", StringUtils.getSpeedParts(context, 0, true, false).second);
assertEquals("min/mi", StringUtils.getSpeedParts(context, 0, false, false).second);
assertEquals("min/km", StringUtils.getSpeedParts(context, Speed.zero(), true, false).second);
assertEquals("min/mi", StringUtils.getSpeedParts(context, Speed.zero(), false, false).second);
}
@Test
public void testFormatSpeed() {
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, 3.34, true, false));
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, 3.34, false, false));
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, Speed.of(3.34), true, false));
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, Speed.of(3.34), false, false));
assertEquals("12.02 km/h", StringUtils.formatSpeed(context, 3.34, true, true));
assertEquals("7.47 mph", StringUtils.formatSpeed(context, 3.34, false, true));
assertEquals("12.02 km/h", StringUtils.formatSpeed(context, Speed.of(3.34), true, true));
assertEquals("7.47 mph", StringUtils.formatSpeed(context, Speed.of(3.34), false, true));
}
}
@@ -13,6 +13,8 @@ import java.time.Instant;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -25,17 +27,17 @@ public class AggregatedStatisticsTest {
private final Context context = ApplicationProvider.getApplicationContext();
private static Track createTrack(Context context, long totalDistance, long totalTime, String category) {
private static Track createTrack(Context context, Distance totalDistance, Duration totalTime, String category) {
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime(Instant.ofEpochMilli(1000L)); // Resulting start time
statistics.setStopTime(Instant.ofEpochMilli(1000L + totalTime));
statistics.setTotalTime(Duration.ofMillis(totalTime));
statistics.setMovingTime(Duration.ofMillis(totalTime));
statistics.setStopTime(statistics.getStartTime().plus(totalTime));
statistics.setTotalTime(totalTime);
statistics.setMovingTime(totalTime);
statistics.setTotalDistance(totalDistance);
statistics.setTotalElevationGain(50.0f);
statistics.setMaxSpeed(50.0); // Resulting max speed
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0); // Resulting min elevation
statistics.setTotalAltitudeGain(50.0f);
statistics.setMaxSpeed(Speed.of(50.0)); // Resulting max speed
statistics.setMaxAltitude(1250.0);
statistics.setMinAltitude(1200.0); // Resulting min altitude
Track track = new Track();
track.setIcon(TrackIconUtils.getIconValue(context, category));
@@ -48,8 +50,8 @@ public class AggregatedStatisticsTest {
public void testAggregate() {
// given
// 10km in 40 minutes.
long totalDistance = 10000;
long totalTime = 2400000;
Distance totalDistance = Distance.of(10000);
Duration totalTime = Duration.ofMillis(2400000);
String biking = context.getString(R.string.activity_type_biking);
Track track = createTrack(context, totalDistance, totalTime, biking);
@@ -62,16 +64,16 @@ public class AggregatedStatisticsTest {
assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
@Test
public void testAggregate_mountainBiking() {
// given
// 10km in 40 minutes.
long totalDistance = 10000;
long totalTime = 2400000;
Distance totalDistance = Distance.of(10000);
Duration totalTime = Duration.ofMillis(2400000);
String mountainBiking = context.getString(R.string.activity_type_mountain_biking);
Track track = createTrack(context, totalDistance, totalTime, mountainBiking);
@@ -86,8 +88,8 @@ public class AggregatedStatisticsTest {
public void testAggregate_trailRunning() {
// given
// 10km in 40 minutes.
long totalDistance = 10000;
long totalTime = 2400000;
Distance totalDistance = Distance.of(10000);
Duration totalTime = Duration.ofMillis(2400000);
String trailRunning = context.getString(R.string.activity_type_trail_running);
Track track = createTrack(context, totalDistance, totalTime, trailRunning);
@@ -102,8 +104,8 @@ public class AggregatedStatisticsTest {
public void testAggregate_twoBikingTracks() {
// given
// 10km in 40 minutes.
long totalDistance = 10000;
long totalTime = 2400000;
Distance totalDistance = Distance.of(10000);
Duration totalTime = Duration.ofMillis(2400000);
String biking = context.getString(R.string.activity_type_biking);
List<Track> tracks = List.of(createTrack(context, totalDistance, totalTime, biking), createTrack(context, totalDistance, totalTime, biking));
@@ -116,20 +118,20 @@ public class AggregatedStatisticsTest {
assertEquals(2, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
}
@Test
public void testAggregate_threeDifferentTracks() {
// given
// 10km in 40 minutes.
long totalDistance = 10000;
Distance totalDistance = Distance.of(10000);
String biking = context.getString(R.string.activity_type_biking);
String running = context.getString(R.string.activity_type_running);
String walking = context.getString(R.string.activity_type_walking);
long totalTime = 2400000;
Duration totalTime = Duration.ofMillis(2400000);
List<Track> tracks = List.of(
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, running),
@@ -150,20 +152,20 @@ public class AggregatedStatisticsTest {
{
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
{
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
{
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
}
@@ -171,8 +173,8 @@ public class AggregatedStatisticsTest {
public void testAggregate_severalTracksWithSeveralActivities() {
// given
// 10km in 40 minutes.
long totalDistance = 10000;
long totalTime = 2400000;
Distance totalDistance = Distance.of(10000);
Duration totalTime = Duration.ofMillis(2400000);
String biking = context.getString(R.string.activity_type_biking);
String running = context.getString(R.string.activity_type_running);
String walking = context.getString(R.string.activity_type_walking);
@@ -213,29 +215,29 @@ public class AggregatedStatisticsTest {
// Biking.
{
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime * 5), statistics2.getMovingTime());
assertEquals(totalDistance.multipliedBy(5), statistics2.getTotalDistance());
assertEquals(totalTime.multipliedBy(5), statistics2.getMovingTime());
}
// Running.
{
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
}
// Walking.
{
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
}
// Driving.
{
TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
// Check order
@@ -6,6 +6,7 @@ import org.junit.runners.JUnit4;
import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -18,121 +19,122 @@ import static org.junit.Assert.assertEquals;
@RunWith(JUnit4.class)
public class IntervalStatisticsTest {
private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) {
return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second;
}
private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) {
return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second;
}
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
for (TrackPoint tp : trackPoints) {
trackStatisticsUpdater.addTrackPoint(tp, 0);
trackStatisticsUpdater.addTrackPoint(tp, Distance.of(0));
}
return trackStatisticsUpdater.getTrackStatistics();
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_1() {
// With 50 points and interval distance of 1000m.
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_1() {
// With 50 points and interval distance of 1000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(50);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// given
List<TrackPoint> trackPoints = buildTrackPoints(50);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_2() {
// With 200 points and interval distance of 1000m.
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_2() {
// With 200 points and interval distance of 1000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// given
List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_3() {
// With 200 points and interval distance of 3000m.
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_3() {
// With 200 points and interval distance of 3000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000f;
// given
List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_4() {
// With 1000 points and interval distance of 3000m.
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_4() {
// With 1000 points and interval distance of 3000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(1000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000f;
// given
List<TrackPoint> trackPoints = buildTrackPoints(1000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_5() {
// With 10000 points and interval distance of 1000m.
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_5() {
// With 10000 points and interval distance of 1000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(10000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// given
List<TrackPoint> trackPoints = buildTrackPoints(10000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
double totalDistance = 0d;
float totalTime = 0L;
float totalGain = 0f;
for (IntervalStatistics.Interval i : intervalList) {
totalDistance += i.getDistance_m();
totalTime += i.getDistance_m() / i.getSpeed_ms();
totalGain += i.getGain_m();
}
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(distanceInterval));
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
Distance totalDistance = Distance.of(0);
float totalTime = 0L;
float totalGain = 0f;
for (IntervalStatistics.Interval i : intervalList) {
totalDistance = totalDistance.plus(i.getDistance());
totalTime += i.getDistance().toM() / i.getSpeed().toMPS();
totalGain += i.getGain_m();
}
// then
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01);
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ELEVATION_GAIN, 0.1);
for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001);
totalDistance -= intervalList.get(i).getDistance_m();
}
assertEquals(intervalList.get(intervalList.size() - 1).getDistance_m(), totalDistance, 0.01);
}
// then
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ALTITUDE_GAIN, 0.1);
for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).getDistance().toM(), distanceInterval, 0.001);
totalDistance = totalDistance.minus(intervalList.get(i).getDistance());
}
assertEquals(intervalList.get(intervalList.size() - 1).getDistance().toM(), totalDistance.toM(), 0.01);
}
}
+6
View File
@@ -351,4 +351,10 @@ limitations under the License.
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
</manifest>
@@ -26,7 +26,6 @@ import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* An abstract class for the following common tasks across
@@ -44,6 +43,8 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3;
protected Track.Id recordingTrackId;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) {
@@ -66,7 +67,6 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
public void onConfirmDeleteDone(Track.Id... trackIds) {
boolean stopRecording = false;
Track.Id recordingTrackId = PreferencesUtils.getRecordingTrackId(PreferencesUtils.getSharedPreferences(this), this);
for (Track.Id trackId : trackIds) {
if (trackId.equals(recordingTrackId)) {
stopRecording = true;
@@ -75,7 +75,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
}
if (stopRecording) {
getTrackRecordingServiceConnection().stopRecording(this, false);
getTrackRecordingServiceConnection().stopRecording(this);
}
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
intent.putParcelableArrayListExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
@@ -18,8 +18,6 @@ package de.dennisguse.opentracks;
import android.app.SearchManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor;
import android.os.Bundle;
import android.view.KeyEvent;
@@ -42,16 +40,18 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.MarkerListBinding;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Activity to show a list of markers in a track.
*
* @author Leif Hendrik Wilden
*/
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller {
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller, TrackRecordingServiceStatus.Listener {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -59,25 +59,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, MarkerListActivity.this);
}
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, MarkerListActivity.this);
}
if (key != null) {
runOnUiThread(MarkerListActivity.this::invalidateOptionsMenu);
}
}
};
private Track track;
private MarkerResourceCursorAdapter resourceCursorAdapter;
@@ -85,6 +68,18 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback();
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Runnable bindCallback = new Runnable() {
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(MarkerListActivity.this);
}
}
};
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
@@ -112,10 +107,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
track = trackId != null ? contentProviderUtils.getTrack(trackId) : null;
@@ -132,18 +125,20 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
viewBinding.markerList.setOnScrollListener(scrollVisibleViews);
viewBinding.markerList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onResume() {
super.onResume();
trackRecordingServiceConnection.bind(this);
this.invalidateOptionsMenu();
loadData(getIntent());
}
@@ -151,7 +146,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind(this);
}
@Override
@@ -161,7 +156,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
viewBinding = null;
resourceCursorAdapter = null;
contentProviderUtils = null;
sharedPreferences = null;
}
@Override
@@ -350,4 +344,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
resourceCursorAdapter.swapCursor(null);
}
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
recordingTrackPaused = isPaused;
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
}
}
@@ -24,10 +24,10 @@ public class Startup extends Application {
public void onCreate() {
super.onCreate();
//Include version information into stack traces.
// Include version information into stack traces.
Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE);
//Set default values of preferences on first start.
// Set default values of preferences on first start.
SharedPreferences sharedPreferences = PreferencesUtils.resetPreferences(this, false);
if (PreferencesUtils.getString(sharedPreferences, this, R.string.stats_units_key, "").equals("")) {
String statsUnits = getString(Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial : R.string.stats_units_metric);
@@ -36,13 +36,7 @@ public class Startup extends Application {
ActivityUtils.applyNightMode(sharedPreferences, this);
//TODO Workaround to reset recordingTrackId on app startup as the TrackRecordingService (likely) crashed.
if (PreferencesUtils.isRecording(sharedPreferences, this)) {
Log.e(TAG, "Reset recordingTrackId; likely the TrackRecordingService crashed.");
PreferencesUtils.setLong(sharedPreferences, this, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
}
//In debug builds: show thread and VM warnings.
// In debug builds: show thread and VM warnings.
if (BuildConfig.DEBUG) {
Log.d(TAG, "Enabling strict mode");
StrictMode.enableDefaults();
@@ -27,11 +27,10 @@ import android.view.accessibility.AccessibilityEvent;
import java.time.Duration;
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Track controller for record, pause, resume, and stop.
@@ -42,6 +41,8 @@ public class TrackController implements View.OnTouchListener {
private static final String TAG = TrackController.class.getSimpleName();
private static final Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
private final Activity activity;
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Handler handlerUpdateTotalTime = new Handler();
@@ -66,7 +67,7 @@ public class TrackController implements View.OnTouchListener {
if (isResumed && isRecording && !isRecordingPaused) {
updateTotalTime();
setTotalTime();
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
handlerUpdateTotalTime.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
}
}
};
@@ -129,7 +130,7 @@ public class TrackController implements View.OnTouchListener {
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
callback.recordStart(); //TODO Should be recordPause();
callback.recordPause();
transition.resetTransition();
@@ -180,7 +181,7 @@ public class TrackController implements View.OnTouchListener {
}
if (isRecording && !isRecordingPaused) {
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UI_UPDATE_INTERVAL.toMillis());
}
viewBinding.trackControllerRecord.setImageResource(isRecording && !isRecordingPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
@@ -221,7 +222,7 @@ public class TrackController implements View.OnTouchListener {
}
private void updateTotalTime() {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
totalTime = trackRecordingService.getTotalTime();
}
@@ -239,7 +240,7 @@ public class TrackController implements View.OnTouchListener {
public interface Callback {
void recordStart();
void recordPause();
void recordStop();
}
}
@@ -48,12 +48,14 @@ import androidx.loader.content.Loader;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.ActivityUtils;
@@ -69,7 +71,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback {
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback, TrackRecordingServiceStatus.Listener {
private static final String TAG = TrackListActivity.class.getSimpleName();
@@ -86,7 +88,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Preferences
private boolean metricUnits = true;
private Track.Id recordingTrackId = null;
private boolean recordingTrackPaused = true;
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@@ -105,29 +108,17 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
};
private boolean recordingTrackPaused;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, TrackListActivity.this);
}
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, TrackListActivity.this);
if (key != null && PreferencesUtils.isRecording(recordingTrackId)) {
trackRecordingServiceConnection.startAndBind(TrackListActivity.this);
}
}
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, TrackListActivity.this);
}
if (key != null) {
runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu();
loaderCallbacks.restart();
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
trackController.update(isRecording, recordingTrackPaused);
trackController.update(isRecording(), recordingTrackPaused);
});
}
}
@@ -141,12 +132,10 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
// After binding changes (e.g., becomes available), update the total time in trackController.
runOnUiThread(() -> trackController.update(isRecording, recordingTrackPaused));
runOnUiThread(() -> trackController.update(isRecording(), recordingTrackPaused));
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "service not available to start gps or a new recording");
gpsStatusValue = GpsStatusValue.GPS_NONE;
@@ -155,19 +144,16 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Get GPS status and listen GPS status changes.
gpsStatusValue = service.getGpsStatus();
updateGpsMenuItem(true, isRecording);
service.addListener(newStatus -> {
gpsStatusValue = newStatus;
updateGpsMenuItem(true, isRecording);
});
updateGpsMenuItem(true, isRecording());
service.addListener(TrackListActivity.this);
if (isGpsStarted()) {
return;
}
service.startGps();
service.tryStartGps();
gpsStatusValue = GpsStatusValue.GPS_ENABLED;
updateGpsMenuItem(true, isRecording);
updateGpsMenuItem(true, isRecording());
}
};
@@ -181,8 +167,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
gpsStatusValue = GpsStatusValue.GPS_NONE;
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
@@ -197,7 +181,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
if (trackId == recordingTrackId.getId()) {
if (isRecording() && trackId == recordingTrackId.getId()) {
// Is recording -> open record activity.
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId));
@@ -232,7 +216,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, Distance.of(cursor.getDouble(totalDistanceIndex)), metricUnits);
int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex);
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
@@ -281,7 +265,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Update UI
this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
trackController.onResume(isRecording(), recordingTrackPaused);
}
@Override
@@ -322,9 +306,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean isGpsStarted = isGpsStarted();
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
updateMenuItems(isGpsStarted, isRecording);
updateMenuItems(isGpsStarted(), isRecording());
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery("", false);
@@ -344,9 +326,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
} else {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopGps();
trackRecordingService.stopGpsAndShutdown();
}
trackRecordingServiceConnection.unbindAndStop(this);
}
@@ -572,7 +554,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public void recordStart() {
if (!PreferencesUtils.isRecording(recordingTrackId)) {
if (recordingTrackId == null) {
// Not recording -> Recording
updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
@@ -582,17 +564,45 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
@Override
public void recordPause() {
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
@Override
public void recordStop() {
updateMenuItems(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
}
private boolean isRecording() {
return recordingTrackId != null;
}
@Override
public void onGpsStatus(GpsStatusValue newStatus) {
gpsStatusValue = newStatus;
updateGpsMenuItem(true, isRecording());
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
if (recordingTrackPaused != isPaused) {
trackController.update(isRecording(), isPaused);
}
recordingTrackPaused = isPaused;
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
if (!isRecording() && trackId != null) {
trackController.update(true, recordingTrackPaused);
}
recordingTrackId = trackId;
}
}
@@ -40,11 +40,12 @@ import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* An activity to show the track detail, record a new track or resumes an existing one.
@@ -52,7 +53,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface, TrackRecordingServiceStatus.Listener {
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
@@ -71,6 +72,18 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
private Track.Id trackId;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Runnable bindCallback = new Runnable() {
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(TrackRecordedActivity.this);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -88,6 +101,8 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
postponeEnterTransition();
}
@@ -107,11 +122,14 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
if (trackId != null) {
trackDataHub.loadTrack(trackId);
}
trackRecordingServiceConnection.bind(this);
}
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind(this);
trackDataHub.stop();
}
@@ -143,7 +161,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.findItem(R.id.track_detail_resume_track).setVisible(!PreferencesUtils.isRecording(PreferencesUtils.getSharedPreferences(this), this));
menu.findItem(R.id.track_detail_resume_track).setVisible(recordingTrackId == null);
Track track = contentProviderUtils.getTrack(trackId);
setTitle(track != null ? track.getName() : "");
return super.onPrepareOptionsMenu(menu);
@@ -250,7 +268,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
case 0:
return StatisticsRecordedFragment.newInstance(trackId);
case 1:
return IntervalsFragment.newInstance();
return IntervalsFragment.newInstance(true);
case 2:
return ChartFragment.newInstance(false);
case 3:
@@ -285,4 +303,9 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
ViewCompat.setTransitionName(viewIcon, TrackRecordedActivity.VIEW_TRACK_ICON);
startPostponedEnterTransition();
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
}
}
@@ -26,8 +26,9 @@ import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -43,7 +44,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
*/
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
//This mode of communication should be removed anyhow.
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback, TrackRecordingServiceStatus.Listener {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -62,8 +63,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
private Track.Id trackId;
// Preferences
private boolean recordingTrackPaused;
private final Runnable bindChangedCallback = new Runnable() {
@@ -72,13 +71,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// After binding changes (is available), update the total time in trackController.
runOnUiThread(() -> trackController.update(true, recordingTrackPaused));
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "could not get TrackRecordingService");
return;
}
if (!service.isRecording()) {
// Starts or resumes a track.
if (trackId == null) {
// trackId isn't initialized -> leads a new recording.
trackId = service.startNewTrack();
@@ -92,31 +90,23 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
trackController.update(true, false);
trackController.onResume(true, recordingTrackPaused);
}
service.addListener(TrackRecordingActivity.this);
}
};
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, TrackRecordingActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
}
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) {
setLockscreenPolicy();
}
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_keep_screen_on_while_recording_key, key)) {
setScreenOnPolicy();
}
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_fullscreen_while_recording_key, key)) {
setFullscreenPolicy();
}
if (key == null) return;
runOnUiThread(() -> {
TrackRecordingActivity.this.invalidateOptionsMenu();
trackController.update(true, recordingTrackPaused);
@@ -145,8 +135,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
}
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackDataHub = new TrackDataHub(this);
@@ -345,22 +333,21 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public void recordStart() {
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
}
@Override
public void recordPause() {
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
@Override
public void recordStop() {
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this, true);
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
@@ -382,7 +369,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
case 0:
return StatisticsRecordingFragment.newInstance();
case 1:
return IntervalsFragment.IntervalsRecordingFragment.newInstance();
return IntervalsFragment.newInstance(false);
case 2:
return ChartFragment.newInstance(false);
case 3:
@@ -412,4 +399,22 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
}
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
if (recordingTrackPaused != isPaused) {
trackController.update(true, isPaused);
}
recordingTrackPaused = isPaused;
setLockscreenPolicy();
setScreenOnPolicy();
}
@Override
public void onTrackRecordingId(Track.Id newTrackId) {
if (newTrackId != null && !newTrackId.equals(trackId)) {
trackId = newTrackId;
trackController.update(true, recordingTrackPaused);
}
}
}
@@ -38,7 +38,7 @@ class ExecutorListViewService<T> {
}
new Thread(() -> {
Future future = executorService.submit(runnable);
Future<?> future = executorService.submit(runnable);
try {
future.get();
} catch (ExecutionException | InterruptedException e) {
@@ -4,73 +4,87 @@ import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> {
public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<IntervalStatistics.Interval> intervalList;
private final Context context;
private final StackMode stackMode;
private final boolean metricUnits;
private final String category;
private boolean metricUnits;
private boolean isReportSpeed;
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, String category, StackMode stackMode) {
super(context, R.layout.interval_stats_list_item, intervalList);
metricUnits = PreferencesUtils.isMetricUnits(PreferencesUtils.getSharedPreferences(context), context);
this.category = category;
public IntervalStatisticsAdapter(Context context, StackMode stackMode, boolean metricUnits, boolean isReportSpeed) {
this.metricUnits = metricUnits;
this.context = context;
this.stackMode = stackMode;
this.isReportSpeed = isReportSpeed;
}
//TODO Check preference handling! Should not be accessed in getView()
@NonNull
@Override
public View getView(int position, @Nullable View intervalView, @NonNull ViewGroup parent) {
int actualPosition = stackMode == StackMode.STACK_FROM_TOP ? position : getCount() - 1 - position;
IntervalStatistics.Interval interval = getItem(actualPosition);
ViewHolder viewHolder;
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
return new IntervalStatisticsAdapter.ViewHolder(view);
}
if (intervalView == null) {
viewHolder = new ViewHolder();
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
int actualPosition = stackMode == StackMode.STACK_FROM_TOP ? position : getItemCount() - 1 - position;
int nextPosition = actualPosition + 1;
boolean isLast = actualPosition == getItemCount() - 1;
IntervalStatisticsAdapter.ViewHolder viewHolder = (IntervalStatisticsAdapter.ViewHolder) holder;
IntervalStatistics.Interval interval = intervalList.get(actualPosition);
viewHolder.itemView.setTag(actualPosition);
intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance);
viewHolder.rate = intervalView.findViewById(R.id.interval_item_rate);
viewHolder.gain = intervalView.findViewById(R.id.interval_item_gain);
viewHolder.loss = intervalView.findViewById(R.id.interval_item_loss);
intervalView.setTag(viewHolder);
Distance sumDistance;
if (isLast && actualPosition > 0) {
sumDistance = intervalList.get(actualPosition - 1).getDistance()
.multipliedBy(actualPosition)
.plus(interval.getDistance());
} else {
viewHolder = (ViewHolder) intervalView.getTag();
sumDistance = interval.getDistance().multipliedBy(nextPosition);
}
viewHolder.distance.setText(StringUtils.formatDistance(context, sumDistance, metricUnits));
viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed(), metricUnits, isReportSpeed));
viewHolder.gain.setText(StringUtils.formatDistance(context, Distance.of(interval.getGain_m()), metricUnits));
viewHolder.loss.setText(StringUtils.formatDistance(context, Distance.of(interval.getLoss_m()), metricUnits));
}
@Override
public int getItemCount() {
if (intervalList == null) {
return 0;
}
return intervalList.size();
}
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) {
if (intervalList == data && this.metricUnits == metricUnits && this.isReportSpeed == isReportSpeed) {
return null;
}
float sumDistance_m;
if (actualPosition + 1 == getCount() && actualPosition > 0) {
sumDistance_m = actualPosition * getItem(actualPosition - 1).getDistance_m() + interval.getDistance_m();
} else {
sumDistance_m = (actualPosition + 1) * interval.getDistance_m();
}
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits));
this.metricUnits = metricUnits;
this.isReportSpeed = isReportSpeed;
intervalList = data;
if (PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category)) {
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, true));
} else {
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, false));
if (data != null) {
this.notifyDataSetChanged();
}
viewHolder.gain.setText(StringUtils.formatDistance(getContext(), interval.getGain_m(), metricUnits));
viewHolder.loss.setText(StringUtils.formatDistance(getContext(), interval.getLoss_m(), metricUnits));
return intervalView;
return data;
}
/**
@@ -81,10 +95,18 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
STACK_FROM_TOP
}
private static class ViewHolder {
private TextView distance;
private TextView rate;
private TextView gain;
private TextView loss;
private static class ViewHolder extends RecyclerView.ViewHolder {
TextView distance;
TextView rate;
TextView gain;
TextView loss;
public ViewHolder(@NonNull View itemView) {
super(itemView);
distance = itemView.findViewById(R.id.interval_item_distance);
rate = itemView.findViewById(R.id.interval_item_rate);
gain = itemView.findViewById(R.id.interval_item_gain);
loss = itemView.findViewById(R.id.interval_item_loss);
}
}
}
@@ -34,7 +34,7 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
private final Activity activity;
//TODO Should be Marker.Id
private final ExecutorListViewService<Long> executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
private final ExecutorListViewService<Long> executorService = new ExecutorListViewService<>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
private boolean scroll = false;
@@ -13,50 +13,43 @@ public class ChartPoint {
private double timeOrDistance;
//Y-axis
private double elevation;
private double speed;
private double pace;
private double heartRate = Double.NaN;
private double cadence = Double.NaN;
private double power = Double.NaN;
private double altitude;
private Double speed;
private Double pace;
private Double heartRate;
private Double cadence;
private Double power;
@VisibleForTesting
ChartPoint(double elevation) {
this.elevation = elevation;
ChartPoint(double altitude) {
this.altitude = altitude;
}
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, TrackPoint trackPoint, boolean chartByDistance, boolean metricUnits) {
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
if (chartByDistance) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
timeOrDistance = trackStatistics.getTotalDistance().to(metricUnits);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
elevation = trackStatisticsUpdater.getSmoothedElevation();
altitude = trackStatisticsUpdater.getSmoothedAltitude();
if (!metricUnits) {
elevation *= UnitConversions.M_TO_FT;
altitude *= UnitConversions.M_TO_FT;
}
speed = trackStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MPS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
speed = trackStatisticsUpdater.getSmoothedSpeed().to(metricUnits);
pace = trackStatisticsUpdater.getSmoothedSpeed().toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
if (trackPoint != null) {
if (trackPoint.hasHeartRate()) {
heartRate = trackPoint.getHeartRate_bpm();
heartRate = (double) trackPoint.getHeartRate_bpm();
}
if (trackPoint.hasCyclingCadence()) {
cadence = trackPoint.getCyclingCadence_rpm();
cadence = (double) trackPoint.getCyclingCadence_rpm();
}
if (trackPoint.hasPower()) {
power = trackPoint.getPower();
power = (double) trackPoint.getPower();
}
}
}
@@ -65,39 +58,27 @@ public class ChartPoint {
return timeOrDistance;
}
public double getElevation() {
return elevation;
public Double getAltitude() {
return altitude;
}
public double getSpeed() {
public Double getSpeed() {
return speed;
}
public double getPace() {
public Double getPace() {
return pace;
}
public boolean hasHeartRate() {
return Double.isNaN(heartRate);
}
public double getHeartRate() {
public Double getHeartRate() {
return heartRate;
}
public boolean hasCadence() {
return Double.isNaN(cadence);
}
public double getCadence() {
public Double getCadence() {
return cadence;
}
public boolean hasPower() {
return Double.isNaN(power);
}
public double getPower() {
public Double getPower() {
return power;
}
}
@@ -132,10 +132,10 @@ abstract class ChartValueSeries {
}
}
abstract double extractDataFromChartPoint(@NonNull ChartPoint chartPoint);
abstract Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint);
boolean isChartPointValid(@NonNull ChartPoint chartPoint) {
return !Double.isNaN(extractDataFromChartPoint(chartPoint));
return extractDataFromChartPoint(chartPoint) != null;
}
protected abstract boolean drawIfChartPointHasNoData();
@@ -41,6 +41,7 @@ import java.text.NumberFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import de.dennisguse.opentracks.MarkerDetailActivity;
import de.dennisguse.opentracks.R;
@@ -50,7 +51,6 @@ import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.ThemeUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Visualization of the chart.
@@ -199,15 +199,15 @@ public class ChartView extends View {
Integer.MIN_VALUE,
Integer.MAX_VALUE,
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border,
R.string.description_altitude_metric,
R.string.description_altitude_imperial,
R.color.chart_altitude_fill,
R.color.chart_altitude_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getAltitude();
}
@Override
@@ -227,7 +227,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getSpeed();
}
@@ -249,7 +249,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPace();
}
@@ -271,7 +271,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getHeartRate();
}
@@ -292,7 +292,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getCadence();
}
@@ -312,7 +312,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPower();
}
@@ -893,35 +893,23 @@ public class ChartView extends View {
private void closePaths() {
for (ChartValueSeries chartValueSeries : seriesList) {
int first = getFirstPopulatedChartDataIndex(chartValueSeries);
Optional<ChartPoint> firstValid = chartPoints.stream().filter(chartValueSeries::isChartPointValid)
.findFirst();
Optional<ChartPoint> lastValid = chartPoints.stream().filter(chartValueSeries::isChartPointValid)
.reduce((first, second) -> second);
if (first != -1) {
int xCorner = getX(chartPoints.get(first).getTimeOrDistance());
int yCorner = topBorder + effectiveHeight;
if (firstValid.isPresent() && lastValid.isPresent()) {
Path path = chartValueSeries.getPath();
// Bottom right corner
path.lineTo(getX(chartPoints.get(chartPoints.size() - 1).getTimeOrDistance()), yCorner);
// Bottom left corner
path.lineTo(xCorner, yCorner);
// Top right corner
double value = chartValueSeries.extractDataFromChartPoint(chartPoints.get(first));
path.lineTo(xCorner, getY(chartValueSeries, value));
}
}
}
/**
* Finds the index of the first data point containing data for a series.
*
* @return -1 if no data point contains data for the series.
*/
private int getFirstPopulatedChartDataIndex(ChartValueSeries chartValueSeries) {
for (int i = 0; i < chartPoints.size(); i++) {
if (chartValueSeries.isChartPointValid(chartPoints.get(i))) {
return i;
int yCorner = topBorder + effectiveHeight;
path.lineTo(getX(lastValid.get().getTimeOrDistance()), yCorner);
path.lineTo(getX(firstValid.get().getTimeOrDistance()), yCorner);
double value = chartValueSeries.extractDataFromChartPoint(firstValid.get());
path.lineTo(getX(firstValid.get().getTimeOrDistance()), getY(chartValueSeries, value));
}
}
return -1;
}
/**
@@ -1005,10 +993,9 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) {
if (chartByDistance) {
double length_km = marker.getLength() * UnitConversions.M_TO_KM;
return metricUnits ? length_km : length_km * UnitConversions.KM_TO_MI;
return marker.getLength().to(metricUnits);
} else {
return marker.getDuration();
return marker.getDuration().toMillis();
}
}
@@ -24,6 +24,8 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -127,24 +129,24 @@ public class DescriptionGenerator {
// Fastest pace
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
// Max elevation
if (stats.hasElevationMax()) {
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
// Max altitude
if (stats.hasAltitudeMax()) {
writeAltitude(stats.getMaxAltitude(), builder, R.string.description_max_altitude, lineBreak);
}
// Min elevation
if (stats.hasElevationMin()) {
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Min altitude
if (stats.hasAltitudeMin()) {
writeAltitude(stats.getMinAltitude(), builder, R.string.description_min_altitude, lineBreak);
}
// Elevation gain
if (stats.hasTotalElevationGain()) {
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Altitude gain
if (stats.hasTotalAltitudeGain()) {
writeAltitude(stats.getTotalAltitudeGain(), builder, R.string.description_altitude_gain, lineBreak);
}
// Elevation loss
if (stats.hasTotalElevationLoss()) {
writeElevation(stats.getTotalElevationLoss(), builder, R.string.description_elevation_loss, lineBreak);
// Altitude loss
if (stats.hasTotalAltitudeLoss()) {
writeAltitude(stats.getTotalAltitudeLoss(), builder, R.string.description_altitude_loss, lineBreak);
}
// Recorded time
@@ -163,9 +165,9 @@ public class DescriptionGenerator {
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
void writeDistance(Distance distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance.toKM();
double distanceInMi = distance.toMI();
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
@@ -193,23 +195,18 @@ public class DescriptionGenerator {
* @param lineBreak line break string
*/
@VisibleForTesting
void writeSpeed(double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MPS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
void writeSpeed(Speed speed, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, speed.toKMH(), speed.toMPH()));
builder.append(lineBreak);
}
/**
* Writes pace.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
@@ -221,18 +218,16 @@ public class DescriptionGenerator {
}
/**
* Writes elevation.
*
* @param elevation elevation in meters
* @param builder StringBuilder to append elevation
* @param resId resource id of elevation string
* @param lineBreak line break string
* @param altitude_m altitude_m in meters
* @param builder StringBuilder to append
* @param resId resource id of altitude string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeElevation(double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt));
void writeAltitude(double altitude_m, StringBuilder builder, int resId, String lineBreak) {
long altitudeInM = Math.round(altitude_m);
long altitudeInFt = Math.round(altitude_m * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, altitudeInM, altitudeInFt));
builder.append(lineBreak);
}
}
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.content;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.Handler;
@@ -31,7 +30,6 @@ import androidx.annotation.VisibleForTesting;
import java.util.Collections;
import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Track;
@@ -40,7 +38,9 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
/**
* Track data hub.
@@ -50,7 +50,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*
* @author Rodrigo Damazio
*/
public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeListener {
public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
/**
* Target number of track points displayed by the diagrams (recommended).
@@ -71,12 +71,12 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
private final ContentProviderUtils contentProviderUtils;
private final int targetNumPoints;
private SharedPreferences sharedPreferences;
private boolean started;
private HandlerThread handlerThread;
private Handler handler;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
// Preference values
private Track.Id selectedTrackId;
private Track.Id recordingTrackId;
@@ -92,6 +92,13 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
private ContentObserver markersTableObserver;
private ContentObserver trackPointsTableObserver;
private final Runnable bindCallback = () -> {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(TrackDataHub.this);
}
};
public TrackDataHub(Context context) {
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
}
@@ -141,10 +148,8 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
};
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
trackRecordingServiceConnection.bind(context);
handler.post(() -> {
if (started) {
@@ -159,9 +164,6 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
return;
}
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
sharedPreferences = null;
started = false;
//Unregister listeners
@@ -175,6 +177,8 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
handlerThread = null;
}
handler = null;
trackRecordingServiceConnection.unbind(context);
}
public void loadTrack(final @NonNull Track.Id trackId) {
@@ -215,7 +219,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
* Returns true if the selected track is recording.
*/
public boolean isSelectedTrackRecording() {
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId) && PreferencesUtils.isRecording(recordingTrackId);
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId);
}
/**
@@ -225,18 +229,6 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId) && recordingTrackPaused;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
handler.post(() -> {
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, context);
}
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, context);
}
});
}
/**
* Loads data for all listeners. To be run in the {@link #handler} thread.
*/
@@ -432,4 +424,14 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
firstSeenTrackPointId = null;
lastSeenTrackPointId = null;
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
recordingTrackPaused = isPaused;
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
}
}
@@ -0,0 +1,140 @@
package de.dennisguse.opentracks.content.data;
import androidx.annotation.NonNull;
import java.util.Objects;
import de.dennisguse.opentracks.util.UnitConversions;
public class Distance {
public static Distance of(double distance_m) {
return new Distance(distance_m);
}
public static Distance of(String distance_m) {
return of(Float.parseFloat(distance_m));
}
public static Distance ofMile(double distance_mile) {
return of(distance_mile * UnitConversions.MI_TO_M);
}
public static Distance ofKilometer(double distance_km) {
return of(distance_km * UnitConversions.KM_TO_M);
}
public static Distance one(boolean metricUnit) {
if (metricUnit) {
return Distance.ofKilometer(1);
} else {
return Distance.ofMile(1);
}
}
public static Distance invalid() {
return of(Double.NaN);
}
private final double distance_m;
private Distance(double distance_m) {
this.distance_m = distance_m;
}
public Distance plus(@NonNull Distance distance) {
return new Distance(distance_m + distance.distance_m);
}
public Distance minus(@NonNull Distance distance) {
return new Distance(distance_m - distance.distance_m);
}
public Distance multipliedBy(double factor) {
return new Distance(factor * distance_m);
}
public Distance dividedBy(double divisor) {
return multipliedBy(1 / divisor);
}
public double dividedBy(@NonNull Distance divisor) {
return distance_m / divisor.distance_m;
}
public boolean isZero() {
return distance_m == 0;
}
public boolean isInvalid() {
return Double.isNaN(distance_m) || Double.isInfinite(distance_m);
}
public boolean lessThan(@NonNull Distance distance) {
return !greaterThan(distance);
}
public boolean greaterThan(@NonNull Distance distance) {
return distance_m > distance.distance_m;
}
public boolean greaterOrEqualThan(@NonNull Distance distance) {
return distance_m >= distance.distance_m;
}
public double toM() {
return distance_m;
}
public double toKM() {
return distance_m * UnitConversions.M_TO_KM;
}
public double toFT() {
return distance_m * UnitConversions.M_TO_FT;
}
public double toMI() {
return toKM() * UnitConversions.KM_TO_MI;
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KM : Unit.MILES);
}
public double to(Unit unit) {
switch (unit) {
case KM:
return toKM();
case MILES:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KM,
MILES
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Distance distance = (Distance) o;
return Double.compare(distance.distance_m, distance_m) == 0;
}
@Override
public int hashCode() {
return Objects.hash(distance_m);
}
@Override
public String toString() {
return "Distance{" +
"distance_m=" + distance_m +
'}';
}
}
@@ -24,15 +24,19 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
//TODO All data should be final; no default values.
public final class Marker {
private Id id;
@@ -50,8 +54,8 @@ public final class Marker {
private Float bearing;
//TODO It is the distance from the track starting point; rename to something more meaningful
private double length = 0.0;
private long duration = 0; //TODO Duration
private Distance length;
private Duration duration;
@Deprecated //TODO Make an URI instead of String
private String photoUrl = "";
@@ -61,6 +65,7 @@ public final class Marker {
this.time = time;
}
@Deprecated //TODO Marker cannot be created without length AND duration!
public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) {
this.trackId = trackId;
@@ -74,16 +79,19 @@ public final class Marker {
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude();
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
this.length = Distance.of(0); //TODO Not cool!
this.duration = Duration.ofMillis(0); //TODO Not cool!
}
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull TrackPoint trackPoint, String photoUrl) {
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
this(trackId, trackPoint);
this.name = name;
this.description = description;
this.category = category;
this.icon = icon;
this.length = length;
this.duration = duration;
this.length = statistics.getTotalDistance();
this.duration = statistics.getTotalTime();
this.photoUrl = photoUrl;
}
@@ -217,19 +225,19 @@ public final class Marker {
this.bearing = bearing;
}
public double getLength() {
public Distance getLength() {
return length;
}
public void setLength(double length) {
public void setLength(Distance length) {
this.length = length;
}
public long getDuration() {
public Duration getDuration() {
return duration;
}
public void setDuration(long duration) {
public void setDuration(@NonNull Duration duration) {
this.duration = duration;
}
@@ -48,7 +48,7 @@ public interface MarkerColumns extends BaseColumns {
String LONGITUDE = "longitude"; // longitude
String LATITUDE = "latitude"; // latitude
String TIME = "time"; // time
String ALTITUDE = "elevation"; // altitude
String ALTITUDE = "elevation"; // altitude //TODO RENAME column
String ACCURACY = "accuracy"; // accuracy
String BEARING = "bearing"; // bearing
@@ -0,0 +1,145 @@
package de.dennisguse.opentracks.content.data;
import java.time.Duration;
import java.util.Objects;
import de.dennisguse.opentracks.util.UnitConversions;
public class Speed {
public static Speed of(Distance distance, Duration duration) {
if (duration.isZero()) {
return zero();
}
return new Speed(distance.toM() / (duration.toMillis() * UnitConversions.MS_TO_S));
}
public static Speed of(double speed_mps) {
return new Speed(speed_mps);
}
public static Speed of(String speed_mps) {
return of(Float.parseFloat(speed_mps));
}
public static Speed invalid() {
return of(Double.NaN);
}
public static Speed zero() {
return of(0.0);
}
public static Speed max(Speed speed1, Speed speed2) {
if (speed1.greaterThan(speed2)) {
return speed1;
}
return speed2;
}
public static Speed absDiff(Speed speed1, Speed speed2) {
//TODO Why Math.abs? Seems to be a leftover.
return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps));
}
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private final double speed_mps;
private Speed(double speed_mps) {
this.speed_mps = speed_mps;
}
public Speed mul(double factor) {
return new Speed(factor * speed_mps);
}
public boolean isZero() {
return speed_mps == 0;
}
public boolean isInvalid() {
return Double.isNaN(speed_mps) || Double.isInfinite(speed_mps);
}
public boolean isMoving() {
return !isInvalid() && speed_mps >= MAX_NO_MOVEMENT_SPEED;
}
public boolean lessThan(Speed speed) {
return !greaterThan(speed);
}
public boolean greaterThan(Speed speed) {
return speed_mps > speed.speed_mps;
}
public boolean greaterOrEqualThan(Speed speed) {
return speed_mps >= speed.speed_mps;
}
public double toMPS() {
return speed_mps;
}
public double toKMH() {
return speed_mps * UnitConversions.MPS_TO_KMH;
}
public double toMPH() {
return toKMH() * UnitConversions.KM_TO_MI;
}
public Duration toPace(boolean metricUnit) {
if (isZero()) {
return Duration.ofSeconds(0);
}
double distance = speed_mps * (metricUnit ? UnitConversions.M_TO_KM : UnitConversions.M_TO_MI);
return Duration.ofSeconds(Math.round(1 / distance));
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KMH : Unit.MPH);
}
public double to(Unit unit) {
switch (unit) {
case KMH:
return toKMH();
case MPH:
return toMPH();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KMH,
MPH,
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Speed speed = (Speed) o;
return Double.compare(speed.speed_mps, speed_mps) == 0;
}
@Override
public int hashCode() {
return Objects.hash(speed_mps);
}
@Override
public String toString() {
return "Speed{" +
"speed_mps=" + speed_mps +
'}';
}
}
@@ -101,11 +101,12 @@ public class Track {
this.icon = icon;
}
@NonNull
public TrackStatistics getTrackStatistics() {
return trackStatistics;
}
public void setTrackStatistics(TrackStatistics trackStatistics) {
public void setTrackStatistics(@NonNull TrackStatistics trackStatistics) {
this.trackStatistics = trackStatistics;
}
@@ -45,9 +45,6 @@ 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;
@@ -55,9 +52,9 @@ public class TrackPoint {
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Float speed_mps;
private Speed speed;
private Float bearing;
private Float sensorDistance_m;
private Distance sensorDistance_m;
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
@@ -93,8 +90,8 @@ public class TrackPoint {
private Float heartRate_bpm = null;
private Float cyclingCadence_rpm = null;
private Float power = null;
private Float elevationGain = null;
private Float elevationLoss = null;
private Float altitudeGain_m = null;
private Float altitudeLoss_m = null;
public TrackPoint(@NonNull Type type) {
this.type = type;
@@ -106,7 +103,7 @@ public class TrackPoint {
this.latitude = location.getLatitude();
this.longitude = location.getLongitude();
this.altitude_m = location.getAltitude();
this.speed_mps = location.getSpeed();
this.speed = Speed.of(location.getSpeed());
this.accuracy = location.getAccuracy();
setTime(Instant.now());
@@ -221,28 +218,28 @@ public class TrackPoint {
return location;
}
public boolean hasElevationGain() {
return elevationGain != null;
public boolean hasAltitudeGain() {
return altitudeGain_m != null;
}
public float getElevationGain() {
return elevationGain;
public float getAltitudeGain() {
return altitudeGain_m;
}
public void setElevationGain(Float elevationGain) {
this.elevationGain = elevationGain;
public void setAltitudeGain(Float altitudeGain_m) {
this.altitudeGain_m = altitudeGain_m;
}
public boolean hasElevationLoss() {
return elevationLoss != null;
public boolean hasAltitudeLoss() {
return altitudeLoss_m != null;
}
public float getElevationLoss() {
return elevationLoss;
public float getAltitudeLoss() {
return altitudeLoss_m;
}
public void setElevationLoss(Float elevationLoss) {
this.elevationLoss = elevationLoss;
public void setAltitudeLoss(Float altitudeLoss_m) {
this.altitudeLoss_m = altitudeLoss_m;
}
public Instant getTime() {
@@ -272,19 +269,19 @@ public class TrackPoint {
}
public boolean hasSpeed() {
return speed_mps != null;
return speed != null;
}
public float getSpeed() {
return speed_mps;
public Speed getSpeed() {
return speed;
}
public void setSpeed(Float speed) {
this.speed_mps = speed;
public void setSpeed(Speed speed) {
this.speed = speed;
}
public boolean isMoving() {
return hasSpeed() && getSpeed() >= MAX_NO_MOVEMENT_SPEED;
return hasSpeed() && getSpeed().isMoving();
}
public boolean hasBearing() {
@@ -311,12 +308,12 @@ public class TrackPoint {
this.accuracy = horizontalAccuracy;
}
public float distanceToPrevious(@NonNull TrackPoint previous) {
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
return getLocation().distanceTo(previous.getLocation());
return Distance.of(getLocation().distanceTo(previous.getLocation()));
}
public boolean fulfillsAccuracy(int poorAccuracy) {
@@ -336,11 +333,11 @@ public class TrackPoint {
return sensorDistance_m != null;
}
public Float getSensorDistance() {
public Distance getSensorDistance() {
return sensorDistance_m;
}
public void setSensorDistance(Float distance_m) {
public void setSensorDistance(Distance distance_m) {
this.sensorDistance_m = distance_m;
}
@@ -52,8 +52,8 @@ public interface TrackPointsColumns extends BaseColumns {
String SENSOR_CADENCE = "sensor_cadence";
String SENSOR_DISTANCE = "sensor_distance"; //DISTANCE from previous TrackPoint
String SENSOR_POWER = "sensor_power";
String ELEVATION_GAIN = "elevation_gain";
String ELEVATION_LOSS = "elevation_loss";
String ALTITUDE_GAIN = "elevation_gain";
String ALTITUDE_LOSS = "elevation_loss";
// Alias for sensor statistics
String ALIAS_AVG_HR = "avg_hr";
@@ -75,8 +75,8 @@ public interface TrackPointsColumns extends BaseColumns {
+ SENSOR_HEARTRATE + " FLOAT, "
+ SENSOR_CADENCE + " FLOAT, "
+ SENSOR_POWER + " FLOAT, "
+ ELEVATION_GAIN + " FLOAT, "
+ ELEVATION_LOSS + " FLOAT, "
+ ALTITUDE_GAIN + " FLOAT, "
+ ALTITUDE_LOSS + " FLOAT, "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1)), "
+ SENSOR_DISTANCE + " FLOAT, "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
@@ -52,10 +52,10 @@ public interface TracksColumns extends BaseColumns {
String AVGSPEED = "avgspeed"; // average speed
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed
String MAXSPEED = "maxspeed"; // maximum speed
String MINELEVATION = "minelevation"; // minimum elevation
String MAXELEVATION = "maxelevation"; // maximum elevation
String ELEVATIONGAIN = "elevationgain"; // elevation gain
String ELEVATIONLOSS = "elevationloss"; // elevation loss
String MIN_ALTITUDE = "minelevation"; // minimum altitude //TODO RENAME column
String MAX_ALTITUDE = "maxelevation"; // maximum altitude //TODO RENAME column
String ALTITUDE_GAIN = "elevationgain"; // altitude gain //TODO RENAME column
String ALTITUDE_LOSS = "elevationloss"; // altitude loss //TODO RENAME column
String ICON = "icon"; // track activity type icon
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
@@ -72,12 +72,12 @@ public interface TracksColumns extends BaseColumns {
+ AVGSPEED + " FLOAT, "
+ AVGMOVINGSPEED + " FLOAT, "
+ MAXSPEED + " FLOAT, "
+ MINELEVATION + " FLOAT, "
+ MAXELEVATION + " FLOAT, "
+ ELEVATIONGAIN + " FLOAT, "
+ MIN_ALTITUDE + " FLOAT, "
+ MAX_ALTITUDE + " FLOAT, "
+ ALTITUDE_GAIN + " FLOAT, "
+ ICON + " TEXT, "
+ UUID + " BLOB, "
+ ELEVATIONLOSS + " FLOAT)";
+ ALTITUDE_LOSS + " FLOAT)";
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
@@ -21,8 +21,8 @@ class CachedTrackPointsIndexes {
final int sensorCadenceIndex;
final int sensorDistanceIndex;
final int sensorPowerIndex;
final int elevationGainIndex;
final int elevationLossIndex;
final int altitudeGainIndex;
final int altitudeLossIndex;
CachedTrackPointsIndexes(Cursor cursor) {
idIndex = cursor.getColumnIndex(TrackPointsColumns._ID);
@@ -38,7 +38,7 @@ class CachedTrackPointsIndexes {
sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
sensorDistanceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_DISTANCE);
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
elevationGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_GAIN);
elevationLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_LOSS);
altitudeGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_GAIN);
altitudeLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_LOSS);
}
}
@@ -39,8 +39,10 @@ import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
@@ -97,10 +99,10 @@ public class ContentProviderUtils {
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
int movingTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME);
int maxSpeedIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED);
int minElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION);
int maxElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION);
int elevationGainIndex = cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN);
int elevationLossIndex = cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONLOSS);
int minAltitudeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MIN_ALTITUDE);
int maxAltitudeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAX_ALTITUDE);
int altitudeGainIndex = cursor.getColumnIndexOrThrow(TracksColumns.ALTITUDE_GAIN);
int altitudeLossIndex = cursor.getColumnIndexOrThrow(TracksColumns.ALTITUDE_LOSS);
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
Track track = new Track();
@@ -127,7 +129,7 @@ public class ContentProviderUtils {
trackStatistics.setStopTime(Instant.ofEpochMilli(cursor.getLong(stopTimeIndex)));
}
if (!cursor.isNull(totalDistanceIndex)) {
trackStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
trackStatistics.setTotalDistance(Distance.of(cursor.getFloat(totalDistanceIndex)));
}
if (!cursor.isNull(totalTimeIndex)) {
trackStatistics.setTotalTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
@@ -136,19 +138,19 @@ public class ContentProviderUtils {
trackStatistics.setMovingTime(Duration.ofMillis(cursor.getLong(movingTimeIndex)));
}
if (!cursor.isNull(maxSpeedIndex)) {
trackStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
trackStatistics.setMaxSpeed(Speed.of(cursor.getFloat(maxSpeedIndex)));
}
if (!cursor.isNull(minElevationIndex)) {
trackStatistics.setMinElevation(cursor.getFloat(minElevationIndex));
if (!cursor.isNull(minAltitudeIndex)) {
trackStatistics.setMinAltitude(cursor.getFloat(minAltitudeIndex));
}
if (!cursor.isNull(maxElevationIndex)) {
trackStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex));
if (!cursor.isNull(maxAltitudeIndex)) {
trackStatistics.setMaxAltitude(cursor.getFloat(maxAltitudeIndex));
}
if (!cursor.isNull(elevationGainIndex)) {
trackStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
if (!cursor.isNull(altitudeGainIndex)) {
trackStatistics.setTotalAltitudeGain(cursor.getFloat(altitudeGainIndex));
}
if (!cursor.isNull(elevationLossIndex)) {
trackStatistics.setTotalElevationLoss(cursor.getFloat(elevationLossIndex));
if (!cursor.isNull(altitudeLossIndex)) {
trackStatistics.setTotalAltitudeLoss(cursor.getFloat(altitudeLossIndex));
}
if (!cursor.isNull(iconIndex)) {
track.setIcon(cursor.getString(iconIndex));
@@ -278,16 +280,16 @@ public class ContentProviderUtils {
if (trackStatistics.getStopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
}
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance());
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed());
values.put(TracksColumns.MINELEVATION, trackStatistics.getMinElevation());
values.put(TracksColumns.MAXELEVATION, trackStatistics.getMaxElevation());
values.put(TracksColumns.ELEVATIONGAIN, trackStatistics.getTotalElevationGain());
values.put(TracksColumns.ELEVATIONLOSS, trackStatistics.getTotalElevationLoss());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed().toMPS());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed().toMPS());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed().toMPS());
values.put(TracksColumns.MIN_ALTITUDE, trackStatistics.getMinAltitude());
values.put(TracksColumns.MAX_ALTITUDE, trackStatistics.getMaxAltitude());
values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.getTotalAltitudeGain());
values.put(TracksColumns.ALTITUDE_LOSS, trackStatistics.getTotalAltitudeLoss());
values.put(TracksColumns.ICON, track.getIcon());
return values;
@@ -343,10 +345,10 @@ public class ContentProviderUtils {
marker.setIcon(cursor.getString(iconIndex));
}
if (!cursor.isNull(lengthIndex)) {
marker.setLength(cursor.getFloat(lengthIndex));
marker.setLength(Distance.of(cursor.getFloat(lengthIndex)));
}
if (!cursor.isNull(durationIndex)) {
marker.setDuration(cursor.getLong(durationIndex));
marker.setDuration(Duration.ofMillis(cursor.getLong(durationIndex)));
}
if (!cursor.isNull(photoUrlIndex)) {
@@ -362,9 +364,9 @@ public class ContentProviderUtils {
}
/**
* @return -1 if not able to get the next marker number.
* @return null if not able to get the next marker number.
*/
public int getNextMarkerNumber(@NonNull Track.Id trackId) {
public Integer getNextMarkerNumber(@NonNull Track.Id trackId) {
String[] projection = {MarkerColumns._ID};
String selection = MarkerColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
@@ -373,7 +375,7 @@ public class ContentProviderUtils {
return cursor.getCount();
}
}
return -1;
return null;
}
public Marker getMarker(@NonNull Marker.Id markerId) {
@@ -480,8 +482,8 @@ public class ContentProviderUtils {
values.put(MarkerColumns.CATEGORY, marker.getCategory());
values.put(MarkerColumns.ICON, marker.getIcon());
values.put(MarkerColumns.TRACKID, marker.getTrackId().getId());
values.put(MarkerColumns.LENGTH, marker.getLength());
values.put(MarkerColumns.DURATION, marker.getDuration());
values.put(MarkerColumns.LENGTH, marker.getLength().toM());
values.put(MarkerColumns.DURATION, marker.getDuration().toMillis());
values.put(MarkerColumns.LONGITUDE, (int) (marker.getLongitude() * 1E6));
values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6));
@@ -543,7 +545,7 @@ public class ContentProviderUtils {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
}
if (!cursor.isNull(indexes.speedIndex)) {
trackPoint.setSpeed(cursor.getFloat(indexes.speedIndex));
trackPoint.setSpeed(Speed.of(cursor.getFloat(indexes.speedIndex)));
}
if (!cursor.isNull(indexes.bearingIndex)) {
trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex));
@@ -556,17 +558,17 @@ public class ContentProviderUtils {
trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex));
}
if (!cursor.isNull(indexes.sensorDistanceIndex)) {
trackPoint.setSensorDistance(cursor.getFloat(indexes.sensorDistanceIndex));
trackPoint.setSensorDistance(Distance.of(cursor.getFloat(indexes.sensorDistanceIndex)));
}
if (!cursor.isNull(indexes.sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex));
}
if (!cursor.isNull(indexes.elevationGainIndex)) {
trackPoint.setElevationGain(cursor.getFloat(indexes.elevationGainIndex));
if (!cursor.isNull(indexes.altitudeGainIndex)) {
trackPoint.setAltitudeGain(cursor.getFloat(indexes.altitudeGainIndex));
}
if (!cursor.isNull(indexes.elevationLossIndex)) {
trackPoint.setElevationLoss(cursor.getFloat(indexes.elevationLossIndex));
if (!cursor.isNull(indexes.altitudeLossIndex)) {
trackPoint.setAltitudeLoss(cursor.getFloat(indexes.altitudeLossIndex));
}
return trackPoint;
@@ -694,7 +696,7 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
}
if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed());
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed().toMPS());
}
if (trackPoint.hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
@@ -707,17 +709,17 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm());
}
if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance());
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM());
}
if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower());
}
if (trackPoint.hasElevationGain()) {
values.put(TrackPointsColumns.ELEVATION_GAIN, trackPoint.getElevationGain());
if (trackPoint.hasAltitudeGain()) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.getAltitudeGain());
}
if (trackPoint.hasElevationLoss()) {
values.put(TrackPointsColumns.ELEVATION_LOSS, trackPoint.getElevationLoss());
if (trackPoint.hasAltitudeLoss()) {
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.getAltitudeLoss());
}
return values;
@@ -6,6 +6,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.util.UintUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -122,21 +126,21 @@ public final class SensorDataCycling {
public void compute(DistanceSpeed previous, int wheel_circumference_mm) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null;
} else {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as it some of those seem to count backwards
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
float distance_m = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
float distance_overall_m = distance_m;
Distance distance = Distance.of(wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
Distance distanceOverall = distance;
if (previous.hasValue()) {
distance_overall_m += previous.getValue().distance_overall_m;
distanceOverall = distance.plus(previous.getValue().distanceOverall);
}
float speed_mps = (float) (distance_m / timeDiff_s);
value = new Data(distance_m, distance_overall_m, speed_mps);
Speed speed_mps = Speed.of(distance, timeDiff);
value = new Data(distance, distanceOverall, speed_mps);
}
}
}
@@ -144,7 +148,7 @@ public final class SensorDataCycling {
@Override
public void reset() {
if (value != null) {
value = new Data(value.distance_m, 0, value.speed_mps);
value = new Data(value.distance, Distance.of(0), value.speed);
}
}
@@ -167,22 +171,22 @@ public final class SensorDataCycling {
}
public static class Data {
public final float distance_m;
public final float distance_overall_m;
public final float speed_mps;
public final Distance distance;
public final Distance distanceOverall;
public final Speed speed;
private Data(float distance_m, float distance_overall_m, float speed_mps) {
this.distance_m = distance_m;
this.distance_overall_m = distance_overall_m;
this.speed_mps = speed_mps;
private Data(Distance distance, Distance distanceOverall, Speed speed) {
this.distance = distance;
this.distanceOverall = distanceOverall;
this.speed = speed;
}
@Override
public String toString() {
return "Data{" +
"distance_m=" + distance_m +
", distance_overall_m=" + distance_overall_m +
", speed_mps=" + speed_mps +
"distance_m=" + distance +
", distance_overall_m=" + distanceOverall +
", speed_mps=" + speed +
'}';
}
}
@@ -58,8 +58,8 @@ public final class SensorDataSet {
}
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distance_overall_m);
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed_mps);
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distanceOverall);
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed);
}
if (cyclingPower != null && cyclingPower.hasValue()) {
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -66,7 +67,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
private SharedPreferences sharedPreferences;
private int recordingDistanceInterval;
private Distance recordingDistanceInterval;
private TrackDataHub trackDataHub;
@@ -346,4 +347,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
void setChartByDistance(boolean value) {
chartByDistance = value;
}
@VisibleForTesting
void setRecordingDistanceInterval(Distance value) {
recordingDistanceInterval = value;
}
}
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -14,6 +13,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
@@ -25,7 +29,6 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
@@ -36,6 +39,8 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
private static final String TAG = IntervalsFragment.class.getSimpleName();
private static final String FROM_TOP_TO_BOTTOM_KEY = "fromTopToBottom";
private IntervalStatisticsModel viewModel;
protected IntervalStatisticsAdapter.StackMode stackModeListView;
private IntervalStatisticsModel.IntervalOption selectedInterval;
@@ -47,22 +52,38 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
private SharedPreferences sharedPreferences;
private TrackDataHub trackDataHub;
private String category;
private boolean isReportSpeed;
private IntervalListViewBinding viewBinding;
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
if (adapter != null) {
adapter.notifyDataSetChanged();
uploadIntervals();
if (spinnerAdapter != null) {
spinnerAdapter.notifyDataSetChanged();
}
}
};
public static Fragment newInstance() {
return new IntervalsFragment();
/**
* Creates an instance of this class.
*
* @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top.
* @return IntervalsFragment instance.
*/
public static Fragment newInstance(boolean fromTopToBottom) {
Bundle bundle = new Bundle();
bundle.putBoolean(FROM_TOP_TO_BOTTOM_KEY, fromTopToBottom);
IntervalsFragment intervalsFragment = new IntervalsFragment();
intervalsFragment.setArguments(bundle);
return intervalsFragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
stackModeListView = getArguments().getBoolean(FROM_TOP_TO_BOTTOM_KEY, true) ? IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP : IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM;
}
@Override
@@ -76,14 +97,11 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
super.onViewCreated(view, savedInstanceState);
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP;
viewModel = new IntervalStatisticsModel();
adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, metricUnits, isReportSpeed);
viewBinding.intervalList.setLayoutManager(new LinearLayoutManager(getContext()));
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
viewBinding.intervalList.setAdapter(adapter);
spinnerAdapter = new ArrayAdapter<IntervalStatisticsModel.IntervalOption>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
@NonNull
@@ -109,7 +127,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
selectedInterval = IntervalStatisticsModel.IntervalOption.values()[i];
loadIntervals();
uploadIntervals();
}
@Override
@@ -121,6 +139,10 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
@Override
public void onResume() {
super.onResume();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
viewModel = new ViewModelProvider(getActivity()).get(IntervalStatisticsModel.class);
loadIntervals();
resumeTrackDataHub();
}
@@ -128,7 +150,6 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
public void onPause() {
super.onPause();
pauseTrackDataHub();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@@ -155,10 +176,15 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
if (viewModel == null) {
return;
}
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(metricUnits, selectedInterval);
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, metricUnits, isReportSpeed));
}
IntervalStatistics intervalStatistics = viewModel.getIntervalStats(metricUnits, selectedInterval);
adapter = new IntervalStatisticsAdapter(getContext(), intervalStatistics.getIntervalList(), category, stackModeListView);
viewBinding.intervalList.setAdapter(adapter);
private synchronized void uploadIntervals() {
if (viewModel == null) {
return;
}
viewModel.upload(metricUnits, selectedInterval);
}
/**
@@ -185,11 +211,11 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
// Set category.
category = track != null ? track.getCategory() : "";
String category = track != null ? track.getCategory() : "";
// Set rate label.
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category); //TODO Handle sharedPreferenceChangeListener
viewBinding.intervalRate.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
viewBinding.intervalRate.setText(isReportSpeed ? R.string.stats_speed : R.string.stats_pace);
}
});
}
@@ -219,7 +245,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
@Override
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
if (isResumed()) {
runOnUiThread(this::loadIntervals);
runOnUiThread(viewModel::onNewTrackPoints);
}
}
@@ -249,48 +275,4 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
fragmentActivity.runOnUiThread(runnable);
}
}
public static class IntervalsRecordingFragment extends IntervalsFragment {
// Refreshing intervals stats it's not so demanding so 5 seconds is enough to balance performance and user experience.
private static final long UI_UPDATE_INTERVAL = 5 * UnitConversions.ONE_SECOND_MS;
private Handler intervalHandler;
private final Runnable intervalRunner = new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateIntervals();
intervalHandler.postDelayed(intervalRunner, UI_UPDATE_INTERVAL);
}
}
};
public static Fragment newInstance() {
return new IntervalsRecordingFragment();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
intervalHandler = new Handler();
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM;
}
@Override
public void onResume() {
super.onResume();
intervalHandler.post(intervalRunner);
}
@Override
public void onPause() {
super.onPause();
intervalHandler.removeCallbacks(intervalRunner);
}
private void updateIntervals() {
loadIntervals();
}
}
}
@@ -34,6 +34,8 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.time.Duration;
import de.dennisguse.opentracks.MarkerEditActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
@@ -43,7 +45,6 @@ import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* A fragment to show marker details.
@@ -55,7 +56,7 @@ public class MarkerDetailFragment extends Fragment {
private static final String TAG = MarkerDetailFragment.class.getSimpleName();
private static final String KEY_MARKER_ID = "markerId";
private static final long HIDE_TEXT_DELAY = 4 * UnitConversions.ONE_SECOND_MS;
private static final Duration HIDE_TEXT_DELAY = Duration.ofSeconds(4);
private MenuItem shareMarkerImageMenuItem;
@@ -126,7 +127,7 @@ public class MarkerDetailFragment extends Fragment {
viewBinding.markerDetailMarkerTextGradient.setVisibility(visibility);
viewBinding.markerDetailMarkerInfo.setVisibility(visibility);
if (visibility == View.VISIBLE) {
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
handler.postDelayed(hideText, HIDE_TEXT_DELAY.toMillis());
}
});
return viewBinding.getRoot();
@@ -236,7 +237,7 @@ public class MarkerDetailFragment extends Fragment {
if (hasPhoto) {
handler.removeCallbacks(hideText);
viewBinding.markerDetailMarkerPhoto.setImageURI(marker.getPhotoURI());
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
handler.postDelayed(hideText, HIDE_TEXT_DELAY.toMillis());
} else {
viewBinding.markerDetailMarkerPhoto.setImageResource(MarkerUtils.ICON_ID);
}
@@ -65,11 +65,13 @@ public class StatisticsRecordedFragment extends Fragment {
return fragment;
}
private TrackStatistics trackStatistics;
private SensorStatistics sensorStatistics;
private SensorsAdapter sensorsAdapter;
private String category = "";
private Track.Id trackId;
@Nullable // Lazily loaded.
private Track track;
private ContentProviderUtils contentProviderUtils;
private StatisticsRecordedBinding viewBinding;
@@ -86,9 +88,9 @@ public class StatisticsRecordedFragment extends Fragment {
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && track != null) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), track.getCategory());
}
if (key != null && updateUInecessary && isResumed()) {
@@ -152,15 +154,14 @@ public class StatisticsRecordedFragment extends Fragment {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
Track track = contentProviderUtils.getTrack(trackId);
trackStatistics = track.getTrackStatistics();
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
String newCategory = track.getCategory();
if (!category.equals(newCategory)) {
category = newCategory;
if ((this.track == null && track != null) || (this.track != null && track != null && !this.track.getCategory().equals(track.getCategory()))) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
this.track = track;
loadTrackDescription(track);
updateUI();
updateSensorUI();
@@ -174,14 +175,14 @@ public class StatisticsRecordedFragment extends Fragment {
private void loadTrackDescription(@NonNull Track track) {
viewBinding.statsNameValue.setText(track.getName());
viewBinding.statsDescriptionValue.setText(track.getDescription());
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTime(getContext(), trackStatistics.getStartTime()));
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTime(getContext(), track.getTrackStatistics().getStartTime()));
}
private void updateUI() {
TrackStatistics trackStatistics = track.getTrackStatistics();
// Set total distance
{
double totalDistance = trackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), trackStatistics.getTotalDistance(), preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -189,7 +190,7 @@ public class StatisticsRecordedFragment extends Fragment {
// Set activity type
{
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
String trackIconValue = TrackIconUtils.getIconValue(getContext(), track.getCategory());
viewBinding.statsActivityTypeIcon.setImageDrawable(ContextCompat.getDrawable(getContext(), TrackIconUtils.getIconDrawable(trackIconValue)));
}
@@ -201,54 +202,49 @@ public class StatisticsRecordedFragment extends Fragment {
// Set average speed/pace
{
double speed = trackStatistics.getAverageSpeed();
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
// Set max speed/pace
{
double speed = trackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getMaxSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
// Set moving speed/pace
{
double speed = trackStatistics.getAverageMovingSpeed();
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageMovingSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
// Set elevation gain and loss
// Set altitude gain and loss
{
// Make elevation visible?
boolean showElevation = PreferencesUtils.isShowStatsElevation(sharedPreferences, getContext());
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
// Make altitude visible?
boolean show = PreferencesUtils.isShowStatsAltitude(sharedPreferences, getContext());
viewBinding.statsAltitudeGroup.setVisibility(show ? View.VISIBLE : View.GONE);
Float elevationGain_m = trackStatistics.getTotalElevationGain();
Float elevationLoss_m = trackStatistics.getTotalElevationLoss();
Float altitudeGain_m = trackStatistics.getTotalAltitudeGain();
Float altitudeLoss_m = trackStatistics.getTotalAltitudeLoss();
Pair<String, String> parts;
parts = StringUtils.formatElevation(getContext(), elevationGain_m, preferenceMetricUnits);
viewBinding.statsElevationGainValue.setText(parts.first);
viewBinding.statsElevationGainUnit.setText(parts.second);
parts = StringUtils.formatAltitude(getContext(), altitudeGain_m, preferenceMetricUnits);
viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second);
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, preferenceMetricUnits);
viewBinding.statsElevationLossValue.setText(parts.first);
viewBinding.statsElevationLossUnit.setText(parts.second);
parts = StringUtils.formatAltitude(getContext(), altitudeLoss_m, preferenceMetricUnits);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
}
}
@@ -24,19 +24,19 @@ import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.adapters.SensorsAdapter;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
/**
@@ -49,12 +49,12 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND_MS;
private static final Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private TrackDataHub trackDataHub;
private Handler handlerUpdateUI;
@@ -62,10 +62,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private TrackPoint lastTrackPoint;
//TODO Should not be nullable
private TrackStatistics lastTrackStatistics;
private String category = "";
@Nullable // Lazily loaded.
private Track track;
private StatisticsRecordingBinding viewBinding;
private SensorsAdapter sensorsAdapter;
@@ -73,7 +71,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private boolean preferenceShowElevation;
private boolean preferenceShowAltitude;
private boolean preferenceShowCoordinate;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
@@ -84,14 +82,14 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && track != null) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), track.getCategory());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_elevation_key, key)) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_altitude_key, key)) {
updateUInecessary = true;
preferenceShowElevation = PreferencesUtils.isShowStatsElevation(sharedPreferences, getContext());
preferenceShowAltitude = PreferencesUtils.isShowStatsAltitude(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_coordinate_key, key)) {
@@ -111,7 +109,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTrackStatistics != null) {
if (!isSelectedTrackPaused() && track != null) {
synchronized (this) {
if (lastTrackPoint != null && lastTrackPoint.hasLocation() && !lastTrackPoint.isRecent()) {
lastTrackPoint = null;
@@ -122,7 +120,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
updateSensorDataUI();
}
handlerUpdateUI.postDelayed(this, UI_UPDATE_INTERVAL);
handlerUpdateUI.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
}
}
};
@@ -140,7 +138,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
@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));
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(track != null ? track.getCategory() : "")); //TODO "" would be useless as Track was not yet loaded.
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
@@ -197,12 +195,10 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
lastTrackStatistics = track != null ? track.getTrackStatistics() : null;
String newCategory = track != null ? track.getCategory() : "";
if (!category.equals(newCategory)) {
category = newCategory;
if ((this.track == null && track != null) || (this.track != null && track != null && !this.track.getCategory().equals(track.getCategory()))) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
this.track = track;
updateUI();
}
});
@@ -303,15 +299,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
/**
* Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}.
* Also sets elevation gain and loss.
* Also sets altitude gain and loss.
*/
private void updateSensorDataUI() {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService == null) {
Log.d(TAG, "Cannot get the track recording service.");
} else {
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
SensorDataSet sensorDataSet = trackRecordingService.getSensorDataSet();
if (sensorDataSet != null) {
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorDataSet.getHeartRate() != null) {
@@ -326,44 +322,44 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
sensorsAdapter.swapData(sensorDataList);
setSpeedSensorData(sensorDataSet);
}
//TODO Check if we can distribute the total elevation gain and loss via trackStatistics instead of doing some computation in the UI layer.
setTotalElevationGain(trackRecordingService.getElevationGain_m());
setTotalElevationLoss(trackRecordingService.getElevationLoss_m());
//TODO Check if we can get the total altitude gain and loss via trackStatistics instead of doing some computation in the UI layer.
setTotalAltitudeGain(trackRecordingService.getAltitudeGain_m());
setTotalAltitudeLoss(trackRecordingService.getAltitudeLoss_m());
}
}
// Set elevation gain
private void setTotalElevationGain(Float elevationGain_m) {
Float totalElevationGain = elevationGain_m;
// Set altitude gain
private void setTotalAltitudeGain(Float altitudeGain_m) {
Float totalAltitudeGain = altitudeGain_m;
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationGain()) {
if (elevationGain_m == null) {
totalElevationGain = lastTrackStatistics.getTotalElevationGain();
} else {
totalElevationGain += lastTrackStatistics.getTotalElevationGain();
if (track != null && track.getTrackStatistics().hasTotalAltitudeGain()) {
if (altitudeGain_m == null) {
totalAltitudeGain = 0f;
}
totalAltitudeGain += track.getTrackStatistics().getTotalAltitudeGain();
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, preferenceMetricUnits);
viewBinding.statsElevationGainValue.setText(parts.first);
viewBinding.statsElevationGainUnit.setText(parts.second);
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), totalAltitudeGain, preferenceMetricUnits);
viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second);
}
// Set elevation loss
private void setTotalElevationLoss(Float elevationLoss_m) {
Float totalElevationLoss = elevationLoss_m;
// Set altitude loss
private void setTotalAltitudeLoss(Float altitudeLoss_m) {
Float totalAltitudeLoss = altitudeLoss_m;
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationLoss()) {
if (elevationLoss_m == null) {
totalElevationLoss = lastTrackStatistics.getTotalElevationLoss();
if (track != null && track.getTrackStatistics().hasTotalAltitudeLoss()) {
if (altitudeLoss_m == null) {
totalAltitudeLoss = track.getTrackStatistics().getTotalAltitudeLoss();
} else {
totalElevationLoss += lastTrackStatistics.getTotalElevationLoss();
totalAltitudeLoss += track.getTrackStatistics().getTotalAltitudeLoss();
}
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, preferenceMetricUnits);
viewBinding.statsElevationLossValue.setText(parts.first);
viewBinding.statsElevationLossUnit.setText(parts.second);
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), totalAltitudeLoss, preferenceMetricUnits);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
}
private void setSpeedSensorData(SensorDataSet sensorDataSet) {
@@ -371,35 +367,35 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
if (data.hasValue() && data.isRecent()) {
setTotalDistance(data.getValue().distance_overall_m);
setSpeed(data.getValue().speed_mps);
setTotalDistance(data.getValue().distanceOverall);
setSpeed(data.getValue().speed);
}
if (data.hasValue() && data.isRecent()) {
setSpeed(data.getValue().speed_mps);
setSpeed(data.getValue().speed);
}
}
}
private void updateStats() {
setTotalDistance(0);
setTotalDistance(Distance.of(0)); //TODO Why?
// Set activity type
{
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
if (track != null) {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), track.getCategory());
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
}
// Set time
if (lastTrackStatistics != null) {
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(lastTrackStatistics.getMovingTime()));
if (track != null && track.getTrackStatistics() != null) {
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
updateTotalTime();
}
// Set average speed/pace
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
Speed speed = track != null ? track.getTrackStatistics().getAverageSpeed() : null;
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
@@ -409,7 +405,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set max speed/pace
{
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
Speed speed = track != null ? track.getTrackStatistics().getMaxSpeed() : null;
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
@@ -420,7 +416,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set moving speed/pace
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
Speed speed = track != null ? track.getTrackStatistics().getAverageMovingSpeed() : null;
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
@@ -429,16 +425,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
// Set elevation gain and loss
// Set altitude gain and loss
{
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
}
}
private void updateTotalTime() {
Duration totalTime = lastTrackStatistics.getTotalTime();
Duration totalTime = track.getTrackStatistics().getTotalTime();
if (isSelectedTrackRecording()) {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
totalTime = trackRecordingService.getTotalTime();
}
@@ -449,18 +445,18 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private void setLocationValues() {
// Set speed/pace
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
Speed speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : null;
setSpeed(speed);
// Set elevation
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
// Set altitude
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
if (preferenceShowElevation) {
// Current elevation
if (preferenceShowAltitude) {
// Current altitude
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsElevationCurrentValue.setText(parts.first);
viewBinding.statsElevationCurrentUnit.setText(parts.second);
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
}
// Set coordinate
@@ -477,15 +473,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
private void setTotalDistance(double sensorDistanceSinceLastTrackpoint) {
double totalDistance = lastTrackStatistics != null ? (lastTrackStatistics.getTotalDistance() + sensorDistanceSinceLastTrackpoint) : Double.NaN;
private void setTotalDistance(Distance sensorDistanceSinceLastTrackpoint) {
Distance totalDistance = track != null ? (track.getTrackStatistics().getTotalDistance().plus(sensorDistanceSinceLastTrackpoint)) : Distance.invalid();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
private void setSpeed(double speed) {
private void setSpeed(Speed speed) {
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
@@ -46,7 +46,7 @@ public class GPXTrackExporter implements TrackExporter {
private static final String TAG = GPXTrackExporter.class.getSimpleName();
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat ALTITUDE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
@@ -59,8 +59,8 @@ public class GPXTrackExporter implements TrackExporter {
* GPX readers expect to see fractional numbers with US-style punctuation.
* That is, they want periods for decimal points, rather than commas.
*/
ELEVATION_FORMAT.setMaximumFractionDigits(1);
ELEVATION_FORMAT.setGroupingUsed(false);
ALTITUDE_FORMAT.setMaximumFractionDigits(1);
ALTITUDE_FORMAT.setGroupingUsed(false);
COORDINATE_FORMAT.setMaximumFractionDigits(6);
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
@@ -200,7 +200,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemes/TrackPointExtension/v2\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
printWriter.println("xsi:schemaLocation=" +
"\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
@@ -237,7 +237,7 @@ public class GPXTrackExporter implements TrackExporter {
if (printWriter != null) {
printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">");
if (marker.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(marker.getAltitude()) + "</ele>");
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(marker.getAltitude()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
@@ -281,16 +281,16 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
if (trackPoint.hasSpeed() || trackPoint.hasHeartRate() || trackPoint.hasCyclingCadence() || trackPoint.hasElevationGain() || trackPoint.hasElevationLoss()) {
if (trackPoint.hasSpeed() || trackPoint.hasHeartRate() || trackPoint.hasCyclingCadence() || trackPoint.hasAltitudeGain() || trackPoint.hasAltitudeLoss()) {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
if (trackPoint.hasSpeed()) {
printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed()) + "</gpxtpx:speed>");
printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>");
}
if (trackPoint.hasHeartRate()) {
@@ -305,16 +305,16 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower()) + "</pwr:PowerInWatts>");
}
if (trackPoint.hasElevationGain()) {
printWriter.println("<opentracks:gain>" + ELEVATION_FORMAT.format(trackPoint.getElevationGain()) + "</opentracks:gain>");
if (trackPoint.hasAltitudeGain()) {
printWriter.println("<opentracks:gain>" + ALTITUDE_FORMAT.format(trackPoint.getAltitudeGain()) + "</opentracks:gain>");
}
if (trackPoint.hasElevationLoss()) {
printWriter.println("<opentracks:loss>" + ELEVATION_FORMAT.format(trackPoint.getElevationLoss()) + "</opentracks:loss>");
if (trackPoint.hasAltitudeLoss()) {
printWriter.println("<opentracks:loss>" + ALTITUDE_FORMAT.format(trackPoint.getAltitudeLoss()) + "</opentracks:loss>");
}
if (trackPoint.hasSensorDistance()) {
printWriter.println("<opentracks:distance>" + DISTANCE_FORMAT.format(trackPoint.getElevationLoss()) + "</opentracks:distance>");
printWriter.println("<opentracks:distance>" + DISTANCE_FORMAT.format(trackPoint.getSensorDistance().toM()) + "</opentracks:distance>");
}
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
@@ -61,8 +61,8 @@ public class KMLTrackExporter implements TrackExporter {
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_HEART_RATE = "heart_rate";
public static final String EXTENDED_DATA_TYPE_POWER = "power";
public static final String EXTENDED_DATA_TYPE_ELEVATION_GAIN = "elevation_gain";
public static final String EXTENDED_DATA_TYPE_ELEVATION_LOSS = "elevation_loss";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_GAIN = "elevation_gain";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_LOSS = "elevation_loss";
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
@@ -79,8 +79,8 @@ public class KMLTrackExporter implements TrackExporter {
private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
private final List<Float> elevationGainList = new ArrayList<>();
private final List<Float> elevationLossList = new ArrayList<>();
private final List<Float> altitudeGainList = new ArrayList<>();
private final List<Float> altitudeLossList = new ArrayList<>();
@Deprecated // Figure out a better way to do this! (if needed)
private TrackPoint startTrackPoint;
@@ -344,8 +344,8 @@ public class KMLTrackExporter implements TrackExporter {
powerList.clear();
cadenceList.clear();
heartRateList.clear();
elevationGainList.clear();
elevationLossList.clear();
altitudeGainList.clear();
altitudeLossList.clear();
}
}
@@ -370,11 +370,11 @@ public class KMLTrackExporter implements TrackExporter {
if (heartRateList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE);
}
if (elevationGainList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(elevationGainList, EXTENDED_DATA_TYPE_ELEVATION_GAIN);
if (altitudeGainList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(altitudeGainList, EXTENDED_DATA_TYPE_ALTITUDE_GAIN);
}
if (elevationLossList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(elevationLossList, EXTENDED_DATA_TYPE_ELEVATION_LOSS);
if (altitudeLossList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(altitudeLossList, EXTENDED_DATA_TYPE_ALTITUDE_LOSS);
}
}
printWriter.println("</SchemaData>");
@@ -395,16 +395,16 @@ public class KMLTrackExporter implements TrackExporter {
} else {
printWriter.println("<gx:coord/>");
}
speedList.add(trackPoint.hasSpeed() ? trackPoint.getSpeed() : null);
speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null);
if (exportSensorData) {
distanceList.add(trackPoint.hasSensorDistance() ? trackPoint.getSensorDistance() : null);
distanceList.add(trackPoint.hasSensorDistance() ? (float) trackPoint.getSensorDistance().toM() : null);
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null);
cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null);
elevationGainList.add(trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : null);
elevationLossList.add(trackPoint.hasElevationLoss() ? trackPoint.getElevationLoss() : null);
altitudeGainList.add(trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : null);
altitudeLossList.add(trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : null);
}
}
}
@@ -42,11 +42,14 @@ import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
@@ -65,7 +68,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private final int recordingDistanceInterval;
private final Distance recordingDistanceInterval;
private final List<Track.Id> trackIds = new ArrayList<>();
private final List<Marker> markers = new ArrayList<>();
@@ -202,11 +205,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) {
String markerDescription = marker.getDescription();
String icon = context.getString(R.string.marker_icon_url);
double length = trackStatisticsUpdater.getTrackStatistics().getTotalDistance();
long duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime().toMillis();
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
// Insert marker
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), length, duration, trackPoint, marker.getPhotoUrl());
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), stats, trackPoint, marker.getPhotoUrl());
contentProviderUtils.insertMarker(newMarker);
}
@@ -374,17 +376,16 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
*/
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
float speed = trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint) / timeDifference.toMillis();
trackPoint.setSpeed(speed);
trackPoint.setSpeed(Speed.of(trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint), timeDifference));
}
}
}
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
long maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation > maxRecordingDistance) {
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
}
@@ -460,7 +461,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (speed != null) {
try {
trackPoint.setSpeed(Float.parseFloat(speed));
trackPoint.setSpeed(Speed.of(speed));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
@@ -491,21 +492,21 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (gain != null) {
try {
trackPoint.setElevationGain(Float.parseFloat(gain));
trackPoint.setAltitudeGain(Float.parseFloat(gain));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation gain: %s", gain)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s", gain)), e);
}
}
if (loss != null) {
try {
trackPoint.setElevationLoss(Float.parseFloat(loss));
trackPoint.setAltitudeLoss(Float.parseFloat(loss));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation loss: %s", loss)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
}
}
if (distance != null) {
try {
trackPoint.setSensorDistance(Float.parseFloat(distance));
trackPoint.setSensorDistance(Distance.of(distance));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
@@ -42,7 +42,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_COMMENT = "cmt";
private static final String TAG_ELEVATION = "ele";
private static final String TAG_ALTITUDE = "ele";
private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
@@ -98,7 +98,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
public void endElement(String uri, String localName, String tag) {
switch (tag) {
case TAG_GPX:
onFileEnd();
@@ -135,7 +135,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
time = content.trim();
}
break;
case TAG_ELEVATION:
case TAG_ALTITUDE:
if (content != null) {
altitude = content.trim();
}
@@ -26,6 +26,8 @@ import org.xml.sax.SAXException;
import java.util.ArrayList;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
@@ -70,8 +72,8 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private final ArrayList<Float> cadenceList = new ArrayList<>();
private final ArrayList<Float> heartRateList = new ArrayList<>();
private final ArrayList<Float> powerList = new ArrayList<>();
private final ArrayList<Float> elevationGainList = new ArrayList<>();
private final ArrayList<Float> elevationLossList = new ArrayList<>();
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
public KmlFileTrackImporter(Context context) {
this(context, new ContentProviderUtils(context));
@@ -193,7 +195,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
markerType = null;
}
private void onMarkerEnd() throws SAXException {
private void onMarkerEnd() {
if (!MARKER_STYLE.equals(markerType)) {
return;
}
@@ -221,8 +223,8 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
heartRateList.clear();
cadenceList.clear();
powerList.clear();
elevationGainList.clear();
elevationLossList.clear();
altitudeGainList.clear();
altitudeLossList.clear();
}
protected void onTrackSegmentEnd() {
@@ -231,11 +233,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i);
if (i < speedList.size()) {
trackPoint.setSpeed(speedList.get(i));
if (i < speedList.size() && speedList.get(i) != null) {
trackPoint.setSpeed(Speed.of(speedList.get(i)));
}
if (i < distanceList.size()) {
trackPoint.setSensorDistance(distanceList.get(i));
if (i < distanceList.size() && distanceList.get(i) != null) {
trackPoint.setSensorDistance(Distance.of(distanceList.get(i)));
}
if (i < heartRateList.size()) {
trackPoint.setHeartRate_bpm(heartRateList.get(i));
@@ -246,11 +248,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (i < powerList.size()) {
trackPoint.setPower(powerList.get(i));
}
if (i < elevationGainList.size()) {
trackPoint.setElevationGain(elevationGainList.get(i));
if (i < altitudeGainList.size()) {
trackPoint.setAltitudeGain(altitudeGainList.get(i));
}
if (i < elevationLossList.size()) {
trackPoint.setElevationLoss(elevationLossList.get(i));
if (i < altitudeLossList.size()) {
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
}
insertTrackPoint(trackPoint);
@@ -260,7 +262,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
/**
* On track point end. gx:coord end tag.
*/
private void onTrackPointEnd() throws SAXException {
private void onTrackPointEnd() {
// Add trackPoint to trackPoints
if (content == null) {
return;
@@ -327,11 +329,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE:
cadenceList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ELEVATION_GAIN:
elevationGainList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN:
altitudeGainList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ELEVATION_LOSS:
elevationLossList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS:
altitudeLossList.add(value);
break;
default:
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
@@ -40,7 +40,6 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Imports a KMZ file.
@@ -77,9 +76,8 @@ public class KmzTrackImporter implements TrackImporter {
for (Track.Id trackId : trackIds) {
if (copyKmzImages(trackId)) {
trackIdsWithImages.add(trackId);
deleteOrphanImages(context, trackId);
deleteOrphanImages(trackId);
} else {
cleanImport(context, trackId);
return new ArrayList<>();
}
}
@@ -201,10 +199,9 @@ public class KmzTrackImporter implements TrackImporter {
/**
* Deletes all images that remained in external storage that doesn't have a marker associated.
*
* @param context the Context object.
* @param trackId the id of the Track.
*/
private void deleteOrphanImages(Context context, Track.Id trackId) {
private void deleteOrphanImages(Track.Id trackId) {
// 1.- Gets all photo names in the markers of the track identified by id.
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<Marker> markers = contentProviderUtils.getMarkers(trackId);
@@ -230,18 +227,6 @@ public class KmzTrackImporter implements TrackImporter {
}
}
/**
* Cleans up import.
*
* @param trackId the trackId
*/
private void cleanImport(Context context, Track.Id trackId) {
if (PreferencesUtils.isRecording(trackId)) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteTrack(context, trackId);
}
}
private List<Track.Id> parseKml(ZipInputStream zipInputStream) {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context);
@@ -27,7 +27,6 @@ import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager.WakeLock;
import android.util.Log;
@@ -37,24 +36,22 @@ import androidx.core.app.TaskStackBuilder;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.HandlerServer;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.sensors.ElevationSumManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.settings.SettingsActivity;
@@ -83,23 +80,13 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private TrackRecordingServiceNotificationManager notificationManager;
private SharedPreferences sharedPreferences;
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private int recordingDistanceInterval;
private int maxRecordingDistance;
private Distance recordingDistanceInterval;
private Distance maxRecordingDistance;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Context context = TrackRecordingService.this;
if (PreferencesUtils.isKey(TrackRecordingService.this, R.string.recording_track_id_key, key)) {
// Only through the TrackRecordingService can one stop a recording and set the recordingTrackId to -1L.
if (PreferencesUtils.isRecording(sharedPreferences, TrackRecordingService.this)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, TrackRecordingService.this);
}
}
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, context);
}
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
voiceExecutor.setMetricUnits(metricUnits);
@@ -122,17 +109,17 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// The following variables are set when recording:
private WakeLock wakeLock;
private BluetoothRemoteSensorManager remoteSensorManager;
private ElevationSumManager elevationSumManager;
private AltitudeSumManager altitudeSumManager;
private TrackStatisticsUpdater trackStatisticsUpdater;
private TrackPoint lastTrackPoint;
private boolean isIdle;
private TrackRecordingServiceBinder binder = new TrackRecordingServiceBinder(this);
private final Binder binder = new Binder();
private HandlerServer handlerServer;
private List<TrackRecordingServiceCallback> listeners = new ArrayList<>();
private final TrackRecordingServiceStatus serviceStatus = new TrackRecordingServiceStatus();
@Override
public void onCreate() {
@@ -145,14 +132,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
notificationManager = new TrackRecordingServiceNotificationManager(this);
// onSharedPreferenceChanged might not set recordingTrackId.
recordingTrackId = null;
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
restartTrackAfterServiceRestart();
}
@Override
@@ -161,19 +143,13 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
@Override
public IBinder onBind(Intent intent) {
public Binder onBind(Intent intent) {
return binder;
}
@Override
public void onDestroy() {
if (listeners != null) {
for (TrackRecordingServiceCallback listener : listeners) {
listener.onGpsStatusChange(GpsStatusValue.GPS_NONE);
}
listeners.clear();
listeners = null;
}
serviceStatus.onStop();
handlerServer.stop(this);
handlerServer = null;
@@ -183,9 +159,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
remoteSensorManager = null;
}
if (elevationSumManager != null) {
elevationSumManager.stop(this);
elevationSumManager = null;
if (altitudeSumManager != null) {
altitudeSumManager.stop(this);
altitudeSumManager = null;
}
// Reverse order from onCreate
@@ -202,9 +178,6 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
contentProviderUtils = null;
binder.detachFromService();
binder = null;
// This should be the next to last operation
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
@@ -212,15 +185,15 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
public boolean isRecording() {
return PreferencesUtils.isRecording(recordingTrackId);
return serviceStatus.isRecording();
}
public boolean isPaused() {
return recordingTrackPaused;
return serviceStatus.getRecordingTrackPaused();
}
public Track.Id getRecordingTrackId() {
return recordingTrackId;
return serviceStatus.getRecordingTrackId();
}
public TrackStatistics getTrackStatistics() {
@@ -250,14 +223,14 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
if (name == null) {
int nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(recordingTrackId);
if (nextMarkerNumber == -1) {
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(serviceStatus.getRecordingTrackId());
if (nextMarkerNumber == null) {
nextMarkerNumber = 1;
}
name = getString(R.string.marker_name_format, nextMarkerNumber + 1);
}
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(recordingTrackId);
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(serviceStatus.getRecordingTrackId());
if (trackPoint == null) {
Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
return null;
@@ -269,11 +242,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
photoUrl = photoUrl != null ? photoUrl : "";
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
double length = stats.getTotalDistance();
long duration = stats.getTotalTime().toMillis();
// Insert marker
Marker marker = new Marker(name, description, category, icon, recordingTrackId, length, duration, trackPoint, photoUrl);
Marker marker = new Marker(name, description, category, icon, serviceStatus.getRecordingTrackId(), stats, trackPoint, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri));
}
@@ -283,7 +254,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
*
* @return the track id
*/
Track.Id startNewTrack() {
public Track.Id startNewTrack() {
if (isRecording()) {
Log.d(TAG, "Ignore startNewTrack. Already recording.");
return null;
@@ -294,8 +265,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
Uri uri = contentProviderUtils.insertTrack(track);
Track.Id trackId = new Track.Id(ContentUris.parseId(uri));
// Update shared preferences
updateRecordingState(trackId, false);
// Set recording status
serviceStatus.onChange(trackId, false);
// Update database
track.setId(trackId);
@@ -323,7 +294,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
*
* @param trackId the id of the track to be resumed.
*/
void resumeTrack(Track.Id trackId) {
public void resumeTrack(Track.Id trackId) {
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "Ignore resumeTrack. Track " + trackId.getId() + " does not exists.");
@@ -336,48 +307,24 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
insertTrackPoint(track, TrackPoint.createSegmentStartManual());
// Update shared preferences.
updateRecordingState(trackId, false);
// Set recording status
serviceStatus.onChange(trackId, false);
startRecording();
}
/**
* Try to restart the previous recording track in case the service has been restarted by the system, which can sometimes happen.
*/
private void restartTrackAfterServiceRestart() {
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track == null) {
if (isRecording()) {
Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId.getId());
updateRecordingState(null, true);
}
showNotification(false);
return;
}
Log.d(TAG, "Restarting track: " + track.getId());
trackStatisticsUpdater = new TrackStatisticsUpdater();
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
trackStatisticsUpdater.addTrackPoint(trackPointIterator, recordingDistanceInterval);
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e);
}
startRecording();
}
void resumeCurrentTrack() {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void resumeCurrentTrack() {
if (!isRecording() || !isPaused()) {
Log.d(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
return;
}
updateRecordingState(recordingTrackId, false);
// Set recording status
serviceStatus.onChange(false);
// Update database
Track track = contentProviderUtils.getTrack(recordingTrackId);
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
if (track != null) {
insertTrackPoint(track, TrackPoint.createSegmentStartManual());
}
@@ -393,8 +340,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start();
elevationSumManager = new ElevationSumManager();
elevationSumManager.start(this);
altitudeSumManager = new AltitudeSumManager();
altitudeSumManager.start(this);
lastTrackPoint = null;
isIdle = false;
@@ -405,7 +352,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
voiceExecutor.restore();
}
void tryStartGps() {
public void tryStartGps() {
if (isRecording()) return;
startGps();
@@ -417,17 +364,19 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
showNotification(true);
}
void endCurrentTrack() {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public Track.Id endCurrentTrack() {
if (!isRecording()) {
Log.d(TAG, "Ignore endCurrentTrack. Not recording.");
return;
return null;
}
// Need to remember the recordingTrackId before setting it to -1L
// Need to remember the recordingTrackId before setting it to null
boolean wasPause = isPaused();
Track.Id trackId = recordingTrackId;
Track.Id trackId = serviceStatus.getRecordingTrackId();
updateRecordingState(null, true);
// Set recording status
serviceStatus.onChange(null, true);
if (!wasPause) {
// Update database
@@ -445,18 +394,22 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
ExportUtils.postWorkoutExport(this, track, new ExportServiceResultReceiver(new Handler(), this));
endRecording(true);
return trackId;
}
void pauseCurrentTrack() {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void pauseCurrentTrack() {
if (!isRecording() || isPaused()) {
Log.d(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
return;
}
updateRecordingState(recordingTrackId, true);
// Set recording status
serviceStatus.onChange(true);
// Update database
Track track = contentProviderUtils.getTrack(recordingTrackId);
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
if (track != null) {
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
@@ -483,9 +436,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
remoteSensorManager.stop();
remoteSensorManager = null;
}
if (elevationSumManager != null) {
elevationSumManager.stop(this);
elevationSumManager = null;
if (altitudeSumManager != null) {
altitudeSumManager.stop(this);
altitudeSumManager = null;
}
lastTrackPoint = null;
@@ -495,6 +448,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
stopGps(trackStopped);
}
public void stopGpsAndShutdown() {
stopGps(true);
}
/**
* Stops gps.
*
@@ -528,21 +485,6 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return lastTrackPoint != null;
}
/**
* Updates the recording states.
* This will inform subscribed {@link OnSharedPreferenceChangeListener}.
*
* @param trackId the recording track id
* @param paused true if the recording is paused
*/
private void updateRecordingState(Track.Id trackId, boolean paused) {
recordingTrackId = trackId;
long currentTrackId = trackId != null ? trackId.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
PreferencesUtils.setLong(sharedPreferences, this, R.string.recording_track_id_key, currentTrackId);
recordingTrackPaused = paused;
PreferencesUtils.setBoolean(sharedPreferences, this, R.string.recording_track_paused_key, recordingTrackPaused);
}
@Override
public void newTrackPoint(TrackPoint trackPoint, int recordingGpsAccuracy) {
if (!isRecording() || isPaused()) {
@@ -550,7 +492,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
Track track = contentProviderUtils.getTrack(recordingTrackId);
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
if (track == null) {
Log.w(TAG, "Ignore newTrackPoint. No track.");
return;
@@ -572,8 +514,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) {
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
insertTrackPointIfNewer(track, lastTrackPoint);
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
@@ -584,7 +526,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
if (trackPoint.hasSensorData() || distanceToLastTrackLocation >= recordingDistanceInterval) {
if (trackPoint.hasSensorData() || distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
@@ -623,16 +565,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@Override
public void newGpsStatus(GpsStatusValue gpsStatusValue) {
if (listeners != null) {
notificationManager.updateContent(getString(gpsStatusValue.message));
for (TrackRecordingServiceCallback listener : listeners) {
listener.onGpsStatusChange(gpsStatusValue);
}
}
notificationManager.updateContent(getString(gpsStatusValue.message));
serviceStatus.onChange(gpsStatusValue);
}
public void addListener(TrackRecordingServiceCallback listener) {
listeners.add(listener);
public void addListener(@NonNull TrackRecordingServiceStatus.Listener listener) {
serviceStatus.addListener(listener);
}
//TODO Check that this is used everywhere, where addListener is called!
//Otherwise, we keep objects in referenced and waste memory until this service instance is terminated.
public void removeListener(@NonNull TrackRecordingServiceStatus.Listener listener) {
serviceStatus.removeListener(listener);
}
/**
@@ -660,10 +604,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
*/
private void insertTrackPoint(@NonNull Track track, @NonNull TrackPoint trackPoint) {
try {
if (elevationSumManager != null) {
trackPoint.setElevationGain(elevationSumManager.getElevationGain_m());
trackPoint.setElevationLoss(elevationSumManager.getElevationLoss_m());
elevationSumManager.reset();
if (altitudeSumManager != null) {
trackPoint.setAltitudeGain(getAltitudeGain_m());
trackPoint.setAltitudeLoss(getAltitudeLoss_m());
altitudeSumManager.reset();
}
if (remoteSensorManager != null) {
fillWithSensorDataSet(trackPoint);
@@ -684,7 +628,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
voiceExecutor.update();
}
SensorDataSet getSensorDataSet() {
public SensorDataSet getSensorDataSet() {
if (remoteSensorManager == null) {
return null;
}
@@ -700,31 +644,31 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
/**
* Returns the relative elevation gain (since last trackpoint).
* Returns the relative altitude gain (since last trackpoint).
*/
Float getElevationGain_m() {
if (elevationSumManager == null || !elevationSumManager.isConnected()) {
public Float getAltitudeGain_m() {
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
return null;
}
return elevationSumManager.getElevationGain_m();
return altitudeSumManager.getAltitudeGain_m();
}
/**
* Returns the relative elevation loss (since last trackpoint).
* Returns the relative altitude loss (since last trackpoint).
*/
Float getElevationLoss_m() {
if (elevationSumManager == null || !elevationSumManager.isConnected()) {
public Float getAltitudeLoss_m() {
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
return null;
}
return elevationSumManager.getElevationLoss_m();
return altitudeSumManager.getAltitudeLoss_m();
}
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, recordingTrackId);
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, serviceStatus.getRecordingTrackId());
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackRecordingActivity.class)
.addNextIntent(intent)
@@ -755,6 +699,11 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
this.remoteSensorManager = remoteSensorManager;
}
@VisibleForTesting
public void setAltitudeSumManager(AltitudeSumManager altitudeSumManager) {
this.altitudeSumManager = altitudeSumManager;
}
public GpsStatusValue getGpsStatus() {
return handlerServer.getGpsStatus();
}
@@ -770,4 +719,14 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
}
public class Binder extends android.os.Binder {
private Binder() {
super();
}
public TrackRecordingService getService() {
return TrackRecordingService.this;
}
}
}
@@ -1,133 +0,0 @@
package de.dennisguse.opentracks.services;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
/**
* TODO: There is a bug in Android that leaks Binder instances. This bug is
* especially visible if we have a non-static class, as there is no way to
* nullify reference to the outer class (the service). A workaround is to use
* a static class and explicitly clear service and detach it from the
* underlying Binder. With this approach, we minimize the leak to 24 bytes per
* each service instance. For more details, see the following bug:
* http://code.google.com/p/android/issues/detail?id=6426.
*/
class TrackRecordingServiceBinder extends android.os.Binder implements TrackRecordingServiceInterface {
private TrackRecordingService trackRecordingService;
TrackRecordingServiceBinder(TrackRecordingService trackRecordingService) {
this.trackRecordingService = trackRecordingService;
}
@Override
public void addListener(TrackRecordingServiceCallback listener) {
trackRecordingService.addListener(listener);
}
@Override
public GpsStatusValue getGpsStatus() {
return trackRecordingService.getGpsStatus();
}
@Override
public void startGps() {
trackRecordingService.tryStartGps();
}
public void stopGps() {
trackRecordingService.stopGps(true);
}
@Override
public Track.Id startNewTrack() {
return trackRecordingService.startNewTrack();
}
@Override
public void resumeTrack(Track.Id trackId) {
trackRecordingService.resumeTrack(trackId);
}
@Override
public void pauseCurrentTrack() {
trackRecordingService.pauseCurrentTrack();
}
@Override
public void resumeCurrentTrack() {
trackRecordingService.resumeCurrentTrack();
}
@Override
public void endCurrentTrack() {
trackRecordingService.endCurrentTrack();
}
@Override
public boolean isRecording() {
return trackRecordingService.isRecording();
}
@Override
public boolean isPaused() {
return trackRecordingService.isPaused();
}
@Override
public Track.Id getRecordingTrackId() {
return trackRecordingService.getRecordingTrackId();
}
@Override
public Duration getTotalTime() {
return trackRecordingService.getTotalTime();
}
@Override
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
return trackRecordingService.insertMarker(name, category, description, photoUrl);
}
@Override
public SensorDataSet getSensorData() {
return trackRecordingService.getSensorDataSet();
}
@Override
public Float getElevationGain_m() {
return trackRecordingService.getElevationGain_m();
}
@Override
public Float getElevationLoss_m() {
return trackRecordingService.getElevationLoss_m();
}
@VisibleForTesting
@Override
public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
trackRecordingService.setRemoteSensorManager(remoteSensorManager);
}
/**
* Detaches from the track recording service. Clears the reference to the
* outer class to minimize the leak.
*/
void detachFromService() {
trackRecordingService = null;
}
@VisibleForTesting
@Override
public void newTrackPoint(TrackPoint trackPoint, int recordingGpsAccuracy) {
trackRecordingService.newTrackPoint(trackPoint, recordingGpsAccuracy);
}
}
@@ -1,10 +0,0 @@
package de.dennisguse.opentracks.services;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
/**
* Interface all activities have to implements to receive information from the service.
*/
public interface TrackRecordingServiceCallback {
void onGpsStatusChange(GpsStatusValue newStatus);
}
@@ -20,7 +20,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
@@ -31,11 +30,7 @@ import androidx.annotation.NonNull;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackEditActivity;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Wrapper for the track recording service.
@@ -51,7 +46,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
private final Runnable callback;
private TrackRecordingServiceInterface trackRecordingService;
private TrackRecordingService trackRecordingService;
public TrackRecordingServiceConnection() {
callback = null;
@@ -64,6 +59,13 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
this.callback = callback;
}
public void bind(@NonNull Context context) {
if (trackRecordingService != null) {
return;
}
context.bindService(new Intent(context, TrackRecordingService.class), this, 0);
}
/**
* Starts and binds the service.
*/
@@ -118,7 +120,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
/**
* Gets the track recording service if bound. Returns null otherwise
*/
public TrackRecordingServiceInterface getServiceIfBound() {
public TrackRecordingService getServiceIfBound() {
return trackRecordingService;
}
@@ -127,7 +129,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
*
* @param value the value
*/
private void setTrackRecordingService(TrackRecordingServiceInterface value) {
private void setTrackRecordingService(TrackRecordingService value) {
trackRecordingService = value;
if (callback != null) {
callback.run();
@@ -142,7 +144,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient.", e);
}
setTrackRecordingService((TrackRecordingServiceInterface) service);
setTrackRecordingService(((TrackRecordingService.Binder) service).getService());
}
@Override
@@ -161,7 +163,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
* Resumes the recording track.
*/
public void resumeTrack() {
TrackRecordingServiceInterface service = getServiceIfBound();
TrackRecordingService service = getServiceIfBound();
if (service != null) {
service.resumeCurrentTrack();
}
@@ -171,7 +173,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
* Pauses the recording track.
*/
public void pauseTrack() {
TrackRecordingServiceInterface service = getServiceIfBound();
TrackRecordingService service = getServiceIfBound();
if (service != null) {
service.pauseCurrentTrack();
}
@@ -183,7 +185,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
* @return the id of the marker or null if none could be created.
*/
public Marker.Id addMarker(Context context, String name, String category, String description, String photoUrl) {
TrackRecordingServiceInterface trackRecordingService = getServiceIfBound();
TrackRecordingService trackRecordingService = getServiceIfBound();
if (trackRecordingService == null) {
Log.d(TAG, "Unable to add marker, no track recording service");
} else {
@@ -202,30 +204,12 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
return null;
}
public void stopRecording(@NonNull Context context, boolean showEditor) {
TrackRecordingServiceInterface trackRecordingService = getServiceIfBound();
public void stopRecording(@NonNull Context context) {
TrackRecordingService trackRecordingService = getServiceIfBound();
if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService not connected.");
} else {
try {
if (showEditor) {
// Need to remember the recordingTrackId before calling endCurrentTrack() as endCurrentTrack() sets the value to -1L.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
Track.Id recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, context);
trackRecordingService.endCurrentTrack();
if (PreferencesUtils.isRecording(sharedPreferences, context)) {
Intent intent = IntentUtils.newIntent(context, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, recordingTrackId)
.putExtra(TrackEditActivity.EXTRA_NEW_TRACK, true);
context.startActivity(intent);
}
} else {
trackRecordingService.endCurrentTrack();
}
} catch (Exception e) {
//TODO What exception are we catching here? Should be removed...
Log.e(TAG, "Unable to stop recording.", e);
}
trackRecordingService.endCurrentTrack();
}
unbindAndStop(context);
}
@@ -1,106 +0,0 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
/**
* App's service.
* This service is the process that actually records and manages tracks.
*/
public interface TrackRecordingServiceInterface {
void startGps();
void stopGps();
Track.Id startNewTrack();
void pauseCurrentTrack();
void resumeCurrentTrack();
void resumeTrack(Track.Id trackId);
void endCurrentTrack();
boolean isRecording();
/**
* Returns true if the current recording track is paused. Returns true if not recording.
*/
boolean isPaused();
/**
* Gets the current recording track ID. Returns null if not recording.
*/
Track.Id getRecordingTrackId();
/**
* Gets the total time for the current recording track. Returns 0 if not recording.
*/
Duration getTotalTime();
Marker.Id insertMarker(String name, String category, String description, String photoUrl);
/**
* Gets the current sensor data.
*
* @return SensorDataSet object or null.
*/
SensorDataSet getSensorData();
/**
* Gets the current elevation gain.
*
* @return elevation gain in m or null.
*/
Float getElevationGain_m();
/**
* Gets the current elevation loss.
*
* @return elevation loss in m or null.
*/
Float getElevationLoss_m();
@VisibleForTesting
void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager);
/**
* Inserts a track point in the current recording track.
* This is used for inserting special track points or for testing.
*
* @param trackPoint the track point object to be inserted.
* @param recordingGpsAccuracy recording GPS accuracy.
*/
@Deprecated //REMOVE
@VisibleForTesting
void newTrackPoint(TrackPoint trackPoint, int recordingGpsAccuracy);
void addListener(TrackRecordingServiceCallback listener);
GpsStatusValue getGpsStatus();
}
@@ -11,6 +11,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -68,7 +69,7 @@ class TrackRecordingServiceNotificationManager {
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, int recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
if (trackPoint.hasAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getAccuracy(), metricUnits);
formattedAccuracy = StringUtils.formatDistance(context, Distance.of(trackPoint.getAccuracy()), metricUnits);
boolean currentLocationWasAccurate = trackPoint.getAccuracy() < recordingGpsAccuracy;
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
@@ -0,0 +1,83 @@
package de.dennisguse.opentracks.services;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
/**
* This class handles the status of the recording service.
* Also offer an interface through which to know the recording Track.Id, pause status and GPS status.
*/
public class TrackRecordingServiceStatus {
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private final List<Listener> listeners = new ArrayList<>();
void addListener(@NonNull Listener listener) {
if (this.listeners.contains(listener)) {
return;
}
this.listeners.add(listener);
listener.onTrackRecordingId(recordingTrackId);
listener.onTrackRecordingPaused(recordingTrackPaused);
}
void removeListener(@NonNull Listener listener) {
this.listeners.remove(listener);
}
public boolean getRecordingTrackPaused() {
return this.recordingTrackPaused;
}
public Track.Id getRecordingTrackId() {
return this.recordingTrackId;
}
public void onStop() {
for (Listener listener : listeners) {
listener.onGpsStatus(GpsStatusValue.GPS_NONE);
listener.onTrackRecordingId(null);
listener.onTrackRecordingPaused(true);
}
listeners.clear();
}
public boolean isRecording() {
return recordingTrackId != null;
}
void onChange(Track.Id trackId, boolean paused) {
recordingTrackId = trackId;
recordingTrackPaused = paused;
for (Listener listener : listeners) {
listener.onTrackRecordingId(recordingTrackId);
listener.onTrackRecordingPaused(recordingTrackPaused);
}
}
void onChange(boolean paused) {
recordingTrackPaused = paused;
for (Listener listener : listeners) {
listener.onTrackRecordingPaused(recordingTrackPaused);
}
}
void onChange(GpsStatusValue statusValue) {
for (Listener listener : listeners) {
listener.onGpsStatus(statusValue);
}
}
public interface Listener {
default void onGpsStatus(GpsStatusValue newValue) {}
default void onTrackRecordingPaused(boolean paused) {}
default void onTrackRecordingId(Track.Id trackId) {}
}
}
@@ -25,7 +25,7 @@ class GpsStatus {
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(10);
// Threshold for accuracy.
private double signalBadThreshold;
private double signalBadThreshold; //TODO Distance?
// Threshold for time without points.
private Duration signalLostThreshold;
@@ -67,7 +67,7 @@ class GpsStatus {
this.context = context;
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
signalBadThreshold = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
signalBadThreshold = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context).toM();
Duration minRecordingInterval = Duration.ofSeconds(PreferencesUtils.getMinRecordingInterval(sharedPreferences, context));
signalLostThreshold = !minRecordingInterval.isNegative() ? SIGNAL_LOST_THRESHOLD.plus(minRecordingInterval) : SIGNAL_LOST_THRESHOLD;
@@ -78,7 +78,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
if (gpsStatus != null) {
gpsStatus.onRecordingDistanceChanged(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context));
gpsStatus.onRecordingDistanceChanged((int) PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context).toM()); //TODO Use Distance?
}
}
}
@@ -10,17 +10,18 @@ import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.util.PressureSensorUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Estimates the elevation gain and elevation loss using the device's pressure sensor (i.e., barometer).
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/
public class ElevationSumManager implements SensorEventListener {
public class AltitudeSumManager implements SensorEventListener {
private static final String TAG = ElevationSumManager.class.getSimpleName();
private static final String TAG = AltitudeSumManager.class.getSimpleName();
private static final int SAMPLING_RATE = 3 * (int) UnitConversions.ONE_SECOND_US;
private static final Duration SAMPLING_RATE = Duration.ofSeconds(3);
private boolean isConnected = false;
@@ -28,8 +29,8 @@ public class ElevationSumManager implements SensorEventListener {
private float lastSeenSensorValue_hPa;
private float elevationGain_m;
private float elevationLoss_m;
private float altitudeGain_m;
private float altitudeLoss_m;
public void start(Context context) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
@@ -39,7 +40,7 @@ public class ElevationSumManager implements SensorEventListener {
Log.w(TAG, "No pressure sensor available.");
isConnected = false;
} else {
isConnected = sensorManager.registerListener(this, pressureSensor, SAMPLING_RATE);
isConnected = sensorManager.registerListener(this, pressureSensor, SensorManager.SENSOR_DELAY_FASTEST);
}
lastAcceptedPressureValue_hPa = Float.NaN;
@@ -66,19 +67,19 @@ public class ElevationSumManager implements SensorEventListener {
}
public @Nullable
Float getElevationGain_m() {
return isConnected ? elevationGain_m : null;
Float getAltitudeGain_m() {
return isConnected ? altitudeGain_m : null;
}
public @Nullable
Float getElevationLoss_m() {
return isConnected ? elevationLoss_m : null;
Float getAltitudeLoss_m() {
return isConnected ? altitudeLoss_m : null;
}
public void reset() {
Log.d(TAG, "Reset");
elevationGain_m = 0;
elevationLoss_m = 0;
altitudeGain_m = 0;
altitudeLoss_m = 0;
}
@Override
@@ -103,15 +104,15 @@ public class ElevationSumManager implements SensorEventListener {
return;
}
PressureSensorUtils.ElevationChange elevationChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedPressureValue_hPa, lastSeenSensorValue_hPa, value_hPa);
if (elevationChange != null) {
elevationGain_m += elevationChange.getElevationGain_m();
elevationLoss_m += elevationChange.getElevationLoss_m();
lastAcceptedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa();
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedPressureValue_hPa, lastSeenSensorValue_hPa, value_hPa);
if (altitudeChange != null) {
altitudeGain_m += altitudeChange.getAltitudeGain_m();
altitudeLoss_m += altitudeChange.getAltitudeLoss_m();
lastAcceptedPressureValue_hPa = altitudeChange.getCurrentSensorValue_hPa();
}
lastSeenSensorValue_hPa = value_hPa;
Log.v(TAG, "elevation gain: " + elevationGain_m + ", elevation loss: " + elevationLoss_m);
Log.v(TAG, "altitude gain: " + altitudeGain_m + ", altitude loss: " + altitudeLoss_m);
}
}
@@ -93,9 +93,8 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
connect(cyclingPower, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, key)) {
preferenceWheelCircumference = PreferencesUtils.getWheelCircumference(sharedPreferences, context);
}
}
};
@@ -185,7 +184,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
Log.d(TAG, "onChanged: speed data repeated.");
return;
}
((SensorDataCycling.DistanceSpeed) sensorData).compute(previous, PreferencesUtils.getWheelCircumference(sharedPreferences, context));
((SensorDataCycling.DistanceSpeed) sensorData).compute(previous, preferenceWheelCircumference);
}
sensorDataSet.set(sensorData);
@@ -24,8 +24,10 @@ import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -34,7 +36,6 @@ import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.AnnouncementUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
/**
@@ -139,7 +140,8 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
Log.e(TAG, "TrackRecordingService is null.");
return;
}
announce(trackRecordingService.getTrackStatistics());
announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics());
}
/**
@@ -147,7 +149,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
*
* @param trackStatistics the track statistics
*/
private void announce(TrackStatistics trackStatistics) {
private void announce(Track.Id trackId, TrackStatistics trackStatistics) {
if (trackStatistics == null) {
Log.e(TAG, "TrackStatistics is null.");
return;
@@ -166,13 +168,18 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
}
}
if (audioManager.getMode() == AudioManager.MODE_IN_CALL || audioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION) {
if (Arrays.asList(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION)
.contains(audioManager.getMode())) {
Log.i(TAG, "Speech is not allowed at this time.");
return;
}
Track track = contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(sharedPreferences, context));
String category = track != null ? track.getCategory() : "";
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.i(TAG, "It doesn't exists a track with trackid = " + track);
return;
}
String category = track.getCategory();
//TODO Querying all TrackPoints all the time is inefficient; use TrackDataHub or something else.
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
@@ -184,7 +191,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category);
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, (float) (isMetricUnits ? 1d * UnitConversions.KM_TO_M : 1d * UnitConversions.MI_TO_M));
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.one(isMetricUnits));
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval);
@@ -20,9 +20,9 @@ import android.util.Log;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Execute a periodic task on a time or distance schedule.
@@ -128,12 +128,9 @@ public class PeriodicTaskExecutor {
return;
}
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
Distance distance = trackStatistics.getTotalDistance();
if (distance > nextTaskDistance) {
if (distance.greaterThan(Distance.of(nextTaskDistance))) {
periodicTask.run(trackRecordingService);
calculateNextTaskDistance();
}
@@ -178,10 +175,7 @@ public class PeriodicTaskExecutor {
return;
}
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
double distance = trackStatistics.getTotalDistance().to(metricUnits);
// The index will be negative since the frequency is negative.
int index = (int) (distance / taskFrequency);
@@ -32,7 +32,7 @@ final class PreferenceHelper {
String[] entryValues = context.getResources().getStringArray(R.array.recording_distance_interval_values);
String[] entries = new String[entryValues.length];
final int recordingDistanceIntervalDefault = PreferencesUtils.getRecordingDistanceIntervalDefault(context);
final int recordingDistanceIntervalDefault = (int) PreferencesUtils.getRecordingDistanceIntervalDefault(context).toM();
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
@@ -20,8 +20,12 @@ import java.util.Locale;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
@@ -89,14 +93,11 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, prefsFragment).commit();
}
public static class PrefsFragment extends PreferenceFragmentCompat {
public static class PrefsFragment extends PreferenceFragmentCompat implements TrackRecordingServiceStatus.Listener {
private SharedPreferences sharedPreferences;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(getActivity(), R.string.recording_track_id_key, key)) {
getActivity().runOnUiThread(this::updateReset);
}
if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) {
getActivity().runOnUiThread(this::updateUnits);
}
@@ -105,6 +106,19 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}
};
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private boolean isRecording;
private final Runnable bindServiceCallback = new Runnable() {
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(PrefsFragment.this);
}
}
};
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog;
@@ -112,6 +126,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindServiceCallback);
try {
setPreferencesFromResource(R.xml.settings, rootKey);
} catch (ClassCastException e) {
@@ -137,13 +153,14 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
public void onResume() {
super.onResume();
trackRecordingServiceConnection.bind(getContext());
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
updateUnits();
updateReset();
updatePrefsDependOnRecording();
updateBluetooth();
updatePostWorkoutExport();
}
@@ -186,6 +203,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
@Override
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection.unbind(getContext());
sharedPreferences = null;
}
@@ -195,11 +213,18 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}
}
private void updateReset() {
final boolean isRecording = PreferencesUtils.isRecording(sharedPreferences, getActivity());
private void updatePrefsDependOnRecording() {
Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
Preference importPreference = findPreference(getString(R.string.settings_import));
Preference exportPreference = findPreference(getString(R.string.settings_export));
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
importPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
exportPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
resetPreference.setEnabled(!isRecording);
importPreference.setEnabled(!isRecording);
exportPreference.setEnabled(!isRecording);
}
private void updateBluetooth() {
@@ -266,6 +291,14 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
return false;
});
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
if (trackId != null && !isRecording && isAdded()) {
isRecording = true;
updatePrefsDependOnRecording();
}
}
}
}
@@ -22,6 +22,9 @@ import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
/**
* Statistical data about a {@link de.dennisguse.opentracks.content.data.Track}.
* The data in this class should be filled out by {@link TrackStatisticsUpdater}.
@@ -32,25 +35,23 @@ import java.time.Instant;
//TODO Check that data ranges are valid (not less than zero etc.)
public class TrackStatistics {
// The min and max elevation (meters) seen on this track.
private final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
// The min and max altitude (meters) seen on this track.
private final ExtremityMonitor altitudeExtremities = new ExtremityMonitor();
// The track start time.
private Instant startTime;
// The track stop time.
private Instant stopTime;
private double totalDistance_m;
private Distance totalDistance;
// Updated when new points are received, may be stale.
private Duration totalTime;
// Based on when we believe the user is traveling.
private Duration movingTime;
// The maximum speed (meters/second) that we believe is valid.
private double maxSpeed_mps;
// The total elevation gained (meters).
private Float totalElevationGain_m = null;
// The total elevation lost (meters).
private Float totalElevationLoss_m = null;
private Speed maxSpeed;
private Float totalAltitudeGain_m = null;
private Float totalAltitudeLoss_m = null;
public TrackStatistics() {
reset();
@@ -64,13 +65,13 @@ public class TrackStatistics {
public TrackStatistics(TrackStatistics other) {
startTime = other.startTime;
stopTime = other.stopTime;
totalDistance_m = other.totalDistance_m;
totalDistance = other.totalDistance;
totalTime = other.totalTime;
movingTime = other.movingTime;
maxSpeed_mps = other.maxSpeed_mps;
elevationExtremities.set(other.elevationExtremities.getMin(), other.elevationExtremities.getMax());
totalElevationGain_m = other.totalElevationGain_m;
totalElevationLoss_m = other.totalElevationLoss_m;
maxSpeed = other.maxSpeed;
altitudeExtremities.set(other.altitudeExtremities.getMin(), other.altitudeExtremities.getMax());
totalAltitudeGain_m = other.totalAltitudeGain_m;
totalAltitudeLoss_m = other.totalAltitudeLoss_m;
}
/**
@@ -91,30 +92,30 @@ public class TrackStatistics {
stopTime = stopTime.isAfter(other.stopTime) ? stopTime : other.stopTime;
}
totalDistance_m += other.totalDistance_m;
totalDistance = totalDistance.plus(other.totalDistance);
totalTime = totalTime.plus(other.totalTime);
movingTime = movingTime.plus(other.movingTime);
maxSpeed_mps = Math.max(maxSpeed_mps, other.maxSpeed_mps);
if (other.elevationExtremities.hasData()) {
elevationExtremities.update(other.elevationExtremities.getMin());
elevationExtremities.update(other.elevationExtremities.getMax());
maxSpeed = Speed.max(maxSpeed, other.maxSpeed);
if (other.altitudeExtremities.hasData()) {
altitudeExtremities.update(other.altitudeExtremities.getMin());
altitudeExtremities.update(other.altitudeExtremities.getMax());
}
if (totalElevationGain_m == null) {
if (other.totalElevationGain_m != null) {
totalElevationGain_m = other.totalElevationGain_m;
if (totalAltitudeGain_m == null) {
if (other.totalAltitudeGain_m != null) {
totalAltitudeGain_m = other.totalAltitudeGain_m;
}
} else {
if (other.totalElevationGain_m != null) {
totalElevationGain_m += other.totalElevationGain_m;
if (other.totalAltitudeGain_m != null) {
totalAltitudeGain_m += other.totalAltitudeGain_m;
}
}
if (totalElevationLoss_m == null) {
if (other.totalElevationLoss_m != null) {
totalElevationLoss_m = other.totalElevationLoss_m;
if (totalAltitudeLoss_m == null) {
if (other.totalAltitudeLoss_m != null) {
totalAltitudeLoss_m = other.totalAltitudeLoss_m;
}
} else {
if (other.totalElevationLoss_m != null) {
totalElevationLoss_m += other.totalElevationLoss_m;
if (other.totalAltitudeLoss_m != null) {
totalAltitudeLoss_m += other.totalAltitudeLoss_m;
}
}
}
@@ -123,12 +124,12 @@ public class TrackStatistics {
startTime = null;
stopTime = null;
setTotalDistance(0);
setTotalDistance(Distance.of(0));
setTotalTime(Duration.ofSeconds(0));
setMovingTime(Duration.ofSeconds(0));
setMaxSpeed(0);
setTotalElevationGain(null);
setTotalElevationLoss(null);
setMaxSpeed(Speed.zero());
setTotalAltitudeGain(null);
setTotalAltitudeLoss(null);
}
public void reset(Instant startTime) {
@@ -159,16 +160,16 @@ public class TrackStatistics {
this.stopTime = stopTime;
}
public double getTotalDistance() {
return totalDistance_m;
public Distance getTotalDistance() {
return totalDistance;
}
public void setTotalDistance(double totalDistance_m) {
this.totalDistance_m = totalDistance_m;
public void setTotalDistance(Distance totalDistance_m) {
this.totalDistance = totalDistance_m;
}
public void addTotalDistance(double distance_m) {
totalDistance_m += distance_m;
public void addTotalDistance(Distance distance_m) {
totalDistance = totalDistance.plus(distance_m);
}
/**
@@ -197,131 +198,98 @@ public class TrackStatistics {
}
/**
* Gets the average speed in meters/second.
* Gets the average speed.
* This calculation only takes into account the displacement until the last point that was accounted for in statistics.
*/
public double getAverageSpeed() {
if (totalTime.isZero()) {
return 0.0;
public Speed getAverageSpeed() {
if (totalDistance.isZero() && totalDistance.isZero()) {
return Speed.of(0);
}
return totalDistance_m / (double) totalTime.getSeconds();
return Speed.of(totalDistance.toM() / totalTime.getSeconds());
}
public Speed getAverageMovingSpeed() {
return Speed.of(totalDistance, movingTime);
}
public Speed getMaxSpeed() {
return Speed.max(maxSpeed, getAverageMovingSpeed());
}
public void setMaxSpeed(Speed maxSpeed) {
this.maxSpeed = maxSpeed;
}
public boolean hasAltitudeMin() {
return !Double.isInfinite(getMinAltitude());
}
public double getMinAltitude() {
return altitudeExtremities.getMin();
}
public void setMinAltitude(double altitude_m) {
altitudeExtremities.setMin(altitude_m);
}
public boolean hasAltitudeMax() {
return !Double.isInfinite(getMaxAltitude());
}
/**
* Gets the average moving speed in meters/second.
* Gets the maximum altitude.
* This is calculated from the smoothed altitude, so this can actually be less than the current altitude.
*/
public double getAverageMovingSpeed() {
if (movingTime.isZero()) {
return 0.0;
}
return totalDistance_m / (double) movingTime.getSeconds();
public double getMaxAltitude() {
return altitudeExtremities.getMax();
}
/**
* Gets the maximum speed in meters/second.
*/
public double getMaxSpeed() {
return Math.max(maxSpeed_mps, getAverageMovingSpeed());
public void setMaxAltitude(double altitude_m) {
altitudeExtremities.setMax(altitude_m);
}
/**
* Sets the maximum speed.
*
* @param maxSpeed the maximum speed in meters/second
*/
public void setMaxSpeed(double maxSpeed) {
this.maxSpeed_mps = maxSpeed;
public void updateAltitudeExtremities(double altitude_m) {
altitudeExtremities.update(altitude_m);
}
public boolean hasElevationMin() {
return !Double.isInfinite(getMinElevation());
}
/**
* Gets the minimum elevation.
* This is calculated from the smoothed elevation, so this can actually be more than the current elevation.
*/
public double getMinElevation() {
return elevationExtremities.getMin();
}
/**
* Sets the minimum elevation.
*
* @param elevation the minimum elevation in meters
*/
public void setMinElevation(double elevation) {
elevationExtremities.setMin(elevation);
}
public boolean hasElevationMax() {
return !Double.isInfinite(getMaxElevation());
}
/**
* Gets the maximum elevation.
* This is calculated from the smoothed elevation, so this can actually be less than the current elevation.
*/
public double getMaxElevation() {
return elevationExtremities.getMax();
}
/**
* Sets the maximum elevation.
*
* @param elevation_m the maximum elevation in meters
*/
public void setMaxElevation(double elevation_m) {
elevationExtremities.setMax(elevation_m);
}
/**
* Updates a new elevation.
*
* @param elevation_m the elevation value in meters
*/
public void updateElevationExtremities(double elevation_m) {
elevationExtremities.update(elevation_m);
}
public boolean hasTotalElevationGain() {
return totalElevationGain_m != null;
public boolean hasTotalAltitudeGain() {
return totalAltitudeGain_m != null;
}
@Nullable
public Float getTotalElevationGain() {
return totalElevationGain_m;
public Float getTotalAltitudeGain() {
return totalAltitudeGain_m;
}
public void setTotalElevationGain(Float totalElevationGain_m) {
this.totalElevationGain_m = totalElevationGain_m;
public void setTotalAltitudeGain(Float totalAltitudeGain_m) {
this.totalAltitudeGain_m = totalAltitudeGain_m;
}
public void addTotalElevationGain(float gain_m) {
if (totalElevationGain_m == null) {
totalElevationGain_m = 0f;
public void addTotalAltitudeGain(float gain_m) {
if (totalAltitudeGain_m == null) {
totalAltitudeGain_m = 0f;
}
totalElevationGain_m += gain_m;
totalAltitudeGain_m += gain_m;
}
public boolean hasTotalElevationLoss() {
return totalElevationLoss_m != null;
public boolean hasTotalAltitudeLoss() {
return totalAltitudeLoss_m != null;
}
@Nullable
public Float getTotalElevationLoss() {
return totalElevationLoss_m;
public Float getTotalAltitudeLoss() {
return totalAltitudeLoss_m;
}
public void setTotalElevationLoss(Float totalElevationLoss_m) {
this.totalElevationLoss_m = totalElevationLoss_m;
public void setTotalAltitudeLoss(Float totalAltitudeLoss_m) {
this.totalAltitudeLoss_m = totalAltitudeLoss_m;
}
public void addTotalElevationLoss(float loss_m) {
if (totalElevationLoss_m == null) {
totalElevationLoss_m = 0f;
public void addTotalAltitudeLoss(float loss_m) {
if (totalAltitudeLoss_m == null) {
totalAltitudeLoss_m = 0f;
}
totalElevationLoss_m += loss_m;
totalAltitudeLoss_m += loss_m;
}
@NonNull
@@ -330,8 +298,8 @@ public class TrackStatistics {
return "TrackStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime()
+ "; Total Distance: " + getTotalDistance() + "; Total Time: " + getTotalTime()
+ "; Moving Time: " + getMovingTime() + "; Max Speed: " + getMaxSpeed()
+ "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation()
+ "; Elevation Gain: " + getTotalElevationGain()
+ "; Elevation Loss: " + getTotalElevationLoss() + "}";
+ "; Min Altitude: " + getMinAltitude() + "; Max Altitude: " + getMaxAltitude()
+ "; Altitude Gain: " + getTotalAltitudeGain()
+ "; Altitude Loss: " + getTotalAltitudeLoss() + "}";
}
}
@@ -23,8 +23,9 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
/**
* Updater for {@link TrackStatistics}.
@@ -38,10 +39,10 @@ import de.dennisguse.opentracks.content.provider.TrackPointIterator;
public class TrackStatisticsUpdater {
/**
* The number of elevation readings to smooth to get a somewhat accurate signal.
* The number of altitude readings to smooth to get a somewhat accurate signal.
*/
@VisibleForTesting
private static final int ELEVATION_SMOOTHING_FACTOR = 25;
private static final int ALTITUDE_SMOOTHING_FACTOR = 25;
/**
* The number of speed reading to smooth to get a somewhat accurate signal.
@@ -60,13 +61,10 @@ public class TrackStatisticsUpdater {
private boolean trackInitialized = false;
private boolean segmentInitialized = false;
// The track's statistics
private final TrackStatistics trackStatistics;
// A buffer of the recent elevation readings (m)
private final DoubleRingBuffer elevationBuffer_m = new DoubleRingBuffer(ELEVATION_SMOOTHING_FACTOR);
// A buffer of the recent speed readings (m/s) for calculating max speed
private final DoubleRingBuffer speedBuffer_ms = new DoubleRingBuffer(SPEED_SMOOTHING_FACTOR);
private final DoubleRingBuffer altitudeBuffer_m = new DoubleRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
private final DoubleRingBuffer speedBuffer_mps = new DoubleRingBuffer(SPEED_SMOOTHING_FACTOR);
// The current segment's statistics
private final TrackStatistics currentSegment = new TrackStatistics();
@@ -106,12 +104,12 @@ public class TrackStatisticsUpdater {
* @param trackPoint the trackPoint
* @param minGPSDistance the min recording distance
*/
public void addTrackPoint(TrackPoint trackPoint, int minGPSDistance) {
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
internalAddTrackPoint(trackPoint, minGPSDistance);
Log.d(TAG, this.toString());
Log.v(TAG, this.toString());
}
private void internalAddTrackPoint(TrackPoint trackPoint, int minGPSDistance) {
private void internalAddTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
if (!trackInitialized) {
trackStatistics.setStartTime(trackPoint.getTime());
trackInitialized = true;
@@ -125,18 +123,18 @@ public class TrackStatisticsUpdater {
currentSegment.setStopTime(trackPoint.getTime());
currentSegment.setTotalTime(Duration.between(currentSegment.getStartTime(), trackPoint.getTime()));
if (trackPoint.isSegmentStart()) {
if (trackPoint.getType() == TrackPoint.Type.SEGMENT_START_MANUAL) {
reset(trackPoint);
return;
}
// Process sensor data
if (trackPoint.hasElevationGain()) {
currentSegment.addTotalElevationGain(trackPoint.getElevationGain());
if (trackPoint.hasAltitudeGain()) {
currentSegment.addTotalAltitudeGain(trackPoint.getAltitudeGain());
}
if (trackPoint.hasElevationLoss()) {
currentSegment.addTotalElevationLoss(trackPoint.getElevationLoss());
if (trackPoint.hasAltitudeLoss()) {
currentSegment.addTotalAltitudeLoss(trackPoint.getAltitudeLoss());
}
if (trackPoint.hasSensorDistance()) {
@@ -148,9 +146,9 @@ public class TrackStatisticsUpdater {
return;
}
//Update absolute (GPS-based) elevation
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
updateAbsoluteElevation(trackPoint.getAltitude());
updateAbsoluteAltitude(trackPoint.getAltitude());
}
if (lastTrackPoint == null || lastMovingTrackPoint == null) {
@@ -161,9 +159,9 @@ public class TrackStatisticsUpdater {
if (!trackPoint.hasSensorDistance()) {
// GPS-based distance/speed
float movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
if (movingDistance < minGPSDistance && !trackPoint.isMoving()) {
speedBuffer_ms.reset();
Distance movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
speedBuffer_mps.reset();
lastTrackPoint = trackPoint;
return;
}
@@ -185,6 +183,11 @@ public class TrackStatisticsUpdater {
updateSpeed(trackPoint, lastTrackPoint);
}
if (trackPoint.getType() == TrackPoint.Type.SEGMENT_START_AUTOMATIC) {
reset(trackPoint);
return;
}
lastTrackPoint = trackPoint;
lastMovingTrackPoint = trackPoint;
}
@@ -195,27 +198,20 @@ public class TrackStatisticsUpdater {
lastTrackPoint = null;
lastMovingTrackPoint = null;
elevationBuffer_m.reset();
speedBuffer_ms.reset();
}
public void addTrackPoint(TrackPointIterator iterator, int minRecordingDistance) {
while (iterator.hasNext()) {
TrackPoint location = iterator.next();
addTrackPoint(location, minRecordingDistance);
}
altitudeBuffer_m.reset();
speedBuffer_mps.reset();
}
/**
* Gets the smoothed elevation over several readings.
* The elevation readings is noisy so the smoothed elevation is better than the raw elevation for many tasks.
* Gets the smoothed altitude over several readings.
* The altitude readings is noisy so the smoothed altitude is better than the raw altitude for many tasks.
*/
public double getSmoothedElevation() {
return elevationBuffer_m.getAverage();
public double getSmoothedAltitude() {
return altitudeBuffer_m.getAverage();
}
public double getSmoothedSpeed() {
return speedBuffer_ms.getAverage();
public Speed getSmoothedSpeed() {
return Speed.of(speedBuffer_mps.getAverage());
}
/**
@@ -224,52 +220,49 @@ public class TrackStatisticsUpdater {
@VisibleForTesting
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
if (!trackPoint.isMoving()) {
speedBuffer_ms.reset();
speedBuffer_mps.reset();
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_ms.setNext(trackPoint.getSpeed());
if (speedBuffer_ms.getAverage() > currentSegment.getMaxSpeed()) {
currentSegment.setMaxSpeed(speedBuffer_ms.getAverage());
speedBuffer_mps.setNext(trackPoint.getSpeed().toMPS());
Speed average = Speed.of(speedBuffer_mps.getAverage());
if (average.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(average);
}
} else {
Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
}
}
/**
* Updates an elevation reading.
*/
private void updateAbsoluteElevation(double elevation) {
// Update elevation using the smoothed average
double oldAverage = elevationBuffer_m.getAverage();
elevationBuffer_m.setNext(elevation);
double newAverage = elevationBuffer_m.getAverage();
private void updateAbsoluteAltitude(double altitude) {
// Update altitude using the smoothed average
double oldAverage = altitudeBuffer_m.getAverage();
altitudeBuffer_m.setNext(altitude);
double newAverage = altitudeBuffer_m.getAverage();
currentSegment.updateElevationExtremities(newAverage);
currentSegment.updateAltitudeExtremities(newAverage);
}
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
// There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last.
if (trackPoint.getSpeed() == 0) {
if (trackPoint.getSpeed().isZero()) {
return false;
}
// The following code will ignore unlikely readings. 128 m/s seems to be an internal android error code.
if (Math.abs(trackPoint.getSpeed() - 128) < 1) {
return false;
}
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
double speedDifference = Math.abs(lastTrackPoint.getSpeed() - trackPoint.getSpeed());
if (speedDifference > MAX_ACCELERATION * timeDifference.toMillis()) {
return false;
Speed maxAcceleration = Speed.of(MAX_ACCELERATION * timeDifference.toMillis());
{
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
if (speedDifference.greaterThan(maxAcceleration)) {
return false;
}
}
// Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration.
if (speedBuffer_ms.isFull()) {
double average = speedBuffer_ms.getAverage();
double diff = Math.abs(average - trackPoint.getSpeed());
return (trackPoint.getSpeed() < average * 10) && (diff < MAX_ACCELERATION * timeDifference.toMillis());
if (speedBuffer_mps.isFull()) {
Speed average = Speed.of(speedBuffer_mps.getAverage());
Speed speedDifference = Speed.absDiff(average, trackPoint.getSpeed());
return trackPoint.getSpeed().lessThan(average.mul(10)) && speedDifference.lessThan(maxAcceleration);
}
return true;
@@ -7,6 +7,8 @@ import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
@@ -16,43 +18,39 @@ public class AnnouncementUtils {
}
public static String getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double distancePerTime = trackStatistics.getAverageMovingSpeed() * UnitConversions.MPS_TO_KMH;
double currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH : 0;
Distance distance = trackStatistics.getTotalDistance();
Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
if (distance == 0) {
if (distance.isZero()) {
return context.getString(R.string.voice_total_distance_zero);
}
if (!isMetricUnits) {
distance *= UnitConversions.KM_TO_MI;
distancePerTime *= UnitConversions.KM_TO_MI;
currentDistancePerTime *= UnitConversions.KM_TO_MI;
}
String rate;
String currentRate;
String currentRateMsg;
if (isReportSpeed) {
int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime);
double distanceInUnit = distancePerTime.to(isMetricUnits);
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRate = context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTime), currentDistancePerTime);
double currentDistancePerTimeInUnit = currentDistancePerTime != null ? currentDistancePerTime.to(isMetricUnits) : 0;
currentRate = context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit);
currentRateMsg = context.getString(R.string.voice_speed_lap, currentRate);
} else {
double timePerDistance = distancePerTime == 0 ? 0.0 : 1 / distancePerTime;
Duration time = distancePerTime.toPace(isMetricUnits);
int paceId = isMetricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
Duration time = Duration.ofMillis((long) (timePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS));
rate = context.getString(paceId, getAnnounceTime(context, time));
double currentTimePerDistance = currentDistancePerTime == 0 ? 0.0 : 1 / currentDistancePerTime;
Duration currentTime = Duration.ofMillis((long) (currentTimePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS));
Duration currentTime = currentDistancePerTime.toPace(isMetricUnits);
currentRate = context.getString(paceId, getAnnounceTime(context, currentTime));
currentRateMsg = context.getString(R.string.voice_pace_lap, currentRate);
}
int totalDistanceId = isMetricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
double distanceInUnit = distance.to(isMetricUnits);
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg;
@@ -27,7 +27,7 @@ import androidx.documentfile.provider.DocumentFile;
import androidx.preference.PreferenceManager;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
/**
@@ -47,21 +47,6 @@ public class PreferencesUtils {
return PreferenceManager.getDefaultSharedPreferences(context);
}
@Deprecated
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
//NOTE: This was also used to recover from a service restart, but this data should not be exposed to the whole application.
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
@Deprecated //Use the TrackRecordingService
public static Track.Id getRecordingTrackId(Context context, SharedPreferences sharedPreferences) {
return new Track.Id(sharedPreferences.getLong(getKey(context, R.string.recording_track_id_key), RECORDING_TRACK_ID_DEFAULT));
}
@Deprecated //Use the TrackRecordingService
public static Track.Id getRecordingTrackId(SharedPreferences sharedPreferences, Context context) {
return getRecordingTrackId(context, sharedPreferences);
}
public static String getDefaultActivity(SharedPreferences sharedPreferences, Context context) {
return getString(sharedPreferences, context, R.string.default_activity_key, context.getString(R.string.default_activity_default));
}
@@ -95,14 +80,6 @@ public class PreferencesUtils {
return sharedPreferences.getBoolean(getKey(context, keyId), defaultValue);
}
//TODO Don't use; this function is only to be used TrackRecordingService and will be removed.
@VisibleForTesting
public static void setBoolean(SharedPreferences sharedPreferences, Context context, int keyId, boolean value) {
Editor editor = sharedPreferences.edit();
editor.putBoolean(getKey(context, keyId), value);
editor.apply();
}
private static int getInt(SharedPreferences sharedPreferences, Context context, int keyId, int defaultValue) {
try {
return sharedPreferences.getInt(getKey(context, keyId), defaultValue);
@@ -119,22 +96,6 @@ public class PreferencesUtils {
}
}
@VisibleForTesting
public static void setInt(SharedPreferences sharedPreferences, Context context, int keyId, int value) {
Editor editor = sharedPreferences.edit();
editor.putInt(getKey(context, keyId), value);
editor.apply();
}
//TODO Don't use; this function is only to be used TrackRecordingService and will be removed.
@Deprecated
@VisibleForTesting
public static void setLong(SharedPreferences sharedPreferences, Context context, int keyId, long value) {
Editor editor = sharedPreferences.edit();
editor.putLong(getKey(context, keyId), value);
editor.apply();
}
public static String getString(SharedPreferences sharedPreferences, Context context, int keyId, String defaultValue) {
return sharedPreferences.getString(getKey(context, keyId), defaultValue);
}
@@ -161,22 +122,6 @@ public class PreferencesUtils {
return currentStatsRate.equals(context.getString(R.string.stats_rate_speed));
}
@Deprecated //Use TrackRecordingService
public static boolean isRecordingTrackPaused(SharedPreferences sharedPreferences, Context context) {
return getBoolean(sharedPreferences, context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(context));
}
@Deprecated //Use TrackRecordingService
public static boolean isRecordingTrackPausedDefault(Context context) {
return context.getResources().getBoolean(R.bool.recording_track_paused_default);
}
@Deprecated //Use TrackRecordingService
public static void defaultRecordingTrackPaused(SharedPreferences sharedPreferences, Context context) {
final boolean RECORDING_TRACK_PAUSED = context.getResources().getBoolean(R.bool.recording_track_paused_default);
setBoolean(sharedPreferences, context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
}
private static String getBluetoothSensorAddressNone(Context context) {
return context.getString(R.string.sensor_type_value_none);
}
@@ -221,9 +166,9 @@ public class PreferencesUtils {
return getBoolean(sharedPreferences, context, R.string.stats_fullscreen_while_recording_key, DEFAULT);
}
public static boolean isShowStatsElevation(SharedPreferences sharedPreferences, Context context) {
final boolean STATS_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_elevation_default);
return getBoolean(sharedPreferences, context, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
public static boolean isShowStatsAltitude(SharedPreferences sharedPreferences, Context context) {
final boolean STATS_SHOW_ALTITUDE = context.getResources().getBoolean(R.bool.stats_show_altitude_default);
return getBoolean(sharedPreferences, context, R.string.stats_show_grade_altitude_key, STATS_SHOW_ALTITUDE);
}
public static boolean isStatsShowCoordinate(SharedPreferences sharedPreferences, Context context) {
@@ -236,17 +181,21 @@ public class PreferencesUtils {
return getInt(sharedPreferences, context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
}
public static int getRecordingDistanceInterval(SharedPreferences sharedPreferences, Context context) {
return getInt(sharedPreferences, context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context));
public static Distance getRecordingDistanceInterval(SharedPreferences sharedPreferences, Context context) {
return Distance.of(getInt(sharedPreferences, context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefaultInternal(context)));
}
public static int getRecordingDistanceIntervalDefault(Context context) {
public static Distance getRecordingDistanceIntervalDefault(Context context) {
return Distance.of(getRecordingDistanceIntervalDefaultInternal(context));
}
private static int getRecordingDistanceIntervalDefaultInternal(Context context) {
return Integer.parseInt(context.getResources().getString(R.string.recording_distance_interval_default));
}
public static int getMaxRecordingDistance(SharedPreferences sharedPreferences, Context context) {
public static Distance getMaxRecordingDistance(SharedPreferences sharedPreferences, Context context) {
final int MAX_RECORDING_DISTANCE = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
return getInt(sharedPreferences, context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE);
return Distance.of(getInt(sharedPreferences, context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE));
}
//TODO Duration
@@ -274,7 +223,7 @@ public class PreferencesUtils {
public static boolean shouldInstantExportAfterWorkout(SharedPreferences sharedPreferences, Context context) {
final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = context.getResources().getBoolean(R.bool.post_workout_export_enabled_default);
return getBoolean(sharedPreferences, context, R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT);
return getBoolean(sharedPreferences, context, R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri(sharedPreferences, context);
}
public static TrackFileFormat getExportTrackFileFormat(SharedPreferences sharedPreferences, Context context) {
@@ -302,20 +251,6 @@ public class PreferencesUtils {
return Integer.parseInt(value);
}
@Deprecated //Use TrackRecordingService
public static boolean isRecording(SharedPreferences sharedPreferences, Context context) {
return isRecording(getRecordingTrackId(context, sharedPreferences));
}
@Deprecated
//TODO Method is very misleading: it only checks if the provided trackId not the default value (i.e., not recording).
public static boolean isRecording(Track.Id recordingTrackId) {
if (recordingTrackId == null) {
return false;
}
return recordingTrackId.getId() != RECORDING_TRACK_ID_DEFAULT;
}
public static SharedPreferences resetPreferences(Context context, boolean readAgain) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
if (readAgain) {
@@ -4,76 +4,76 @@ import android.hardware.SensorManager;
public class PressureSensorUtils {
//Everything above is considered a meaningful change in elevation.
private static final float ELEVATION_CHANGE_DIFF_M = 3.0f;
//Everything above is considered a meaningful change in altitude.
private static final float ALTITUDE_CHANGE_DIFF_M = 3.0f;
private static final float EXPONENTIAL_SMOOTHING = 0.3f;
private PressureSensorUtils() {
}
public static class ElevationChange {
public static class AltitudeChange {
private final float currentSensorValue_hPa;
private final float elevationChange_m;
private final float altitudeChange_m;
public ElevationChange(float currentSensorValue_hPa, float elevationChange_m) {
public AltitudeChange(float currentSensorValue_hPa, float altitudeChange_m) {
this.currentSensorValue_hPa = currentSensorValue_hPa;
this.elevationChange_m = elevationChange_m;
this.altitudeChange_m = altitudeChange_m;
}
public float getCurrentSensorValue_hPa() {
return currentSensorValue_hPa;
}
public float getElevationChange_m() {
return elevationChange_m;
public float getAltitudeChange_m() {
return altitudeChange_m;
}
public float getElevationGain_m() {
return elevationChange_m > 0 ? elevationChange_m : 0;
public float getAltitudeGain_m() {
return altitudeChange_m > 0 ? altitudeChange_m : 0;
}
public float getElevationLoss_m() {
return elevationChange_m < 0 ? -1 * elevationChange_m : 0;
public float getAltitudeLoss_m() {
return altitudeChange_m < 0 ? -1 * altitudeChange_m : 0;
}
}
/**
* Applies exponential smoothing to sensor value before computation.
*/
public static ElevationChange computeChangesWithSmoothing_m(float lastAcceptedSensorValue_hPa, float lastSeenSensorValue_hPa, float currentSensorValue_hPa) {
public static AltitudeChange computeChangesWithSmoothing_m(float lastAcceptedSensorValue_hPa, float lastSeenSensorValue_hPa, float currentSensorValue_hPa) {
float nextSensorValue_hPa = EXPONENTIAL_SMOOTHING * currentSensorValue_hPa + (1 - EXPONENTIAL_SMOOTHING) * lastSeenSensorValue_hPa;
return computeChanges_m(lastAcceptedSensorValue_hPa, nextSensorValue_hPa);
}
/**
* Computes the elevation gain and elevation loss.
* Computes the altitude gain and altitude loss.
*
* @return null if no meaningful elevation change occurred.
* @return null if no meaningful altitudeC change occurred.
*/
public static ElevationChange computeChanges_m(float lastAcceptedSensorValue_hPa, float currentSensorValue_hPa) {
public static AltitudeChange computeChanges_m(float lastAcceptedSensorValue_hPa, float currentSensorValue_hPa) {
float lastSensorValue_m = SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastAcceptedSensorValue_hPa);
float currentSensorValue_m = SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, currentSensorValue_hPa);
float elevationChange_m = currentSensorValue_m - lastSensorValue_m;
if (Math.abs(elevationChange_m) < ELEVATION_CHANGE_DIFF_M) {
float altitudeChange_m = currentSensorValue_m - lastSensorValue_m;
if (Math.abs(altitudeChange_m) < ALTITUDE_CHANGE_DIFF_M) {
return null;
}
// Limit elevation change by ELEVATION_CHANGE_DIFF and computes pressure value accordingly.
ElevationChange elevationChange = new ElevationChange(currentSensorValue_hPa, elevationChange_m);
if (elevationChange.getElevationChange_m() > 0) {
return new ElevationChange(getBarometricPressure(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastSensorValue_m + ELEVATION_CHANGE_DIFF_M), ELEVATION_CHANGE_DIFF_M);
// Limit altitudeC change by ALTITUDE_CHANGE_DIFF and computes pressure value accordingly.
AltitudeChange altitudeChange = new AltitudeChange(currentSensorValue_hPa, altitudeChange_m);
if (altitudeChange.getAltitudeChange_m() > 0) {
return new AltitudeChange(getBarometricPressure(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastSensorValue_m + ALTITUDE_CHANGE_DIFF_M), ALTITUDE_CHANGE_DIFF_M);
} else {
return new ElevationChange(getBarometricPressure(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastSensorValue_m - ELEVATION_CHANGE_DIFF_M), -1 * ELEVATION_CHANGE_DIFF_M);
return new AltitudeChange(getBarometricPressure(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastSensorValue_m - ALTITUDE_CHANGE_DIFF_M), -1 * ALTITUDE_CHANGE_DIFF_M);
}
}
/*
* Barometeric pressure to elevation estimation; inverts of SensorManager.getAltitude(float, float)
* Barometeric pressure to altitude estimation; inverts of SensorManager.getAltitude(float, float)
* https://de.wikipedia.org/wiki/Barometrische_H%C3%B6henformel#Internationale_H%C3%B6henformel
* {\color{White} p(h)} = p_0 \cdot \left( 1 - \frac{0{,}0065 \frac{\mathrm K}{\mathrm m} \cdot h}{T_0\ } \right)^{5{,}255}
*/
@@ -36,6 +36,8 @@ import java.time.temporal.TemporalAccessor;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
/**
* Various string manipulation methods.
@@ -86,50 +88,38 @@ public class StringUtils {
* Formats the distance in meters.
*
* @param context the context
* @param distance_m the distance_m
* @param distance the distance
* @param metricUnits true to use metric units. False to use imperial units
*/
public static String formatDistance(Context context, double distance_m, boolean metricUnits) {
if (Double.isNaN(distance_m) || Double.isInfinite(distance_m)) {
public static String formatDistance(Context context, Distance distance, boolean metricUnits) {
if (distance.isInvalid()) {
return context.getString(R.string.value_unknown);
}
if (metricUnits) {
if (distance_m > 500.0) {
distance_m *= UnitConversions.M_TO_KM;
return context.getString(R.string.value_float_kilometer, distance_m);
if (distance.greaterThan(Distance.of(500))) {
return context.getString(R.string.value_float_kilometer, distance.toKM());
} else {
return context.getString(R.string.value_float_meter, distance_m);
return context.getString(R.string.value_float_meter, distance.toM());
}
} else {
if (distance_m * UnitConversions.M_TO_MI > 0.5) {
distance_m *= UnitConversions.M_TO_MI;
return context.getString(R.string.value_float_mile, distance_m);
if (distance.greaterThan(Distance.ofMile(0.5))) {
return context.getString(R.string.value_float_mile, distance.toMI());
} else {
distance_m *= UnitConversions.M_TO_FT;
return context.getString(R.string.value_float_feet, distance_m);
return context.getString(R.string.value_float_feet, distance.toFT());
}
}
}
public static String formatSpeed(Context context, double speed_mps, boolean metricUnits, boolean reportSpeed) {
if (Double.isNaN(speed_mps) || Double.isInfinite(speed_mps)) {
speed_mps = 0;
}
double speed = speed_mps * UnitConversions.M_TO_KM; //KM p sec
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
public static String formatSpeed(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
if (reportSpeed) {
if (metricUnits) {
return context.getString(R.string.value_float_kilometer_hour, speed * UnitConversions.S_TO_HR);
return context.getString(R.string.value_float_kilometer_hour, speed.toKMH());
} else {
return context.getString(R.string.value_float_mile_hour, speed * UnitConversions.S_TO_HR);
return context.getString(R.string.value_float_mile_hour, speed.toMPH());
}
} else {
int pace = speed == 0 ? 0 : (int) Math.round(1 / speed); //sec / [KM | MI]
int pace = (int) speed.toPace(metricUnits).getSeconds();
int minutes = pace / 60;
int seconds = pace % 60;
if (metricUnits) {
@@ -167,45 +157,40 @@ public class StringUtils {
* Get the formatted distance with unit.
*
* @param context the context
* @param distance_m the distance
* @param distance the distance
* @param metricUnits true to use metric unit
* @return the formatted distance (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getDistanceParts(Context context, double distance_m, boolean metricUnits) {
if (Double.isNaN(distance_m) || Double.isInfinite(distance_m)) {
public static Pair<String, String> getDistanceParts(Context context, Distance distance, boolean metricUnits) {
if (distance.isInvalid()) {
return new Pair<>(null, context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet));
}
int unitId;
if (metricUnits) {
if (distance_m > 500.0) {
distance_m *= UnitConversions.M_TO_KM;
unitId = R.string.unit_kilometer;
if (distance.greaterThan(Distance.of(500))) {
return new Pair<>(formatDecimal(distance.toKM()), context.getString(R.string.unit_kilometer));
} else {
unitId = R.string.unit_meter;
return new Pair<>(formatDecimal(distance.toM()), context.getString(R.string.unit_meter));
}
} else {
if (distance_m * UnitConversions.M_TO_MI > 0.5) {
distance_m *= UnitConversions.M_TO_MI;
unitId = R.string.unit_mile;
if (distance.greaterThan(Distance.of(0.5 * UnitConversions.M_TO_MI))) {
return new Pair<>(formatDecimal(distance.toMI()), context.getString(R.string.unit_mile));
} else {
distance_m *= UnitConversions.M_TO_FT;
unitId = R.string.unit_feet;
return new Pair<>(formatDecimal(distance.toFT()), context.getString(R.string.unit_feet));
}
}
return new Pair<>(formatDecimal(distance_m), context.getString(unitId));
}
/**
* Gets the formatted speed with unit.
*
* @param context the context
* @param speed_mps the speed
* @param speed the speed
* @param metricUnits true to use metric unit
* @param reportSpeed true to report speed; false for pace
* @return the formatted speed (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getSpeedParts(Context context, double speed_mps, boolean metricUnits, boolean reportSpeed) {
public static Pair<String, String> getSpeedParts(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
int unitId;
if (metricUnits) {
unitId = reportSpeed ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
@@ -214,21 +199,15 @@ public class StringUtils {
}
String unitString = context.getString(unitId);
if (Double.isNaN(speed_mps) || Double.isInfinite(speed_mps)) {
speed_mps = 0;
}
double speed = speed_mps * UnitConversions.M_TO_KM; //KM p sec
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
if (speed == null) {
speed = Speed.zero();
}
if (reportSpeed) {
return new Pair<>(StringUtils.formatDecimal(speed * UnitConversions.S_TO_HR, 1), unitString);
return new Pair<>(StringUtils.formatDecimal(speed.to(metricUnits), 1), unitString);
}
int pace = speed == 0 ? 0 : (int) Math.round(1 / speed); //sec / [KM | MI]
int pace = (int) speed.toPace(metricUnits).getSeconds();
int minutes = pace / 60;
int seconds = pace % 60;
@@ -320,21 +299,16 @@ public class StringUtils {
}
/**
* Sets an elevation_m value.
*
* @param context the context
* @param elevation_m the elevation_m
* @param metricUnits true if metric units
* @return the formatted elevation_m (or null) and it's unit as {@link Pair}
* @return the formatted altitude_m (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> formatElevation(Context context, Float elevation_m, boolean metricUnits) {
public static Pair<String, String> formatAltitude(Context context, Float altitude_m, boolean metricUnits) {
String value = context.getString(R.string.value_unknown);
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
if (elevation_m != null) {
if (altitude_m != null) {
if (!metricUnits) {
elevation_m *= (float) UnitConversions.M_TO_FT;
altitude_m *= (float) UnitConversions.M_TO_FT;
}
value = StringUtils.formatDecimal(elevation_m, 0);
value = StringUtils.formatDecimal(altitude_m, 0);
}
return new Pair<>(value, unit);
}
@@ -38,6 +38,7 @@ public class TrackIconUtils {
private static final String BIKE = "BIKE";
private static final String MOUNTAIN_BIKE = "MOUNTAIN_BIKE";
private static final String MOTOR_BIKE = "MOTOR_BIKE";
private static final String KAYAK = "KAYAK";
private static final String BOAT = "BOAT";
private static final String SAILING = "SAILING";
private static final String DRIVE = "DRIVE";
@@ -67,6 +68,7 @@ public class TrackIconUtils {
private static final int[] SKI_LIST = new int[]{R.string.activity_type_cross_country_skiing, R.string.activity_type_skiing};
private static final int[] SNOW_BOARDING_LIST = new int[]{R.string.activity_type_snow_boarding};
private static final int[] SKATE_BOARDING_LIST = new int[]{R.string.activity_type_skate_boarding};
private static final int[] KAYAKING_LIST = new int[]{R.string.activity_type_kayaking};
private static final int[] WALK_LIST = new int[]{R.string.activity_type_hiking, R.string.activity_type_off_trail_hiking, R.string.activity_type_speed_walking, R.string.activity_type_trail_hiking, R.string.activity_type_walking};
// List of icons whose sports associated use speed (in km/h or mi/h).
@@ -78,7 +80,7 @@ public class TrackIconUtils {
// All bike categories.
R.string.activity_type_biking, R.string.activity_type_cycling, R.string.activity_type_dirt_bike, R.string.activity_type_motor_bike, R.string.activity_type_mountain_biking, R.string.activity_type_road_biking, R.string.activity_type_track_cycling, R.string.activity_type_inline_skating,
// All boat categories.
R.string.activity_type_boat, R.string.activity_type_ferry, R.string.activity_type_motor_boating, R.string.activity_type_rc_boat, R.string.activity_type_sailing,
R.string.activity_type_boat, R.string.activity_type_ferry, R.string.activity_type_motor_boating, R.string.activity_type_rc_boat, R.string.activity_type_sailing, R.string.activity_type_kayaking,
// All drive categories.
R.string.activity_type_atv, R.string.activity_type_driving, R.string.activity_type_driving_bus, R.string.activity_type_driving_car, R.string.activity_type_escooter, R.string.activity_type_skate_boarding,
// All wintersport categories
@@ -104,6 +106,7 @@ public class TrackIconUtils {
MAP.put(MOTOR_BIKE, new Pair<>(R.string.activity_type_motor_bike, R.drawable.ic_activity_motorbike_24dp));
MAP.put(DRIVE, new Pair<>(R.string.activity_type_driving, R.drawable.ic_activity_drive_24dp));
MAP.put(AIRPLANE, new Pair<>(R.string.activity_type_airplane, R.drawable.ic_activity_flight_24dp));
MAP.put(KAYAK, new Pair<>(R.string.activity_type_kayaking, R.drawable.ic_activity_kayaking_24dp));
MAP.put(BOAT, new Pair<>(R.string.activity_type_boat, R.drawable.ic_activity_boat_24dp));
MAP.put(SAILING, new Pair<>(R.string.activity_type_sailing, R.drawable.ic_activity_sailing_24dp));
}
@@ -170,6 +173,9 @@ public class TrackIconUtils {
if (inList(context, activityType, MOTOR_BIKE_LIST)) {
return MOTOR_BIKE;
}
if (inList(context, activityType, KAYAKING_LIST)) {
return KAYAK;
}
if (inList(context, activityType, BOAT_LIST)) {
return BOAT;
}
@@ -22,34 +22,31 @@ package de.dennisguse.opentracks.util;
*/
public class UnitConversions {
// Time //TODO Use Duration
// multiplication factor to convert seconds to milliseconds
public static final long S_TO_MS = 1000;
// Time
// TODO Use int instead of long.
public static final long ONE_SECOND_US = UnitConversions.S_TO_MS * 1000;
public static final long ONE_SECOND_MS = UnitConversions.S_TO_MS;
// multiplication factor to convert milliseconds to seconds
@Deprecated
public static final double MS_TO_S = 1d / S_TO_MS;
// multiplication factor to convert minutes to seconds
@Deprecated
public static final double MIN_TO_S = 60.0;
// multiplication factor to convert seconds to minutes
@Deprecated
public static final double S_TO_MIN = 1 / MIN_TO_S;
// multiplication factor to convert hours to minutes
public static final double HR_TO_MIN = 60.0;
// multiplication factor to convert minutes to hours
public static final double MIN_TO_HR = 1 / HR_TO_MIN;
public static final double S_TO_HR = 1d / (S_TO_MIN * MIN_TO_HR);
@Deprecated
public static final double MIN_TO_HR = 1 / 60.0;
// multiplication factor to convert kilometers to miles
public static final double KM_TO_MI = 0.621371192;
public static final double MM_TO_M = 0.001;
// Distance
// Distance //TODO Make private to Distance class!
// multiplication factor to convert miles to feet
public static final double MI_TO_FT = 5280.0;
private static final double MI_TO_FT = 5280.0;
// multiplication factor to convert feet to miles
public static final double FT_TO_MI = 1 / MI_TO_FT;
// multiplication factor to covert kilometers to meters
@@ -6,56 +6,50 @@ import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.UnitConversions;
public class IntervalStatistics {
private final List<Interval> intervalList = new ArrayList<>();
private final float distanceInterval_m;
private final Distance distanceInterval;
/**
* @param trackPoints the list of TrackPoint.
* @param distanceInterval_m the meters of every interval.
*/
public IntervalStatistics(@NonNull List<TrackPoint> trackPoints, float distanceInterval_m) {
public IntervalStatistics(@NonNull List<TrackPoint> trackPoints, Distance distanceInterval) {
intervalList.clear();
this.distanceInterval_m = distanceInterval_m;
this.distanceInterval = distanceInterval;
if (trackPoints.size() == 0) {
return;
}
Interval interval = new Interval();
interval.gain_m += trackPoints.get(0).hasElevationGain() ? trackPoints.get(0).getElevationGain() : 0;
interval.loss_m += trackPoints.get(0).hasElevationLoss() ? trackPoints.get(0).getElevationLoss() : 0;
interval.gain_m += trackPoints.get(0).hasAltitudeGain() ? trackPoints.get(0).getAltitudeGain() : 0;
interval.loss_m += trackPoints.get(0).hasAltitudeLoss() ? trackPoints.get(0).getAltitudeLoss() : 0;
for (int i = 1; i < trackPoints.size(); i++) {
TrackPoint prevTrackPoint = trackPoints.get(i - 1);
TrackPoint trackPoint = trackPoints.get(i);
if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) {
interval.distance_m += prevTrackPoint.distanceToPrevious(trackPoint);
interval.distance = interval.distance.plus(prevTrackPoint.distanceToPrevious(trackPoint));
interval.time = interval.time.plus(Duration.between(prevTrackPoint.getTime(), trackPoint.getTime()));
interval.gain_m += trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : 0;
interval.loss_m += trackPoint.hasElevationLoss() ? trackPoint.getElevationLoss() : 0;
interval.gain_m += trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : 0;
interval.loss_m += trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : 0;
if (interval.distance_m >= distanceInterval_m) {
float adjustFactor = distanceInterval_m / interval.distance_m;
Interval adjustedInterval = new Interval(interval);
adjustedInterval.adjust(adjustFactor);
if (interval.distance.greaterThan(distanceInterval)) {
Interval adjustedInterval = new Interval(interval, distanceInterval.dividedBy(interval.distance));
intervalList.add(adjustedInterval);
interval = new Interval(interval.distance_m - adjustedInterval.distance_m, interval.time.minus(adjustedInterval.time));
interval = new Interval(interval.distance.minus(adjustedInterval.distance), interval.time.minus(adjustedInterval.time));
}
}
}
if (interval.distance_m > 1f) {
if (interval.distance.greaterThan(Distance.of(1))) {
intervalList.add(interval);
}
}
public List<Interval> getIntervalList() {
return intervalList;
}
@@ -67,12 +61,12 @@ public class IntervalStatistics {
* @return the interval object or null if any interval is completed.
*/
public Interval getLastInterval() {
if (intervalList.size() == 1 && intervalList.get(0).getDistance_m() < distanceInterval_m) {
if (intervalList.size() == 1 && intervalList.get(0).getDistance().lessThan(distanceInterval)) {
return null;
}
for (int i = intervalList.size() - 1; i >= 0; i--) {
if (intervalList.get(i).getDistance_m() >= distanceInterval_m) {
if (intervalList.get(i).getDistance().greaterOrEqualThan(distanceInterval)) {
return this.intervalList.get(i);
}
}
@@ -81,7 +75,7 @@ public class IntervalStatistics {
}
public static class Interval {
private float distance_m = 0f;
private Distance distance = Distance.of(0);
private Duration time = Duration.ofSeconds(0);
private float gain_m = 0f;
private float loss_m = 0f;
@@ -89,35 +83,25 @@ public class IntervalStatistics {
public Interval() {
}
public Interval(float distance_m, Duration time) {
this.distance_m = distance_m;
public Interval(Distance distance, Duration time) {
this.distance = distance;
this.time = time;
}
public Interval(Interval i) {
distance_m = i.distance_m;
public Interval(Interval i, double adjustFactor) {
distance = i.distance.multipliedBy(adjustFactor);
time = Duration.ofMillis((long) (i.time.toMillis() * adjustFactor));
time = i.time;
gain_m = i.gain_m;
loss_m = i.loss_m;
}
public float getDistance_m() {
return distance_m;
public Distance getDistance() {
return distance;
}
public void adjust(float adjustFactor) {
distance_m *= adjustFactor;
time = Duration.ofMillis((long) (time.toMillis() * adjustFactor));
}
/**
* @return speed of the interval in m/s.
*/
public float getSpeed_ms() {
if (distance_m == 0f) {
return 0f;
}
return (distance_m / (time.toMillis() * (float) UnitConversions.MS_TO_S));
public Speed getSpeed() {
return Speed.of(distance, time);
}
public float getGain_m() {
@@ -1,44 +1,83 @@
package de.dennisguse.opentracks.viewmodels;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* This model is used to load intervals for a track.
* It uses a default interval but it can be set from outside to manage the interval length.
*/
public class IntervalStatisticsModel {
public class IntervalStatisticsModel extends AndroidViewModel {
private final List<TrackPoint> trackPoints = new ArrayList<>();
private MutableLiveData<List<IntervalStatistics.Interval>> intervalsLiveData;
private Distance distanceInterval;
public IntervalStatistics getIntervalStats(boolean metricUnits, @Nullable IntervalOption interval) {
public IntervalStatisticsModel(@NonNull Application application) {
super(application);
}
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(boolean metricUnits, @Nullable IntervalOption interval) {
synchronized (trackPoints) {
if (interval == null) {
interval = IntervalOption.OPTION_1;
}
if (intervalsLiveData == null) {
if (interval == null) {
interval = IntervalOption.OPTION_1;
}
float distanceInterval = metricUnits ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M);
return new IntervalStatistics(trackPoints, distanceInterval);
intervalsLiveData = new MutableLiveData<>();
distanceInterval = interval.getDistance(metricUnits);
loadIntervalStatistics();
}
return intervalsLiveData;
}
}
private void loadIntervalStatistics() {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
intervalsLiveData.postValue(intervalStatistics.getIntervalList());
}
public void add(TrackPoint trackPoint) {
synchronized (trackPoints) {
trackPoints.add(trackPoint);
}
}
public void onNewTrackPoints() {
synchronized (trackPoints) {
if (intervalsLiveData != null) {
loadIntervalStatistics();
}
}
}
public void clear() {
synchronized (trackPoints) {
trackPoints.clear();
}
}
public void upload(boolean metricUnits, @Nullable IntervalOption interval) {
synchronized (trackPoints) {
if (interval == null) {
interval = IntervalOption.OPTION_1;
}
distanceInterval = interval.getDistance(metricUnits);
loadIntervalStatistics();
}
}
/**
* Intervals length this view model support.
*/
@@ -52,19 +91,21 @@ public class IntervalStatisticsModel {
OPTION_20(20),
OPTION_50(50);
private final int value;
private final int multiplier;
IntervalOption(int value) {
this.value = value;
IntervalOption(int multiplier) {
this.multiplier = multiplier;
}
public int getValue() {
return value;
public Distance getDistance(boolean metricUnits) {
return Distance
.one(metricUnits)
.multipliedBy(multiplier);
}
@Override
public String toString() {
return "" + value;
return "" + multiplier; //TODO Somehow IntervalsFragment relies on a parsable Integer.
}
}
}
@@ -0,0 +1,10 @@
<vector android:height="24dp" android:viewportHeight="21.99934"
android:tint="?attr/colorControlNormal"
android:viewportWidth="21.84952" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="m18.296,13.5167c-1.0171,-0.0182 -1.9033,0.7707 -2.9456,0.5005 -0.9523,-0.2718 -1.9505,-0.7655 -2.9493,-0.3389 -0.9384,0.4921 -2.0789,0.4938 -3.017,0C8.3845,13.2515 7.3852,13.7448 6.4322,14.0172 5.2299,14.312 4.2005,13.2564 2.9984,13.5587 2.2077,13.8068 1.0222,14.113 1.0222,14.113v1.7649c1.0227,0.0195 1.9128,-0.7764 2.9601,-0.4982 0.4417,0.2168 1.6682,0.3639 1.653,0.6019l-1.3879,1.3835c-0.7386,-0.5021 -1.5119,0.0527 -1.9901,0.6253 -0.5088,0.568 -1.1635,1.0259 -1.5505,1.6871 -0.4461,0.9528 0.6577,2.0565 1.6105,1.6105 0.7017,-0.428 1.2007,-1.1229 1.8094,-1.6725 0.6219,-0.4296 0.7776,-1.2477 0.6298,-1.8616l2.0038,-1.999c0.9554,-0.4404 2.0942,-0.6063 3.0559,-0.0798 0.9567,0.4291 1.9816,0.0931 2.8934,-0.2638 1.0381,-0.3395 1.9553,0.3821 2.956,0.4619 1.0872,0.1057 2.0198,-0.7841 3.1216,-0.4936 0.5567,-0.0151 1.9432,1.034 1.9777,0.1483v-1.4713c-0.8787,0.0509 -1.5921,-0.5889 -2.4688,-0.5401z" android:strokeWidth="0.20758"/>
<path android:fillColor="#ffffff"
android:pathData="M19.1355,4.6094C19.7777,3.924 20.5181,3.3189 21.0906,2.576 21.6308,1.6723 20.6318,0.4635 19.6424,0.8157 18.9521,1.1178 18.5254,1.7969 17.9665,2.2834 17.4409,2.7778 16.7484,3.4079 17.1237,4.208 17.3165,4.5792 16.6709,4.8231 16.4793,5.1359L15.0659,6.5509C13.4595,5.957 11.8493,5.3584 10.2468,4.7652 9.2046,4.531 8.2546,5.3828 8.1061,6.3824 7.7002,7.769 7.2645,9.148 6.8777,10.5394h-5.8511v2.3662c1.0341,-0.0658 1.9719,-0.79 3.0482,-0.4914 0.8515,0.1826 1.7033,0.7429 2.5975,0.3773 1.0893,-0.5536 2.4097,-0.5626 3.4983,-0.0022 0.896,0.3687 1.7515,-0.1904 2.6047,-0.3752 1.0783,-0.3027 2.0158,0.4279 3.0515,0.4909 0.9851,-0.0775 1.8675,-0.4708 2.8841,-0.5791 0.8477,-0.0903 2.1639,-1.7856 2.1639,-1.7856 0,0 -1.8192,-0.0154 -2.9351,0.0029 -1.9905,-0.0002 -3.9809,-0.0021 -5.9714,-0.0044l2.4839,-2.4814c0.7624,0.5727 1.957,-0.3878 1.2027,-1.1977l2.0322,-2.0397c0.4628,0.252 1.0792,0.1707 1.4483,-0.2105zM11.0752,10.5394c-0.4865,0.1015 -0.6121,-0.0654 -0.3745,-0.5026l0.9094,-3.0294 2.1896,0.81z" android:strokeWidth="0.20758"/>
<path android:fillColor="#ffffff"
android:pathData="m12.5202,2.6733c0.0551,1.1808 -1.4064,2.0133 -2.3942,1.3656 -1.0445,-0.5537 -1.0179,-2.2353 0.0433,-2.7559 1.0184,-0.592 2.3788,0.2078 2.3509,1.3903z" android:strokeWidth="1.16861"/>
</vector>
+3 -3
View File
@@ -44,9 +44,9 @@
tools:text="Speed" />
<TextView
android:id="@+id/interval_elevation"
android:id="@+id/interval_altitude"
style="@style/IntervalLabel"
android:text="@string/stats_elevation" />
android:text="@string/stats_altitude" />
</LinearLayout>
@@ -57,7 +57,7 @@
android:layout_marginEnd="8dp"
android:background="@color/stats_separator" />
<ListView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/interval_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
+23 -23
View File
@@ -261,81 +261,81 @@
<!-- Horizontal line -->
<View
android:id="@+id/stats_elevation_horizontal_line"
android:id="@+id/stats_altitude_horizontal_line"
style="@style/StatsHorizontalLine"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintTop_toBottomOf="@id/stats_speed_barrier" />
<!-- Elevation gain and loss -->
<!-- Altitude gain and loss -->
<androidx.constraintlayout.widget.Group
android:id="@+id/stats_elevation_group"
android:id="@+id/stats_altitude_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="stats_elevation_horizontal_line,stats_elevation_gain_label,stats_elevation_gain_value,stats_elevation_gain_unit,stats_elevation_loss_label,stats_elevation_loss_value,stats_elevation_loss_unit" />
app:constraint_referenced_ids="stats_altitude_horizontal_line,stats_altitude_gain_label,@+id/stats_altitude_gain_value,@+id/stats_altitude_gain_unit,stats_altitude_loss_label,@+id/stats_altitude_loss_value,@+id/stats_altitude_loss_unit" />
<TextView
android:id="@+id/stats_elevation_gain_label"
android:id="@+id/stats_altitude_gain_label"
style="@style/StatsLargeLabel"
android:text="@string/stats_gain"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintEnd_toStartOf="@id/guideline2"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_horizontal_line" />
app:layout_constraintTop_toBottomOf="@id/stats_altitude_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_gain_value"
android:id="@+id/stats_altitude_gain_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintEnd_toStartOf="@id/stats_elevation_gain_unit"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_gain_label"
app:layout_constraintEnd_toStartOf="@id/stats_altitude_gain_unit"
app:layout_constraintTop_toBottomOf="@id/stats_altitude_gain_label"
tools:text="100" />
<TextView
android:id="@+id/stats_elevation_gain_unit"
android:id="@+id/stats_altitude_gain_unit"
style="@style/StatsUnit"
app:layout_constraintBottom_toBottomOf="@id/stats_elevation_gain_value"
app:layout_constraintStart_toEndOf="@id/stats_elevation_gain_value"
app:layout_constraintBottom_toBottomOf="@id/stats_altitude_gain_value"
app:layout_constraintStart_toEndOf="@id/stats_altitude_gain_value"
app:layout_constraintEnd_toStartOf="@id/guideline2"
tools:text="m" />
<TextView
android:id="@+id/stats_elevation_loss_label"
android:id="@+id/stats_altitude_loss_label"
style="@style/StatsLargeLabel"
android:text="@string/stats_loss"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/guideline2"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_horizontal_line" />
app:layout_constraintTop_toBottomOf="@id/stats_altitude_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_loss_value"
android:id="@+id/stats_altitude_loss_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/guideline2"
app:layout_constraintEnd_toStartOf="@id/stats_elevation_loss_unit"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_loss_label"
app:layout_constraintEnd_toStartOf="@id/stats_altitude_loss_unit"
app:layout_constraintTop_toBottomOf="@id/stats_altitude_loss_label"
tools:text="100" />
<TextView
android:id="@+id/stats_elevation_loss_unit"
android:id="@+id/stats_altitude_loss_unit"
style="@style/StatsUnit"
app:layout_constraintBottom_toBottomOf="@id/stats_elevation_loss_value"
app:layout_constraintStart_toEndOf="@id/stats_elevation_loss_value"
app:layout_constraintBottom_toBottomOf="@id/stats_altitude_loss_value"
app:layout_constraintStart_toEndOf="@id/stats_altitude_loss_value"
app:layout_constraintEnd_toStartOf="@id/guideline3"
tools:text="m" />
<!-- Barrier -->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/stats_elevation_barrier"
android:id="@+id/stats_altitude_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="stats_elevation_gain_value,stats_elevation_loss_value" />
app:constraint_referenced_ids="stats_altitude_gain_value,stats_altitude_loss_value" />
<!-- Horizontal line -->
<View
@@ -344,7 +344,7 @@
android:layout_marginBottom="8dp"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_barrier" />
app:layout_constraintTop_toBottomOf="@id/stats_altitude_barrier" />
<!-- Sensors data -->
<androidx.recyclerview.widget.RecyclerView
+35 -35
View File
@@ -257,102 +257,102 @@
<!-- Horizontal line -->
<View
android:id="@+id/stats_elevation_horizontal_line"
android:id="@+id/stats_altitude_horizontal_line"
style="@style/StatsHorizontalLine"
android:layout_marginBottom="8dp"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/stats_speed_barrier" />
<!-- Elevation -->
<!-- altitude -->
<androidx.constraintlayout.widget.Group
android:id="@+id/stats_elevation_group"
android:id="@+id/stats_altitude_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="stats_elevation_horizontal_line,stats_elevation_current_label,stats_elevation_current_value,stats_elevation_current_unit,stats_elevation_gain_label,stats_elevation_gain_value,stats_elevation_gain_unit,stats_elevation_loss_label,stats_elevation_loss_value,stats_elevation_loss_unit" />
app:constraint_referenced_ids="stats_altitude_horizontal_line,stats_altitude_current_label,stats_altitude_current_value,stats_altitude_current_unit,stats_altitude_gain_label,stats_altitude_gain_value,stats_altitude_gain_unit,stats_altitude_loss_label,stats_altitude_loss_value,stats_altitude_loss_unit" />
<!-- Elevation -->
<!-- altitude -->
<TextView
android:id="@+id/stats_elevation_current_label"
android:id="@+id/stats_altitude_current_label"
style="@style/StatsSmallLabel"
android:text="@string/stats_elevation"
android:text="@string/stats_altitude"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/guideline2"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_horizontal_line" />
app:layout_constraintTop_toBottomOf="@id/stats_altitude_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_current_value"
android:id="@+id/stats_altitude_current_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown"
app:layout_constraintEnd_toStartOf="@id/stats_elevation_current_unit"
app:layout_constraintEnd_toStartOf="@id/stats_altitude_current_unit"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_current_label"
app:layout_constraintTop_toBottomOf="@id/stats_altitude_current_label"
tools:text="100" />
<TextView
android:id="@+id/stats_elevation_current_unit"
android:id="@+id/stats_altitude_current_unit"
style="@style/StatsUnit"
app:layout_constraintBottom_toBottomOf="@id/stats_elevation_current_value"
app:layout_constraintBottom_toBottomOf="@id/stats_altitude_current_value"
app:layout_constraintEnd_toStartOf="@id/guideline2"
app:layout_constraintStart_toEndOf="@id/stats_elevation_current_value"
app:layout_constraintStart_toEndOf="@id/stats_altitude_current_value"
tools:text="m" />
<!-- Elevation gain -->
<!-- Altitude gain -->
<TextView
android:id="@+id/stats_elevation_gain_label"
android:id="@+id/stats_altitude_gain_label"
style="@style/StatsSmallLabel"
android:text="@string/stats_gain"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintStart_toEndOf="@id/guideline2"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_horizontal_line" />
app:layout_constraintTop_toBottomOf="@id/stats_altitude_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_gain_value"
android:id="@+id/stats_altitude_gain_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown"
app:layout_constraintEnd_toStartOf="@id/stats_elevation_gain_unit"
app:layout_constraintEnd_toStartOf="@id/stats_altitude_gain_unit"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/guideline2"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_gain_label"
app:layout_constraintTop_toBottomOf="@id/stats_altitude_gain_label"
tools:text="100" />
<TextView
android:id="@+id/stats_elevation_gain_unit"
android:id="@+id/stats_altitude_gain_unit"
style="@style/StatsUnit"
app:layout_constraintBottom_toBottomOf="@id/stats_elevation_gain_value"
app:layout_constraintBottom_toBottomOf="@id/stats_altitude_gain_value"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintStart_toEndOf="@id/stats_elevation_gain_value"
app:layout_constraintStart_toEndOf="@id/stats_altitude_gain_value"
tools:text="m" />
<!-- Elevation loss -->
<!-- Altitude loss -->
<TextView
android:id="@+id/stats_elevation_loss_label"
android:id="@+id/stats_altitude_loss_label"
style="@style/StatsSmallLabel"
android:text="@string/stats_loss"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintStart_toEndOf="@id/guideline2"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_gain_value" />
app:layout_constraintTop_toBottomOf="@id/stats_altitude_gain_value" />
<TextView
android:id="@+id/stats_elevation_loss_value"
android:id="@+id/stats_altitude_loss_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown"
app:layout_constraintEnd_toStartOf="@id/stats_elevation_loss_unit"
app:layout_constraintEnd_toStartOf="@id/stats_altitude_loss_unit"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/guideline2"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_loss_label"
app:layout_constraintTop_toBottomOf="@id/stats_altitude_loss_label"
tools:text="100" />
<TextView
android:id="@+id/stats_elevation_loss_unit"
android:id="@+id/stats_altitude_loss_unit"
style="@style/StatsUnit"
app:layout_constraintBottom_toBottomOf="@id/stats_elevation_loss_value"
app:layout_constraintBottom_toBottomOf="@id/stats_altitude_loss_value"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintStart_toEndOf="@id/stats_elevation_loss_value"
app:layout_constraintStart_toEndOf="@id/stats_altitude_loss_value"
tools:text="m" />
<!-- Coordinate group -->
@@ -364,11 +364,11 @@
<!-- Barrier -->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/stats_elevation_barrier"
android:id="@+id/stats_altitude_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="stats_elevation_current_value,stats_elevation_gain_value,stats_elevation_loss_value" />
app:constraint_referenced_ids="stats_altitude_current_value,stats_altitude_gain_value,stats_altitude_loss_value" />
<!-- Horizontal line -->
<View
@@ -377,7 +377,7 @@
android:layout_marginBottom="8dp"
app:layout_constraintEnd_toStartOf="@id/guideline3"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/stats_elevation_barrier" />
app:layout_constraintTop_toBottomOf="@id/stats_altitude_barrier" />
<!-- Latitude -->
<TextView
+6 -6
View File
@@ -87,9 +87,9 @@ limitations under the License.
<string name="description_average_moving_speed">متوسط سرعة التحرك: %1$.2f كم/ساعة (%2$.1f ميل/ساعة)</string>
<string name="description_average_pace_in_minute">متوسط السرعة:‏ %1$s‏ دقيقة/كم‏ (%2$s‏ دقيقة/ميل)‏</string>
<string name="description_average_speed">سرعة متوسطة: %1$.2f كم/ساعة (%2$.1f ميل/ساعة)</string>
<string name="description_elevation_gain">الزيادة في الارتفاع: %1$d م (%2$d قدم)</string>
<string name="description_elevation_imperial">الارتفاع (قدم)</string>
<string name="description_elevation_metric">الارتفاع (م)</string>
<string name="description_altitude_gain">الزيادة في الارتفاع: %1$d م (%2$d قدم)</string>
<string name="description_altitude_imperial">الارتفاع (قدم)</string>
<string name="description_altitude_metric">الارتفاع (م)</string>
<string name="description_fastest_pace_in_minute">أسرع سرعة:‏ %1$s‏ دقيقة/كم‏ (%2$s‏ دقيقة/ميل)‏</string>
<string name="description_location_accuracy">الدقة (م)</string>
<string name="description_location_altitude">الارتفاع (م)</string>
@@ -97,10 +97,10 @@ limitations under the License.
<string name="description_location_latitude">خط العرض (درجة)</string>
<string name="description_location_longitude">خط الطول (درجة)</string>
<string name="description_location_speed">السرعة (متر/ثانية)</string>
<string name="description_max_elevation">أقصى ارتفاع: %1$d م (%2$d قدم)</string>
<string name="description_max_altitude">أقصى ارتفاع: %1$d م (%2$d قدم)</string>
<string name="description_max_grade">أقصى درجة: %1$d %%</string>
<string name="description_max_speed">أقصى سرعة: %1$.2f كم/ساعة ( %2$.1f ميل/ساعة)</string>
<string name="description_min_elevation">أدنى ارتفاع: %1$d م ( %2$d قدم)</string>
<string name="description_min_altitude">أدنى ارتفاع: %1$d م ( %2$d قدم)</string>
<string name="description_min_grade">أدنى درجة: %1$d %%</string>
<string name="description_moving_time">وقت النقل: %1$s</string>
<string name="description_pace_imperial">السرعة (دقيقة/ميل)</string>
@@ -286,7 +286,7 @@ limitations under the License.
<string name="stats_average_moving_speed">متوسط سرعة التحرك</string>
<string name="stats_average_pace">متوسط ​​الإيقاع</string>
<string name="stats_average_speed">متوسط السرعة</string>
<string name="stats_elevation">الارتفاع</string>
<string name="stats_altitude">الارتفاع</string>
<string name="stats_fastest_pace">أسرع إيقاع</string>
<string name="stats_gain">الاكتساب</string>
<string name="stats_grade">الدرجة</string>
+6 -6
View File
@@ -71,9 +71,9 @@
<string name="description_average_moving_speed">Average moving speed: %1$.2f km/h (%2$.1f mi/h)</string>
<string name="description_average_pace_in_minute">Average pace: %1$s min/km (%2$s min/mi)</string>
<string name="description_average_speed">Average speed: %1$.2f km/h (%2$.1f mi/h)</string>
<string name="description_elevation_gain">Elevation gain: %1$d m (%2$d ft)</string>
<string name="description_elevation_imperial">Elevation (ft)</string>
<string name="description_elevation_metric">Elevation (m)</string>
<string name="description_altitude_gain">Elevation gain: %1$d m (%2$d ft)</string>
<string name="description_altitude_imperial">Elevation (ft)</string>
<string name="description_altitude_metric">Elevation (m)</string>
<string name="description_fastest_pace_in_minute">Fastest pace: %1$s min/km (%2$s min/mi)</string>
<string name="description_location_accuracy">Accuracy (m)</string>
<string name="description_location_altitude">Altitude (m)</string>
@@ -81,10 +81,10 @@
<string name="description_location_latitude">Latitude (deg)</string>
<string name="description_location_longitude">Longitude (deg)</string>
<string name="description_location_speed">Speed (m/s)</string>
<string name="description_max_elevation">Max elevation: %1$d m (%2$d ft)</string>
<string name="description_max_altitude">Max elevation: %1$d m (%2$d ft)</string>
<string name="description_max_grade">Max grade: %1$d %%</string>
<string name="description_max_speed">Max speed: %1$.2f km/h (%2$.1f mi/h)</string>
<string name="description_min_elevation">Min elevation: %1$d m (%2$d ft)</string>
<string name="description_min_altitude">Min elevation: %1$d m (%2$d ft)</string>
<string name="description_min_grade">Min grade: %1$d %%</string>
<string name="description_moving_time">Moving time: %1$s</string>
<string name="description_pace_imperial">Pace (min/mi)</string>
@@ -247,7 +247,7 @@
<string name="stats_average_moving_speed">Avg. moving speed</string>
<string name="stats_average_pace">Avg. pace</string>
<string name="stats_average_speed">Avg. speed</string>
<string name="stats_elevation">Elevation</string>
<string name="stats_altitude">Elevation</string>
<string name="stats_fastest_pace">Fastest pace</string>
<string name="stats_gain">Gain</string>
<string name="stats_grade">Grade</string>

Some files were not shown because too many files have changed in this diff Show More