Compare commits

..

113 Commits

Author SHA1 Message Date
Dennis Guse 3db0a53377 Release: v4.23.2 2025-11-06 21:41:13 +01:00
Dennis Guse 58dac8b706 Translations 2025-11-06 18:12:42 +01:00
Codeberg Translate f29b6d4088 Merge remote-tracking branch 'origin/main' 2025-11-05 21:38:42 +00:00
ghose 80fb4f9998 Translated using Weblate (Galician)
Currently translated at 100.0% (544 of 544 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2025-11-05 21:38:39 +00:00
Dennis Guse 6340e55fff Bump org.mockito:mockito-android from 5.19.0 to 5.20.0 2025-11-05 21:34:54 +01:00
Dennis Guse 625549d381 Bump androidx.work:work-runtime from 2.10.5 to 2.11.0 2025-11-05 21:34:23 +01:00
Dennis Guse 7d9878b32e Bump androidx.core:core-location-altitude from 1.0.0-alpha03 to 1.0.0-beta01 2025-11-05 21:33:47 +01:00
Dennis Guse a5553211af Bump androidx.core:core-splashscreen from 1.0.1 to 1.2.0 2025-11-05 21:33:15 +01:00
Dennis Guse 044e84154c Bugfix: crash in settings.
Introduced in 696939112a
2025-11-05 21:31:40 +01:00
Outbreak2096 f61d718b2d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (544 of 544 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2025-11-04 06:06:45 +00:00
Codeberg Translate c7a54dcee8 Merge remote-tracking branch 'origin/main' 2025-11-03 00:23:28 +00:00
Eryk Michalak 3dc2ee3462 Translated using Weblate (Polish)
Currently translated at 93.9% (511 of 544 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/pl/
2025-11-03 00:23:26 +00:00
Stzyxh 6817ff3234 Translated using Weblate (German)
Currently translated at 4.5% (8 of 174 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/de/
2025-11-03 00:23:22 +00:00
Codeberg Translate fb0400001a Merge remote-tracking branch 'origin/main' 2025-11-01 10:10:24 +00:00
ovl-005 16591b3e80 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.3% (534 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2025-11-01 10:09:54 +00:00
Dennis Guse 28da32f573 Fixed TrackRecordingServiceRecordingTest: disable the real GPS (finally). 2025-11-01 10:13:52 +01:00
Dennis Guse c687308d97 Move WakeLock to SensorManager (was TrackRecordingService). 2025-11-01 09:56:31 +01:00
Dennis Guse 65c8e8542e Cleanup: pass RecordingData to VoiceAnnouncementManager. 2025-11-01 09:30:47 +01:00
Dennis Guse ca4cb648e7 Predictive back: shown also for TrackRecordedActivity.
However: the shared element animation needed to be removed. ActivityOptions.makeSceneTransitionAnimation() seems to overwrite the predictive back animation.
2025-11-01 08:49:08 +01:00
Dennis Guse 53240c3d1a IntervalStatisticsUpdater exposes Statistics (instead of Interval). 2025-10-31 19:00:32 +01:00
Dennis Guse 95b27f66fb AndroidStudio: set line width (for autoformatting) to 200. 2025-10-31 18:34:07 +01:00
Dennis Guse f31e146cda Cleanup: TrackPoint uses AltitudeGainLoss. 2025-10-30 22:33:00 +01:00
Dennis Guse 696939112a Cleanup: Statistics doesn't need isIdle.
isIdle is only used by SegmentStatisticsUpdater.
2025-10-30 21:52:00 +01:00
Dennis Guse 73ce97a63e VoiceAnnouncementUtilsTest do not store data. 2025-10-30 21:45:03 +01:00
Dennis Guse 5988ae3d3f VoiceAnnouncementUtilsTest do not use SegmentStatisticsUpdater. 2025-10-30 21:30:01 +01:00
Dennis Guse 58655814b2 VoiceAnnouncementManager should close TrackPointIterator to avoid memory leaks. 2025-10-30 07:54:05 +01:00
Dennis Guse 11f1b6e72d Cleanup: TrackPointIterator is now package private. 2025-10-30 07:54:05 +01:00
Dennis Guse 8f537f33a7 Cleanup: removed Track.setTrackStatistics(SegmentStatisticsUpdater). 2025-10-30 07:54:05 +01:00
Dennis Guse 4db72230ff Refactored VoiceAnnouncementUtilsTest. 2025-10-30 07:54:05 +01:00
Dennis Guse fd92793c2a Test: IntervalStatisticsUpdaterTest doesn't store data anymore. 2025-10-30 07:54:05 +01:00
Dennis Guse 05e45b6870 TrackStatisticsUpdater now provides Statistics.
Used for non-testing code.
2025-10-30 07:54:04 +01:00
Dennis Guse 7521f906ed IntervalStatisticsUpdater now uses Statistics. 2025-10-30 07:54:04 +01:00
Dennis Guse 3222a0c497 Rename IntervalStatistics to IntervalStatisticsUpdater. 2025-10-30 07:54:04 +01:00
Dennis Guse caf3d957cd Cleanup: SegmentStatisticUpdaterTest uses Statistic. 2025-10-30 07:54:04 +01:00
Dennis Guse 005d00cfaa Cleanup: SegmentStatisticUpdaterTest. 2025-10-30 07:54:04 +01:00
Dennis Guse c227d54f26 Cleanup: SegmentStatisticUpdater and Track do not need equal(). 2025-10-30 07:54:04 +01:00
Dennis Guse bc62320b9f Track only has Statistics (instead of SegmentStatisticsUpdater). 2025-10-30 07:54:04 +01:00
Dennis Guse 0ad32f676c Create SegmentStatisticUpdater by passing Statistics (2/2). 2025-10-30 07:54:04 +01:00
Dennis Guse fff5f2d406 Create SegmentStatisticUpdater by passing Statistics (1/2). 2025-10-30 07:54:04 +01:00
Dennis Guse be653b73ee Added value object: AltitudeExtremities. 2025-10-30 07:54:04 +01:00
Dennis Guse 4580f2a7bb ContentProviderUtils use Statistics for saving. 2025-10-30 07:54:04 +01:00
Dennis Guse 5dd56892fb Cleanup: move Statistics to package models. 2025-10-30 07:54:04 +01:00
Dennis Guse e3460d94ed Statistics: used when only the data is needed (instead of passing SegmentStatisticUpdater). 2025-10-30 07:54:04 +01:00
Dennis Guse 920ad5129a Introduced Statistics as a value object. 2025-10-30 07:54:04 +01:00
Dennis Guse fba08fe8dd Cleanup: rename TrackStatistics to SegmentStatisticsUpdater. 2025-10-30 07:54:04 +01:00
Dennis Guse 5746a41905 Release: v4.23.1 2025-10-30 07:48:12 +01:00
Dennis Guse 361ae909e7 Bugfix: post-recording workout crashes.
Fixes #2180.

Introduced in 02a77e5ce7
2025-10-29 22:14:46 +01:00
Dennis Guse 4b59660ea1 Revert "Allow adding newly developed UI Components to existing custom layouts."
This reverts commit daa4689f44.
2025-10-29 22:14:12 +01:00
SomeTr cabf5d238e Translated using Weblate (Ukrainian)
Currently translated at 94.6% (514 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-10-28 17:35:25 +01:00
Outbreak2096 9c902b89ed Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (543 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2025-10-28 17:35:25 +01:00
Claudinux c31b8a5802 Translated using Weblate (Italian)
Currently translated at 100.0% (543 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/it/
2025-10-28 17:35:25 +01:00
SomeTr 901c548d13 Translated using Weblate (Ukrainian)
Currently translated at 94.4% (513 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-10-28 17:35:25 +01:00
SomeTr b77de1e10d Translated using Weblate (Ukrainian)
Currently translated at 94.2% (512 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-10-28 17:35:25 +01:00
SomeTr b8c7fba7d8 Translated using Weblate (Ukrainian)
Currently translated at 94.2% (512 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-10-28 17:35:25 +01:00
ghose 466719fbc8 Translated using Weblate (Galician)
Currently translated at 100.0% (543 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2025-10-28 17:35:25 +01:00
ghose 4f0ab71503 Translated using Weblate (Galician)
Currently translated at 100.0% (541 of 541 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2025-10-28 17:35:25 +01:00
Jeff Shelley daa4689f44 Allow adding newly developed UI Components to existing custom layouts.
Fixes #2176.
2025-10-28 09:51:51 -05:00
Jeff Shelley 023a5aa221 added new UI component which shows elapsed segment time 2025-10-28 09:51:29 -05:00
SomeTr 291633c1d5 Translated using Weblate (Ukrainian)
Currently translated at 94.6% (514 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-10-28 07:53:05 +00:00
Codeberg Translate 00990076fb Merge remote-tracking branch 'origin/main' 2025-10-28 05:27:33 +00:00
Outbreak2096 e672df7a60 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (543 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2025-10-28 05:27:31 +00:00
Claudinux 82eb237117 Translated using Weblate (Italian)
Currently translated at 100.0% (543 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/it/
2025-10-28 05:27:31 +00:00
Peter Storch 832005565a run forgejo actions on pull requests from forks (#2183)
Reviewed-on: https://codeberg.org/OpenTracksApp/OpenTracks/pulls/2183
Co-authored-by: Peter Storch <peter.storch@tuta.io>
Co-committed-by: Peter Storch <peter.storch@tuta.io>
2025-10-27 20:58:18 +01:00
SomeTr 894699f009 Translated using Weblate (Ukrainian)
Currently translated at 94.4% (513 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-10-26 21:02:55 +00:00
SomeTr f45dba258b Translated using Weblate (Ukrainian)
Currently translated at 94.2% (512 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-10-26 20:23:21 +00:00
SomeTr 17be49af70 Translated using Weblate (Ukrainian)
Currently translated at 94.2% (512 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-10-26 13:17:55 +00:00
ghose e91d36fc16 Translated using Weblate (Galician)
Currently translated at 100.0% (543 of 543 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2025-10-26 13:17:55 +00:00
ghose 56f333180b Translated using Weblate (Galician)
Currently translated at 100.0% (541 of 541 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2025-10-25 17:24:16 +00:00
Dennis Guse 9af8b0d353 Cleanup. 2025-10-24 22:26:08 +02:00
Dennis Guse 57c0750980 Release: v4.23.0 2025-10-24 10:23:14 +02:00
Dennis Guse 8e36ee2e7d Cleanup: TrackRecordingServiceNotificationManager uses RecordingData. 2025-10-24 10:19:43 +02:00
Dennis Guse ec2e527fd1 Cleanup. 2025-10-24 10:04:04 +02:00
Dennis Guse d968e5d0c1 Notification opens recording UI.
Features was removed as it broke occasionally and re-implemented after refactoring.

Fixes #1156.
2025-10-22 23:25:55 +02:00
Dennis Guse 4257f21cae Fixed tests.
Missed in1fa76301e23bf709402c19838efea597e7e38ca7
2025-10-22 23:24:13 +02:00
Dennis Guse 26e7059af2 Cleanup. 2025-10-22 22:49:20 +02:00
Jeff Shelley 94595f2a1f Added support for average pace of the current segment 2025-10-21 19:34:19 -05:00
Dennis Guse 1fa76301e2 Cleanup: TrackShareSummaryGenerator (was DescriptionGenerator) now only produces plaintext (HTML was not used anymore). 2025-10-20 09:15:39 +02:00
Dennis Guse ce2c608cc9 Cleanup: TrackStatistics has altitudeExtremities or not. 2025-10-19 22:27:11 +02:00
Dennis Guse 916f26dfda Merge pull request 'Cleanup' (#2173) from cleanup into main
Reviewed-on: https://codeberg.org/OpenTracksApp/OpenTracks/pulls/2173
2025-10-19 18:43:38 +02:00
Dennis Guse a108edc125 Bump androidx.work:work-runtime from 2.10.2 to 2.10.5 2025-10-19 18:28:13 +02:00
Dennis Guse 1a1a8a851b Cleanup: use Stream.toList()
Replaces Stream.collect(Collection.toList() | Collectors.toCollection()).
2025-10-19 18:26:49 +02:00
Dennis Guse ddd1efeb58 Cleanup: use .isEmpty() (except for CharSequence and StringBuilder as it requires SDK33). 2025-10-19 18:26:48 +02:00
Dennis Guse 6c30dd8509 Lists should show vertical scrolling indicator (was lost by switching to RecyclerView). 2025-10-19 17:59:47 +02:00
Dennis Guse 21fe59f32c Cleanup: SDK_INT is always >= Android O. 2025-10-19 17:48:23 +02:00
Dennis Guse f79b2b51c8 Cleanup: use correct order for assert in tests. 2025-10-18 21:48:45 +02:00
Dennis Guse b16e9ad964 Cleanup: UintUtilsTest was only partly executed. 2025-10-18 21:41:46 +02:00
Dennis Guse 65f8ba16b3 Cleanup: RecyclerViewSwipeDeleteCallback.onChildDraw() should only call super once. 2025-10-18 21:28:49 +02:00
Dennis Guse 9b48993cd0 Cleanup. 2025-10-18 21:27:31 +02:00
Dennis Guse 54cec3e821 Cleanup: limit method visibility. 2025-10-18 21:10:26 +02:00
Dennis Guse daeb972c8a Cleanup: simplify stream by using List operations (desugar). 2025-10-18 21:10:26 +02:00
Dennis Guse 73872e8926 Cleanup. 2025-10-18 21:09:21 +02:00
Dennis Guse c15fa21a56 Cleanup: use viewHolder.getAbsoluteAdapterPosition(). 2025-10-18 09:40:56 +02:00
Dennis Guse e9d1080e5d Cleanup: SpeedFormatter doesn't support decimal places. 2025-10-18 09:40:54 +02:00
Claudinux d0b3f45151 Translated using Weblate (Italian)
Currently translated at 4.0% (7 of 172 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/it/
2025-10-16 21:50:25 +02:00
happylocalisation 32b0b02d28 Added translation using Weblate (English (Middle)) 2025-10-16 21:50:25 +02:00
Dennis Guse d6617f61b7 Cleanup: use PreferenceUtils.isKey(List). 2025-10-15 18:10:35 +02:00
Dennis Guse b5180f235f Cleanup: StatisticsUtils were unused. 2025-10-15 18:10:15 +02:00
Dennis Guse e879eedbd6 Migrate ImportService to WorkManager.
Part of #1315.
2025-10-15 17:25:48 +02:00
Dennis Guse c7ca6800f7 Cleanup: ImportActivity. 2025-10-15 09:02:49 +02:00
Dennis Guse fed884a6ce ExportActivity inherits AbstractActivity. 2025-10-15 08:40:19 +02:00
Dennis Guse 02a77e5ce7 Migrate ExportService to WorkManager.
Part of #1315.
2025-10-14 22:45:00 +02:00
Dennis Guse 6f97399aeb Migrate TrackDeletionService to WorkManager.
Part of #1315.
2025-10-14 19:13:10 +02:00
pstorch 1c4f0e67ab Merge pull request 'use forgejo runner with simple unit tests' (#2159) from connected_check into main
Reviewed-on: https://codeberg.org/OpenTracksApp/OpenTracks/pulls/2159
2025-10-13 11:27:17 +02:00
Peter Storch ebed386d96 use forgejo runners with unit tests 2025-10-12 17:39:35 +02:00
Dennis Guse e44f63feb7 Merge remote-tracking branch 'weblate/main' 2025-09-25 19:08:49 +02:00
SomeTr e72d2a750c Translated using Weblate (Ukrainian)
Currently translated at 93.5% (507 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2025-09-22 10:09:12 +00:00
Claudinux 81f6bbe955 Translated using Weblate (Italian)
Currently translated at 3.5% (6 of 171 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/it/
2025-09-09 15:09:10 +00:00
mikini c162c0a0ff Translated using Weblate (Danish)
Currently translated at 97.4% (528 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/da/
2025-09-07 08:51:08 +00:00
Outbreak2096 385d05dcdb Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (542 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2025-09-07 08:51:08 +00:00
Claudinux 308ce4ac07 Translated using Weblate (Italian)
Currently translated at 100.0% (542 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/it/
2025-09-07 08:51:08 +00:00
Outbreak2096 f8a0241e1f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 93.5% (159 of 170 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/zh_Hans/
2025-09-07 08:51:08 +00:00
Claudinux c74a5c8749 Translated using Weblate (Italian)
Currently translated at 2.9% (5 of 170 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/it/
2025-09-07 08:51:08 +00:00
Claudinux defdd49a65 Translated using Weblate (Italian)
Currently translated at 97.4% (528 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/it/
2025-09-07 08:51:08 +00:00
155 changed files with 2726 additions and 2796 deletions
+36
View File
@@ -0,0 +1,36 @@
name: Android Test
on:
push:
branches:
- main
pull_request:
jobs:
lint:
name: Android Lint
runs-on: docker
container:
image: 'ubuntu:android-24.04'
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Lint
shell: bash
run: ./gradlew lint
android-check:
runs-on: docker
container:
image: 'ubuntu:android-24.04'
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Test
shell: bash
run: ./gradlew testNightlyDebugUnitTest --stacktrace
+6
View File
@@ -47,6 +47,12 @@
<MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="72" />
</MarkdownNavigatorCodeStyleSettings>
<editorconfig>
<option name="ENABLED" value="false" />
</editorconfig>
<codeStyleSettings language="JAVA">
<option name="RIGHT_MARGIN" value="200" />
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
-10
View File
@@ -1,10 +0,0 @@
steps:
build:
when:
event: [ push ]
image: androidsdk/android-31
commands:
- apt-get update
- apt-get install -y openjdk-17-jdk-headless
- update-java-alternatives -a
- ./gradlew lint
-10
View File
@@ -1,10 +0,0 @@
steps:
build:
when:
event: [ push ]
image: androidsdk/android-31
commands:
- apt-get update
- apt-get install -y openjdk-17-jdk-headless
- update-java-alternatives -a
- ./gradlew testNightlyDebugUnitTest --stacktrace
+6 -5
View File
@@ -46,8 +46,8 @@ android {
defaultConfig {
applicationId 'de.dennisguse.opentracks'
versionCode 6294
versionName "v4.22.2"
versionCode 6407
versionName "v4.23.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -149,16 +149,17 @@ dependencies {
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.core:core:1.17.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.core:core-splashscreen:1.2.0'
implementation 'androidx.mediarouter:mediarouter:1.8.1'
implementation 'androidx.core:core-location-altitude:1.0.0-alpha03'
implementation 'androidx.core:core-location-altitude:1.0.0-beta01'
implementation 'androidx.work:work-runtime:2.11.0'
androidTestImplementation 'androidx.test:core:1.7.0'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test:rules:1.7.0'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
androidTestImplementation 'org.mockito:mockito-android:5.19.0'
androidTestImplementation 'org.mockito:mockito-android:5.20.0'
androidTestUtil 'androidx.test:orchestrator:1.6.1'
}
@@ -0,0 +1,10 @@
v4.23.0: OpenTracks
Änderungen:
- Benutzeroberfläche: Durchschnittsgeschwindigkeit des aktuellen Segments hinzugefügt
(derzeit nur in neuen Layouts verfügbar)
- Durch Klicken auf die Benachrichtigung wird die eigentliche Aufzeichnungs-Benutzeroberfläche geöffnet
Entwickler:
- Veralteter JobIntentService durch WorkManager ersetzt
- Verwende .isEmpty() außer für CharSequence und Stream.toList()
@@ -0,0 +1,8 @@
v4.23.1: OpenTracks
Änderungen:
- Neue UI-Komponente zur Anzeige der verstrichenen Zeit im aktuellen Segment
Fehlerbehebung:
- Sofortiger Export nach Workout abgestürzt (eingeführt in v4.23.0)
@@ -0,0 +1,10 @@
v4.23.0: OpenTracks
Changes:
- UI: added average pace of current segment
(currently only available in new layouts)
- Clicking on notification opens actual recording UI
Developer:
- Deprecated JobIntentService replaced with WorkManager
- Use .isEmpty() except for CharSequence and Stream.toList()
@@ -0,0 +1,7 @@
v4.23.1: OpenTracks
Changes:
- New UI component to show elapsed time in the current segment
Bugfix:
- Instant post-workout export crashed (introduced in v4.23.0)
@@ -0,0 +1,10 @@
v4.23.2: OpenTracks
Changes:
- Predictive back works completely
Bugfix:
- Fixed in crash related to the track filename in the settings
Developer:
- Introduced data class for Statistics
@@ -0,0 +1,4 @@
v4.22.0: OpenTracks
Modifiche:
- Migrazione a https://codeberg.org/OpenTracksApp/OpenTracks
@@ -0,0 +1,4 @@
v4.22.1: OpenTracks
Modiciche:
- Primo rilascio da Codeberg
@@ -0,0 +1,4 @@
v4.22.2: OpenTracks
Correzione di errori:
- L'impostazione degli annunci vocali per l'ora e l'unità di misura è stata collegata
@@ -0,0 +1,10 @@
(versionName): OpenTracks
Modifiche:
-
Correzioni di errori:
-
Sviluppatore:
-
@@ -1,22 +1,22 @@
一款尊重您的隐私的运动追踪应用。
尊重您隐私的运动追踪伙伴。
健康幸福的生活总少不了各种运动和户外活动。
运动与户外活动能带来快乐、提升技能、增强自信。
使用本应用,您可以保持对您的训练数据的有效追踪,为您的健康助力。
本应用可以在您跑步或散步时记录数据,也可以在您骑车时作为一个大屏幕自行车电脑。
您可以为路上的有趣位置创建标记,还可以用图片作为标记。
您的数据会被详尽地记录下来,供您查阅分析。
您的数据只会分享给您想要分享给的人。
通过跟踪训练数据,珍视您的健康。
记录您的跑步或步行数据,并提供大屏的专用骑行码表。
用照片标记沿途有趣的地点。
保留详细记录的统计数据以供分析。
仅分享您愿意公开的数据。
* 语音播报
* 支持蓝牙LE传感器:心率、速度和距离(骑车)、踏频(骑车)和功率计(骑车)。
* 海拔增减:通过气压传感器。
* EGM2008 海拔显示(平均海平面以上);导出为 WGS84。
* 导出路线数据,支持 KMZ(包括照片)、KML 和 GPX 格式。
* 不需要联网,不需要额外权限。
* 语音播报。
* 支持蓝牙低功耗传感器:心率、速度和距离(骑行)、踏频(骑行)、功率计(骑行)。
* 海拔上升和下降:通过气压传感器。
* 采用 EGM2008 显示海拔(平均海平面以上);导出为 WGS84。
* 以 KMZ(含照片)、KML 或 GPX 格式导出轨迹数据。
* 无需互联网访问或额外权限。
* 深色和浅色主题,跟随系统设置。
* 没有广告。
* 无广告。
本应用是开源软件
意味着您可以使用、学习、修改和分发本应用的源代码。
本应用使用 Apache 2.0 协议发布。
‌自由软件/开放源代码
意味着您可以使用、研究、修改和分享源代码。
采用 Apache 2.0 许可
@@ -1 +1 @@
一款尊重您的隐私的运动追踪应用。
尊重您隐私的运动追踪伙伴。
@@ -13,18 +13,20 @@ import java.time.Duration;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
//TODO assertEquals with ChartPoint (instead of individual comparisons)
@RunWith(AndroidJUnit4.class)
public class ChartPointTest {
private static final Statistics STATISTIC_NULL = new Statistics(null, null, Duration.ZERO, null, null, null, null, null, null, null);
@Test
public void create_by_time() {
// given
TrackStatistics statistics = new TrackStatistics();
statistics.setTotalTime(Duration.ofSeconds(1000));
Statistics statistics = new Statistics(null, null, Duration.ofSeconds(1000), null, null, null, null, null, null, null);
// when
ChartPoint point = ChartPoint.create(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), false, UnitSystem.IMPERIAL_FEET);
@@ -36,8 +38,7 @@ public class ChartPointTest {
@Test
public void create_by_distance() {
// given
TrackStatistics statistics = new TrackStatistics();
statistics.setTotalDistance(Distance.of(1000));
Statistics statistics = new Statistics(null, null, null, null, Distance.of(1000), null, null, null, null, null);
// when
ChartPoint point = ChartPoint.create(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), true, UnitSystem.METRIC);
@@ -49,12 +50,11 @@ public class ChartPointTest {
@Test
public void create_get_altitude_speed_and_pace() {
// given
TrackStatistics statistics = new TrackStatistics();
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
.setAltitude(Altitude.EGM2008.of(50));
// when
ChartPoint point = ChartPoint.create(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
ChartPoint point = ChartPoint.create(STATISTIC_NULL, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertEquals(50, point.altitude(), 0.01);
@@ -65,11 +65,10 @@ public class ChartPointTest {
@Test
public void create_sensorNotAvailable() {
// given
TrackStatistics statistics = new TrackStatistics();
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
.setAltitude(Altitude.EGM2008.of(50));
// when
ChartPoint point = ChartPoint.create(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
ChartPoint point = ChartPoint.create(STATISTIC_NULL, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertNull(point.heartRate());
@@ -86,10 +85,8 @@ public class ChartPointTest {
.setCadence(101f)
.setPower(102f);
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = ChartPoint.create(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
ChartPoint point = ChartPoint.create(STATISTIC_NULL, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertEquals(100.0, point.heartRate(), 0.01);
@@ -6,13 +6,11 @@ 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.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Marker;
@@ -20,9 +18,8 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
import de.dennisguse.opentracks.util.FileUtils;
public class TestDataUtil {
@@ -85,10 +82,6 @@ public class TestDataUtil {
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(trackId, trackPoints.get(1), "Marker 1", "Marker description 1", "Marker category 3", "", null),
new Marker(trackId, trackPoints.get(4), "Marker 2", "Marker description 2", "Marker category 3", "", null),
@@ -126,8 +119,7 @@ public class TestDataUtil {
trackPoint.setHeartRate(100f + i % 80);
trackPoint.setCadence(300f + i);
trackPoint.setPower(400f + i);
trackPoint.setAltitudeGain(ALTITUDE_GAIN);
trackPoint.setAltitudeLoss(ALTITUDE_LOSS);
trackPoint.setAltitudeGainLoss(ALTITUDE_GAIN, ALTITUDE_LOSS);
return trackPoint;
}
@@ -153,16 +145,11 @@ public class TestDataUtil {
dstFile.createNewFile();
Uri photoUri = FileUtils.getUriForFile(context, dstFile);
//TODO Use TrackStatisticsUpdater
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(0));
stats.setTotalTime(Duration.ofMillis(0));
return new Marker(trackId, trackPoint, "Marker name", "Marker description", "Marker category", "", photoUri);
}
public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
try (AutoCloseableIterator<TrackPoint> trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
while (trackPointIterator.hasNext()) {
trackPoints.add(trackPointIterator.next());
@@ -170,20 +157,4 @@ public class TestDataUtil {
return trackPoints;
}
}
public static Pair<Track.Id, TrackStatistics> buildTrackWithTrackPoints(ContentProviderUtils contentProviderUtils, int numberOfPoints) {
Track dummyTrack = new Track();
dummyTrack.setId(new Track.Id(System.currentTimeMillis()));
dummyTrack.setName("Dummy Track");
contentProviderUtils.insertTrack(dummyTrack);
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
for (int i = 0; i < numberOfPoints; i++) {
TrackPoint tp = TestDataUtil.createTrackPoint(i);
contentProviderUtils.insertTrackPoint(tp, dummyTrack.getId());
trackStatisticsUpdater.addTrackPoint(tp);
}
dummyTrack.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(dummyTrack);
return new Pair<>(dummyTrack.getId(), trackStatisticsUpdater.getTrackStatistics());
}
}
@@ -41,8 +41,7 @@ public class TestSensorDataUtil {
if (power != null) {
tp.setPower(power);
}
tp.setAltitudeGain(3f);
tp.setAltitudeLoss(3f);
tp.setAltitudeGainLoss(3, 3);
trackPointList.add(tp);
}
@@ -47,23 +47,25 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -115,7 +117,7 @@ public class CustomContentProviderUtilsTest {
private void testIterator(Track.Id trackId, int numPoints) {
TrackPoint.Id lastPointId = initializeTrack(trackId, numPoints);
List<TrackPoint> locations = new ArrayList<>(numPoints);
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
try (AutoCloseableIterator<TrackPoint> it = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
while (it.hasNext()) {
TrackPoint trackPoint = it.next();
assertNotNull(trackPoint);
@@ -153,7 +155,7 @@ public class CustomContentProviderUtilsTest {
// Load all inserted trackPoints.
TrackPoint.Id lastPointId = null;
int counter = 0;
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(id, null)) {
try (AutoCloseableIterator<TrackPoint> it = contentProviderUtils.getTrackPointLocationIterator(id, null)) {
while (it.hasNext()) {
TrackPoint trackPoint = it.next();
lastPointId = trackPoint.getId();
@@ -407,18 +409,18 @@ public class CustomContentProviderUtilsTest {
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
// AverageSpeed
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime(Instant.ofEpochMilli(1000));
statistics.setStopTime(Instant.ofEpochMilli(2500));
statistics.setTotalTime(Duration.ofMillis(1500));
statistics.setMovingTime(Duration.ofMillis(700));
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);
track.first.setStatistics(new Statistics(
Instant.ofEpochMilli(1000),
Instant.ofEpochMilli(2500),
Duration.ofMillis(1500),
Duration.ofMillis(700),
Distance.of(750),
Speed.of(60),
new AltitudeExtremities(1250, 1200),
new AltitudeGainLoss(50, 50),
null,
null
));
contentProviderUtils.insertTrack(track.first);
Marker marker = new Marker(trackId, track.second.get(0));
@@ -451,7 +453,7 @@ public class CustomContentProviderUtilsTest {
contentProviderUtils.insertMarker(marker1);
// Check insert was done.
assertEquals(contentProviderUtils.getMarkers(trackId).size(), 1);
assertEquals(1, contentProviderUtils.getMarkers(trackId).size());
// Get marker id that needs to delete.
Marker.Id marker1Id = contentProviderUtils.insertMarker(marker1);
@@ -478,7 +480,7 @@ public class CustomContentProviderUtilsTest {
contentProviderUtils.insertMarker(marker1);
// Check insert was done.
assertEquals(contentProviderUtils.getMarkers(trackId).size(), 1);
assertEquals(1, contentProviderUtils.getMarkers(trackId).size());
// Get marker id that needs to delete.
Marker.Id marker1Id = contentProviderUtils.insertMarker(marker1);
@@ -832,7 +834,7 @@ public class CustomContentProviderUtilsTest {
List<TrackPoint.Id> trackpointIds = track.second.stream()
.map(it -> ContentUris.parseId(contentProviderUtils.insertTrackPoint(it, track.first.getId())))
.map(TrackPoint.Id::new).collect(Collectors.toList());
.map(TrackPoint.Id::new).toList();
// when
try (Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds.get(8))) {
@@ -850,12 +852,12 @@ public class CustomContentProviderUtilsTest {
List<TrackPoint.Id> trackpointIds = track.second.stream()
.map(it -> ContentUris.parseId(contentProviderUtils.insertTrackPoint(it, track.first.getId())))
.map(TrackPoint.Id::new).collect(Collectors.toList());
.map(TrackPoint.Id::new).toList();
TrackPoint.Id startTrackPointId = trackpointIds.get(0);
// when
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, trackpointIds.get(0));
AutoCloseableIterator<TrackPoint> trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, trackpointIds.get(0));
// then
for (int i = 0; i < trackpointIds.size(); i++) {
@@ -48,10 +48,8 @@ public class CustomSQLiteOpenHelperTest {
public static Map<String, String> getSQL(SQLiteDatabase db, String type) {
HashMap<String, String> sqlMap = new HashMap<>();
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "type=?", new String[]{type}, null, null, "name")) {
if (cursor != null) {
while (cursor.moveToNext()) {
sqlMap.put(cursor.getString(0), cursor.getString(1));
}
while (cursor.moveToNext()) {
sqlMap.put(cursor.getString(0), cursor.getString(1));
}
}
return sqlMap;
@@ -64,12 +62,10 @@ public class CustomSQLiteOpenHelperTest {
*/
private static boolean hasSqlCreate(SQLiteDatabase db, String sqlCreate) {
try (Cursor cursor = db.rawQuery("SELECT SQL FROM sqlite_master", null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
String sql = cursor.getString(0);
if (sqlCreate.equals(sql)) {
return true;
}
while (cursor.moveToNext()) {
String sql = cursor.getString(0);
if (sqlCreate.equals(sql)) {
return true;
}
}
}
@@ -37,9 +37,9 @@ public class TrackSelectionTest extends TestCase {
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.selection(), "_id IN (?)");
assertEquals(selection.selectionArgs().length, 1);
assertEquals(selection.selectionArgs()[0], "1");
assertEquals("_id IN (?)", selection.selection());
assertEquals(1, selection.selectionArgs().length);
assertEquals("1", selection.selectionArgs()[0]);
}
@Test
@@ -57,11 +57,11 @@ public class TrackSelectionTest extends TestCase {
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.selection(), "_id IN (?,?,?)");
assertEquals(selection.selectionArgs().length, 3);
assertEquals(selection.selectionArgs()[0], "1");
assertEquals(selection.selectionArgs()[1], "2");
assertEquals(selection.selectionArgs()[2], "3");
assertEquals("_id IN (?,?,?)", selection.selection());
assertEquals(3, selection.selectionArgs().length);
assertEquals("1", selection.selectionArgs()[0]);
assertEquals("2", selection.selectionArgs()[1]);
assertEquals("3", selection.selectionArgs()[2]);
}
@Test
@@ -73,9 +73,9 @@ public class TrackSelectionTest extends TestCase {
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.selection(), "category IN (?)");
assertEquals(selection.selectionArgs().length, 1);
assertEquals(selection.selectionArgs()[0], "running");
assertEquals("category IN (?)", selection.selection());
assertEquals(1, selection.selectionArgs().length);
assertEquals("running", selection.selectionArgs()[0]);
}
@Test
@@ -91,12 +91,12 @@ public class TrackSelectionTest extends TestCase {
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.selection(), "category IN (?,?,?,?)");
assertEquals(selection.selectionArgs().length, 4);
assertEquals(selection.selectionArgs()[0], "running");
assertEquals(selection.selectionArgs()[1], "road biking");
assertEquals(selection.selectionArgs()[2], "mountain biking");
assertEquals(selection.selectionArgs()[3], "trail walking");
assertEquals("category IN (?,?,?,?)", selection.selection());
assertEquals(4, selection.selectionArgs().length);
assertEquals("running", selection.selectionArgs()[0]);
assertEquals("road biking", selection.selectionArgs()[1]);
assertEquals("mountain biking", selection.selectionArgs()[2]);
assertEquals("trail walking", selection.selectionArgs()[3]);
}
@Test
@@ -120,8 +120,8 @@ public class TrackSelectionTest extends TestCase {
assertNull(selectionWrong2.selection());
assertNull(selectionWrong2.selectionArgs());
assertEquals(selectionOk.selection(), "starttime BETWEEN ? AND ?");
assertEquals(selectionOk.selectionArgs().length, 2);
assertEquals("starttime BETWEEN ? AND ?", selectionOk.selection());
assertEquals(2, selectionOk.selectionArgs().length);
assertEquals(selectionOk.selectionArgs()[0], Long.toString(instant.toEpochMilli()));
assertEquals(selectionOk.selectionArgs()[1], Long.toString(instant.toEpochMilli() + oneDay));
}
@@ -143,13 +143,13 @@ public class TrackSelectionTest extends TestCase {
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.selection(), "_id IN (?,?,?) AND category IN (?,?)");
assertEquals(selection.selectionArgs().length, 5);
assertEquals(selection.selectionArgs()[0], "1");
assertEquals(selection.selectionArgs()[1], "2");
assertEquals(selection.selectionArgs()[2], "3");
assertEquals(selection.selectionArgs()[3], "running");
assertEquals(selection.selectionArgs()[4], "road biking");
assertEquals("_id IN (?,?,?) AND category IN (?,?)", selection.selection());
assertEquals(5, selection.selectionArgs().length);
assertEquals("1", selection.selectionArgs()[0]);
assertEquals("2", selection.selectionArgs()[1]);
assertEquals("3", selection.selectionArgs()[2]);
assertEquals("running", selection.selectionArgs()[3]);
assertEquals("road biking", selection.selectionArgs()[4]);
}
@Test
@@ -172,11 +172,11 @@ public class TrackSelectionTest extends TestCase {
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.selection(), "_id IN (?,?,?) AND starttime BETWEEN ? AND ?");
assertEquals(selection.selectionArgs().length, 5);
assertEquals(selection.selectionArgs()[0], "1");
assertEquals(selection.selectionArgs()[1], "2");
assertEquals(selection.selectionArgs()[2], "3");
assertEquals("_id IN (?,?,?) AND starttime BETWEEN ? AND ?", selection.selection());
assertEquals(5, selection.selectionArgs().length);
assertEquals("1", selection.selectionArgs()[0]);
assertEquals("2", selection.selectionArgs()[1]);
assertEquals("3", selection.selectionArgs()[2]);
assertEquals(selection.selectionArgs()[3], Long.toString(instant.toEpochMilli()));
assertEquals(selection.selectionArgs()[4], Long.toString(instant.toEpochMilli() + oneDay));
}
@@ -196,10 +196,10 @@ public class TrackSelectionTest extends TestCase {
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.selection(), "category IN (?,?) AND starttime BETWEEN ? AND ?");
assertEquals(selection.selectionArgs().length, 4);
assertEquals(selection.selectionArgs()[0], "running");
assertEquals(selection.selectionArgs()[1], "road biking");
assertEquals("category IN (?,?) AND starttime BETWEEN ? AND ?", selection.selection());
assertEquals(4, selection.selectionArgs().length);
assertEquals("running", selection.selectionArgs()[0]);
assertEquals("road biking", selection.selectionArgs()[1]);
assertEquals(selection.selectionArgs()[2], Long.toString(instant.toEpochMilli()));
assertEquals(selection.selectionArgs()[3], Long.toString(instant.toEpochMilli() + oneDay));
}
@@ -21,7 +21,7 @@ public class DistanceFormatterTest {
@Test
public void testFormatDistance_metric() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setDecimalPlaces(2)
.setUnit(UnitSystem.METRIC)
.build(context);
@@ -34,7 +34,7 @@ public class DistanceFormatterTest {
@Test
public void testFormatDistance_imperial() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setDecimalPlaces(2)
.setUnit(UnitSystem.IMPERIAL_FEET)
.build(context);
@@ -47,7 +47,7 @@ public class DistanceFormatterTest {
@Test
public void testFormatDistance_nautical() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setDecimalPlaces(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.build(context);
@@ -21,7 +21,6 @@ public class SpeedFormatterTest {
@Test
public void testGetSpeedParts_pace_metric() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(false)
.build(context);
@@ -35,7 +34,6 @@ public class SpeedFormatterTest {
@Test
public void testGetSpeedParts_pace_imperial() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.IMPERIAL_FEET)
.setReportSpeedOrPace(false)
.build(context);
@@ -47,7 +45,6 @@ public class SpeedFormatterTest {
@Test
public void testGetSpeedParts_pace_nautical() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.setReportSpeedOrPace(false)
.build(context);
@@ -59,7 +56,6 @@ public class SpeedFormatterTest {
@Test
public void testGetSpeedParts_speed_metric() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(true)
.build(context);
@@ -71,7 +67,6 @@ public class SpeedFormatterTest {
@Test
public void testGetSpeedParts_speed_imperial() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.IMPERIAL_FEET)
.setReportSpeedOrPace(true)
.build(context);
@@ -83,7 +78,6 @@ public class SpeedFormatterTest {
@Test
public void testGetSpeedParts_speed_nautical() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.setReportSpeedOrPace(true)
.build(context);
@@ -95,7 +89,6 @@ public class SpeedFormatterTest {
@Test
public void testFormatSpeed() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(false)
.build(context);
@@ -9,6 +9,7 @@ import java.util.Collection;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
//TODO Merge with TrackFilenameGeneratorTest whenever Junit5 gets available.
@@ -36,9 +37,9 @@ public class TrackFilenameGenerator2Test {
Track track = new Track();
track.setName("Best Track");
track.setUuid(UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6"));
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
track.setStatistics(new Statistics(Instant.parse("2020-02-02T02:02:02Z"), null, null, null, null, null, null, null, null, null));
// when
String filename = subject.format(track, TrackFileFormat.GPX);
subject.format(track, TrackFileFormat.GPX);
}
}
@@ -17,6 +17,7 @@ import java.util.UUID;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TimezoneRule;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
@RunWith(Parameterized.class)
@@ -50,7 +51,7 @@ public class TrackFilenameGeneratorTest {
Track track = new Track();
track.setName("Best Track");
track.setUuid(UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"));
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
track.setStatistics(new Statistics(Instant.parse("2020-02-02T02:02:02Z"), null, null, null, null, null, null, null, null, null));
// when
String filename = subject.format(track, TrackFileFormat.GPX);
@@ -60,6 +60,7 @@ import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -75,7 +76,6 @@ import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
@@ -203,20 +203,20 @@ public class ExportImportTest {
setUp();
Track track = contentProviderUtils.getTrack(trackId);
TrackStatistics trackStatistics = track.getTrackStatistics();
Statistics trackStatistics = track.getStatistics();
assertEquals(ZoneOffset.of("+01:00"), track.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), trackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:04:00Z"), trackStatistics.getStopTime());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), trackStatistics.startTime());
assertEquals(Instant.parse("2020-02-02T02:04:00Z"), trackStatistics.stopTime());
assertEquals(Duration.ofSeconds(56), trackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(26), trackStatistics.getMovingTime()); //TODO Likely too low
assertEquals(Duration.ofSeconds(56), trackStatistics.totalTime());
assertEquals(Duration.ofSeconds(26), trackStatistics.movingTime()); //TODO Likely too low
// Distance
assertEquals(222049.34375, trackStatistics.getTotalDistance().toM(), 0.01); //TODO Too low
assertEquals(222049.34375, trackStatistics.totalDistance().toM(), 0.01); //TODO Too low
// Speed
assertEquals(8540.359, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(8540.359, trackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(3965.166, trackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(8540.359, trackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
@@ -224,8 +224,8 @@ public class ExportImportTest {
assertEquals(999.122, trackStatistics.getMinAltitude(), 0.01);
assertEquals(1020.25, trackStatistics.getMaxAltitude(), 0.01);
assertEquals(2, trackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(2, trackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(2, trackStatistics.altitudeGainLoss().gain_m(), 0.01);
assertEquals(2, trackStatistics.altitudeGainLoss().loss_m(), 0.01);
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
@@ -237,17 +237,14 @@ public class ExportImportTest {
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(15)))
.setAltitudeLoss(1f)
.setAltitudeGain(1f),
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Instant.parse("2020-02-02T02:02:04Z"))
.setSensorDistance(Distance.of(10))
.setSpeed(Speed.of(15))
.setHeartRate(HeartRate.of(66))
.setCadence(3)
.setPower(50)
.setAltitudeLoss(1f)
.setAltitudeGain(1f),
.setPower(50).setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:15Z"))
.setHeartRate(HeartRate.of(68))
.setCadence(3)
@@ -260,8 +257,7 @@ public class ExportImportTest {
null,
Speed.of(5)))
.setSensorDistance(Distance.of(2))
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(69))
.setCadence(3)
.setPower(50),
@@ -274,27 +270,23 @@ public class ExportImportTest {
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z"))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null, Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
}
@@ -334,29 +326,29 @@ public class ExportImportTest {
a.assertEquals(trackPoints, actual);
// 3. trackstatistics
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
Statistics importedTrackStatistics = importedTrack.getStatistics();
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:04:00Z"), importedTrackStatistics.getStopTime());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.startTime());
assertEquals(Instant.parse("2020-02-02T02:04:00Z"), importedTrackStatistics.stopTime());
assertEquals(Duration.ofSeconds(56), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(26), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(56), importedTrackStatistics.totalTime());
assertEquals(Duration.ofSeconds(26), importedTrackStatistics.movingTime());
// Distance
assertEquals(222049.421, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(222049.421, importedTrackStatistics.totalDistance().toM(), 0.01);
// Speed
assertEquals(8540.362, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(8540.362, importedTrackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(3965.168, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(8540.362, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
// Altitude
assertEquals(999.122, importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(1020.25, importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(2, importedTrackStatistics.altitudeGainLoss().gain_m(), 0.01);
assertEquals(2, importedTrackStatistics.altitudeGainLoss().loss_m(), 0.01);
// 4. markers
@@ -433,8 +425,7 @@ public class ExportImportTest {
Altitude.WGS84.of(1020.2), null,
null,
Speed.of(15)))
.setAltitudeLoss(1f)
.setAltitudeGain(1f),
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
@@ -442,8 +433,7 @@ public class ExportImportTest {
Altitude.WGS84.of(1020.2), null,
null,
Speed.of(5)))
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setAltitudeGainLoss(1, 1)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
@@ -455,8 +445,7 @@ public class ExportImportTest {
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
@@ -464,8 +453,7 @@ public class ExportImportTest {
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
@@ -473,35 +461,34 @@ public class ExportImportTest {
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(10)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setAltitudeGainLoss(0, 0)
.setSpeed(Speed.of(15))
), actual);
// 3. trackstatistics
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
Statistics importedTrackStatistics = importedTrack.getStatistics();
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:03:50Z"), importedTrackStatistics.getStopTime());
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.startTime());
assertEquals(Instant.parse("2020-02-02T02:03:50Z"), importedTrackStatistics.stopTime());
assertEquals(Duration.ofSeconds(107), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(107), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(107), importedTrackStatistics.totalTime());
assertEquals(Duration.ofSeconds(107), importedTrackStatistics.movingTime());
// Distance
assertEquals(222271.734, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(222271.734, importedTrackStatistics.totalDistance().toM(), 0.01);
// Speed
assertEquals(2077.305, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(2077.305, importedTrackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(2077.305, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(2077.305, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
// Altitude
assertEquals(999.099, importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(1020.2, importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(2, importedTrackStatistics.altitudeGainLoss().gain_m(), 0.01);
assertEquals(2, importedTrackStatistics.altitudeGainLoss().loss_m(), 0.01);
// 4. markers
assertMarkers();
@@ -30,11 +30,11 @@ import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Test that legacy KML/GPX formats can still be imported.
@@ -149,9 +149,9 @@ public class GPXTrackImporterTest {
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
assertEquals(0.75, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
Statistics trackStatistics = importedTrack.getStatistics();
assertEquals(0.75, trackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(101), trackStatistics.movingTime());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
@@ -204,9 +204,9 @@ public class GPXTrackImporterTest {
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
assertEquals(5.0, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
Statistics trackStatistics = importedTrack.getStatistics();
assertEquals(5.0, trackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(101), trackStatistics.movingTime());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
@@ -267,8 +267,7 @@ public class GPXTrackImporterTest {
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeLoss(1f)
.setAltitudeGain(1f),
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
@@ -276,8 +275,7 @@ public class GPXTrackImporterTest {
Altitude.WGS84.of(10), null,
null,
Speed.of(5)))
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setAltitudeGainLoss(1, 1)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
@@ -289,8 +287,7 @@ public class GPXTrackImporterTest {
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
@@ -298,8 +295,7 @@ public class GPXTrackImporterTest {
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
@@ -307,8 +303,7 @@ public class GPXTrackImporterTest {
Altitude.WGS84.of(10), null,
null,
Speed.of(10)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setAltitudeGainLoss(0, 0)
.setSpeed(Speed.of(15))
), actual);
}
@@ -189,24 +189,21 @@ public class KMLTrackImporterTest {
12.340097, 1.234156, null,
Altitude.WGS84.of(469.286376953125), null,
null,
Speed.of(0.539)))
.setAltitudeGain(0f),
Speed.of(0.539))),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-11-28T17:06:25.448Z"),
12.340036, 1.23415, null,
Altitude.WGS84.of(439.1626281738281), null,
null,
Speed.of(0.1577)))
.setAltitudeGain(0f),
Speed.of(0.1577))),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-11-28T17:06:47.888Z"),
12.340057, 1.23405, null,
Altitude.WGS84.of(421.8070983886719), null,
null,
Speed.of(0)))
.setAltitudeGain(0f),
Speed.of(0))),
// created resume trackpoint with time of next valid trackpoint
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
@@ -214,16 +211,14 @@ public class KMLTrackImporterTest {
12.340057, 1.23405, null,
Altitude.WGS84.of(419.93902587890625), null,
null,
Speed.of(0)))
.setAltitudeGain(0f),
Speed.of(0))),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-11-28T17:06:56.905Z"),
12.340057, 1.23405, null,
Altitude.WGS84.of(419.9036560058594), null,
null,
Speed.of(0)))
.setAltitudeGain(0f),
Speed.of(0))),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-11-28T17:07:20.870Z"),
@@ -231,7 +226,6 @@ public class KMLTrackImporterTest {
Altitude.WGS84.of(417.99432373046875), null,
null,
Speed.of(0)))
.setAltitudeGain(0f)
), importedTrackPoints);
}
@@ -29,13 +29,9 @@ public class TrackPointAssert {
Assert.assertEquals("altitude", expected.getAltitude().toM(), actual.getAltitude().toM(), delta);
}
Assert.assertEquals("has altitudeGain", expected.hasAltitudeGain(), actual.hasAltitudeGain());
if (expected.hasAltitudeGain()) {
Assert.assertEquals("altitudeGain", expected.getAltitudeGain(), actual.getAltitudeGain(), delta);
}
Assert.assertEquals("has altitudeLoss", expected.hasAltitudeLoss(), actual.hasAltitudeLoss());
if (expected.hasAltitudeLoss()) {
Assert.assertEquals("altitudeLoss", expected.getAltitudeLoss(), actual.getAltitudeLoss(), delta);
Assert.assertEquals("has altitudeGainLoss", expected.hasAltitudeGainLoss(), actual.hasAltitudeGainLoss());
if (expected.hasAltitudeGainLoss()) {
Assert.assertEquals("altitudeGainLoss", expected.getAltitudeGainLoss(), actual.getAltitudeGainLoss());
}
Assert.assertEquals("has speed", expected.hasSpeed(), actual.hasSpeed());
@@ -1,50 +1,54 @@
package de.dennisguse.opentracks.sensors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import org.junit.Test;
public class UintUtilsTest {
@Test
public void diff() {
public void diff_uint16_valid() {
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT16_MAX));
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT16_MAX));
assertEquals(3, UintUtils.diff(5, 2, UintUtils.UINT16_MAX));
assertEquals(65535, UintUtils.diff(1, 2, UintUtils.UINT16_MAX)); /* unsigned 16 arithmetic is modulo UINT16_MAX + 1, not modulo UINT16_MAX */
assertEquals(65530, UintUtils.diff(UintUtils.UINT16_MAX, 5, UintUtils.UINT16_MAX));
}
@Test
public void diff_uint16_invalid() {
/* Test modulo arithmetic for arguments that are out of range */
assertThrows(RuntimeException.class, () -> UintUtils.diff(65537, 1, UintUtils.UINT16_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(1, 65537, UintUtils.UINT16_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(65537, 65537, UintUtils.UINT16_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(-65535, 1, UintUtils.UINT16_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(1, -65535, UintUtils.UINT16_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(-65535, -65535, UintUtils.UINT16_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(-65535, 65537, UintUtils.UINT16_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(65537, -65535, UintUtils.UINT16_MAX));
}
if (false) { /* false means UintUtils.diff() throws if arguments out of range */
assertEquals(0, UintUtils.diff(65537, 1, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(1, 65537, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(65537, 65537, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(-65535, 1, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(1, -65535, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(-65535, -65535, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(-65535, 65537, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(65537, -65535, UintUtils.UINT16_MAX));
}
/* The following tests are the above, but for 32-bit unsigned. */
@Test
public void diff_uint32() {
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT32_MAX));
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT32_MAX));
assertEquals(3, UintUtils.diff(5, 2, UintUtils.UINT32_MAX));
assertEquals(4294967295L, UintUtils.diff(1, 2, UintUtils.UINT32_MAX));
assertEquals(4294967290L, UintUtils.diff(UintUtils.UINT32_MAX, 5, UintUtils.UINT32_MAX));
}
if (false) { /* false means UintUtils.diff() throws if arguments out of range */
assertEquals(0, UintUtils.diff(4294967297L, 1, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(1, 4294967297L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(4294967297L, 4294967297L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(-4294967295L, 1, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(1, -4294967295L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(-4294967295L, -4294967295L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(-4294967295L, 4294967297L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(65537, -4294967297L, UintUtils.UINT32_MAX));
}
@Test
public void diff_unint32_invalid() {
/* Test modulo arithmetic for arguments that are out of range */
assertThrows(RuntimeException.class, () -> UintUtils.diff(4294967297L, 1, UintUtils.UINT32_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(1, 4294967297L, UintUtils.UINT32_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(4294967297L, 4294967297L, UintUtils.UINT32_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(-4294967295L, 1, UintUtils.UINT32_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(1, -4294967295L, UintUtils.UINT32_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(-4294967295L, -4294967295L, UintUtils.UINT32_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(-4294967295L, 4294967297L, UintUtils.UINT32_MAX));
assertThrows(RuntimeException.class, () -> UintUtils.diff(65537, -4294967297L, UintUtils.UINT32_MAX));
}
@Test
@@ -16,23 +16,20 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(MockitoJUnitRunner.class)
public class TrackRecordingServiceNotificationManagerTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Mock
private TrackPoint trackPointMock;
@Mock
private TrackStatistics trackStatisticsMock;
@Mock
private NotificationCompat.Builder notificationCompatBuilder;
@@ -41,21 +38,40 @@ public class TrackRecordingServiceNotificationManagerTest {
@Test
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
when(trackPointMock.hasHorizontalAccuracy()).thenReturn(true);
when(trackPointMock.getHorizontalAccuracy()).thenReturn(Distance.of(999f));
when(trackPointMock.getSpeed()).thenReturn(Speed.of(0));
when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
TrackPoint trackPointMock = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
null,
null, null, Distance.of(999f),
Altitude.WGS84.of(10), null,
null,
Speed.of(0))
);
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean()))
.thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
subject.setUnitSystem(UnitSystem.METRIC);
Track track = new Track(new Statistics(
null,
null,
null,
null,
Distance.of(0),
null,
null,
null,
null,
null
));
RecordingData data = new RecordingData(track, trackPointMock, null, null);
// when
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(1000));
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
subject.updateTrackPoint(context, data, Distance.of(100));
subject.updateTrackPoint(context, data, Distance.of(100));
subject.updateTrackPoint(context, data, Distance.of(1000));
subject.updateTrackPoint(context, data, Distance.of(100));
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
@@ -5,8 +5,11 @@ import static org.junit.Assert.assertFalse;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationManager;
import android.os.Looper;
import androidx.core.location.LocationManagerCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
@@ -30,11 +33,13 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
@@ -46,7 +51,6 @@ import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests insert location.
@@ -106,8 +110,8 @@ public class TrackRecordingServiceRecordingTest {
// then
assertEquals(new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(startTime), Duration.ZERO, Duration.ZERO, Distance.of(0), Speed.zero(), new AltitudeExtremities(0.0, 0.0), null, null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime))
@@ -120,14 +124,13 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 1, 1, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(stopTime), Duration.ofSeconds(1), Duration.ofSeconds(1), Distance.of(0), Speed.zero(), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -143,10 +146,14 @@ public class TrackRecordingServiceRecordingTest {
String startTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
//We do not want the real GPS to interfere.
LocationManagerCompat.removeUpdates((LocationManager) context.getSystemService(Context.LOCATION_SERVICE), trackPointCreator.getSensorManager().getGpsManager());
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
String gps2 = "2020-02-02T02:02:04Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 15);
// when
String idleTime = "2020-02-02T02:02:17Z";
@@ -192,14 +199,13 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, pauseTime, 0, 1, 1, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(pauseTime), Duration.ofSeconds(1), Duration.ofSeconds(1), Distance.of(0), Speed.zero(), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
//when
@@ -208,14 +214,13 @@ public class TrackRecordingServiceRecordingTest {
service.resumeTrack(trackId);
// then
assertEquals(new TrackStatistics(startTime, resumeTime, 0, 1, 1, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(resumeTime), Duration.ofSeconds(1), Duration.ofSeconds(1), Distance.of(0), Speed.zero(), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -248,8 +253,7 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(starTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
), trackPoints);
}
@@ -278,8 +282,7 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -315,12 +318,10 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor3))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(7)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(7))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -338,28 +339,27 @@ public class TrackRecordingServiceRecordingTest {
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps1, 0, 1, 1, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(gps1), Duration.ofSeconds(1), Duration.ofSeconds(1), Distance.of(0), Speed.of(15), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0001, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps2, 45.0001, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps2, 11.113178253173828f, 4, 4, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(gps2), Duration.ofSeconds(4), Duration.ofSeconds(4), Distance.of(11.113178253173828), Speed.of(15), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String gps3 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0002, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps3, 45.0002, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps3, 22.226356506347656, 6, 6, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(gps3), Duration.ofSeconds(6), Duration.ofSeconds(6), Distance.of(22.226356506347656), Speed.of(15), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
@@ -367,8 +367,8 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 22.226356506347656, 10, 10, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(stopTime), Duration.ofSeconds(10), Duration.ofSeconds(10), Distance.of(22.226356506347656), Speed.of(15), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
@@ -379,8 +379,7 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps2),
@@ -388,8 +387,7 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
@@ -397,11 +395,9 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -417,25 +413,25 @@ public class TrackRecordingServiceRecordingTest {
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
TrackStatistics gps1Statistics = new TrackStatistics(startTime, gps1, 0, 1, 1, 15, 0f, 0f);
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
Statistics gps1Statistics = new Statistics(Instant.parse(startTime), Instant.parse(gps1), Duration.ofSeconds(1), Duration.ofSeconds(1), Distance.of(0), Speed.of(15), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null);
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.00001, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps2, 45.00001, 35.0, 1, 15);
// then
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String gps3 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.00002, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps3, 45.00002, 35.0, 1, 15);
// then
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
@@ -443,8 +439,8 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 2.222635507583618, 10, 10, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(stopTime), Duration.ofSeconds(10), Duration.ofSeconds(10), Distance.of(2.222635507583618), Speed.of(15), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
@@ -455,8 +451,7 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
@@ -464,8 +459,7 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse(stopTime),
@@ -473,8 +467,7 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -491,15 +484,15 @@ public class TrackRecordingServiceRecordingTest {
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// when - will be ignored
String gps2 = "2020-02-02T02:02:04Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 15);
// when
String gps3 = "2020-02-02T02:02:05Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0001, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps3, 45.0001, 35.0, 1, 15);
// when
String stopTime = "2020-02-02T02:02:12Z";
@@ -516,8 +509,7 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
@@ -525,11 +517,9 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -545,19 +535,19 @@ public class TrackRecordingServiceRecordingTest {
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 100, 15);
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 100, 15);
// then
TrackStatistics startStatistics = new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null);
assertEquals(startStatistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
Statistics startStatistics = new Statistics(Instant.parse(startTime), Instant.parse(startTime), Duration.ofSeconds(0), Duration.ofSeconds(0), Distance.of(0), Speed.of(0), new AltitudeExtremities(0.0, 0.0), null, null, null);
assertEquals(startStatistics, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.1, 35.0, 100, 15);
sendGPSLocation(trackPointCreator, gps2, 45.1, 35.0, 100, 15);
// then
assertEquals(startStatistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(startStatistics, contentProviderUtils.getTrack(trackId).getStatistics());
// when
@@ -566,16 +556,14 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 10, 10, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(stopTime), Duration.ofSeconds(10), Duration.ofSeconds(10), Distance.of(0), Speed.of(0), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -591,18 +579,19 @@ public class TrackRecordingServiceRecordingTest {
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps1, 0, 1, 1, 15, 0f, 0f), contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(gps1), Duration.ofSeconds(1), Duration.ofSeconds(1), Distance.of(0), Speed.of(15), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.1, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps2, 45.1, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps2, 11113.275390625, 4, 4, 2778.31884765625f, 0f, 0f).toString(), contentProviderUtils.getTrack(trackId).getTrackStatistics().toString());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(gps2), Duration.ofSeconds(4), Duration.ofSeconds(4), Distance.of(11113.275390625), Speed.of(2778.31884765625f), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
@@ -610,7 +599,8 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 11113.275390625, 10, 10, 1111.3275390625f, 0f, 0f).toString(), contentProviderUtils.getTrack(trackId).getTrackStatistics().toString());
assertEquals(new Statistics(Instant.parse(startTime), Instant.parse(stopTime), Duration.ofSeconds(10), Duration.ofSeconds(10), Distance.of(11113.275390625), Speed.of(1111.3275146484375), new AltitudeExtremities(0.0, 0.0), new AltitudeGainLoss(0, 0), null, null)
, contentProviderUtils.getTrack(trackId).getStatistics());
// then
@@ -623,8 +613,7 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse(gps2),
@@ -632,11 +621,9 @@ public class TrackRecordingServiceRecordingTest {
null, null,
null,
Speed.of(15)))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setAltitudeGainLoss(0, 0)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -669,7 +656,7 @@ public class TrackRecordingServiceRecordingTest {
// when
String gps1 = "2020-02-02T02:02:05Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// when
String sensor3 = "2020-02-02T02:02:06Z";
@@ -683,7 +670,7 @@ public class TrackRecordingServiceRecordingTest {
// when
String gps2 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 4, 15); //Should be ignored
sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 4, 15); //Should be ignored
// when
String sensor5 = "2020-02-02T02:02:10Z";
@@ -692,11 +679,11 @@ public class TrackRecordingServiceRecordingTest {
// when
String gps3 = "2020-02-02T02:02:12Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.001, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps3, 45.001, 35.0, 1, 15);
// when
String gps4 = "2020-02-02T02:02:14Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps4, 45.001, 35.0, 1, 15);
sendGPSLocation(trackPointCreator, gps4, 45.001, 35.0, 1, 15);
// when
@@ -761,4 +748,16 @@ public class TrackRecordingServiceRecordingTest {
}
};
}
private static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
Location location = new Location("mock");
location.setTime(1L); // Should be ignored anyhow.
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
}
}
@@ -1,20 +0,0 @@
package de.dennisguse.opentracks.services;
import android.location.Location;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
public class TrackRecordingServiceTestUtils {
static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
Location location = new Location("mock");
location.setTime(1L); // Should be ignored anyhow.
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
}
}
@@ -22,7 +22,7 @@ import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.SegmentStatisticUpdater;
@RunWith(AndroidJUnit4.class)
public class VoiceAnnouncementManagerTest {
@@ -54,13 +54,13 @@ public class VoiceAnnouncementManagerTest {
voiceAnnouncementManager.setFrequency(Distance.ofKilometer(5));
// when
TrackStatistics statistics = new TrackStatistics();
SegmentStatisticUpdater statistics = new SegmentStatisticUpdater();
statistics.setTotalDistance(Distance.ofKilometer(13));
voiceAnnouncementManager.start(statistics);
voiceAnnouncementManager.start(statistics.getStatistics());
assertEquals(Distance.of(15000), voiceAnnouncementManager.getNextTotalDistance());
statistics.setTotalDistance(Distance.of(15100));
voiceAnnouncementManager.start(statistics);
voiceAnnouncementManager.start(statistics.getStatistics());
assertEquals(Distance.of(20000), voiceAnnouncementManager.getNextTotalDistance());
}
@@ -74,13 +74,13 @@ public class VoiceAnnouncementManagerTest {
voiceAnnouncementManager.setFrequency(Duration.ofSeconds(5));
// when
TrackStatistics statistics = new TrackStatistics();
SegmentStatisticUpdater statistics = new SegmentStatisticUpdater();
statistics.setTotalTime(Duration.ofSeconds(91));
voiceAnnouncementManager.start(statistics);
voiceAnnouncementManager.start(statistics.getStatistics());
assertEquals(Duration.ofSeconds(95), voiceAnnouncementManager.getNextTotalTime());
statistics.setTotalTime(Duration.ofSeconds(95));
voiceAnnouncementManager.start(statistics);
voiceAnnouncementManager.start(statistics.getStatistics());
assertEquals(Duration.ofSeconds(100), voiceAnnouncementManager.getNextTotalTime());
}
@@ -19,23 +19,24 @@ import org.mockito.Mockito;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Map;
import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatisticsUpdater;
@RunWith(AndroidJUnit4.class)
public class VoiceAnnouncementUtilsTest {
@@ -44,12 +45,9 @@ public class VoiceAnnouncementUtilsTest {
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@Before
public void setUp() {
contentProviderUtils = new ContentProviderUtils(context);
PreferencesUtils.setVoiceAnnounceHeartRateCurrent(false);
PreferencesUtils.setVoiceAnnounceLapHeartRate(false);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
@@ -66,16 +64,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_metric_speed() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
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(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1).plusMinutes(5).plusSeconds(10),
Distance.of(20000),
Speed.of(100),
null,
null,
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -89,16 +89,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_metric_speed_rounding_check() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusSeconds(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1).plusSeconds(1),
Distance.of(20000),
Speed.of(100),
null,
new AltitudeGainLoss(6000, 6000),
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -112,16 +114,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_metric_distance_rounding_check() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(19999));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1),
Distance.of(19999),
Speed.of(100),
null,
new AltitudeGainLoss(6000, 6000),
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -135,16 +139,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_metric_distance_rounding_check_two() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(19900));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1),
Distance.of(19900),
Speed.of(100),
null,
new AltitudeGainLoss(6000, 6000),
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -159,26 +165,13 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_withInterval_metric_speed() {
// given
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
Track.Id trackId = trackWithStats.first;
TrackStatistics stats = trackWithStats.second;
IntervalStatistics.Interval lastInterval;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPointIterator);
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
Track track = new Track();
track.setTrackStatistics(stats);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, null).toString();
// then
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
@@ -186,16 +179,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_metric_pace() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
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(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1).plusMinutes(5).plusSeconds(10),
Distance.of(20000),
Speed.of(100),
null,
new AltitudeGainLoss(6000, 6000),
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -210,26 +205,13 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_withInterval_metric_pace() {
// given
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
Track.Id trackId = trackWithStats.first;
TrackStatistics stats = trackWithStats.second;
IntervalStatistics.Interval lastInterval;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPointIterator);
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Track track = new Track();
track.setTrackStatistics(stats);
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, false, data.second, null).toString();
// then
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
@@ -237,16 +219,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_imperial_speed() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
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(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1).plusMinutes(5).plusSeconds(10),
Distance.of(20000),
Speed.of(100),
null,
new AltitudeGainLoss(6000, 6000),
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -260,14 +244,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_imperial_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.ofMile(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1),
Distance.ofMile(1.1),
null,
null,
null,
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -281,14 +269,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_imperial_meter_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.ofMile(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1),
Distance.ofMile(1.1),
null,
null,
null,
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -302,14 +294,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_metric_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.ofKilometer(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1),
Distance.ofKilometer(1.1),
null,
null,
null,
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -324,26 +320,13 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_withInterval_imperial_speed() {
// given
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
Track.Id trackId = trackWithStats.first;
TrackStatistics stats = trackWithStats.second;
IntervalStatistics.Interval lastInterval;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPointIterator);
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Track track = new Track();
track.setTrackStatistics(stats);
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.IMPERIAL_FEET, true, data.second, null).toString();
// then
assertEquals("12:16 AM. Total distance 8.8 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
@@ -351,16 +334,18 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_imperial_pace() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
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(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1).plusMinutes(5).plusSeconds(10),
Distance.of(20000),
Speed.of(100),
null,
new AltitudeGainLoss(6000, 6000),
null,
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
@@ -375,26 +360,13 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_withInterval_imperial_pace() {
// given
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
Track.Id trackId = trackWithStats.first;
TrackStatistics stats = trackWithStats.second;
IntervalStatistics.Interval lastInterval;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPointIterator);
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Track track = new Track();
track.setTrackStatistics(stats);
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.IMPERIAL_FEET, false, data.second, null).toString();
// then
assertEquals("12:16 AM. Total distance 8.8 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
@@ -402,31 +374,19 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_heart_rate_and_sensor_statistics() {
// given
PreferencesUtils.setVoiceAnnounceAverageHeartRate(true);
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
Track.Id trackId = trackWithStats.first;
TrackStatistics stats = trackWithStats.second;
IntervalStatistics.Interval lastInterval;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPointIterator);
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
// then
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
@@ -434,6 +394,7 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_heart_rate() {
// given
PreferencesUtils.setVoiceAnnounceHeartRateCurrent(true);
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(true);
@@ -443,28 +404,15 @@ public class VoiceAnnouncementUtilsTest {
PreferencesUtils.setVoiceAnnounceAverageSpeedPace(false);
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
Track.Id trackId = trackWithStats.first;
TrackStatistics stats = trackWithStats.second;
IntervalStatistics.Interval lastInterval;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPointIterator);
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
// then
assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
@@ -484,4 +432,32 @@ public class VoiceAnnouncementUtilsTest {
assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.11)));
assertEquals("1.2 miles", MessageFormat.format(template, Map.of("n", 1.18)));
}
private static Pair<Track, Statistics> buildTrackWithTrackPoints() {
int numberOfPoints = 1000;
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
for (int i = 0; i < numberOfPoints; i++) {
trackPoints.add(TestDataUtil.createTrackPoint(i));
}
IntervalStatisticsUpdater intervalStatistics = new IntervalStatisticsUpdater(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPoints.iterator());
Statistics lastInterval = intervalStatistics.getLastInterval();
Track track = new Track(new Statistics(
Instant.EPOCH,
Instant.EPOCH.plusSeconds(999),
Duration.ofMinutes(16).plusSeconds(39),
Duration.ofMinutes(16).plusSeconds(39),
Distance.of(14208),
Speed.of(104),
null,
null,
null,
null
));
return new Pair<>(track, lastInterval);
}
}
@@ -41,7 +41,7 @@ public class PreferencesOpenHelperTest {
// then there should be one layout with old custom layout that has the new CSV value.
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(recordingLayouts);
assertEquals(recordingLayouts.size(), 1);
assertEquals(1, recordingLayouts.size());
assertEquals(recordingLayouts.get(0).toCsv(), PreferencesUtils.getCustomLayout().toCsv());
}
@@ -66,7 +66,7 @@ public class PreferencesOpenHelperTest {
// then there should be one layout with old custom layout that has the new CSV value.
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(recordingLayouts);
assertEquals(recordingLayouts.size(), 1);
assertEquals(1, recordingLayouts.size());
assertEquals(recordingLayouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
}
@@ -90,8 +90,8 @@ public class PreferencesOpenHelperTest {
// then there should be one layout with old custom layout that has the new CSV value.
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(recordingLayouts);
assertEquals(recordingLayouts.size(), 1);
assertEquals(recordingLayouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
assertEquals(1, recordingLayouts.size());
assertEquals(updatedOldCustomLayoutCsv, recordingLayouts.get(0).toCsv());
}
@Test
@@ -113,7 +113,7 @@ public class PreferencesOpenHelperTest {
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsv(PreferencesUtils.buildDefaultLayout(), resources);
List<RecordingLayout> customRecordingLayout = PreferencesUtils.getAllCustomLayouts();
assertEquals(customRecordingLayout.size(), 1);
assertEquals(1, customRecordingLayout.size());
assertEquals(defaultRecordingLayout.toCsv(), customRecordingLayout.get(0).toCsv());
}
}
@@ -89,8 +89,8 @@ public class PreferencesUtilsTest {
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
// then
assertEquals(recordingLayouts.size(), 1);
assertTrue(recordingLayouts.get(0).getFields().size() > 0);
assertEquals(1, recordingLayouts.size());
assertFalse(recordingLayouts.get(0).getFields().isEmpty());
assertEquals(recordingLayouts.get(0).getName(), context.getString(R.string.stats_custom_layout_default_layout));
assertTrue(recordingLayouts.get(0).getFields().stream().anyMatch(DataField::isVisible));
}
@@ -107,7 +107,7 @@ public class PreferencesUtilsTest {
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
// then
assertTrue(recordingLayout.getFields().size() > 0);
assertFalse(recordingLayout.getFields().isEmpty());
assertEquals(recordingLayout.getName(), context.getString(R.string.stats_custom_layout_default_layout));
assertTrue(recordingLayout.getFields().stream().anyMatch(DataField::isVisible));
}
@@ -130,9 +130,9 @@ public class PreferencesUtilsTest {
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
// then
assertEquals(recordingLayout.getFields().size(), 4);
assertEquals(recordingLayout.getName(), "run");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(4, recordingLayout.getFields().size());
assertEquals("run", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(recordingLayout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(recordingLayout.getFields().get(0).isVisible());
@@ -169,9 +169,9 @@ public class PreferencesUtilsTest {
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
// then
assertEquals(recordingLayout.getFields().size(), 4);
assertEquals(recordingLayout.getName(), "walking");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(4, recordingLayout.getFields().size());
assertEquals("walking", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(recordingLayout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(recordingLayout.getFields().get(0).isVisible());
@@ -266,11 +266,11 @@ public class PreferencesUtilsTest {
// then only updated profile is modified in the custom layouts
List<RecordingLayout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
assertEquals(layoutsBefore.size(), 2);
assertEquals(layoutsAfter.size(), 2);
assertEquals(2, layoutsBefore.size());
assertEquals(2, layoutsAfter.size());
assertEquals(layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count(), 4);
assertEquals(layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count(), 1);
assertEquals(4, layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count());
assertEquals(1, layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count());
}
@Test
@@ -309,11 +309,11 @@ public class PreferencesUtilsTest {
// then only updated profile is modified in the custom layouts
List<RecordingLayout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
assertEquals(layoutsBefore.size(), 2);
assertEquals(layoutsAfter.size(), 2);
assertEquals(2, layoutsBefore.size());
assertEquals(2, layoutsAfter.size());
assertEquals(layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count(), 4);
assertEquals(layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count(), 1);
assertEquals(4, layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count());
assertEquals(1, layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count());
}
@Test
@@ -341,6 +341,6 @@ public class PreferencesUtilsTest {
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
// then the first one was returned
assertEquals(recordingLayout.getName(), "cycling");
assertEquals("cycling", recordingLayout.getName());
}
}
@@ -1,193 +0,0 @@
/*
* Copyright 2012 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.share;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;
import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Tests for {@link DescriptionGenerator}.
*
* @author Jimmy Shih
*/
@RunWith(AndroidJUnit4.class)
public class DescriptionGeneratorTest {
@Rule
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
private static final Instant START_TIME = Instant.ofEpochMilli(1288721514000L);
private DescriptionGenerator descriptionGenerator;
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
descriptionGenerator = new DescriptionGenerator(ApplicationProvider.getApplicationContext());
}
/**
* Tests {@link DescriptionGenerator#generateTrackDescription(Track, boolean)}.
*/
@Test
public void testGenerateTrackDescription() {
Track track = new Track();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
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.setActivityTypeLocalized("hiking");
String expected = //"Created by"
"<a href='https://codeberg.org/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>"
+ "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>"
+ "Max speed: 360.00 km/h (223.7 mi/h)<br>"
+ "Average pace: 0:30 min/km (0:48 min/mi)<br>"
+ "Average moving pace: 0:15 min/km (0:24 min/mi)<br>"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Max elevation: 550 m (1804 ft)<br>"
+ "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@Test
public void testGenerateTrackDescriptionWithoutMaxMinAltitude() {
Track track = new Track();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
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.setActivityTypeLocalized("hiking");
String expected = //"Created by"
"<a href='https://codeberg.org/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>"
+ "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>"
+ "Max speed: 360.00 km/h (223.7 mi/h)<br>"
+ "Average pace: 0:30 min/km (0:48 min/mi)<br>"
+ "Average moving pace: 0:15 min/km (0:24 min/mi)<br>"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
/**
* Tests {@link DescriptionGenerator#writeDistance(Distance, StringBuilder, int, String)}.
*/
@Test
public void testWriteDistance() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeDistance(Distance.of(1100), builder, R.string.description_total_distance, "<br>");
assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeTime(Duration, StringBuilder, int, String)}.
*/
@Test
public void testWriteTime() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeTime(Duration.ofMillis(1000), builder, R.string.description_total_time, "<br>");
assertEquals("Total time: 00:01<br>", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeSpeed(Speed, StringBuilder, int, String)}.
*/
@Test
public void testWriteSpeed() {
StringBuilder builder = new StringBuilder();
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#writeAltitude(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteAltitude() {
StringBuilder builder = new StringBuilder();
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(Speed, StringBuilder, int, String)}.
*/
@Test
public void testWritePace() {
StringBuilder builder = new StringBuilder();
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());
}
}
@@ -0,0 +1,137 @@
/*
* Copyright 2012 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.share;
import static org.junit.Assert.assertEquals;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;
import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Tests for {@link TrackShareSummaryGenerator}.
*
* @author Jimmy Shih
*/
@RunWith(AndroidJUnit4.class)
public class TrackShareSummaryGeneratorTest {
@Rule
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
private static final Instant START_TIME = Instant.ofEpochMilli(1288721514000L);
private TrackShareSummaryGenerator descriptionGenerator;
@Before
public void setUp() {
descriptionGenerator = new TrackShareSummaryGenerator(ApplicationProvider.getApplicationContext());
}
@Test
public void testGenerateTrackDescription() {
Track track = new Track(new Statistics(
START_TIME,
START_TIME,
Duration.ofSeconds(600),
Duration.ofSeconds(300),
Distance.of(20000),
Speed.of(100),
new AltitudeExtremities(-500, 550),
new AltitudeGainLoss(6000, 6000),
null,
null
));
track.setActivityTypeLocalized("hiking");
String expected = //"Created by"
"OpenTracks (Debug)\n\n"
+ "Name: -\n"
+ "Activity type: hiking\n"
+ "Description: -\n"
+ "Total distance: 20.00 km (12.4 mi)\n"
+ "Total time: 10:00\n"
+ "Moving time: 05:00\n"
+ "Average speed: 120.00 km/h (74.6 mi/h)\n"
+ "Average moving speed: 240.00 km/h (149.1 mi/h)\n"
+ "Max speed: 360.00 km/h (223.7 mi/h)\n"
+ "Average pace: 0:30 min/km (0:48 min/mi)\n"
+ "Average moving pace: 0:15 min/km (0:24 min/mi)\n"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)\n"
+ "Max elevation: 550 m (1804 ft)\n"
+ "Min elevation: -500 m (-1640 ft)\n"
+ "Elevation gain: 6000 m (19685 ft)\n"
+ "Elevation loss: 6000 m (19685 ft)\n"
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "\n";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track));
}
@Test
public void testWriteDistance() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeDistance(Distance.of(1100), builder, R.string.description_total_distance);
assertEquals("Total distance: 1.10 km (0.7 mi)\n", builder.toString());
}
@Test
public void testWriteTime() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeTime(Duration.ofMillis(1000), builder, R.string.description_total_time);
assertEquals("Total time: 00:01\n", builder.toString());
}
@Test
public void testWriteSpeed() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeSpeed(Speed.of(1.1), builder, R.string.description_average_speed);
assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
}
@Test
public void testWriteAltitude() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeAltitude(4.2, builder, R.string.description_min_altitude);
assertEquals("Min elevation: 4 m (14 ft)\n", builder.toString());
}
@Test
public void testWritePace() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writePace(Speed.of(1.1), builder, R.string.description_average_pace_in_minute);
assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
}
}
@@ -32,25 +32,25 @@ import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
/**
* Tests for {@link TrackStatistics}.
* Tests for {@link SegmentStatisticUpdater}.
* This only tests non-trivial pieces of that class.
*
* @author Rodrigo Damazio
*/
@RunWith(AndroidJUnit4.class)
public class TrackStatisticsTest {
public class SegmentStatisticUpdaterTest {
private TrackStatistics statistics;
private SegmentStatisticUpdater statistics;
@Before
public void setUp() {
statistics = new TrackStatistics();
statistics = new SegmentStatisticUpdater();
}
@Test
public void testMerge_no_data() {
// given
TrackStatistics statistics2 = new TrackStatistics();
SegmentStatisticUpdater statistics2 = new SegmentStatisticUpdater();
// when
statistics.merge(statistics2);
@@ -74,7 +74,7 @@ public class TrackStatisticsTest {
@Test
public void testMerge() {
// given
TrackStatistics statistics2 = new TrackStatistics();
SegmentStatisticUpdater statistics2 = new SegmentStatisticUpdater();
statistics.setStartTime(Instant.ofEpochMilli(1000)); // Resulting start time
statistics.setStopTime(Instant.ofEpochMilli(2500));
statistics2.setStartTime(Instant.ofEpochMilli(3000));
@@ -31,7 +31,7 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
// then
TrackStatistics statistics = subject.getTrackStatistics();
SegmentStatisticUpdater statistics = subject.getSegmentStatisticUpdater();
assertNull(statistics.getStartTime());
assertNull(statistics.getStopTime());
assertEquals(Duration.ZERO, statistics.getTotalTime());
@@ -58,7 +58,7 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp);
// then
TrackStatistics statistics = subject.getTrackStatistics();
SegmentStatisticUpdater statistics = subject.getSegmentStatisticUpdater();
assertEquals(startTime, statistics.getStartTime());
assertEquals(startTime, statistics.getStopTime());
assertEquals(Duration.ZERO, statistics.getTotalTime());
@@ -83,7 +83,7 @@ public class TrackStatisticsUpdaterTest {
data.trackPoints().forEach(subject::addTrackPoint);
// then
TrackStatistics statistics = subject.getTrackStatistics();
SegmentStatisticUpdater statistics = subject.getSegmentStatisticUpdater();
assertEquals(142.26, statistics.getTotalDistance().toM(), 0.01);
assertEquals(Duration.ofSeconds(12), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(12), statistics.getMovingTime());
@@ -115,7 +115,7 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3);
// then
assertEquals(1.10, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(1.10, subject.getSegmentStatisticUpdater().getTotalDistance().toM(), 0.01);
}
@Test
@@ -140,14 +140,14 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3);
// then
assertEquals(110.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(110.57, subject.getSegmentStatisticUpdater().getTotalDistance().toM(), 0.01);
// when
subject.addTrackPoint(tp4);
subject.addTrackPoint(tp5);
// then
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(125.57, subject.getSegmentStatisticUpdater().getTotalDistance().toM(), 0.01);
}
@Test
@@ -171,20 +171,20 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3);
// then
assertEquals(5, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(5, subject.getSegmentStatisticUpdater().getTotalDistance().toM(), 0.01);
// when
subject.addTrackPoint(tp4);
subject.addTrackPoint(tp5);
// then
assertEquals(59.18, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(59.18, subject.getSegmentStatisticUpdater().getTotalDistance().toM(), 0.01);
}
@Test
public void addTrackPoint_maxSpeed_multiple_segments() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
assertEquals(Speed.of(0f), subject.getSegmentStatisticUpdater().getMaxSpeed());
subject.addTrackPoints(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
@@ -194,7 +194,7 @@ public class TrackStatisticsUpdaterTest {
.setSpeed(Speed.of(2f)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(4))
));
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
assertEquals(Speed.of(2f), subject.getSegmentStatisticUpdater().getMaxSpeed());
// when
subject.addTrackPoints(List.of(
@@ -207,7 +207,7 @@ public class TrackStatisticsUpdaterTest {
));
// then
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
assertEquals(Speed.of(2f), subject.getSegmentStatisticUpdater().getMaxSpeed());
}
@Test
@@ -234,7 +234,7 @@ public class TrackStatisticsUpdaterTest {
));
// then
assertEquals(Duration.ofSeconds(35), subject.getTrackStatistics().getMovingTime());
assertEquals(Duration.ofSeconds(35), subject.getSegmentStatisticUpdater().getMovingTime());
}
@Test
@@ -264,8 +264,8 @@ public class TrackStatisticsUpdaterTest {
));
// then
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
assertEquals(Distance.of(1040), subject.getTrackStatistics().getTotalDistance());
assertEquals(Duration.ofSeconds(40), subject.getSegmentStatisticUpdater().getMovingTime());
assertEquals(Distance.of(1040), subject.getSegmentStatisticUpdater().getTotalDistance());
}
@Test
@@ -280,49 +280,49 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)));
// then
assertTrue(subject.getTrackStatistics().isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getTotalTime());
assertEquals(Distance.of(10), subject.getTrackStatistics().getTotalDistance());
assertTrue(subject.getSegmentStatisticUpdater().isIdle());
assertEquals(Duration.ofSeconds(30), subject.getSegmentStatisticUpdater().getMovingTime());
assertEquals(Duration.ofSeconds(30), subject.getSegmentStatisticUpdater().getTotalTime());
assertEquals(Distance.of(10), subject.getSegmentStatisticUpdater().getTotalDistance());
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setSensorDistance(Distance.of(0)));
// then
assertTrue(subject.getTrackStatistics().isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getTotalTime());
assertEquals(Distance.of(10), subject.getTrackStatistics().getTotalDistance());
assertTrue(subject.getSegmentStatisticUpdater().isIdle());
assertEquals(Duration.ofSeconds(30), subject.getSegmentStatisticUpdater().getMovingTime());
assertEquals(Duration.ofSeconds(40), subject.getSegmentStatisticUpdater().getTotalTime());
assertEquals(Distance.of(10), subject.getSegmentStatisticUpdater().getTotalDistance());
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setSensorDistance(Distance.of(1)));
// then
assertTrue(subject.getTrackStatistics().isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
assertEquals(Duration.ofSeconds(45), subject.getTrackStatistics().getTotalTime());
assertEquals(Distance.of(11), subject.getTrackStatistics().getTotalDistance());
assertTrue(subject.getSegmentStatisticUpdater().isIdle());
assertEquals(Duration.ofSeconds(30), subject.getSegmentStatisticUpdater().getMovingTime());
assertEquals(Duration.ofSeconds(45), subject.getSegmentStatisticUpdater().getTotalTime());
assertEquals(Distance.of(11), subject.getSegmentStatisticUpdater().getTotalDistance());
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(50))
.setSensorDistance(Distance.of(10)));
// then
assertFalse(subject.getTrackStatistics().isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
assertEquals(Duration.ofSeconds(50), subject.getTrackStatistics().getTotalTime());
assertEquals(Distance.of(21), subject.getTrackStatistics().getTotalDistance());
assertFalse(subject.getSegmentStatisticUpdater().isIdle());
assertEquals(Duration.ofSeconds(30), subject.getSegmentStatisticUpdater().getMovingTime());
assertEquals(Duration.ofSeconds(50), subject.getSegmentStatisticUpdater().getTotalTime());
assertEquals(Distance.of(21), subject.getSegmentStatisticUpdater().getTotalDistance());
// when
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(60)));
// then
assertFalse(subject.getTrackStatistics().isIdle());
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
assertEquals(Duration.ofSeconds(60), subject.getTrackStatistics().getTotalTime());
assertEquals(Distance.of(21), subject.getTrackStatistics().getTotalDistance());
assertFalse(subject.getSegmentStatisticUpdater().isIdle());
assertEquals(Duration.ofSeconds(40), subject.getSegmentStatisticUpdater().getMovingTime());
assertEquals(Duration.ofSeconds(60), subject.getSegmentStatisticUpdater().getTotalTime());
assertEquals(Distance.of(21), subject.getSegmentStatisticUpdater().getTotalDistance());
}
@Test
@@ -350,8 +350,8 @@ public class TrackStatisticsUpdaterTest {
copy.addTrackPoint(tp5);
// then
assertEquals(55.287, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(55.287, copy.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(55.287, subject.getSegmentStatisticUpdater().getTotalDistance().toM(), 0.01);
assertEquals(55.287, copy.getSegmentStatisticUpdater().getTotalDistance().toM(), 0.01);
}
public TrackPoint createTrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
@@ -17,10 +17,13 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.SegmentStatisticUpdater;
@RunWith(JUnit4.class)
public class AggregatedStatisticsTest {
@@ -28,21 +31,20 @@ public class AggregatedStatisticsTest {
private final Context context = ApplicationProvider.getApplicationContext();
private static Track createTrack(Context context, Distance totalDistance, Duration totalTime, String activityTypeLocalized) {
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime(Instant.ofEpochMilli(1000L)); // Resulting start time
statistics.setStopTime(statistics.getStartTime().plus(totalTime));
statistics.setTotalTime(totalTime);
statistics.setMovingTime(totalTime);
statistics.setTotalDistance(totalDistance);
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 track = new Track(new Statistics(
Instant.ofEpochMilli(1000),
Instant.ofEpochMilli(1000).plus(totalTime),
totalTime,
totalTime,
totalDistance,
Speed.of(50),
new AltitudeExtremities(1250, 1250),
new AltitudeGainLoss(50, 50),
null,
null
));
track.setActivityType(ActivityType.findByLocalizedString(context, activityTypeLocalized));
track.setActivityTypeLocalized(activityTypeLocalized);
track.setTrackStatistics(statistics);
return track;
}
@@ -63,7 +65,7 @@ public class AggregatedStatisticsTest {
assertNotNull(aggregatedStatistics.get(biking));
assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
@@ -117,7 +119,7 @@ public class AggregatedStatisticsTest {
assertNotNull(aggregatedStatistics.get(biking));
assertEquals(2, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
}
@@ -151,19 +153,19 @@ public class AggregatedStatisticsTest {
assertEquals(1, aggregatedStatistics.get(walking).getCountTracks());
{
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
{
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
{
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
@@ -214,28 +216,28 @@ public class AggregatedStatisticsTest {
// Biking.
{
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance.multipliedBy(5), statistics2.getTotalDistance());
assertEquals(totalTime.multipliedBy(5), statistics2.getMovingTime());
}
// Running.
{
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
}
// Walking.
{
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
}
// Driving.
{
TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
SegmentStatisticUpdater statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(totalTime, statistics2.getMovingTime());
}
@@ -26,11 +26,11 @@ public class CsvRecordingLayoutUtilsTest extends TestCase {
// then
assertNotNull(partsOk);
assertEquals(partsOk.size(), 4);
assertEquals(partsOk.get(0), "Layout Name");
assertEquals(partsOk.get(1), "2");
assertEquals(partsOk.get(2), "distance,1,1");
assertEquals(partsOk.get(3), "speed,1,1");
assertEquals(4, partsOk.size());
assertEquals("Layout Name", partsOk.get(0));
assertEquals("2", partsOk.get(1));
assertEquals("distance,1,1", partsOk.get(2));
assertEquals("speed,1,1", partsOk.get(3));
assertNull(partsWrong1);
assertNull(partsWrong2);
}
@@ -32,9 +32,9 @@ public class RecordingLayoutTest extends TestCase {
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), "running");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(dataFieldList.size(), 4);
assertEquals("running", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(4, dataFieldList.size());
assertEquals(dataFieldList.get(0).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertEquals(dataFieldList.get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertEquals(dataFieldList.get(2).getKey(), context.getString(R.string.stats_custom_layout_altitude_key));
@@ -66,7 +66,7 @@ public class RecordingLayoutTest extends TestCase {
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(recordingLayout.getFields().size(), 0);
assertEquals(0, recordingLayout.getFields().size());
}
@Test
@@ -81,7 +81,7 @@ public class RecordingLayoutTest extends TestCase {
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(recordingLayout.getFields().size(), 0);
assertEquals(0, recordingLayout.getFields().size());
}
@Test
@@ -94,9 +94,9 @@ public class RecordingLayoutTest extends TestCase {
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), "Layout Name");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(recordingLayout.getFields().size(), 0);
assertEquals("Layout Name", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(0, recordingLayout.getFields().size());
}
@Test
@@ -109,9 +109,9 @@ public class RecordingLayoutTest extends TestCase {
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), "Layout Name");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(recordingLayout.getFields().size(), 1);
assertEquals("Layout Name", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(1, recordingLayout.getFields().size());
}
@Test
@@ -128,7 +128,7 @@ public class RecordingLayoutTest extends TestCase {
String csv = recordingLayout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
assertEquals("Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;", csv);
}
@Test
@@ -3,47 +3,24 @@ package de.dennisguse.opentracks.ui.intervals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
@RunWith(JUnit4.class)
public class IntervalStatisticsTest {
private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@Before
public void setUp() {
contentProviderUtils = new ContentProviderUtils(context);
}
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
for (TrackPoint tp : trackPoints) {
trackStatisticsUpdater.addTrackPoint(tp);
}
return trackStatisticsUpdater.getTrackStatistics();
}
public class IntervalStatisticsUpdaterTest {
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
@@ -125,77 +102,81 @@ public class IntervalStatisticsTest {
Track dummyTrack = new Track();
dummyTrack.setId(new Track.Id(System.currentTimeMillis()));
dummyTrack.setName("Dummy Track Without Elevation Loss");
contentProviderUtils.insertTrack(dummyTrack);
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
for (int i = 0; i < numberOfPoints; i++) {
TrackPoint tp = TestDataUtil.createTrackPoint(i);
tp.setAltitudeLoss(null);
contentProviderUtils.insertTrackPoint(tp, dummyTrack.getId());
trackStatisticsUpdater.addTrackPoint(tp);
trackPoints.add(TestDataUtil.createTrackPoint(i).setAltitudeGainLoss(null));
}
dummyTrack.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(dummyTrack);
Pair<Track.Id, TrackStatistics> trackWithStats = new Pair<>(dummyTrack.getId(), trackStatisticsUpdater.getTrackStatistics());
trackStatisticsUpdater.addTrackPoints(trackPoints);
dummyTrack.setStatistics(trackStatisticsUpdater.getTrackStatistics());
Pair<Track, List<TrackPoint>> trackWithStats = new Pair<>(dummyTrack, trackPoints);
// when and then
whenAndThen(trackWithStats, numberOfPoints, distanceInterval);
whenAndThen(trackWithStats.first, trackWithStats.second, numberOfPoints, distanceInterval);
}
private void whenAndThen(int numberOfPoints, float distanceInterval) {
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
whenAndThen(trackWithStats, numberOfPoints, distanceInterval);
Pair<Track, List<TrackPoint>> trackWithStats = buildTrackWithTrackPoints(numberOfPoints);
whenAndThen(trackWithStats.first, trackWithStats.second, numberOfPoints, distanceInterval);
}
private void whenAndThen(Pair<Track.Id, TrackStatistics> trackWithStats, int numberOfPoints, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(distanceInterval));
Track.Id trackId = trackWithStats.first;
TrackStatistics trackStatistics = trackWithStats.second;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
intervalStatistics.addTrackPoints(trackPointIterator);
}
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
private void whenAndThen(Track track, List<TrackPoint> trackPoints, int numberOfPoints, float distanceInterval) {
IntervalStatisticsUpdater intervalStatistics = new IntervalStatisticsUpdater(Distance.of(distanceInterval));
intervalStatistics.addTrackPoints(trackPoints.iterator());
List<Statistics> intervalList = intervalStatistics.getIntervalList();
Distance totalDistance = Distance.of(0);
float totalTime = 0L;
Float totalGain = null;
Float totalLoss = null;
for (IntervalStatistics.Interval i : intervalList) {
totalDistance = totalDistance.plus(i.getDistance());
totalTime += i.getDistance().toM() / i.getSpeed().toMPS();
for (Statistics i : intervalList) {
totalDistance = totalDistance.plus(i.totalDistance());
totalTime += i.totalDistance().toM() / i.getAverageSpeed().toMPS();
if (totalGain == null) {
totalGain = i.getGain_m();
} else if (i.getGain_m() != null) {
totalGain += i.getGain_m();
totalGain = i.altitudeGainLoss() != null ? i.altitudeGainLoss().gain_m() : null;
} else if (i.altitudeGainLoss() != null) {
totalGain += i.altitudeGainLoss().gain_m();
}
if (totalLoss == null) {
totalLoss = i.getLoss_m();
} else if (i.getLoss_m() != null) {
totalLoss += i.getLoss_m();
totalLoss = i.altitudeGainLoss() != null ? i.altitudeGainLoss().loss_m() : null;
} else if (i.altitudeGainLoss() != null) {
totalLoss += i.altitudeGainLoss().loss_m();
}
}
// then
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(trackStatistics.getTotalTime().toSeconds(), totalTime, 0.01);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
if (totalGain != null) {
assertEquals(track.getStatistics().totalTime().toSeconds(), totalTime, 0.01);
assertEquals(track.getStatistics().totalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(intervalList.size(), (int) Math.ceil(track.getStatistics().totalDistance().toM() / distanceInterval));
if (totalGain != null && totalLoss != null) {
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
} else {
assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(IntervalStatistics.Interval::hasGain));
}
if (totalLoss != null) {
assertEquals(totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
} else {
assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(IntervalStatistics.Interval::hasLoss));
assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(i -> i.altitudeGainLoss() != null));
}
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(i).totalDistance().toM(), distanceInterval, 0.001);
totalDistance = totalDistance.minus(intervalList.get(i).totalDistance());
}
assertEquals(intervalList.get(intervalList.size() - 1).getDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(intervalList.get(intervalList.size() - 1).totalDistance().toM(), totalDistance.toM(), 0.01);
}
private static Pair<Track, List<TrackPoint>> buildTrackWithTrackPoints(int numberOfPoints) {
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
for (int i = 0; i < numberOfPoints; i++) {
trackPoints.add(TestDataUtil.createTrackPoint(i));
}
trackStatisticsUpdater.addTrackPoints(trackPoints);
return new Pair<>(new Track(trackStatisticsUpdater.getTrackStatistics()), trackPoints);
}
}
@@ -1,68 +0,0 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
@RunWith(AndroidJUnit4.class)
public class StatisticsUtilsTest extends TestCase {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void testEmptyValue() {
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_distance_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_total_time_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_moving_time_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_max_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_altitude_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_gain_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_loss_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_fastest_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_coordinates), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_coordinates_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_heart_rate_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_cadence_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_power_key)));
}
@Test
public void testFilterVisible() {
// given
RecordingLayout recordingLayout = new RecordingLayout("profile");
recordingLayout.addField(new DataField("key1", true, true, false));
recordingLayout.addField(new DataField("key2", false, true, false));
recordingLayout.addField(new DataField("key3", true, true, false));
recordingLayout.addField(new DataField("key4", false, true, false));
recordingLayout.addField(new DataField("key5", true, true, false));
// when
RecordingLayout resultTrue = recordingLayout.toRecordingLayout(true);
RecordingLayout resultFalse = recordingLayout.toRecordingLayout(false);
// then
assertEquals(resultTrue.getFields().size(), 3);
assertTrue(resultTrue.getFields().stream().anyMatch(f -> "key1".equals(f.getKey())));
assertTrue(resultTrue.getFields().stream().anyMatch(f -> "key3".equals(f.getKey())));
assertTrue(resultTrue.getFields().stream().anyMatch(f -> "key5".equals(f.getKey())));
assertEquals(resultFalse.getFields().size(), 2);
assertTrue(resultFalse.getFields().stream().anyMatch(f -> "key2".equals(f.getKey())));
assertTrue(resultFalse.getFields().stream().anyMatch(f -> "key4".equals(f.getKey())));
}
}
@@ -38,11 +38,10 @@ import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.TextStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.TimezoneRule;
@@ -133,8 +132,10 @@ public class StringUtilsTest {
@Test
public void testFormatDateTodayRelative() {
// given
ArrayList<String> shortDays = Arrays.stream(DayOfWeek.values()).map(d -> d.getDisplayName(TextStyle.FULL, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
ArrayList<String> shortMonths = Arrays.stream(Month.values()).map(m -> m.getDisplayName(TextStyle.SHORT, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
List<String> shortDays = Arrays.stream(DayOfWeek.values()).map(d -> d.getDisplayName(TextStyle.FULL, Locale.getDefault()))
.toList();
List<String> shortMonths = Arrays.stream(Month.values()).map(m -> m.getDisplayName(TextStyle.SHORT, Locale.getDefault()))
.toList();
LocalDate today = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toLocalDate();
LocalDate yesterday = today.minusDays(1);
@@ -74,7 +74,6 @@ Average moving pace: 0:00 min/km (0:00 min/mi)
Fastest pace: 0:00 min/km (0:00 min/mi)
Max elevation: -9223372036854775808 m (-9223372036854775808 ft)
Min elevation: 9223372036854775807 m (9223372036854775807 ft)
Elevation gain: 0 m (0 ft)
Max grade: 0 %
Min grade: 0 %
Recorded: 6/23 11:29]]></description>
@@ -126,11 +125,6 @@ Recorded: 6/23 11:29]]></description>
<gx:value>0.15777911</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
<gx:SimpleArrayData name="elevation_gain">
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
</SchemaData>
</ExtendedData>
</gx:Track>
@@ -148,14 +142,6 @@ Recorded: 6/23 11:29]]></description>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
<gx:SimpleArrayData name="elevation_gain">
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
</SchemaData>
</ExtendedData>
</gx:Track>
@@ -179,7 +165,6 @@ Average moving pace: 0:00 min/km (0:00 min/mi)
Fastest pace: 0:00 min/km (0:00 min/mi)
Max elevation: 469 m (1540 ft)
Min elevation: 419 m (1376 ft)
Elevation gain: 0 m (0 ft)
Recorded: 11/28 18:06]]></description>
<TimeStamp>
<when>2020-11-28T17:07:20.870Z</when>
-15
View File
@@ -395,21 +395,6 @@ limitations under the License.
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/recording_service" />
<service
android:name=".io.file.exporter.ExportService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".io.file.importer.ImportService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".services.TrackDeleteService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<!--Necessary for SDK < 33 to (re-)store per app locale information-->
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@@ -16,15 +16,18 @@
package de.dennisguse.opentracks;
import android.os.Handler;
import android.widget.Toast;
import java.util.ArrayList;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.List;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.TrackDeleteService;
import de.dennisguse.opentracks.services.TrackDeletionWorker;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
@@ -37,7 +40,7 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm
* @author Jimmy Shih
*/
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteService.TrackDeleteResultReceiver.Receiver {
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller {
protected void deleteTracks(Track.Id... trackIds) {
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
@@ -46,8 +49,8 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
//TODO A callback is better.
@Override
public void onConfirmDeleteDone(Track.Id... trackIds) {
ArrayList<Track.Id> trackIdList = Arrays.stream(trackIds)
.filter(trackId -> !trackId.equals(getRecordingTrackId())).collect(Collectors.toCollection(ArrayList::new));
List<Track.Id> trackIdList = Arrays.stream(trackIds)
.filter(trackId -> !trackId.equals(getRecordingTrackId())).toList();
onDeleteConfirmed();
@@ -55,10 +58,29 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
Toast.makeText(this, getString(R.string.track_delete_not_recording), Toast.LENGTH_LONG).show();
}
TrackDeleteService.enqueue(this, new TrackDeleteService.TrackDeleteResultReceiver(new Handler(), this), trackIdList);
WorkManager workManager = WorkManager.getInstance(this);
WorkRequest deleteRequest = new OneTimeWorkRequest.Builder(TrackDeletionWorker.class)
.setInputData(new Data.Builder()
.putLongArray(TrackDeletionWorker.TRACKIDS_KEY, Arrays.stream(trackIds).mapToLong(Track.Id::id).toArray())
.build())
.build();
workManager
.getWorkInfoByIdLiveData(deleteRequest.getId())
.observe(this, workInfo -> {
if (workInfo != null) {
if (workInfo.getState().isFinished()) {
onDeleteFinished();
}
}
});
workManager.enqueue(deleteRequest);
}
protected abstract void onDeleteConfirmed();
protected abstract void onDeleteFinished();
protected abstract Track.Id getRecordingTrackId();
}
@@ -25,7 +25,6 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
@@ -60,8 +59,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
public static final String VIEW_TRACK_ICON = "track_icon";
public static final String EXTRA_TRACK_ID = "track_id";
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
@@ -98,8 +95,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
viewBinding.bottomAppBarLayout.bottomAppBar.replaceMenu(R.menu.track_detail);
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
postponeEnterTransition();
}
@Override
@@ -284,11 +279,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
}
public void startPostponedEnterTransitionWith(View viewIcon) {
ViewCompat.setTransitionName(viewIcon, TrackRecordedActivity.VIEW_TRACK_ICON);
startPostponedEnterTransition();
}
private void onRecordingStatusChanged(RecordingStatus status) {
recordingStatus = status;
}
@@ -16,10 +16,10 @@ import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackStoppedBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.exporter.ExportUtils;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
@@ -68,14 +68,14 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
viewBinding.trackEditDescription.setText(track.getDescription());
viewBinding.time.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
viewBinding.time.setText(StringUtils.formatElapsedTime(track.getStatistics().movingTime()));
{
Pair<String, String> parts = SpeedFormatter.Builder()
.setUnit(PreferencesUtils.getUnitSystem())
.setReportSpeedOrPace(PreferencesUtils.isReportSpeed(track))
.build(this)
.getSpeedParts(track.getTrackStatistics().getAverageMovingSpeed());
.getSpeedParts(track.getStatistics().getAverageMovingSpeed());
viewBinding.speed.setText(parts.first);
viewBinding.speedUnit.setText(parts.second);
}
@@ -84,7 +84,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
Pair<String, String> parts = DistanceFormatter.Builder()
.setUnit(PreferencesUtils.getUnitSystem())
.build(this)
.getDistanceParts(track.getTrackStatistics().getTotalDistance());
.getDistanceParts(track.getStatistics().totalDistance());
viewBinding.distance.setText(parts.first);
viewBinding.distanceUnit.setText(parts.second);
}
@@ -33,17 +33,18 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.SegmentStatisticUpdater;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
/**
* A fragment to display track chart to the user.
* ChartFragment uses a {@link TrackStatisticsUpdater} internally and recomputes the {@link TrackStatistics} from the beginning.
* ChartFragment uses a {@link TrackStatisticsUpdater} internally and recomputes the {@link SegmentStatisticUpdater} from the beginning.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
@@ -202,7 +203,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
}
}
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull Statistics trackStatistics) {
if (isResumed()) {
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.getSpeed(), chartByDistance, viewBinding.chartView.getUnitSystem());
pendingPoints.add(point);
@@ -4,9 +4,9 @@ import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
public record ChartPoint(
//X-axis
@@ -22,11 +22,11 @@ public record ChartPoint(
) {
public static ChartPoint create(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, boolean chartByDistance, UnitSystem unitSystem) {
public static ChartPoint create(@NonNull Statistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, boolean chartByDistance, UnitSystem unitSystem) {
return new ChartPoint(
chartByDistance
? trackStatistics.getTotalDistance().toKM_Miles(unitSystem)
: trackStatistics.getTotalTime().toMillis(),
? trackStatistics.totalDistance().toKM_Miles(unitSystem)
: trackStatistics.totalTime().toMillis(),
trackPoint.hasAltitude()
? Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem)
: null,
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks.chart;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
@@ -49,10 +48,8 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.ui.util.ThemeUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -857,7 +854,7 @@ public class ChartView extends View {
* @param chartValueSeries the chart value series
* @param canvas the canvas
* @param xPosition the right most x position
* @parm yOffset offset to apply to y position
* @param yOffset offset to apply to y position
* @param yValue the y value
* @return the marker width.
*/
@@ -883,7 +880,7 @@ public class ChartView extends View {
break;
}
}
if (firstChartValueSeries != null && chartPoints.size() > 0) {
if (firstChartValueSeries != null && !chartPoints.isEmpty()) {
int dx = getX(maxX) - pointer.getIntrinsicWidth() / 2;
double value = firstChartValueSeries.extractDataFromChartPoint(last);
int dy = getY(firstChartValueSeries, value) - pointer.getIntrinsicHeight();
@@ -897,7 +894,7 @@ public class ChartView extends View {
*/
private void updateSeries() {
synchronized (chartPoints) {
seriesList.stream().forEach(this::updateSerie);
seriesList.forEach(this::updateSerie);
}
}
@@ -41,6 +41,8 @@ import java.util.UUID;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
@@ -48,14 +50,15 @@ import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -116,7 +119,6 @@ public class ContentProviderUtils {
int altitudeLossIndex = cursor.getColumnIndexOrThrow(TracksColumns.ALTITUDE_LOSS);
Track track = new Track(ZoneOffset.ofTotalSeconds(cursor.getInt(startTimeOffsetIndex)));
TrackStatistics trackStatistics = track.getTrackStatistics();
if (!cursor.isNull(idIndex)) {
track.setId(new Track.Id(cursor.getLong(idIndex)));
}
@@ -136,37 +138,23 @@ public class ContentProviderUtils {
track.setActivityTypeLocalized(cursor.getString(activityTypeLocalizedIndex));
}
if (!cursor.isNull(startTimeIndex)) {
trackStatistics.setStartTime(Instant.ofEpochMilli(cursor.getLong(startTimeIndex)));
}
if (!cursor.isNull(stopTimeIndex)) {
trackStatistics.setStopTime(Instant.ofEpochMilli(cursor.getLong(stopTimeIndex)));
}
if (!cursor.isNull(totalDistanceIndex)) {
trackStatistics.setTotalDistance(Distance.of(cursor.getFloat(totalDistanceIndex)));
}
if (!cursor.isNull(totalTimeIndex)) {
trackStatistics.setTotalTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
}
if (!cursor.isNull(movingTimeIndex)) {
trackStatistics.setMovingTime(Duration.ofMillis(cursor.getLong(movingTimeIndex)));
}
if (!cursor.isNull(maxSpeedIndex)) {
trackStatistics.setMaxSpeed(Speed.of(cursor.getFloat(maxSpeedIndex)));
}
if (!cursor.isNull(minAltitudeIndex)) {
trackStatistics.setMinAltitude(cursor.getFloat(minAltitudeIndex));
}
if (!cursor.isNull(maxAltitudeIndex)) {
trackStatistics.setMaxAltitude(cursor.getFloat(maxAltitudeIndex));
}
if (!cursor.isNull(altitudeGainIndex)) {
trackStatistics.setTotalAltitudeGain(cursor.getFloat(altitudeGainIndex));
}
if (!cursor.isNull(altitudeLossIndex)) {
trackStatistics.setTotalAltitudeLoss(cursor.getFloat(altitudeLossIndex));
}
Statistics trackStatistics = new Statistics(
cursor.isNull(startTimeIndex) ? null : Instant.ofEpochMilli(cursor.getLong(startTimeIndex)),
cursor.isNull(stopTimeIndex) ? null : Instant.ofEpochMilli(cursor.getLong(stopTimeIndex)),
cursor.isNull(totalTimeIndex) ? null : Duration.ofMillis(cursor.getLong(totalTimeIndex)),
cursor.isNull(movingTimeIndex) ? null : Duration.ofMillis(cursor.getLong(movingTimeIndex)),
cursor.isNull(totalDistanceIndex) ? null : Distance.of(cursor.getFloat(totalDistanceIndex)),
cursor.isNull(maxSpeedIndex) ? null : Speed.of(cursor.getFloat(maxSpeedIndex)),
cursor.isNull(minAltitudeIndex) || cursor.isNull(maxAltitudeIndex) ? null : new AltitudeExtremities(cursor.getFloat(minAltitudeIndex), cursor.getFloat(maxAltitudeIndex)),
cursor.isNull(altitudeGainIndex) || cursor.isNull(altitudeLossIndex) ? null : new AltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex)),
null,
null
);
track.setStatistics(trackStatistics);
return track;
}
@@ -314,7 +302,7 @@ public class ContentProviderUtils {
private ContentValues createContentValues(Track track) {
ContentValues values = new ContentValues();
TrackStatistics trackStatistics = track.getTrackStatistics();
Statistics trackStatistics = track.getStatistics();
if (track.getId() != null) {
values.put(TracksColumns._ID, track.getId().id());
@@ -325,48 +313,56 @@ public class ContentProviderUtils {
values.put(TracksColumns.ACTIVITY_TYPE, track.getActivityType() != null ? track.getActivityType().getId() : null);
values.put(TracksColumns.ACTIVITY_TYPE_LOCALIZED, track.getActivityTypeLocalized());
values.put(TracksColumns.STARTTIME_OFFSET, track.getZoneOffset().getTotalSeconds());
if (trackStatistics.getStartTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
if (trackStatistics.startTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.startTime().toEpochMilli());
}
if (trackStatistics.getStopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
if (trackStatistics.stopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.stopTime().toEpochMilli());
}
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.totalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.totalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.movingTime().toMillis());
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.MAXSPEED, trackStatistics.maxSpeed().toMPS());
if (trackStatistics.altitudeExtremities() != null) {
values.put(TracksColumns.MIN_ALTITUDE, trackStatistics.getMinAltitude());
values.put(TracksColumns.MAX_ALTITUDE, trackStatistics.getMaxAltitude());
}
if (trackStatistics.altitudeGainLoss() != null) {
values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.altitudeGainLoss().gain_m());
values.put(TracksColumns.ALTITUDE_LOSS, trackStatistics.altitudeGainLoss().loss_m());
}
return values;
}
public void updateTrackStatistics(@NonNull Track.Id trackId, @NonNull TrackStatistics trackStatistics) {
public void updateTrackStatistics(@NonNull Track.Id trackId, @NonNull Statistics trackStatistics) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
}
private ContentValues createContentValues(TrackStatistics trackStatistics) {
private ContentValues createContentValues(Statistics trackStatistics) {
ContentValues values = new ContentValues();
if (trackStatistics.getStartTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
if (trackStatistics.startTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.startTime().toEpochMilli());
}
if (trackStatistics.getStopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
if (trackStatistics.stopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.stopTime().toEpochMilli());
}
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.totalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.totalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.movingTime().toMillis());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed().toMPS());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed().toMPS());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed().toMPS());
values.put(TracksColumns.MAXSPEED, trackStatistics.maxSpeed().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());
if (trackStatistics.altitudeGainLoss() != null) {
values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.altitudeGainLoss().gain_m());
values.put(TracksColumns.ALTITUDE_LOSS, trackStatistics.altitudeGainLoss().loss_m());
}
return values;
}
@@ -656,11 +652,8 @@ public class ContentProviderUtils {
trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex));
}
if (!cursor.isNull(indexes.altitudeGainIndex)) {
trackPoint.setAltitudeGain(cursor.getFloat(indexes.altitudeGainIndex));
}
if (!cursor.isNull(indexes.altitudeLossIndex)) {
trackPoint.setAltitudeLoss(cursor.getFloat(indexes.altitudeLossIndex));
if (!cursor.isNull(indexes.altitudeGainIndex) && !cursor.isNull(indexes.altitudeLossIndex)) {
trackPoint.setAltitudeGainLoss(cursor.getFloat(indexes.altitudeGainIndex), cursor.getFloat(indexes.altitudeLossIndex));
}
return trackPoint;
@@ -814,13 +807,10 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower().getW());
}
if (trackPoint.hasAltitudeGain()) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.getAltitudeGain());
if (trackPoint.hasAltitudeGainLoss()) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.getAltitudeGainLoss().gain_m());
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.getAltitudeGainLoss().loss_m());
}
if (trackPoint.hasAltitudeLoss()) {
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.getAltitudeLoss());
}
return values;
}
@@ -834,7 +824,7 @@ public class ContentProviderUtils {
* @param trackId the track id
* @param startTrackPointId the starting trackPoint id. `null` to ignore
*/
public TrackPointIterator getTrackPointLocationIterator(final Track.Id trackId, final TrackPoint.Id startTrackPointId) {
public AutoCloseableIterator<TrackPoint> getTrackPointLocationIterator(final Track.Id trackId, final TrackPoint.Id startTrackPointId) {
return new TrackPointIterator(this, trackId, startTrackPointId);
}
@@ -2,7 +2,7 @@ package de.dennisguse.opentracks.data;
public record SelectionData(
String selection,
String[] selectionArgs
String[] selectionArgs //TODO Refactor to List<String>
) {
public SelectionData() {
@@ -32,6 +32,7 @@ import java.util.HashSet;
import java.util.Set;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
@@ -40,8 +41,8 @@ import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
/**
* Track data hub.
@@ -330,7 +331,7 @@ public class TrackDataHub {
}
TrackPoint trackPoint = null;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
try (AutoCloseableIterator<TrackPoint> trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
while (trackPointIterator.hasNext()) {
//Prevents a NPE if stop() is happening while notifyTrackPointsTableUpdate()
@@ -386,7 +387,7 @@ public class TrackDataHub {
lastSeenTrackPointId = localLastSeenTrackPointIdId;
}
listeners.stream().forEach(Listener::onNewTrackPointsDone);
listeners.forEach(Listener::onNewTrackPointsDone);
}
@@ -428,7 +429,7 @@ public class TrackDataHub {
*
* @param trackPoint the trackPoint
*/
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull Statistics trackStatistics) {
}
/**
@@ -436,7 +437,7 @@ public class TrackDataHub {
*
* @param trackPoint the trackPoint
*/
default void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
default void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint, @NonNull Statistics trackStatistics) {
}
/**
@@ -5,16 +5,16 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.Iterator;
import java.util.NoSuchElementException;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
/**
* A lightweight wrapper around the original {@link Cursor}.
*/
public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
class TrackPointIterator extends AutoCloseableIterator<TrackPoint> {
private static final String TAG = TrackPointIterator.class.getSimpleName();
@@ -23,7 +23,7 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private final CachedTrackPointsIndexes indexes;
private Cursor cursor;
public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) {
TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) {
this.contentProviderUtils = contentProviderUtils;
this.trackId = trackId;
@@ -6,7 +6,6 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
@@ -67,10 +66,11 @@ public class TrackSelection implements ContentProviderUtils.ContentProviderSelec
}
// Builds selection arguments.
ArrayList<String> args = trackIds.stream().map(id -> Long.toString(id.id())).collect(Collectors.toCollection(ArrayList::new));
ArrayList<String> args = new ArrayList<>();
args.addAll(trackIds.stream().map(id -> Long.toString(id.id())).toList());
args.addAll(categories);
args.addAll(fromToArgs);
selectionArgs = args.stream().toArray(String[]::new);
selectionArgs = args.toArray(String[]::new);
return new SelectionData(selection, selectionArgs);
}
@@ -7,7 +7,6 @@ import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
@@ -118,7 +117,7 @@ public enum ActivityType {
return Arrays.stream(values())
.map(ActivityType::getLocalizedStringId)
.map(context::getString)
.collect(Collectors.toList());
.toList();
}
@NonNull
@@ -0,0 +1,4 @@
package de.dennisguse.opentracks.data.models;
public record AltitudeExtremities(double min_m, double max_m) {
}
@@ -14,15 +14,15 @@ public class DistanceFormatter {
private final Resources resources;
private final int decimalCount;
private final int decimalPlaces;
private final double threshold;
private final UnitSystem unitSystem;
private DistanceFormatter(Resources resources, int decimalCount, double threshold, UnitSystem unitSystem) {
private DistanceFormatter(Resources resources, int decimalPlaces, double threshold, UnitSystem unitSystem) {
this.resources = resources;
this.decimalCount = decimalCount;
this.decimalPlaces = decimalPlaces;
this.threshold = threshold;
this.unitSystem = unitSystem;
assert unitSystem != null;
@@ -57,30 +57,30 @@ public class DistanceFormatter {
switch (unitSystem) {
case METRIC -> {
if (distance.greaterThan(Distance.ofKilometer(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalPlaces), resources.getString(R.string.unit_kilometer));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalPlaces), resources.getString(R.string.unit_meter));
}
}
case IMPERIAL_FEET -> {
if (distance.greaterThan(Distance.ofMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalPlaces), resources.getString(R.string.unit_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalPlaces), resources.getString(R.string.unit_feet));
}
}
case IMPERIAL_METER -> {
if (distance.greaterThan(Distance.ofMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalPlaces), resources.getString(R.string.unit_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalPlaces), resources.getString(R.string.unit_meter));
}
}
case NAUTICAL_IMPERIAL -> {
if (distance.greaterThan(Distance.ofNauticalMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalPlaces), resources.getString(R.string.unit_nautical_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalPlaces), resources.getString(R.string.unit_feet));
}
}
default -> throw new RuntimeException("Not implemented");
@@ -93,19 +93,19 @@ public class DistanceFormatter {
public static class Builder {
private int decimalCount;
private int decimalPlaces;
private UnitSystem unitSystem;
private double threshold;
public Builder() {
decimalCount = 2;
decimalPlaces = 2;
threshold = 0.5;
}
public Builder setDecimalCount(int decimalCount) {
this.decimalCount = decimalCount;
public Builder setDecimalPlaces(int decimalPlaces) {
this.decimalPlaces = decimalPlaces;
return this;
}
@@ -120,7 +120,7 @@ public class DistanceFormatter {
}
public DistanceFormatter build(Resources resource) {
return new DistanceFormatter(resource, decimalCount, threshold, unitSystem);
return new DistanceFormatter(resource, decimalPlaces, threshold, unitSystem);
}
public DistanceFormatter build(Context context) {
@@ -26,6 +26,7 @@ public record Speed(double speed_mps) {
return of(Distance.ofKilometer(speed_kmh), Duration.ofHours(1));
}
//TODO Like Duration.ZERO
public static Speed zero() {
return of(0.0);
}
@@ -12,17 +12,16 @@ import de.dennisguse.opentracks.util.StringUtils;
public class SpeedFormatter {
private final Resources resources;
private final int DECIMAL_PLACES = 1;
private final int decimalCount;
private final Resources resources;
private final UnitSystem unitSystem;
private final boolean reportSpeedOrPace;
private SpeedFormatter(Resources resources, int decimalCount, UnitSystem unitSystem, boolean reportSpeedOrPace) {
private SpeedFormatter(Resources resources, UnitSystem unitSystem, boolean reportSpeedOrPace) {
this.resources = resources;
this.decimalCount = decimalCount;
this.unitSystem = unitSystem;
this.reportSpeedOrPace = reportSpeedOrPace;
}
@@ -55,7 +54,7 @@ public class SpeedFormatter {
}
if (reportSpeedOrPace) {
return new Pair<>(StringUtils.formatDecimal(speed.to(unitSystem), 1), unitString);
return new Pair<>(StringUtils.formatDecimal(speed.to(unitSystem), DECIMAL_PLACES), unitString);
}
int pace = (int) speed.toPace(unitSystem).getSeconds();
@@ -71,22 +70,14 @@ public class SpeedFormatter {
public static class Builder {
private int decimalCount;
private UnitSystem unitSystem;
private boolean reportSpeedOrPace;
public Builder() {
decimalCount = 2;
reportSpeedOrPace = true;
}
public Builder setDecimalCount(int decimalCount) {
this.decimalCount = decimalCount;
return this;
}
public Builder setUnit(@NonNull UnitSystem unitSystem) {
this.unitSystem = unitSystem;
return this;
@@ -98,7 +89,7 @@ public class SpeedFormatter {
}
public SpeedFormatter build(Resources resource) {
return new SpeedFormatter(resource, decimalCount, unitSystem, reportSpeedOrPace);
return new SpeedFormatter(resource, unitSystem, reportSpeedOrPace);
}
public SpeedFormatter build(Context context) {
@@ -0,0 +1,61 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
public record Statistics(
Instant startTime,
Instant stopTime,
//TODO Rename to totalDuration
Duration totalTime,
//TODO Rename to movingDuration
Duration movingTime, // Based on when we believe the user is traveling
Distance totalDistance,
//TODO Check if this is persisted; if not: remove
Speed maxSpeed,
@Nullable
AltitudeExtremities altitudeExtremities,
@Nullable
AltitudeGainLoss altitudeGainLoss,
//NOTE: The following values are not persisted.
@Nullable
HeartRate avgHeartRate,
@Nullable
Power avgPower
) {
public Duration getStoppedTime() {
return totalTime.minus(movingTime);
}
public Speed getAverageSpeed() {
if (totalTime.isZero()) {
return Speed.of(0);
}
return Speed.of(totalDistance, totalTime);
}
public Speed getAverageMovingSpeed() {
return Speed.of(totalDistance, movingTime);
}
@Deprecated
//TODO null should be return (need adjusting ContentPtoviderUtils.createContentValues()
public double getMinAltitude() {
if (altitudeExtremities == null) return 0;
return altitudeExtremities.min_m();
}
@Deprecated
//TODO null should be return (need adjusting ContentPtoviderUtils.createContentValues()
public double getMaxAltitude() {
if (altitudeExtremities == null) return 0;
return altitudeExtremities.max_m();
}
}
@@ -26,10 +26,9 @@ import androidx.annotation.VisibleForTesting;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Objects;
import java.util.UUID;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.SegmentStatisticUpdater;
/**
* A track.
@@ -49,17 +48,31 @@ public class Track {
private ActivityType activityType;
//TODO Move to Statistics?
private ZoneOffset zoneOffset;
private TrackStatistics trackStatistics = new TrackStatistics();
@NonNull
private Statistics trackStatistics;
@Deprecated
@VisibleForTesting
public Track() {
this(ZoneOffset.UTC);
}
@VisibleForTesting
public Track(Statistics statistics) {
this(ZoneOffset.UTC, statistics);
}
@Deprecated //TODO Remove
public Track(@NonNull ZoneOffset zoneOffset) {
setZoneOffset(zoneOffset);
this(zoneOffset, new SegmentStatisticUpdater().getStatistics());
}
public Track(@NonNull ZoneOffset zoneOffset, @NonNull Statistics trackStatistics) {
this.zoneOffset = zoneOffset;
this.trackStatistics = trackStatistics;
}
/**
@@ -129,23 +142,23 @@ public class Track {
public OffsetDateTime getStartTime() {
return trackStatistics
.getStartTime().atOffset(zoneOffset);
.startTime().atOffset(zoneOffset);
}
public OffsetDateTime getStopTime() {
return trackStatistics
.getStopTime().atOffset(zoneOffset);
.stopTime().atOffset(zoneOffset);
}
@NonNull
public TrackStatistics getTrackStatistics() {
public Statistics getStatistics() {
return trackStatistics;
}
public void setTrackStatistics(@NonNull TrackStatistics trackStatistics) {
public void setStatistics(Statistics trackStatistics) {
this.trackStatistics = trackStatistics;
}
@NonNull
@Override
public String toString() {
return "Track{" +
@@ -156,20 +169,6 @@ public class Track {
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Track track = (Track) o;
return id.equals(track.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public record Id(long id) implements Parcelable {
@Override
@@ -35,7 +35,7 @@ import java.util.Optional;
public class TrackPoint {
@Nullable
private TrackPoint.Id id;
private final TrackPoint.Id id;
//Requires: position.time must be non-null
@NonNull
@@ -83,8 +83,7 @@ public class TrackPoint {
private HeartRate heartRate = null;
private Cadence cadence = null;
private Power power = null;
private Float altitudeGain_m = null;
private Float altitudeLoss_m = null;
private AltitudeGainLoss altitudeGainLoss = null;
public TrackPoint(@Nullable TrackPoint.Id id, @NonNull Type type, @NonNull Position position) {
this.id = id;
@@ -163,31 +162,21 @@ public class TrackPoint {
return getPosition().toLocation();
}
public boolean hasAltitudeGain() {
return altitudeGain_m != null;
public boolean hasAltitudeGainLoss() {
return altitudeGainLoss != null;
}
public float getAltitudeGain() {
return altitudeGain_m;
public AltitudeGainLoss getAltitudeGainLoss() {
return altitudeGainLoss;
}
@Deprecated
public TrackPoint setAltitudeGain(Float altitudeGain_m) {
this.altitudeGain_m = altitudeGain_m;
public TrackPoint setAltitudeGainLoss(AltitudeGainLoss altitudeGainLoss) {
this.altitudeGainLoss = altitudeGainLoss;
return this;
}
public boolean hasAltitudeLoss() {
return altitudeLoss_m != null;
}
public float getAltitudeLoss() {
return altitudeLoss_m;
}
@Deprecated
public TrackPoint setAltitudeLoss(Float altitudeLoss_m) {
this.altitudeLoss_m = altitudeLoss_m;
public TrackPoint setAltitudeGainLoss(float altitudeGain_m, float altitudeLoss_m) {
setAltitudeGainLoss(new AltitudeGainLoss(altitudeGain_m, altitudeLoss_m));
return this;
}
@@ -326,11 +315,10 @@ public class TrackPoint {
if (hasSensorDistance() && lastTrackPoint.hasSensorDistance()) {
sensorDistance = sensorDistance.minus(lastTrackPoint.getSensorDistance());
}
if (hasAltitudeGain() && lastTrackPoint.hasAltitudeGain()) {
altitudeGain_m -= lastTrackPoint.altitudeGain_m;
}
if (hasAltitudeLoss() && lastTrackPoint.hasAltitudeLoss()) {
altitudeLoss_m -= lastTrackPoint.altitudeLoss_m;
if (hasAltitudeGainLoss() && lastTrackPoint.hasAltitudeGainLoss()) {
altitudeGainLoss = new AltitudeGainLoss(
altitudeGainLoss.gain_m() - lastTrackPoint.altitudeGainLoss.gain_m(),
altitudeGainLoss.loss_m() - lastTrackPoint.altitudeGainLoss.loss_m());
}
return this;
}
@@ -391,14 +379,13 @@ public class TrackPoint {
public String toString() {
return "TrackPoint{" +
"id=" + id +
", type=" + type +
", position=" + position +
", sensorDistance=" + sensorDistance +
", type=" + type +
", heartRate=" + heartRate +
", cadence=" + cadence +
", power=" + power +
", altitudeGain_m=" + altitudeGain_m +
", altitudeLoss_m=" + altitudeLoss_m +
", altitudeGainLoss=" + altitudeGainLoss +
'}';
}
@@ -23,7 +23,6 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
private static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
@Deprecated
public static void showDialog(FragmentManager fragmentManager, Context context, String activityTypeLocalized) {
ActivityType activityType = ActivityType.findByLocalizedString(context, activityTypeLocalized);
showDialog(fragmentManager, activityType);
@@ -30,16 +30,15 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -157,8 +156,6 @@ public class StatisticsRecordedFragment extends Fragment {
loadTrackDescription(track);
updateUI();
updateSensorUI();
((TrackRecordedActivity) getActivity()).startPostponedEnterTransitionWith(viewBinding.statsActivityTypeIcon);
}
});
}
@@ -171,12 +168,12 @@ public class StatisticsRecordedFragment extends Fragment {
}
private void updateUI() {
TrackStatistics trackStatistics = track.getTrackStatistics();
Statistics trackStatistics = track.getStatistics();
// Set total distance
{
Pair<String, String> parts = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(getContext()).getDistanceParts(trackStatistics.getTotalDistance());
.build(getContext()).getDistanceParts(trackStatistics.totalDistance());
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -187,8 +184,8 @@ public class StatisticsRecordedFragment extends Fragment {
// Set time and start datetime
{
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getMovingTime()));
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.movingTime()));
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.totalTime()));
}
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(preferenceReportSpeed).build(getContext());
@@ -205,7 +202,7 @@ public class StatisticsRecordedFragment extends Fragment {
{
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.getMaxSpeed());
Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.maxSpeed());
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
@@ -221,8 +218,8 @@ public class StatisticsRecordedFragment extends Fragment {
// Set altitude gain and loss
{
Float altitudeGain_m = trackStatistics.getTotalAltitudeGain();
Float altitudeLoss_m = trackStatistics.getTotalAltitudeLoss();
Float altitudeGain_m = trackStatistics.altitudeGainLoss() != null ? trackStatistics.altitudeGainLoss().gain_m() : null;
Float altitudeLoss_m = trackStatistics.altitudeGainLoss() != null ? trackStatistics.altitudeGainLoss().loss_m() : null;
Pair<String, String> parts;
@@ -62,7 +62,7 @@ public class StatisticsRecordingFragment extends Fragment {
updateDataOnUI();
}
if (PreferencesUtils.isKey(R.string.stats_custom_layouts_key, key) || PreferencesUtils.isKey(R.string.stats_custom_layout_selected_layout_key, key)) {
if (PreferencesUtils.isKey(List.of(R.string.stats_custom_layouts_key, R.string.stats_custom_layout_selected_layout_key), key)) {
onLayoutChanged(PreferencesUtils.getCustomLayout());
}
};
@@ -10,9 +10,8 @@ import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.util.FileUtils;
@@ -25,8 +24,7 @@ public class TrackFilenameGenerator {
public static final String STARTTIME_DATE_KEY = "{date}";
public static String getAllOptions() {
return Stream.of(UUID_KEY, TRACKNAME_KEY, ACTIVITY_TYPE_KEY, STARTTIME_TIME_KEY, STARTTIME_DATE_KEY)
.collect(Collectors.joining(", "));
return String.join(", ", UUID_KEY, TRACKNAME_KEY, ACTIVITY_TYPE_KEY, STARTTIME_TIME_KEY, STARTTIME_DATE_KEY);
}
public static String format(@NonNull String name, @NonNull TrackFileFormat trackFileFormat) {
@@ -94,7 +92,7 @@ public class TrackFilenameGenerator {
Track track = new Track();
track.setName("Berlin");
track.setUuid(UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6"));
track.getTrackStatistics().setStartTime(Instant.ofEpochMilli(0));
track.setStatistics(new Statistics(Instant.EPOCH, null, null, null, null, null, null, null, null, null));
return format(track, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
}
@@ -28,10 +28,10 @@ import java.util.Locale;
import java.util.function.Function;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -98,8 +98,8 @@ public class CSVTrackExporter implements TrackExporter {
new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""),
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
new Column("altitude_gain", t -> t.hasAltitudeGain() ? ALTITUDE_FORMAT.format(t.getAltitudeGain()) : ""),
new Column("altitude_loss", t -> t.hasAltitudeLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeLoss()) : ""),
new Column("altitude_gain", t -> t.hasAltitudeGainLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeGainLoss().gain_m()) : ""),
new Column("altitude_loss", t -> t.hasAltitudeGainLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeGainLoss().loss_m()) : ""),
new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""),
new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""),
new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""),
@@ -131,7 +131,7 @@ public class CSVTrackExporter implements TrackExporter {
}
private void writeTrackPoints(List<Column> columns, Track track) throws InterruptedException {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
try (AutoCloseableIterator<TrackPoint> trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) throw new InterruptedException();
@@ -149,12 +149,12 @@ public class CSVTrackExporter implements TrackExporter {
printWriter = null;
}
public void writeHeader(List<Column> columns) {
private void writeHeader(List<Column> columns) {
String columnNames = columns.stream().map(c -> c.columnName).reduce((s, s2) -> s + "," + s2).orElseThrow(() -> new RuntimeException("No columns defined"));
printWriter.println("#" + columnNames);
}
public void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
private void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint)).reduce((s, s2) -> s + "," + s2).orElseThrow(() -> new RuntimeException("No columns defined"));
printWriter.println(columnNames);
}
@@ -24,15 +24,19 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
@@ -41,14 +45,12 @@ import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.FileUtils;
/**
* An activity for saving tracks to the external storage.
*
* @author Rodrigo Damazio
* TODO: This class needs some refactoring.
* * It pushes each export job (usually one Track) for export; although export could be run in parallel.
* Also this requires that the ExportActivity stays in foreground, so the user has to activily wait.
* It would be better to let the ExportService handle this and let it report progress / conflicts to ExportActivity
@@ -56,7 +58,9 @@ import de.dennisguse.opentracks.util.FileUtils;
* So, for this check actually a different file name might be used than in the ExportService.
* * Saved state as an object instead of individual values.
*/
public class ExportActivity extends AppCompatActivity implements ExportService.ExportServiceResultReceiver.Receiver {
//TODO Show progress per exported track (currently per exported file)
//TODO export tracks in parallel (for multiple files)
public class ExportActivity extends AbstractActivity {
private static final String TAG = ExportActivity.class.getSimpleName();
@@ -84,8 +88,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
private TrackFileFormat trackFileFormat;
private Uri directoryUri;
private ExportService.ExportServiceResultReceiver resultReceiver;
private List<String> directoryFiles;
private int trackExportSuccessCount;
@@ -147,8 +149,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = ExportActivityBinding.inflate(getLayoutInflater());
setContentView(viewBinding.getRoot());
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
@@ -161,8 +161,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri);
String directoryDisplayName = FileUtils.getPath(documentFile);
resultReceiver = new ExportService.ExportServiceResultReceiver(new Handler(), this);
if (savedInstanceState == null) {
autoConflict = ConflictResolutionStrategy.CONFLICT_NONE;
setProgress();
@@ -190,6 +188,13 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
viewBinding.exportActivityToolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
}
@NonNull
@Override
protected View createRootView() {
viewBinding = ExportActivityBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
@@ -207,6 +212,7 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
conflictsQueue.clear();
exportTasks.clear();
}
@@ -216,9 +222,9 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
exportTasks = new ArrayList<>();
if (allInOneFile) {
String filename = "OpenTracks-Backup";
exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::getId).collect(Collectors.toList())));
exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::getId).toList()));
} else {
exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).collect(Collectors.toList()));
exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).toList());
}
trackExportTotalCount = exportTasks.size();
}
@@ -231,19 +237,45 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_NONE) {
conflict(exportTask);
} else if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) {
return;
}
if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) {
trackExportSkippedCount++;
nextExport(exportTask);
} else {
ExportService.enqueue(this, resultReceiver, exportTask, directoryUri);
return;
}
WorkManager workManager = WorkManager.getInstance(this);
WorkRequest exportRequest = new OneTimeWorkRequest.Builder(ExportWorker.class)
.setInputData(new Data.Builder()
.putLongArray(ExportWorker.TRACKIDS_KEY, exportTask.getTrackIds().stream().mapToLong(Track.Id::id).toArray())
.putString(ExportWorker.DIRECTORY_URI_KEY, directoryUri.toString())
.putString(ExportWorker.TRACKFILEFORMAT_KEY, exportTask.getTrackFileFormat().toString())
.putString(ExportWorker.FILENAME_KEY, exportTask.getFilename())
.build())
.build();
workManager
.getWorkInfoByIdLiveData(exportRequest.getId())
.observe(this, workInfo -> {
if (workInfo != null) {
WorkInfo.State state = workInfo.getState();
switch (state) {
case SUCCEEDED -> onExportSuccess(exportTask);
case FAILED ->
onExportError(exportTask, workInfo.getOutputData().getString(ExportWorker.RESULT_EXPORT_ERROR_MESSAGE_KEY));
}
}
});
workManager.enqueue(exportRequest);
}
private void export(ExportTask exportTask) {
export(exportTask, autoConflict);
}
@Deprecated //TODO Check should be done in ExportService
@Deprecated //TODO Check should be done in ExportWorker
private boolean exportFileExists(ExportTask exportTask) {
String filename;
if (exportTask.isMultiExport()) {
@@ -314,7 +346,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
}
}
@Override
public void onExportSuccess(ExportTask exportTask) {
if (exportFileExists(exportTask)) {
trackExportOverwrittenCount++;
@@ -325,7 +356,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
nextExport(exportTask);
}
@Override
public void onExportError(ExportTask exportTask, String errorMessage) {
trackExportErrorCount++;
String name;
@@ -1,103 +0,0 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.app.job.JobService;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.ExportUtils;
public class ExportService extends JobIntentService {
private static final String TAG = ExportService.class.getSimpleName();
private static final int JOB_ID = 1;
private static final String EXTRA_RECEIVER = "extra_receiver";
private static final String EXTRA_EXPORT_TASK = "export_task";
private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri";
public static void enqueue(Context context, ExportServiceResultReceiver receiver, ExportTask exportTask, Uri directoryUri) {
Intent intent = new Intent(context, JobService.class);
intent.putExtra(EXTRA_RECEIVER, receiver);
intent.putExtra(EXTRA_EXPORT_TASK, exportTask);
intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri);
enqueueWork(context, ExportService.class, JOB_ID, intent);
}
@Override
protected void onHandleWork(@NonNull Intent intent) {
// Get all data.
ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
ExportTask exportTask = intent.getParcelableExtra(EXTRA_EXPORT_TASK);
Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI);
// Prepare resultCode and bundle to send to the receiver.
Bundle bundle = new Bundle();
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK, exportTask);
// Build directory file.
DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
if (directoryFile == null || !directoryFile.canWrite()) {
bundle.putString(ExportServiceResultReceiver.EXTRA_EXPORT_ERROR_MESSAGE, getString(R.string.export_cannot_write_to_dir) + ": " + directoryFile);
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle);
return;
}
// Export and send result
try {
ExportUtils.exportTrack(this, directoryFile, exportTask);
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_SUCCESS, bundle);
} catch (Exception e) {
Log.e(TAG, "Export failed: " + e);
e.printStackTrace(); //TODO Remove
bundle.putString(ExportServiceResultReceiver.EXTRA_EXPORT_ERROR_MESSAGE, e.getMessage());
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle);
}
}
public static class ExportServiceResultReceiver extends ResultReceiver {
public static final int RESULT_CODE_SUCCESS = 1;
public static final int RESULT_CODE_ERROR = 0;
public static final String RESULT_EXTRA_EXPORT_TASK = "result_extra_export_task";
public static final String EXTRA_EXPORT_ERROR_MESSAGE = "extra_export_error_message";
private final Receiver receiver;
public ExportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) {
super(handler);
this.receiver = receiver;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
ExportTask exportTask = resultData.getParcelable(RESULT_EXTRA_EXPORT_TASK);
switch (resultCode) {
case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(exportTask);
case RESULT_CODE_ERROR -> receiver.onExportError(exportTask, resultData.getString(EXTRA_EXPORT_ERROR_MESSAGE));
default -> throw new RuntimeException("Unknown resultCode.");
}
}
public interface Receiver {
default void onExportSuccess(ExportTask exportTask) {
}
default void onExportError(ExportTask exportTask, String errorMessage) {
}
}
}
}
@@ -1,60 +1,112 @@
package de.dennisguse.opentracks.util;
package de.dennisguse.opentracks.io.file.exporter;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.provider.DocumentsContract;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.io.file.exporter.ExportService;
import de.dennisguse.opentracks.io.file.exporter.ExportService.ExportServiceResultReceiver;
import de.dennisguse.opentracks.io.file.exporter.ExportTask;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentUtils;
public class ExportUtils {
private static final String TAG = ExportUtils.class.getSimpleName();
public static void postWorkoutExport(Context context, Track.Id trackId) {
public static void postWorkoutExport(AppCompatActivity context, Track.Id trackId) {
if (PreferencesUtils.shouldInstantExportAfterWorkout()) {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
DocumentFile directory = IntentUtils.toDocumentFile(context, PreferencesUtils.getDefaultExportDirectoryUri());
ExportServiceResultReceiver resultReceiver = new ExportServiceResultReceiver(new Handler(), new ExportServiceResultReceiver.Receiver() {
@Override
public void onExportError(ExportTask unused, String errorMessage) {
Intent intent = new Intent(context, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(SettingsActivity.EXTRAS_EXPORT_ERROR_MESSAGE, errorMessage);
context.startActivity(intent);
}
});
WorkManager workManager = WorkManager.getInstance(context);
WorkRequest exportRequest = new OneTimeWorkRequest.Builder(ExportWorker.class)
.setInputData(new Data.Builder()
.putLongArray(ExportWorker.TRACKIDS_KEY, new long[]{trackId.id()})
.putString(ExportWorker.DIRECTORY_URI_KEY, directory.getUri().toString())
.putString(ExportWorker.TRACKFILEFORMAT_KEY, trackFileFormat.toString())
.putString(ExportWorker.FILENAME_KEY, null)
.build())
.build();
ExportService.enqueue(context, resultReceiver, new ExportTask(null, trackFileFormat, List.of(trackId)), directory.getUri());
workManager
.getWorkInfoByIdLiveData(exportRequest.getId())
.observe(context, workInfo -> {
if (workInfo != null) {
if (workInfo.getState() == WorkInfo.State.FAILED) {
Intent intent = new Intent(context, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(SettingsActivity.EXTRAS_EXPORT_ERROR_MESSAGE, workInfo.getProgress().getString(ExportWorker.RESULT_EXPORT_ERROR_MESSAGE_KEY));
context.startActivity(intent);
}
}
});
workManager.enqueue(exportRequest);
}
}
public static void exportTrack(Context context, DocumentFile directory, @Nullable String filenameForMultiple, TrackFileFormat trackFileFormat, List<Track.Id> trackIds) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<Track> tracks = trackIds.stream()
.map(contentProviderUtils::getTrack)
.toList();
Uri exportDocumentFileUri;
if (tracks.size() == 1) {
exportDocumentFileUri = getExportDocumentFileUri(context, tracks.get(0), trackFileFormat, directory);
} else {
String filename = TrackFilenameGenerator.format(filenameForMultiple, trackFileFormat);
exportDocumentFileUri = getExportDocumentFileUri(context, filename, trackFileFormat, directory);
}
if (exportDocumentFileUri == null) {
throw new RuntimeException("Couldn't create document file for export");
}
TrackExporter trackExporter = trackFileFormat.createTrackExporter(context, contentProviderUtils);
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri, "wt")) {
if (!trackExporter.writeTrack(tracks, outputStream)) {
if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
throw new RuntimeException("Unable to delete exportDocumentFile");
}
throw new RuntimeException("Unable to export track");
}
} catch (FileNotFoundException e) {
throw new RuntimeException("Unable to open exportDocumentFile " + exportDocumentFileUri, e);
} catch (IOException e) {
throw new RuntimeException("Unable to close exportDocumentFile output stream", e);
}
}
@Deprecated
public static void exportTrack(Context context, DocumentFile directory, ExportTask exportTask) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<Track> tracks = exportTask.getTrackIds().stream().map(contentProviderUtils::getTrack).collect(Collectors.toList());
List<Track> tracks = exportTask.getTrackIds().stream()
.map(contentProviderUtils::getTrack)
.toList();
Uri exportDocumentFileUri;
if (tracks.size() == 1) {
exportDocumentFileUri = getExportDocumentFileUri(context, tracks.get(0), exportTask.getTrackFileFormat(), directory);
@@ -0,0 +1,70 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
public class ExportWorker extends Worker {
private static final String TAG = ExportWorker.class.getSimpleName();
static final String TRACKIDS_KEY = "TRACKIDS_KEY";
static final String DIRECTORY_URI_KEY = "DIRECTORY_URI_KEY";
static final String TRACKFILEFORMAT_KEY = "TRACKFILEFORMAT_KEY";
static final String FILENAME_KEY = "FILENAME_KEY"; //optional; only used for multiple tracks into one file
static final String RESULT_EXPORT_ERROR_MESSAGE_KEY = "EXPORT_ERROR_MESSAGE";
private final List<Track.Id> trackIds;
private final DocumentFile directoryFile;
private final TrackFileFormat trackFileFormat;
private final String filename;
public ExportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
trackIds = Arrays.stream(getInputData().getLongArray(TRACKIDS_KEY))
.mapToObj(Track.Id::new)
.toList();
directoryFile = DocumentFile.fromTreeUri(context, Uri.parse(getInputData().getString(DIRECTORY_URI_KEY)));
trackFileFormat = TrackFileFormat.valueOf(getInputData().getString(TRACKFILEFORMAT_KEY));
filename = getInputData().getString(FILENAME_KEY);
}
@NonNull
@Override
public Result doWork() {
if (directoryFile == null || !directoryFile.canWrite()) {
return Result.failure(new Data.Builder().putString(RESULT_EXPORT_ERROR_MESSAGE_KEY, getApplicationContext().getString(R.string.export_cannot_write_to_dir) + ": " + directoryFile).build());
}
try {
//TODO move method to ExportWorker?
ExportUtils.exportTrack(getApplicationContext(), directoryFile, filename, trackFileFormat, trackIds);
return Result.success();
} catch (Exception e) {
Log.e(TAG, "Export failed: " + e);
return Result.failure(new Data.Builder().putString(RESULT_EXPORT_ERROR_MESSAGE_KEY, e.getMessage()).build());
}
}
}
@@ -33,13 +33,13 @@ import java.util.function.Function;
import java.util.stream.Stream;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -128,7 +128,7 @@ public class GPXTrackExporter implements TrackExporter {
LinkedList<TrackPoint> sensorPoints = new LinkedList<>();
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
try (AutoCloseableIterator<TrackPoint> trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) throw new InterruptedException();
@@ -279,18 +279,16 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<opentracks:typeTranslated>" + StringUtils.formatCData(track.getActivityTypeLocalized()) + "</opentracks:typeTranslated>");
}
TrackStatistics trackStatistics = track.getTrackStatistics();
Statistics trackStatistics = track.getStatistics();
printWriter.println("<gpxtrkx:TrackStatsExtension>");
printWriter.println("<gpxtrkx:Distance>" + trackStatistics.getTotalDistance().toM() + "</gpxtrkx:Distance>");
printWriter.println("<gpxtrkx:TimerTime>" + trackStatistics.getTotalTime().getSeconds() + "</gpxtrkx:TimerTime>");
printWriter.println("<gpxtrkx:MovingTime>" + trackStatistics.getMovingTime().getSeconds() + "</gpxtrkx:MovingTime>");
printWriter.println("<gpxtrkx:Distance>" + trackStatistics.totalDistance().toM() + "</gpxtrkx:Distance>");
printWriter.println("<gpxtrkx:TimerTime>" + trackStatistics.totalTime().getSeconds() + "</gpxtrkx:TimerTime>");
printWriter.println("<gpxtrkx:MovingTime>" + trackStatistics.movingTime().getSeconds() + "</gpxtrkx:MovingTime>");
printWriter.println("<gpxtrkx:StoppedTime>" + trackStatistics.getStoppedTime().getSeconds() + "</gpxtrkx:StoppedTime>");
printWriter.println("<gpxtrkx:MaxSpeed>" + trackStatistics.getMaxSpeed().toMPS() + "</gpxtrkx:MaxSpeed>");
if (trackStatistics.hasTotalAltitudeGain()) {
printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.getTotalAltitudeGain() + "</gpxtrkx:Ascent>");
}
if (trackStatistics.hasTotalAltitudeLoss()) {
printWriter.println("<gpxtrkx:Descent>" + trackStatistics.getTotalAltitudeLoss() + "</gpxtrkx:Descent>");
printWriter.println("<gpxtrkx:MaxSpeed>" + trackStatistics.maxSpeed().toMPS() + "</gpxtrkx:MaxSpeed>");
if (trackStatistics.altitudeGainLoss() != null) {
printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.altitudeGainLoss().gain_m() + "</gpxtrkx:Ascent>");
printWriter.println("<gpxtrkx:Descent>" + trackStatistics.altitudeGainLoss().loss_m() + "</gpxtrkx:Descent>");
}
printWriter.println("</gpxtrkx:TrackStatsExtension>");
@@ -340,12 +338,12 @@ public class GPXTrackExporter implements TrackExporter {
extensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower().getW()) + "</pwr:PowerInWatts>\n";
}
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGain() ? (double) tp.getAltitudeGain() : null);
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGainLoss() ? (double) tp.getAltitudeGainLoss().gain_m() : null);
if (cumulativeGain != null) {
extensionContent += ("<opentracks:gain>" + ALTITUDE_FORMAT.format(cumulativeGain) + "</opentracks:gain>\n");
}
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeLoss() ? (double) tp.getAltitudeLoss() : null);
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGainLoss() ? (double) tp.getAltitudeGainLoss().loss_m() : null);
if (cumulativeLoss != null) {
extensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
}
@@ -36,13 +36,13 @@ import java.util.Objects;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -162,7 +162,7 @@ public class KMLTrackExporter implements TrackExporter {
boolean wroteTrack = false;
boolean wroteSegment = false;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
try (AutoCloseableIterator<TrackPoint> trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) throw new InterruptedException();
@@ -384,9 +384,13 @@ public class KMLTrackExporter implements TrackExporter {
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate().getBPM() : null);
cadenceList.add(trackPoint.hasCadence() ? trackPoint.getCadence().getRPM() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower().getW() : null);
altitudeGainList.add(trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : null);
altitudeLossList.add(trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : null);
if (trackPoint.hasAltitudeGainLoss()) {
altitudeGainList.add(trackPoint.getAltitudeGainLoss().loss_m());
altitudeLossList.add(trackPoint.getAltitudeGainLoss().loss_m());
} else {
altitudeGainList.add(null);
altitudeLossList.add(null);
}
accuracyHorizontal.add(trackPoint.hasHorizontalAccuracy() ? (float) trackPoint.getHorizontalAccuracy().toM() : null);
accuracyVertical.add(trackPoint.hasVerticalAccuracy() ? (float) trackPoint.getVerticalAccuracy().toM() : null);
}
@@ -4,7 +4,6 @@ import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.widget.Toast;
@@ -149,9 +148,7 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
super.configureDirectoryChooserIntent(intent);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
if (PreferencesUtils.isDefaultExportDirectoryUri()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri());
}
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri());
}
return null;
}
@@ -38,6 +38,7 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.stats.SegmentStatisticUpdater;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -48,7 +49,7 @@ import de.dennisguse.opentracks.util.StringUtils;
* <p>
* {@link de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter} does not export information if a segment was started automatic or manually.
* Therefore, all segments starts are marked as SEGMENT_START_AUTOMATIC.
* Thus, the {@link de.dennisguse.opentracks.stats.TrackStatistics} cannot be restored correctly.
* Thus, the {@link SegmentStatisticUpdater} cannot be restored correctly.
*
* @author Jimmy Shih
*/
@@ -368,18 +369,11 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
}
if (gain != null) {
if (gain != null && loss != null) {
try {
trackPoint.setAltitudeGain(Float.parseFloat(gain));
trackPoint.setAltitudeGainLoss(Float.parseFloat(gain), Float.parseFloat(loss));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s", gain)), e);
}
}
if (loss != null) {
try {
trackPoint.setAltitudeLoss(Float.parseFloat(loss));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s or los: %s", gain, loss)), e);
}
}
if (sensorDistance != null) {
@@ -25,13 +25,11 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.databinding.ImportActivityBinding;
@@ -43,7 +41,7 @@ import de.dennisguse.opentracks.util.IntentUtils;
*
* @author Rodrigo Damazio
*/
public class ImportActivity extends FragmentActivity {
public class ImportActivity extends AbstractActivity {
private static final String TAG = ImportActivity.class.getSimpleName();
@@ -57,14 +55,11 @@ public class ImportActivity extends FragmentActivity {
private ArrayList<Uri> documentUris = new ArrayList<>();
private boolean isDirectory;
private ImportViewModel viewModel;
private ImportViewModel.Summary summary;
private Importer importer;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = ImportActivityBinding.inflate(getLayoutInflater());
setContentView(viewBinding.getRoot());
final Intent intent = getIntent();
final ClipData intentClipData = intent.getClipData();
@@ -96,21 +91,39 @@ public class ImportActivity extends FragmentActivity {
documentFiles = new ArrayList<>();
documentFiles.add(DocumentFile.fromTreeUri(this, documentUris.get(0)));
} else {
documentFiles = documentUris.stream().map(it -> DocumentFile.fromSingleUri(this, it)).collect(Collectors.toList());
documentFiles = documentUris.stream()
.map(it -> DocumentFile.fromSingleUri(this, it))
.toList();
}
initViews();
viewModel = new ViewModelProvider(this).get(ImportViewModel.class);
viewModel.getImportData(documentFiles).observe(this, data -> {
summary = data;
setProgress();
});
importer = new Importer(this, documentFiles);
importer.getLiveData()
.observe(this, this::setProgress);
importer.startImport();
//Works for a directory, but we might have received multiple files via SEND_MULTIPLE.
viewBinding.importActivityToolbar.setTitle(getString(R.string.import_progress_message, documentFiles.get(0).getName()));
}
@NonNull
@Override
protected View createRootView() {
viewBinding = ImportActivityBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
@Override
public boolean onSupportNavigateUp() {
importer.cancel();
return super.onSupportNavigateUp();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
@@ -118,21 +131,8 @@ public class ImportActivity extends FragmentActivity {
outState.putBoolean(BUNDLE_IS_DIRECTORY, isDirectory);
}
private void initViews() {
viewBinding.importProgressDone.setText("0");
viewBinding.importProgressTotal.setText("0");
viewBinding.importProgressSummaryOk.setText("0");
viewBinding.importProgressSummaryExists.setText("0");
viewBinding.importProgressSummaryErrors.setText("0");
}
private int getTotalDone() {
return summary != null ? summary.getSuccessCount() + summary.getExistsCount() + summary.getErrorCount() : 0;
}
private void setProgress() {
int done = getTotalDone();
private void setProgress(Importer.Summary summary) {
int done = summary.getCount();
viewBinding.importProgressDone.setText("" + done);
viewBinding.importProgressTotal.setText("" + summary.getTotalCount());
@@ -144,12 +144,12 @@ public class ImportActivity extends FragmentActivity {
viewBinding.importProgressSummaryExistsGroup.setVisibility(summary.getExistsCount() > 0 ? View.VISIBLE : View.GONE);
viewBinding.importProgressSummaryErrorsGroup.setVisibility(summary.getErrorCount() > 0 ? View.VISIBLE : View.GONE);
if (done == summary.getTotalCount()) {
onImportEnded();
if (summary.isDone()) {
onImportEnded(summary);
}
}
private void onImportEnded() {
private void onImportEnded(Importer.Summary summary) {
viewBinding.importProgressAlertMsg.setVisibility(View.VISIBLE);
viewBinding.importProgressAlertIcon.setVisibility(View.VISIBLE);
@@ -161,13 +161,13 @@ public class ImportActivity extends FragmentActivity {
});
if (summary.getErrorCount() > 0) {
toggleUIEndWithErrors();
toggleUIEndWithErrors(summary);
} else {
toggleUIEndOk();
toggleUIEndOk(summary);
}
}
private void toggleUIEndWithErrors() {
private void toggleUIEndWithErrors(Importer.Summary summary) {
viewBinding.importProgressLeftButton.setVisibility(View.VISIBLE);
viewBinding.importProgressLeftButton.setText(getString(R.string.generic_show_errors));
viewBinding.importProgressLeftButton.setOnClickListener((view) -> ErrorListDialog.showDialog(getSupportFragmentManager(), getString(R.string.import_error_list_dialog_title), summary.getFileErrors()));
@@ -175,7 +175,7 @@ public class ImportActivity extends FragmentActivity {
viewBinding.importProgressAlertMsg.setText(getResources().getQuantityString(R.plurals.generic_completed_with_errors, summary.getErrorCount(), summary.getErrorCount()));
}
private void toggleUIEndOk() {
private void toggleUIEndOk(Importer.Summary summary) {
viewBinding.importProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_dialog_success_24dp));
viewBinding.importProgressAlertMsg.setText(getString(R.string.generic_completed));
@@ -1,97 +0,0 @@
package de.dennisguse.opentracks.io.file.importer;
import android.app.job.JobService;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.util.ArrayList;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class ImportService extends JobIntentService {
private static final String TAG = ImportService.class.getSimpleName();
private static final int JOB_ID = 2;
private static final String EXTRA_RECEIVER = "extra_receiver";
private static final String EXTRA_URI = "extra_uri";
private ResultReceiver resultReceiver;
public static void enqueue(Context context, ImportServiceResultReceiver receiver, Uri uri) {
Intent intent = new Intent(context, JobService.class);
intent.putExtra(EXTRA_RECEIVER, receiver);
intent.putExtra(EXTRA_URI, uri);
enqueueWork(context, ImportService.class, JOB_ID, intent);
}
@Override
protected void onHandleWork(@NonNull Intent intent) {
resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
Uri uri = intent.getParcelableExtra(EXTRA_URI);
importFile(DocumentFile.fromSingleUri(this, uri));
}
private void importFile(DocumentFile file) {
ArrayList<Track.Id> trackIds = new ArrayList<>();
String fileExtension = FileUtils.getExtension(file);
try {
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance();
boolean preventReimport = PreferencesUtils.getPreventReimportTracks();
TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), maxRecordingDistance, preventReimport);
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new GPXTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new KMLTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackIds.addAll(new KMZTrackImporter(this, trackImporter).importFile(file.getUri()));
} else {
Log.d(TAG, "Unsupported file format.");
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
return;
}
if (!trackIds.isEmpty()) {
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackIds, file, getString(R.string.import_file_imported, file.getName()));
} else {
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackIds, file, getString(R.string.import_unable_to_import_file, file.getName()));
}
} catch (IOException e) {
Log.d(TAG, "Unable to import file", e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unable_to_import_file, e.getMessage()));
} catch (ImportParserException e) {
Log.d(TAG, "Parser error: " + e.getMessage(), e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_parser_error, e.getMessage()));
} catch (ImportAlreadyExistsException e) {
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file, e.getMessage());
}
}
private void sendResult(int resultCode, ArrayList<Track.Id> trackId, DocumentFile file, String message) {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(ImportServiceResultReceiver.RESULT_EXTRA_LIST_TRACK_ID, trackId);
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, file.getName());
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE, message);
resultReceiver.send(resultCode, bundle);
}
}
@@ -1,38 +0,0 @@
package de.dennisguse.opentracks.io.file.importer;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
/**
* Create a new ResultReceive to receive results.
* Your {@link #onReceiveResult} method will be called from the thread running <var>handler</var> if given, or from an arbitrary thread if null.
*/
public class ImportServiceResultReceiver extends ResultReceiver {
public static final int RESULT_CODE_ERROR = 0;
public static final int RESULT_CODE_IMPORTED = 1;
public static final int RESULT_CODE_ALREADY_EXISTS = 2;
public static final String RESULT_EXTRA_LIST_TRACK_ID = "result_track_id";
public static final String RESULT_EXTRA_FILENAME = "result_extra_filename";
public static final String RESULT_EXTRA_MESSAGE = "result_extra_message";
private final Receiver receiver;
public ImportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) {
super(handler);
this.receiver = receiver;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
receiver.onReceiveResult(resultCode, resultData);
}
public interface Receiver {
void onReceiveResult(int resultCode, Bundle resultData);
}
}
@@ -1,130 +0,0 @@
package de.dennisguse.opentracks.io.file.importer;
import android.app.Application;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.util.FileUtils;
public class ImportViewModel extends AndroidViewModel implements ImportServiceResultReceiver.Receiver {
private static final String TAG = ImportViewModel.class.getSimpleName();
private MutableLiveData<Summary> importData;
private final ImportServiceResultReceiver resultReceiver;
private final Summary summary;
private boolean cancel = false;
private final List<DocumentFile> filesToImport = new ArrayList<>();
public ImportViewModel(@NonNull Application application) {
super(application);
resultReceiver = new ImportServiceResultReceiver(new Handler(), this);
summary = new Summary();
}
LiveData<Summary> getImportData(List<DocumentFile> documentFiles) {
if (importData == null) {
importData = new MutableLiveData<>();
loadData(documentFiles);
}
return importData;
}
void cancel() {
cancel = true;
}
private void loadData(List<DocumentFile> documentFiles) {
List<ArrayList<DocumentFile>> nestedFileList = documentFiles.stream()
.map(FileUtils::getFiles)
.collect(Collectors.toList());
List<DocumentFile> fileList = new ArrayList<>();
nestedFileList.forEach(fileList::addAll);
summary.totalCount = fileList.size();
filesToImport.addAll(fileList);
importNextFile();
}
private void importNextFile() {
if (cancel || filesToImport.isEmpty()) {
return;
}
ImportService.enqueue(getApplication(), resultReceiver, filesToImport.get(0).getUri());
filesToImport.remove(0);
}
@Override
public void onReceiveResult(int resultCode, Bundle resultData) {
if (resultData == null) {
throw new RuntimeException(TAG + ": onReceiveResult resultData NULL");
}
ArrayList<Track.Id> trackIds = resultData.getParcelableArrayList(ImportServiceResultReceiver.RESULT_EXTRA_LIST_TRACK_ID);
String fileName = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME);
String message = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE);
switch (resultCode) {
case ImportServiceResultReceiver.RESULT_CODE_ERROR -> {
summary.errorCount++;
summary.fileErrors.add(getApplication().getString(R.string.import_error_info, fileName, message));
}
case ImportServiceResultReceiver.RESULT_CODE_IMPORTED -> {
summary.importedTrackIds.addAll(trackIds);
summary.successCount++;
}
case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS -> summary.existsCount++;
default ->
throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
}
importData.postValue(summary);
importNextFile();
}
static class Summary {
private int totalCount;
private int successCount;
private int existsCount;
private int errorCount;
private final ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
private final ArrayList<String> fileErrors = new ArrayList<>();
public int getTotalCount() {
return totalCount;
}
public int getSuccessCount() {
return successCount;
}
public int getExistsCount() {
return existsCount;
}
public int getErrorCount() {
return errorCount;
}
public ArrayList<Track.Id> getImportedTrackIds() {
return importedTrackIds;
}
public ArrayList<String> getFileErrors() {
return fileErrors;
}
}
}
@@ -0,0 +1,100 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.io.IOException;
import java.util.ArrayList;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class ImportWorker extends Worker {
private static final String TAG = ImportWorker.class.getSimpleName();
static final String URI_KEY = "DIRECTORY_URI_KEY";
static final String RESULT_SUCCESS_LIST_TRACKIDS_KEY = "RESULT_TRACK_IDS";
static final String RESULT_URI_KEY = "RESULT_URI";
static final String RESULT_MESSAGE_KEY = "RESULT_MESSAGE";
static final String RESULT_FAILURE_IS_DUPLICATE = "RESULT_FAILURE_IS_DUPLICATE";
private final Uri uri;
public ImportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
uri = Uri.parse(getInputData().getString(URI_KEY));
}
@NonNull
@Override
public Result doWork() {
ArrayList<Track.Id> trackIds = new ArrayList<>();
Context context = getApplicationContext();
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance();
boolean preventReimport = PreferencesUtils.getPreventReimportTracks();
TrackImporter trackImporter = new TrackImporter(context, new ContentProviderUtils(context), maxRecordingDistance, preventReimport);
Data.Builder data = new Data.Builder()
.putString(RESULT_URI_KEY, uri.toString());
String fileExtension = FileUtils.getExtension(uri);
try {
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new GPXTrackImporter(getApplicationContext(), trackImporter)).importFile(context, uri));
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new KMLTrackImporter(getApplicationContext(), trackImporter)).importFile(context, uri));
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackIds.addAll(new KMZTrackImporter(context, trackImporter).importFile(uri));
} else {
Log.d(TAG, "Unsupported file format.");
return Result.failure(data
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unsupported_format))
.build());
}
if (!trackIds.isEmpty()) {
return Result.success(data
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_file_imported, uri.toString())) //TODO unused?
.putLongArray(RESULT_SUCCESS_LIST_TRACKIDS_KEY, trackIds.stream().mapToLong(Track.Id::id).toArray())
.build());
}
return Result.failure(data
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unable_to_import_file, uri.toString()))
.build());
} catch (IOException e) {
Log.d(TAG, "Unable to import file", e);
return Result.failure(data
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unable_to_import_file, e.getMessage()))
.build());
} catch (ImportParserException e) {
Log.d(TAG, "Parser error: " + e.getMessage(), e);
return Result.failure(data
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_parser_error, e.getMessage()))
.build());
} catch (ImportAlreadyExistsException e) {
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
return Result.failure(data
.putString(RESULT_MESSAGE_KEY, e.getMessage()) //TODO This is not used
.putBoolean(RESULT_FAILURE_IS_DUPLICATE, true)
.build());
}
}
}
@@ -0,0 +1,150 @@
package de.dennisguse.opentracks.io.file.importer;
import androidx.documentfile.provider.DocumentFile;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.util.FileUtils;
//TODO Cancel does not abort current import
//TODO Provide some intermediate progress (not only when a file got imported)
public class Importer {
private final MutableLiveData<Summary> liveData = new MutableLiveData<>();
private final Summary summary = new Summary();
private final List<DocumentFile> filesToImport = new ArrayList<>();
private boolean cancel = false;
private final ImportActivity context;
private final WorkManager workManager;
//TODO Add summary callback
Importer(ImportActivity context, List<DocumentFile> documentFiles) {
this.context = context;
this.workManager = WorkManager.getInstance(context);
List<ArrayList<DocumentFile>> nestedFileList = documentFiles.stream()
.map(FileUtils::getFiles)
.toList();
List<DocumentFile> fileList = new ArrayList<>();
nestedFileList.forEach(fileList::addAll);
summary.totalCount = fileList.size();
filesToImport.addAll(fileList);
}
LiveData<Summary> getLiveData() {
return liveData;
}
void startImport() {
liveData.postValue(summary);
importNextFile();
}
void cancel() {
cancel = true;
}
private void importNextFile() {
if (cancel || filesToImport.isEmpty()) {
return;
}
final DocumentFile documentFile = filesToImport.get(0);
WorkRequest importRequest = new OneTimeWorkRequest.Builder(ImportWorker.class)
.setInputData(new Data.Builder()
.putString(ImportWorker.URI_KEY, documentFile.getUri().toString())
.build())
.build();
workManager
.getWorkInfoByIdLiveData(importRequest.getId())
.observe(context, workInfo -> {
if (workInfo != null) {
WorkInfo.State state = workInfo.getState();
if (state.isFinished()) {
switch (state) {
case SUCCEEDED -> {
summary.importedTrackIds.addAll(
Arrays.stream(workInfo.getOutputData().getLongArray(ImportWorker.RESULT_SUCCESS_LIST_TRACKIDS_KEY))
.mapToObj(Track.Id::new)
.toList());
summary.successCount++;
}
case FAILED -> {
if (workInfo.getOutputData().getBoolean(ImportWorker.RESULT_FAILURE_IS_DUPLICATE, false)) {
summary.existsCount++;
} else {
// Some error happened
String errorMessage = workInfo.getOutputData().getString(ImportWorker.RESULT_MESSAGE_KEY);
summary.fileErrors.add(context.getString(R.string.import_error_info, documentFile.getName(), errorMessage));
}
}
}
liveData.postValue(summary);
importNextFile();
}
}
});
workManager.enqueue(importRequest);
filesToImport.remove(0);
}
static class Summary {
private int totalCount;
private int successCount;
private int existsCount;
private final ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
private final ArrayList<String> fileErrors = new ArrayList<>();
public int getTotalCount() {
return totalCount;
}
public int getSuccessCount() {
return successCount;
}
public int getExistsCount() {
return existsCount;
}
public int getErrorCount() {
return fileErrors.size();
}
public ArrayList<Track.Id> getImportedTrackIds() {
return importedTrackIds;
}
public ArrayList<String> getFileErrors() {
return fileErrors;
}
public int getCount() {
return getSuccessCount() + getExistsCount() + getErrorCount();
}
public boolean isDone() {
return getTotalCount() == getCount();
}
}
}
@@ -342,11 +342,10 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
if (i < sensorPowerList.size() && sensorPowerList.get(i) != null) {
trackPoint.setPower(sensorPowerList.get(i));
}
if (i < altitudeGainList.size()) {
trackPoint.setAltitudeGain(altitudeGainList.get(i));
}
if (i < altitudeLossList.size()) {
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
if (i < altitudeGainList.size() && i < altitudeLossList.size()) {
if (altitudeGainList.get(i) != null && altitudeLossList.get(i) != null) {
trackPoint.setAltitudeGainLoss(altitudeGainList.get(i), altitudeLossList.get(i));
}
}
if (i < accuracyHorizontal.size() && accuracyHorizontal.get(i) != null) {
trackPoint.setHorizontalAccuracy(Distance.of(accuracyHorizontal.get(i)));
@@ -147,7 +147,7 @@ public class TrackImporter {
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
updater.addTrackPoints(trackPoints);
track.setTrackStatistics(updater.getTrackStatistics());
track.setStatistics(updater.getTrackStatistics());
Track.Id trackId = contentProviderUtils.insertTrack(track);
@@ -1,9 +1,9 @@
package de.dennisguse.opentracks.publicapi;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.exporter.ExportUtils;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.util.ExportUtils;
public class StopRecording extends AbstractAPIActivity {
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.PowerManager;
import android.util.Log;
import androidx.annotation.Nullable;
@@ -15,6 +16,7 @@ import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.util.SystemUtils;
public class SensorManager implements SharedPreferences.OnSharedPreferenceChangeListener {
@@ -24,6 +26,8 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
@VisibleForTesting
public SensorDataSet sensorDataSet;
private PowerManager.WakeLock wakeLock;
private final TrackPointCreator observer;
private final SensorDataChangedObserver listener = new SensorDataChangedObserver() {
@@ -71,6 +75,8 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
throw new RuntimeException("SensorManager cannot be started twice; stop first.");
}
wakeLock = SystemUtils.acquireWakeLock(context, wakeLock);
gpsManager = new GpsManager(observer, listener);
altitudeSumManager = new GainManager(listener);
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
@@ -83,6 +89,8 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
}
public void stop(Context context) {
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
bluetoothSensorManager.stop(context);
bluetoothSensorManager = null;
@@ -185,8 +185,7 @@ public class SensorDataSet {
}
if (barometer != null && barometer.hasReceivedData()) {
trackPoint.setAltitudeGain(barometer.getAggregatedValue(trackPointCreator.createNow()).gain_m());
trackPoint.setAltitudeLoss(barometer.getAggregatedValue(trackPointCreator.createNow()).loss_m());
trackPoint.setAltitudeGainLoss(barometer.getAggregatedValue(trackPointCreator.createNow()).gain_m(), barometer.getAggregatedValue(trackPointCreator.createNow()).loss_m());
}
}
@@ -2,16 +2,16 @@ package de.dennisguse.opentracks.services;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.SegmentStatisticUpdater;
/**
* {@link Track} and {@link TrackPoint} must be immutable (i.e., their content does not change).
*/
public record RecordingData(Track track, TrackPoint latestTrackPoint, SensorDataSet sensorDataSet) {
public record RecordingData(Track track, TrackPoint latestTrackPoint, SensorDataSet sensorDataSet, Statistics currentSegment) {
public String getTrackCategory() {
if (track == null) {
return "";
@@ -19,12 +19,13 @@ public record RecordingData(Track track, TrackPoint latestTrackPoint, SensorData
return track.getActivityTypeLocalized();
}
@Deprecated
@NonNull
public TrackStatistics getTrackStatistics() {
public Statistics getStatisticsTrack() {
if (track == null) {
return new TrackStatistics();
return new SegmentStatisticUpdater().getStatistics();
}
return track.getTrackStatistics();
return track.getStatistics();
}
}
@@ -1,67 +0,0 @@
package de.dennisguse.opentracks.services;
import android.app.job.JobService;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import java.util.ArrayList;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
public class TrackDeleteService extends JobIntentService {
private static final int JOB_ID = 3;
private static final String EXTRA_RECEIVER = "extra_receiver";
private static final String EXTRA_TRACK_IDS = "extra_track_ids";
public static void enqueue(Context context, TrackDeleteResultReceiver receiver, ArrayList<Track.Id> toBeDeleted) {
Intent intent = new Intent(context, JobService.class);
intent.putExtra(EXTRA_RECEIVER, receiver);
intent.putParcelableArrayListExtra(EXTRA_TRACK_IDS, toBeDeleted);
enqueueWork(context, TrackDeleteService.class, JOB_ID, intent);
}
@Override
protected void onHandleWork(@NonNull Intent intent) {
ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
ArrayList<Track.Id> trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
contentProviderUtils.deleteTracks(this, trackIds);
resultReceiver.send(TrackDeleteResultReceiver.RESULT_CODE_SUCCESS, new Bundle());
}
public static class TrackDeleteResultReceiver extends ResultReceiver {
public static final int RESULT_CODE_SUCCESS = 1;
private final Receiver receiver;
public TrackDeleteResultReceiver(Handler handler, @NonNull Receiver receiver) {
super(handler);
this.receiver = receiver;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
switch (resultCode) {
case RESULT_CODE_SUCCESS -> receiver.onDeleteFinished();
default -> throw new RuntimeException("Unknown resultCode.");
}
}
public interface Receiver {
void onDeleteFinished();
}
}
}
@@ -0,0 +1,36 @@
package de.dennisguse.opentracks.services;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
public class TrackDeletionWorker extends Worker {
public static final String TRACKIDS_KEY = "TRACKIDS_KEY";
private final List<Track.Id> trackIds;
public TrackDeletionWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
trackIds = Arrays.stream(getInputData().getLongArray(TRACKIDS_KEY))
.mapToObj(Track.Id::new)
.toList();
}
@NonNull
@Override
public Result doWork() {
new ContentProviderUtils(getApplicationContext()).deleteTracks(getApplicationContext(), trackIds);
return Result.success();
}
}
@@ -17,13 +17,13 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.TrackNameUtils;
@@ -80,7 +80,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
String activityTypeLocalized = PreferencesUtils.getDefaultActivityTypeLocalized();
track.setActivityTypeLocalized(activityTypeLocalized);
track.setActivityType(ActivityType.findByLocalizedString(context, activityTypeLocalized));
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
track.setStatistics(trackStatisticsUpdater.getTrackStatistics());
track.setName(TrackNameUtils.getTrackName(context, trackId, track.getStartTime()));
contentProviderUtils.updateTrack(track);
@@ -98,7 +98,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
return false;
}
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getStatistics());
onNewTrackPoint(trackPointCreator.createSegmentStartManual());
reset();
@@ -116,7 +116,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
reset();
}
Pair<Track, Pair<TrackPoint, SensorDataSet>> getDataForUI() {
RecordingData getDataForUI() {
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater);
Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastTrackPointUIWithSpeed, lastTrackPointUIWithAltitude, lastStoredTrackPointWithLocation);
@@ -124,15 +124,15 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
ALTITUDE_CORRECTION_MANAGER.correctAltitude(context, current.first);
Track track = contentProviderUtils.getTrack(trackId); //Get copy
Track track = contentProviderUtils.getTrack(trackId); //Get copy TODO: Avoid re-loading data from DB
if (track == null) {
Log.w(TAG, "Requesting data if not recording is taking place, should not be done.");
return null;
}
track.setTrackStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
track.setStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
return new Pair<>(track, current);
return new RecordingData(track, current.first, current.second, tmpTrackStatisticsUpdater.getCurrentSegment());
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
@@ -224,7 +224,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
}
TrackStatistics getTrackStatistics() {
Statistics getTrackStatistics() {
return trackStatisticsUpdater == null ? null : trackStatisticsUpdater.getTrackStatistics();
}
@@ -22,9 +22,7 @@ import android.content.SharedPreferences;
import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -42,11 +40,9 @@ import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver {
@@ -55,7 +51,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
public static final RecordingStatus STATUS_DEFAULT = RecordingStatus.notRecording();
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null, null);
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
public TrackPoint getLastStoredTrackPointWithLocation() {
@@ -91,7 +87,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private MutableLiveData<RecordingData> recordingDataObservable;
// The following variables are set when recording:
private WakeLock wakeLock; //TODO Move to SensorManager
private Handler handler;
private TrackPointCreator trackPointCreator;
@@ -127,7 +122,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
if (isRecording()) {
endCurrentTrack();
}
if (isSensorStarted()) {
if (trackPointCreator != null && trackPointCreator.isStarted()) {
stopSensors();
}
@@ -197,10 +192,12 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
startSensors();
voiceAnnouncementManager.start(trackRecordingManager.getTrackStatistics());
notificationManager.onRecordingStarted(this, recordingStatus.trackId());
}
public void tryStartSensors() {
if (isSensorStarted()) return;
if (trackPointCreator.isStarted()) return;
Log.i(TAG, "tryStartSensors");
@@ -208,15 +205,14 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
private synchronized void startSensors() {
if (isSensorStarted()) {
if (trackPointCreator.isStarted()) {
Log.i(TAG, "sensors already started; skipping");
return;
}
Log.i(TAG, "startSensors");
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
trackPointCreator.start(this, handler);
ServiceCompat.startForeground(this, TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
ServiceCompat.startForeground(this, TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.onSensorsStarted(this), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
}
public void endCurrentTrack() {
@@ -240,8 +236,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
void stopSensors() {
trackPointCreator.stop();
stopForeground(true);
notificationManager.cancelNotification();
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
notificationManager.stop();
gpsStatusObservable.postValue(STATUS_GPS_DEFAULT);
}
@@ -267,7 +262,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
boolean stored = trackRecordingManager.onNewTrackPoint(trackPoint);
notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy);
//TODO Should be done during updateRecordingDataWhileRecording()
notificationManager.updateTrackPoint(this, trackRecordingManager.getDataForUI(), thresholdHorizontalAccuracy);
return stored;
}
@@ -284,7 +280,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
Log.e(TAG, e.getMessage() + " " + writer);
return;
}
notificationManager.updateContent(getString(gpsStatusValue.message));
notificationManager.updateGPSStatus(getString(gpsStatusValue.message));
gpsStatusObservable.postValue(gpsStatusValue);
}
@@ -315,11 +311,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
// Compute temporary track statistics using sensorData and update time.
Pair<Track, Pair<TrackPoint, SensorDataSet>> data = trackRecordingManager.getDataForUI();
RecordingData data = trackRecordingManager.getDataForUI();
voiceAnnouncementManager.announceStatisticsIfNeeded(data.first, data.second.second);
voiceAnnouncementManager.announceStatisticsIfNeeded(data);
recordingDataObservable.postValue(new RecordingData(data.first, data.second.first, data.second.second));
recordingDataObservable.postValue(data);
}
public void onIdle() {
@@ -346,9 +342,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return recordingStatus.isRecording();
}
private boolean isSensorStarted() {
return wakeLock != null;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
@@ -9,19 +9,20 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.IntentUtils;
/**
@@ -43,15 +44,13 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
TrackRecordingServiceNotificationManager(Context context) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
notificationChannel.setAllowBubbles(true);
}
notificationManager.createNotificationChannel(notificationChannel);
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
notificationChannel.setAllowBubbles(true);
}
notificationManager.createNotificationChannel(notificationChannel);
notificationBuilder = new NotificationCompat.Builder(context, CHANNEL_ID);
notificationBuilder
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
@@ -62,57 +61,21 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
.setSmallIcon(R.drawable.ic_logo_color_24dp);
}
void stop() {
cancelNotification();
}
@VisibleForTesting
TrackRecordingServiceNotificationManager(NotificationManager notificationManager, NotificationCompat.Builder notificationBuilder) {
this.notificationManager = notificationManager;
this.notificationBuilder = notificationBuilder;
}
void updateContent(String content) {
notificationBuilder.setSubText(content);
updateNotification();
}
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, Distance recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
DistanceFormatter formatter = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context);
// The decision if a trackpoint is not accurate enough should happen in the sensor subsystem.
if (trackPoint.hasHorizontalAccuracy()) {
formattedAccuracy = formatter.formatDistance(trackPoint.getHorizontalAccuracy());
boolean currentLocationWasAccurate = trackPoint.getHorizontalAccuracy().lessThan(recordingGpsAccuracy);
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
previousLocationWasAccurate = currentLocationWasAccurate;
}
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(trackStatistics.getTotalDistance())));
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(trackPoint.getSpeed());
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
notificationBuilder.setOnlyAlertOnce(true);
}
Notification setGPSonlyStarted(Context context) {
Notification onSensorsStarted(Context context) {
Intent intent = IntentUtils.newIntent(context, TrackListActivity.class);
int pendingIntentFlags = 0;
pendingIntentFlags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT;
PendingIntent pendingIntent = TaskStackBuilder.create(context)
.addParentStack(TrackListActivity.class)
.addNextIntent(intent)
.getPendingIntent(0, pendingIntentFlags);
.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
updateContent(context.getString(R.string.gps_starting));
updateGPSStatus(context.getString(R.string.gps_starting));
notificationBuilder.setContentIntent(pendingIntent);
updateNotification();
@@ -120,12 +83,52 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
return getNotification();
}
void cancelNotification() {
notificationManager.cancel(NOTIFICATION_ID);
void updateGPSStatus(String content) {
notificationBuilder.setSubText(content);
updateNotification();
}
void setUnitSystem(UnitSystem unitSystem) {
this.unitSystem = unitSystem;
public void onRecordingStarted(Context context, @NonNull Track.Id trackId) {
Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
PendingIntent pendingIntent = TaskStackBuilder.create(context)
.addParentStack(TrackListActivity.class)
.addNextIntent(intent)
.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
notificationBuilder.setContentIntent(pendingIntent);
updateNotification();
}
void updateTrackPoint(Context context, RecordingData recordingData, Distance thresholdHorizontalAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
DistanceFormatter formatter = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context);
//TODO The decision if a trackpoint is not accurate enough should happen in the TrackRecordingManager (via RecordingData).
if (recordingData.latestTrackPoint().hasHorizontalAccuracy()) {
formattedAccuracy = formatter.formatDistance(recordingData.latestTrackPoint().getHorizontalAccuracy());
boolean currentLocationWasAccurate = recordingData.latestTrackPoint().getHorizontalAccuracy().lessThan(thresholdHorizontalAccuracy);
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
previousLocationWasAccurate = currentLocationWasAccurate;
}
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(recordingData.getStatisticsTrack().totalDistance())));
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(recordingData.latestTrackPoint().getSpeed());
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
notificationBuilder.setOnlyAlertOnce(true);
}
void stop() {
notificationManager.cancel(NOTIFICATION_ID);
}
private void updateNotification() {
@@ -136,6 +139,11 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
return notificationBuilder.build();
}
@VisibleForTesting
void setUnitSystem(UnitSystem unitSystem) {
this.unitSystem = unitSystem;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
@@ -26,18 +26,18 @@ import androidx.annotation.VisibleForTesting;
import androidx.mediarouter.media.MediaRouter;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatisticsUpdater;
import de.dennisguse.opentracks.util.AutoCloseableIterator;
/**
* Execute a periodic task on a time or distance schedule.
@@ -52,7 +52,7 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
private TTSManager voiceAnnouncement;
private TrackStatistics trackStatistics;
private Statistics trackStatistics;
private static final Distance DISTANCE_OFF = Distance.of(Double.MAX_VALUE);
private Distance distanceFrequency = DISTANCE_OFF;
@@ -68,23 +68,23 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
private final ContentProviderUtils contentProviderUtils;
private TrackPoint.Id startTrackPointId = null;
private IntervalStatistics intervalStatistics;
private IntervalStatisticsUpdater intervalStatistics;
private Distance intervalDistance;
public VoiceAnnouncementManager(@NonNull Context context) {
this.context = context;
contentProviderUtils = new ContentProviderUtils(context);
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
intervalStatistics = new IntervalStatistics(intervalDistance);
intervalStatistics = new IntervalStatisticsUpdater(intervalDistance);
}
public void start(@Nullable TrackStatistics trackStatistics) {
public void start(@Nullable Statistics trackStatistics) {
voiceAnnouncement = new TTSManager(context);
voiceAnnouncement.start();
update(trackStatistics);
}
void update(@Nullable TrackStatistics trackStatistics) {
void update(@Nullable Statistics trackStatistics) {
this.trackStatistics = trackStatistics;
updateNextDuration();
updateNextTaskDistance();
@@ -119,44 +119,46 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
voiceAnnouncement.speak(VoiceAnnouncementUtils.createIdle(context));
}
public void announceStatisticsIfNeeded(@NonNull Track track, @NonNull SensorDataSet sensorDataSet) {
public void announceStatisticsIfNeeded(@NonNull RecordingData data) {
if (shouldNotAnnounce()) {
return;
}
boolean announce = false;
this.trackStatistics = track.getTrackStatistics();
if (trackStatistics.getTotalDistance().greaterThan(nextTotalDistance)) {
this.trackStatistics = data.track().getStatistics();
if (trackStatistics.totalDistance().greaterThan(nextTotalDistance)) {
updateNextTaskDistance();
announce = true;
}
if (!trackStatistics.getTotalTime().minus(nextTotalTime).isNegative()) {
if (!trackStatistics.totalTime().minus(nextTotalTime).isNegative()) {
updateNextDuration();
announce = true;
}
if (announce) {
voiceAnnouncement.speak(createAnnouncement(track, sensorDataSet));
voiceAnnouncement.speak(createAnnouncement(data));
}
}
private Spannable createAnnouncement(Track track, SensorDataSet sensorDataSet) {
private Spannable createAnnouncement(RecordingData data) {
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
if (currentIntervalDistance != intervalDistance) {
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
intervalStatistics = new IntervalStatisticsUpdater(currentIntervalDistance);
intervalDistance = currentIntervalDistance;
startTrackPointId = null;
}
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
if (track.getId() != null) {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
//TODO Avoid reloading (some) TrackPoints just to announce the last interval.
try (AutoCloseableIterator<TrackPoint> trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(data.track().getId(), startTrackPointId)) {
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
Statistics lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
if (data.track().getId() != null) {
sensorStatistics = contentProviderUtils.getSensorStats(data.track().getId());
}
return VoiceAnnouncementUtils.createStatistics(context, track, sensorDataSet, PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics);
return VoiceAnnouncementUtils.createStatistics(context, data.track(), data.sensorDataSet(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(data.track()), lastInterval, sensorStatistics);
}
}
public void stop() {
@@ -181,7 +183,7 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
nextTotalDistance = DISTANCE_OFF;
} else {
Distance distance = trackStatistics.getTotalDistance();
Distance distance = trackStatistics.totalDistance();
int index = (int) (distance.dividedBy(distanceFrequency));
nextTotalDistance = distanceFrequency.multipliedBy(index + 1);
@@ -194,10 +196,9 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
nextTotalTime = TOTALTIME_OFF;
} else {
Duration totalTime = trackStatistics.getTotalTime();
Duration intervalMod = Duration.ofMillis(trackStatistics.getTotalTime().toMillis() % totalTimeFrequency.toMillis());
Duration intervalMod = Duration.ofMillis(trackStatistics.totalTime().toMillis() % totalTimeFrequency.toMillis());
nextTotalTime = totalTime.plus(totalTimeFrequency.minus(intervalMod));
nextTotalTime = trackStatistics.totalTime().plus(totalTimeFrequency.minus(intervalMod));
}
}
@@ -219,7 +220,7 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
setFrequency(PreferencesUtils.getVoiceAnnouncementFrequency());
}
if (PreferencesUtils.isKey(new int[]{R.string.voice_announcement_distance_key, R.string.stats_units_key}, key)) {
if (PreferencesUtils.isKey(List.of(R.string.voice_announcement_distance_key, R.string.stats_units_key), key)) {
setFrequency(PreferencesUtils.getVoiceAnnouncementDistance());
}
}

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