Compare commits

...

69 Commits

Author SHA1 Message Date
Dennis Guse 14e63a1a54 Release: v3.15.4 2021-03-09 21:44:02 +01:00
Román Martínez e559583d23 Voice announcement in seconds instead of minutes. Fixes #640. 2021-03-09 20:14:54 +01:00
Dennis Guse 3b9711d82e Release: v3.15.3 2021-03-08 18:16:18 +01:00
Dennis Guse 114fc8255f KML export: remove #start (was broken due to #316) and #end (included track statistics) placemarks.
Fixes #629.
2021-03-08 18:11:36 +01:00
Dennis Guse 26e655d58f Cleanup. 2021-03-08 18:11:36 +01:00
Dennis Guse 815b6a3cf1 StatisticsRecordingFragment: set speed=0 when TrackPoint is outdated. 2021-03-07 22:28:53 +01:00
Dennis Guse 3dc8a724d8 Bug: StatisticsRecordingFragment: speed and location where sometimes 0.
Fixes #628.
2021-03-07 22:28:53 +01:00
Dennis Guse 70f571cd09 CI: avoid OutOfMemoryError.
Fixes #599.
2021-03-07 19:46:37 +01:00
Dennis Guse 3f5c3a5c08 Bugfix: Close cursor. 2021-03-06 18:27:24 +01:00
Dennis Guse a13465391f Import: replaced SAXParserException as it only shows the message of it's cause. 2021-03-06 18:13:06 +01:00
Dennis Guse 97454b5369 Statistics*Fragment now updates the units on track category change. 2021-03-04 19:18:31 +01:00
Dennis Guse f6a47c4594 Statistics*Fragement now uses SharedPreferenceListener instead of accessing preferences repeatedly. 2021-03-04 19:18:31 +01:00
Dennis Guse fce76230e1 TrackDataListener now passes most recent TrackPoint to onNewTrackPointsDone(). 2021-03-04 19:18:31 +01:00
Dennis Guse fdf4336a9b Cleanup. 2021-03-04 19:18:31 +01:00
Dennis Guse 844c6e9609 TrackPoint: moved methods into class. 2021-03-04 19:18:31 +01:00
Manuel Zangl 784942ffd2 Fixed longitudeText in StatisticsRecordingFragment 2021-03-04 17:38:42 +01:00
Román Martínez edf4ccc941 It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence. 2021-03-02 19:55:18 +01:00
Dennis Guse d90258294f Release: v3.15.2 2021-03-01 19:42:16 +01:00
mondstern 01f4c13307 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-02-28 20:56:11 +01:00
FTno 873956426d Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-02-28 20:56:11 +01:00
Xosé M 4128c54960 Translated using Weblate (Galician)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-02-28 20:56:11 +01:00
mondstern 9978bddf21 Translated using Weblate (Czech)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-02-28 20:56:11 +01:00
Nikita Epifanov a9726b9a94 Translated using Weblate (Russian)
Currently translated at 81.5% (429 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-02-28 20:56:11 +01:00
Crystal-RainSlide 05ba52bab5 Translated using Weblate (Chinese (Simplified))
Currently translated at 80.7% (425 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/zh_Hans/
2021-02-28 20:56:11 +01:00
FTno ca95f40e77 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-02-28 20:56:11 +01:00
mondstern 5cb85c4383 Translated using Weblate (Italian)
Currently translated at 98.4% (513 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-02-28 20:56:11 +01:00
mondstern d0a477b112 Translated using Weblate (Finnish)
Currently translated at 94.4% (492 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-02-28 20:56:11 +01:00
mondstern ec365a18ca Translated using Weblate (Danish)
Currently translated at 88.2% (460 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-02-28 20:56:11 +01:00
mondstern 0ff735e559 Translated using Weblate (Czech)
Currently translated at 100.0% (521 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-02-28 20:56:11 +01:00
Dennis Guse 8a35835791 GPX export: re-implemented basic support for multiple tracks.
Fixes #614.
2021-02-28 20:56:05 +01:00
Dennis Guse e3569dab56 Release: v3.15.1 2021-02-27 18:40:05 +01:00
Dennis Guse 4040e3f45b Bugfix: crash on recording start due to show latitude/longitude option.
Fixes #613.
2021-02-27 18:37:42 +01:00
Dennis Guse eac7efba64 Upgrade desugar lib. 2021-02-26 21:30:08 +01:00
dependabot[bot] 21a3971a56 Bump mockito-android from 3.7.7 to 3.8.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.7.7 to 3.8.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.7.7...v3.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-23 08:07:43 +01:00
Dennis Guse 4e4a9abf6f Localization: fix/remove translation for wheel circumference (instead of wheel size).
Fixes #605.
2021-02-22 23:11:06 +01:00
Román Martínez a9624f72e7 Track recorded stats shows sensor data from trackpoints table.
Heart rate: maximum and average.
Cadence: maximum and average.
Power: average.
Fixes #525 and #502.
2021-02-14 17:34:19 +01:00
Dennis Guse bb38d274fb Voice announcements: cleanup. 2021-02-13 14:27:54 +01:00
Dennis Guse 418f092db9 Release: v3.15.0 2021-02-12 23:29:58 +01:00
Dennis Guse fa12086951 GPX: only export TrackPoints with lat/lng. (#591)
GPX: only export TrackPoints with lat/lng.
Thus, segmentation (manual/automatic) cannot be restored on import.
This also affects the TrackStatistics.
2021-02-09 19:00:28 +01:00
dependabot[bot] 4a340380ac Bump espresso-core from 3.4.0-alpha03 to 3.4.0-alpha04
Bumps espresso-core from 3.4.0-alpha03 to 3.4.0-alpha04.

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-09 18:50:10 +01:00
Dennis Guse cd4e0abd6f SensorData: use Instant instead of long. 2021-02-07 23:16:12 +01:00
dependabot[bot] 854c8727e5 Bump material from 1.2.1 to 1.3.0
Bumps [material](https://github.com/material-components/material-components-android) from 1.2.1 to 1.3.0.
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.2.1...1.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-05 16:52:57 +01:00
Román Martínez 2f3d9a29f3 Fixed a bug: TrackDataHub will send to listeners all TrackPoints every time a change on trackpoints table was observed. Fixes #582. 2021-01-30 23:15:54 +01:00
Dennis Guse 18cdbe94a9 i18n: app_name cannot be translated anymore.
Fixes #581.
2021-01-30 21:33:17 +01:00
Dennis Guse 18e179cfe3 Merge remote-tracking branch 'weblate/main' into main 2021-01-30 21:24:20 +01:00
Román Martínez 63e3c7a4e7 Updated README file with new screenshots. 2021-01-29 16:04:28 +01:00
Xosé M c6deca9bbd Translated using Weblate (Galician)
Currently translated at 100.0% (522 of 522 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-01-29 16:03:27 +01:00
Vinicius 9ef6ae5e25 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (522 of 522 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-01-29 16:03:27 +01:00
Román Martínez 29b1ded68c Updated README file with new screenshots. 2021-01-29 16:03:16 +01:00
Román Martínez 6239e2fcad New screenshots and script to convert SVG to PNG. 2021-01-29 15:56:34 +01:00
Dennis Guse bdfcd9c89f TrackStatistics: merge previous segment whenever a new starts/ends.
(also adapting the test).
2021-01-24 22:49:40 +01:00
Dennis Guse f8f78d28dd TrackStatistics: merge previous segment whenever a new starts/ends. 2021-01-24 22:21:37 +01:00
Dennis Guse cbe469d5e4 Import: empty tracks cannot be imported as TrackStatistics cannot be restored.
#316.
2021-01-24 22:21:37 +01:00
Dennis Guse e0553f812f Import: segments might contain no TrackPoints.
#316.
2021-01-24 22:21:37 +01:00
Dennis Guse 5aa016b5b9 Import: compute TrackStatistics onTrackEnd.
ExportImportTest were not checking TrackStatistics correctly.

#316.
2021-01-24 22:21:37 +01:00
Dennis Guse c3741611dc TrackStatisticsUpdater updates only by adding TrackPoints.
#316.
2021-01-24 22:21:37 +01:00
Dennis Guse d157e94220 Marker refactoring.
Location values are stored directly.
2021-01-24 22:21:37 +01:00
Dennis Guse 022d8146fe TrackDataHub always sends via handler. 2021-01-24 22:21:37 +01:00
Dennis Guse 912fd1aa2f TrackStatistics startTime/stopTime is null on creation. 2021-01-24 22:21:36 +01:00
Dennis Guse 1d46981717 Cleanup. 2021-01-24 22:21:36 +01:00
Dennis Guse e990b413cd Replace long time_ms with Instant and Duration, respectively.
Fixes #561.
2021-01-24 22:21:36 +01:00
Dennis Guse 3bbcbfe202 Dashboard API: send protocol version v2. 2021-01-24 22:21:36 +01:00
Dennis Guse 9d8f49f1dc Segments: document new TrackPoint segment structure.
#316.
2021-01-24 22:21:36 +01:00
Dennis Guse 9f6d6711e5 Segments: on import restore manual segment start/end if resume/pause trackpoints were exported. 2021-01-24 22:21:36 +01:00
Dennis Guse f635d56ceb Cleanup. 2021-01-24 22:21:36 +01:00
Dennis Guse ab47320216 Segments: import/export is working incl. restoring TrackStatistics.
KML exports sensor data if actually present.

#316.
2021-01-24 22:21:36 +01:00
Dennis Guse 47b2d0748a Segments: only insert one trackpoint and set the type.
Existing data is not migrated.
#316.
2021-01-24 22:21:36 +01:00
Dennis Guse bc66cd33a9 Use system time instead of GPS-provided time.
#316.
2021-01-24 22:21:36 +01:00
Dennis Guse fcc17413e3 Segments: use TrackPoint.type.
So, far specific latitude for segment start/end (i.e., PAUSE/RESUME) was used.
#316.
2021-01-24 22:21:36 +01:00
192 changed files with 5316 additions and 2583 deletions
+2
View File
@@ -18,4 +18,6 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
profile: Nexus 6
script: ./gradlew connectedCheck
+8 -10
View File
@@ -84,18 +84,16 @@ However, this is rather slow and does not provide updates while recording.
## Screenshots
<div>
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_5.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot2.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot3.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot4.png">
</div>
<div>
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_6.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_7.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_8.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_9.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_10.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot5.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot6.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot7.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png">
</div>
## Project history
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
set -e
# Folder where svg screenshots are expected.
SRC_DIR="screenshots-svg/"
# Folder where png files will be saved.
DST_DIR="fastlane/metadata/android/en-US/phoneScreenshots/"
echo "Exporting svg files to png..."
# Source and destination folder must exist.
if [ ! -d "$SRC_DIR" ]
then
echo "\nERROR: ${SRC_DIR} doesn't exist."
exit
fi
if [ ! -d "$DST_DIR" ]
then
echo "\nERROR: ${DST_DIR} doesn't exist."
exit
fi
# It needs Inkscape that is the tool used to convert svg to png.
if ! command -v inkscape > /dev/null
then
echo "\nERROR: inkscape couldn't be found and it's needed to convert screenshots from svg to png."
exit
fi
# Convert all svg files from source directory.
for path in ${SRC_DIR}/*.svg
do
filename="${path##*/}"
basename="${filename%.svg}"
`inkscape ${path} --export-filename ${DST_DIR}/${basename}.png`
done
+6 -6
View File
@@ -51,8 +51,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3823
versionName "v3.14.3"
versionCode 3892
versionName "v3.15.4"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -86,18 +86,18 @@ android {
}
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha03'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha04'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'org.mockito:mockito-android:3.7.7'
androidTestImplementation 'org.mockito:mockito-android:3.8.0'
}
+30 -32
View File
@@ -9,9 +9,9 @@ class Track {
}
Track o-- TrackPoint
Track o-- Marker
Track "1" - "1" TripStatistics
Track "1" - "1" TrackStatistics
class TripStatistics {
class TrackStatistics {
- startTime
- stopTime
- totalDistance
@@ -21,13 +21,15 @@ class TripStatistics {
- totalElevationGain
}
note left of TripStatistics
The time is based upon device time.
end note
class TrackStatisticsUpdater {
}
TrackStatisticsUpdater ..> TrackStatistics : creates/updates
TrackStatisticsUpdater ..> TrackPoint : uses
class TrackPoint {
- id
- id (database id, order)
- type
- trackId
- longitude
- latitude
@@ -41,30 +43,6 @@ class TrackPoint {
- sensor_power
}
note right of TrackPoint
TODO
TrackPoints are mixing two concepts:
1. a GPS measurement provided by the GPS hardware
2. and also segment markers.
These are marked by PAUSE / RESUME TrackPoints.
While exporting PAUSE/RESUME are not used to create these segments and these are partly recreated when imported.
However, the timestamp of PAUSE/RESUME is not exported and can therefore not be restored.
This prevents us from restoring the exact TripStatistics.
It is also noteworthy that neither PAUSE/RESUME TrackPoints are used for computing the TrackStatistics.
Only the timestamp to _create_ those is used.
end note
note left of TrackPoint
TODO
The time stored in TrackPoints is either:
1. created via GPS: provided by the GPS hardware
2. created by the user (pause/resume): device time.
We know that the GPS time and device time are often not in sync.
end note
TrackPoint "1" - "1" android.location.Location
class Marker {
- id
@@ -75,8 +53,28 @@ class Marker {
- icon
- length
- duration
- location
- tripStatistics
- longitude
- latitude
- photoUrl
}
note left of TrackPoint
As of OpenTracks version 3.15.0, all times are using device time.
Before that TrackPoint.time contained GPS time (determined by GPS hardware).
However, start/pause/stop events (also stored as TrackPoints) used device time.
end note
note right of Track
A track is an ordered collection of one or more segments (i.e., continuous parts of distance covered).
Segments may be started by a user (i.e., start a track recording, continue a paused track, or resume a track) as well as stopped by the user (i.e., pausing or stopping a recording).
Also segments may started automatically while recording (i.e., distance to previous location was to large).
Note that this finishes the previous segment.
Segment data is stored as TrackPoints (Type.SEGMENT_START_MANUAL, Type.SEGMENT_START_AUTOMATIC, or Type.SEGMENT_END_MANUAL).
Trackpoints with Type.SEGMENT_START_AUTOMATIC also mark the end of the previous segment.
All TrackPoints of Type.TRACKPOINT belong to the segment started by the prior TrackPoint with Type.SEGMENT_START_(MANUAL|AUTOMATIC).
Trackpoints of Type.SEGMENT_START_MANUAL or Type.SEGMENT_END_MANUAL do not contain location data or sensor data.
Tracks recorded prior to OpenTracks version 3.15.0, do neither begin with a Type.SEGMENT_START_MANUAL nor end with a Type.SEGMENT_END_MANUAL.
end note
@enduml
@@ -0,0 +1,22 @@
v3.15.0: OpenTracks
WARNING:
This release applies database changes that CANNOT BE REVERTED.
As KMZ/KML export/import behaviour changes as well, it is recommended to (A) take a backup BEFORE upgrading and (B) afterwards.
Recommended format: KMZ.
BREAKING CHANGES:
- Internal storage of track segments is changed
(user generated events are now considered trackpoints without a location but with timestamp)
- Device time instead of GPS time is stored
(existing data is not modified)
- Dashboard API v2
(please upgrade OSMDashboard 2.10.0)
Changes:
- KML/KMZ can used to backup and restore exactly (incl. statistics)
Developer:
- Reworked track segmentation and it's storage (formerly PAUSE/RESUME marker)
- TrackStatistics are computed solely using TrackPoints
- Replaced long with Instant/Duration
@@ -0,0 +1,7 @@
v3.15.1: OpenTracks
Changes:
- After recording show sensor statistics (e.g., avg HR)
Bugfix:
- Crash on recording start with show latitude/longitude enabled
@@ -0,0 +1,7 @@
v3.15.2: OpenTracks
Changes:
- Updated translations
Bugfix:
- Crash on GPX export
@@ -0,0 +1,8 @@
v3.15.3: OpenTracks
Changes:
- Computation avg/max: heart rate, cadence, and power
Bugfix:
- KML/KMZ do not export #start/#end placemarks anymore
- While recording: Speed/location was sometimes falsly shown as 0.
@@ -0,0 +1,4 @@
v3.15.4: OpenTracks
Bugfix:
- Voice annoucements interval should be in minutes (introduced in v3.15.0)
Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 174 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 862 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.6 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 338 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 665 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 248 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 661 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1001 KiB

@@ -25,6 +25,9 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -40,7 +43,7 @@ import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class DescriptionGeneratorTest {
private static final long START_TIME = 1288721514000L;
private static final Instant START_TIME = Instant.ofEpochMilli(1288721514000L);
private DescriptionGenerator descriptionGenerator;
private final Context context = ApplicationProvider.getApplicationContext();
@@ -58,14 +61,14 @@ public class DescriptionGeneratorTest {
Track track = new Track();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
stats.setMaxSpeed(100);
stats.setMaxElevation(550);
stats.setMinElevation(-500);
stats.setTotalElevationGain(6000);
stats.setTotalElevationLoss(6000);
stats.setStartTime_ms(START_TIME);
stats.setTotalElevationGain(6000f);
stats.setTotalElevationLoss(6000f);
stats.setStartTime(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
@@ -96,14 +99,14 @@ public class DescriptionGeneratorTest {
Track track = new Track();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
stats.setMaxSpeed(100);
stats.setMaxElevation(Double.POSITIVE_INFINITY);
stats.setMinElevation(Double.NEGATIVE_INFINITY);
stats.setTotalElevationGain(6000);
stats.setTotalElevationLoss(6000);
stats.setStartTime_ms(START_TIME);
stats.setTotalElevationGain(6000f);
stats.setTotalElevationLoss(6000f);
stats.setStartTime(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
@@ -139,12 +142,12 @@ public class DescriptionGeneratorTest {
}
/**
* Tests {@link DescriptionGenerator#writeTime(long, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeTime(Duration, StringBuilder, int, String)}.
*/
@Test
public void testWriteTime() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeTime(1000, builder, R.string.description_total_time, "<br>");
descriptionGenerator.writeTime(Duration.ofMillis(1000), builder, R.string.description_total_time, "<br>");
assertEquals("Total time: 00:01<br>", builder.toString());
}
@@ -1,12 +1,12 @@
package de.dennisguse.opentracks.content.data;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import android.util.Pair;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -38,6 +38,7 @@ public class TestDataUtil {
* @param trackId the trackId of the track
* @param numPoints the trackPoints number in the track
*/
@Deprecated //TODO Should start with SEGMENT_START_MANUAL and end with SEGMENT_END_MANUAL.
public static Pair<Track, List<TrackPoint>> createTrack(Track.Id trackId, int numPoints) {
Track track = createTrack(trackId);
@@ -49,6 +50,51 @@ public class TestDataUtil {
return new Pair<>(track, trackPoints);
}
public static TrackData createTestingTrack(Track.Id trackId) {
Track track = createTrack(trackId);
int i = 0;
List<TrackPoint> trackPoints = List.of(
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochMilli(i++ + 1)),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++, TrackPoint.Type.SEGMENT_START_AUTOMATIC),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1)),
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochSecond(i++)),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1))
);
List<Marker> markers = List.of(
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(1), null),
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(4), null),
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(5), null)
);
return new TrackData(track, trackPoints, markers);
}
public static class TrackData {
public final Track track;
public final List<TrackPoint> trackPoints;
public final List<Marker> markers;
public TrackData(Track track, List<TrackPoint> trackPoints, List<Marker> markers) {
this.track = track;
this.trackPoints = trackPoints;
this.markers = markers;
}
}
public static Track createTrackAndInsert(ContentProviderUtils contentProviderUtils, Track.Id trackId, int numPoints) {
Pair<Track, List<TrackPoint>> pair = createTrack(trackId, numPoints);
@@ -63,12 +109,12 @@ public class TestDataUtil {
* @param i the index for the TrackPoint.
*/
public static TrackPoint createTrackPoint(int i) {
TrackPoint trackPoint = new TrackPoint();
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
trackPoint.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
trackPoint.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
trackPoint.setAccuracy((float) i / 100.0f);
trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
trackPoint.setTime(i + 1);
trackPoint.setTime(Instant.ofEpochSecond(i + 1));
trackPoint.setSpeed(5f + (i / 10f));
trackPoint.setHeartRate_bpm(100f + i);
@@ -79,6 +125,12 @@ public class TestDataUtil {
return trackPoint;
}
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type) {
TrackPoint trackPoint = createTrackPoint(i);
trackPoint.setType(type);
return trackPoint;
}
/**
* Inserts a track with locations into the database.
*
@@ -90,20 +142,12 @@ public class TestDataUtil {
contentProviderUtils.bulkInsertTrackPoint(trackPoints, track.getId());
}
/**
* Creates a Marker with a photo.
*
* @param context The context.
* @param trackId The track id.
* @param location The location.
* @return the Marker created.
*/
public static Marker createMarkerWithPhoto(Context context, Track.Id trackId, Location location) throws IOException {
public static Marker createMarkerWithPhoto(Context context, Track.Id trackId, TrackPoint trackPoint) throws IOException {
File dstFile = new File(FileUtils.getImageUrl(context, trackId));
dstFile.createNewFile();
Uri photoUri = FileUtils.getUriForFile(context, dstFile);
String photoUrl = photoUri.toString();
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, location, photoUrl);
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, trackPoint, photoUrl);
}
}
@@ -0,0 +1,92 @@
package de.dennisguse.opentracks.content.data;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class TestSensorDataUtil {
private List<TrackPoint> trackPointList = new ArrayList<>();
private List<SensorData> sensorDataList = new ArrayList<>();
public void add(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
TrackPoint tp = new TrackPoint(type);
int i = trackPointList.size() + 1;
tp.setLatitude(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0);
tp.setLongitude(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0);
tp.setHeartRate_bpm(hr);
tp.setCyclingCadence_rpm(cadence);
tp.setPower(power);
tp.setAccuracy(1f);
tp.setAltitude(1f);
tp.setTime(time);
tp.setSpeed(5f + (i / 10f));
tp.setElevationGain(3f);
tp.setElevationLoss(3f);
trackPointList.add(tp);
}
public List<TrackPoint> getTrackPointList() {
return this.trackPointList;
}
public SensorDataStats computeStats() {
if (sensorDataList == null || sensorDataList.size() <= 1) {
return null;
}
SensorDataStats stats = new SensorDataStats();
long timeElapsed;
long movingTime = 0;
SensorData dataPrev = sensorDataList.get(0);
stats.maxHr = dataPrev.hr;
stats.maxCadence = dataPrev.cadence;
SensorData dataCurrent;
for (int i = 1; i < sensorDataList.size(); i++) {
dataCurrent = sensorDataList.get(i);
if (dataPrev.type != TrackPoint.Type.SEGMENT_START_MANUAL) {
timeElapsed = dataCurrent.type != TrackPoint.Type.SEGMENT_START_MANUAL ? dataCurrent.time.getEpochSecond() - dataPrev.time.getEpochSecond() : 0;
stats.avgHr += (dataPrev.hr * timeElapsed);
stats.maxHr = dataPrev.hr > stats.maxHr ? dataPrev.hr : stats.maxHr;
stats.avgCadence += (dataPrev.cadence * timeElapsed);
stats.maxCadence = dataPrev.cadence > stats.maxCadence ? dataPrev.cadence : stats.maxCadence;
stats.avgPower += (dataPrev.power * timeElapsed);
movingTime += timeElapsed;
}
dataPrev = dataCurrent;
}
stats.avgHr /= movingTime;
stats.avgCadence /= movingTime;
stats.avgPower /= movingTime;
return stats;
}
private static class SensorData {
Instant time;
float hr;
float cadence;
float power;
TrackPoint.Type type;
public SensorData(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
this.time = time;
this.hr = hr == null ? 0 : hr;
this.cadence = cadence == null ? 0 : cadence;
this.power = power == null ? 0 : power;
this.type = type;
}
}
public static class SensorDataStats {
public float avgHr = 0f;
public float maxHr = 0f;
public float avgCadence = 0f;
public float maxCadence = 0f;
public float avgPower = 0f;
}
}
@@ -0,0 +1,28 @@
package de.dennisguse.opentracks.content.data;
import org.junit.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TrackPointTest {
@Test
public void isRecent_true() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
tp.setTime(Instant.now());
assertTrue(tp.isRecent());
}
@Test
public void isRecent_false() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
tp.setTime(Instant.now().minus(2, ChronoUnit.MINUTES));
assertFalse(tp.isRecent());
}
}
@@ -33,18 +33,24 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
@@ -231,7 +237,7 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
contentProviderUtils.insertMarker(marker);
ContentResolver contentResolver = context.getContentResolver();
@@ -270,7 +276,7 @@ public class CustomContentProviderUtilsTest {
Track.Id trackId2 = new Track.Id(random + 1);
Track.Id trackId3 = new Track.Id(random + 2);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId1, 0);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId1, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10);
@@ -282,7 +288,7 @@ public class CustomContentProviderUtilsTest {
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(20, tracksPointsCursor.getCount());
assertEquals(30, tracksPointsCursor.getCount());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
// Delete one track.
@@ -313,12 +319,12 @@ public class CustomContentProviderUtilsTest {
// Insert a marker in tracks trackId and trackId + 1.
TrackPoint trackPoint1 = contentProviderUtils.getLastValidTrackPoint(trackId1);
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId1, trackPoint1.getLocation());
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId1, trackPoint1);
contentProviderUtils.insertMarker(marker1);
File dir1 = FileUtils.getPhotoDir(context, trackId1);
TrackPoint trackPoint2 = contentProviderUtils.getLastValidTrackPoint(trackId2);
Marker marker2 = TestDataUtil.createMarkerWithPhoto(context, trackId2, trackPoint2.getLocation());
Marker marker2 = TestDataUtil.createMarkerWithPhoto(context, trackId2, trackPoint2);
contentProviderUtils.insertMarker(marker2);
File dir2 = FileUtils.getPhotoDir(context, trackId2);
@@ -423,14 +429,12 @@ public class CustomContentProviderUtilsTest {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
// Bottom
long startTime = 1000L;
// AverageSpeed
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime_ms(startTime);
statistics.setStopTime_ms(2500L);
statistics.setTotalTime(1500L);
statistics.setMovingTime(700L);
statistics.setStartTime(Instant.ofEpochMilli(1000));
statistics.setStopTime(Instant.ofEpochMilli(2500));
statistics.setTotalTime(Duration.ofMillis(1500));
statistics.setMovingTime(Duration.ofMillis(700));
statistics.setTotalDistance(750.0);
statistics.setTotalElevationGain(50.0f);
statistics.setMaxSpeed(60.0);
@@ -527,7 +531,7 @@ public class CustomContentProviderUtilsTest {
// Insert at first.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
contentProviderUtils.insertMarker(marker1);
// Check insert was done.
@@ -661,7 +665,7 @@ public class CustomContentProviderUtilsTest {
// Insert at first.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
marker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
@@ -696,7 +700,7 @@ public class CustomContentProviderUtilsTest {
// Insert at first.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
marker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
@@ -730,9 +734,9 @@ public class CustomContentProviderUtilsTest {
// Insert two markers with photos.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
marker.setDescription(TEST_DESC);
Marker otherMarker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
Marker otherMarker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
otherMarker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
contentProviderUtils.insertMarker(otherMarker);
@@ -809,7 +813,7 @@ public class CustomContentProviderUtilsTest {
// then
assertEquals(longitude, trackPoint.getLongitude(), 0.01);
assertEquals(latitude, trackPoint.getLatitude(), 0.01);
assertEquals(time, trackPoint.getTime(), 0.01);
assertEquals(time, trackPoint.getTime().toEpochMilli());
assertEquals(speed, trackPoint.getSpeed(), 0.01);
assertFalse(trackPoint.hasHeartRate());
}
@@ -927,4 +931,337 @@ public class CustomContentProviderUtilsTest {
assertEquals("12", ContentProviderUtils.formatIdListForUri(new Track.Id(12)));
assertEquals("42,43,44", ContentProviderUtils.formatIdListForUri(new Track.Id(42), new Track.Id(43), new Track.Id(44)));
}
@Test
public void testGetSensorStats_noSensorData() {
// given
List<TrackPoint> trackPointList = new ArrayList<>();
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
trackPoint.setPower(null);
trackPoint.setCyclingCadence_rpm(null);
trackPoint.setHeartRate_bpm(null);
trackPointList.add(trackPoint);
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, trackPointList);
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_needAtLeastTwoTrackPointsFalse() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 90 300 0
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.TRACKPOINT);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_needAtLeastTwoTrackPointsTrue() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 90 300 -1
* 1 140 90 300 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, 90f, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertTrue(sensorStatistics.hasHeartRate());
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertTrue(sensorStatistics.hasCadence());
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertTrue(sensorStatistics.hasPower());
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_onlyHr() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 NULL NULL -1
* 1 140 NULL NULL 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, null, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, null, null, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertTrue(sensorStatistics.hasHeartRate());
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_onlyCadence() {
// given
/*
* time elapsed hr cadence power track type
* 0 NULL 90 NULL -1
* 1 NULL 90 NULL 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, null, 90f, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, 90f, null, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertFalse(sensorStatistics.hasHeartRate());
assertTrue(sensorStatistics.hasCadence());
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_onlyPower() {
// given
/*
* time elapsed hr cadence power track type
* 0 NULL NULL 300 -1
* 1 NULL NULL 300 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, null, null, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, null, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertTrue(sensorStatistics.hasPower());
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 7 160 90 275 0
* 4 155 85 280 0
* 1 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(10, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(14, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(15, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_withManualResume() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 3 174 88 400 0
* 20 127 54 175 -2
* 3 160 90 275 0
* 7 155 85 280 0
* 3 150 90 267 0
* 3 170 90 240 0
* 2 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_MANUAL);
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_withStartAutomatic() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 3 174 88 400 0
* 20 127 54 175 -1
* 3 160 90 275 0
* 7 155 85 280 0
* 3 150 90 267 0
* 3 170 90 240 0
* 2 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
private void testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
// given
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
Random random = new Random();
for (int i = 0; i < totalPoints; i++) {
int randomNum = withStartSegments ? random.nextInt(50) - 2 : 0;
TrackPoint.Type type = randomNum >= 0 ? TrackPoint.Type.TRACKPOINT : TrackPoint.Type.getById(randomNum);
float randomHr = random.nextFloat() * (200f - 90f) + 90f;
float randomCadence = random.nextFloat() * (110f - 40f) + 40f;
float randomPower = random.nextFloat() * (500f - 100f) + 100f;
sensorDataUtil.add(start.plus(i, ChronoUnit.SECONDS), randomHr, randomCadence, randomPower, type);
}
sensorDataUtil.add(start.plus(totalPoints, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0.01f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0.01f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0.01f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0.01f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0.01f);
}
@Test
public void testGetSensorStats_veryLongActivity12h() {
testGetSensorStats_randomData(43200, false);
}
@Test
public void testGetSensorStats_withSeveralRandomStartSegments() {
testGetSensorStats_randomData(5000, true);
}
}
@@ -24,7 +24,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
assertEquals(60, current.getValue(), 0.01);
}
@Test
@@ -37,7 +37,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(33.53, current.getCadence_rpm(), 0.01);
assertEquals(33.53, current.getValue(), 0.01);
}
@Test
@@ -50,7 +50,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(0, current.getCadence_rpm(), 0.01);
assertEquals(0, current.getValue(), 0.01);
}
@@ -64,7 +64,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertFalse(current.hasCadence_rpm());
assertFalse(current.hasValue());
}
@Test
@@ -77,7 +77,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
assertEquals(60, current.getValue(), 0.01);
}
@Test
@@ -90,7 +90,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
assertEquals(60, current.getValue(), 0.01);
}
@Test
@@ -103,7 +103,7 @@ public class SensorDataCyclingTest {
current.compute(previous, 2150);
// then
assertEquals(1.20, current.getSpeed_mps(), 0.01);
assertEquals(1.20, current.getValue(), 0.01);
}
@Test
@@ -116,7 +116,7 @@ public class SensorDataCyclingTest {
current.compute(previous, 2000);
// then
assertEquals(2, current.getSpeed_mps(), 0.01);
assertEquals(2, current.getValue(), 0.01);
}
@Test
@@ -28,6 +28,9 @@ import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -61,9 +64,8 @@ public class ChartFragmentTest {
@Before
public void setUp() {
boolean chartByDistance = false;
chartFragment = (ChartFragment) ChartFragment.newInstance(chartByDistance);
chartFragment = ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setTrackStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
}
/**
@@ -191,15 +193,21 @@ public class ChartFragmentTest {
*/
@Test
public void testCreatePendingPoint_time() {
// given
chartFragment.setChartByDistance(false);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setTime(Instant.ofEpochMilli(TrackStubUtils.INITIAL_TIME)); //Keep old TrackPoint behavior of having time=0 for this test
// when
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
// then
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
long timeSpan = 222;
Duration timeSpan = Duration.ofMillis(222);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setTime(trackPoint1.getTime() + timeSpan);
trackPoint2.setTime(Instant.ofEpochMilli(TrackStubUtils.INITIAL_TIME).plus(timeSpan));
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals((double) timeSpan, point.getTimeOrDistance(), 0.01);
assertEquals(timeSpan, Duration.ofMillis((long) point.getTimeOrDistance()));
}
/**
@@ -250,8 +258,8 @@ public class ChartFragmentTest {
/*
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
*/
trackPoint2.setTime(trackPoint1.getTime() + 222);
trackPoint2.setSpeed(130);
trackPoint2.setTime(trackPoint1.getTime().plusMillis(222));
trackPoint2.setSpeed(130f);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01);
}
@@ -275,8 +283,8 @@ public class ChartFragmentTest {
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102);
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
trackPoint2.setSpeed(102f);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
}
@@ -300,8 +308,8 @@ public class ChartFragmentTest {
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102);
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
trackPoint2.setSpeed(102f);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01);
}
@@ -313,7 +321,7 @@ public class ChartFragmentTest {
public void testCreatePendingPoint_pace_zeroSpeed() {
chartFragment.setReportSpeed(false);
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setSpeed(0);
trackPoint.setSpeed(0f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
assertEquals(0.0, point.getPace(), 0.01);
}
@@ -57,7 +57,7 @@ public class TrackStubUtils {
location.setAltitude(altitude);
location.setAccuracy(INITIAL_ACCURACY);
location.setSpeed(INITIAL_SPEED);
location.setTime(INITIAL_TIME);
location.setTime(INITIAL_TIME); //TODO This is nowadays ignored as the constructor will replace the time.
location.setBearing(INITIAL_BEARING);
return new TrackPoint(location);
@@ -0,0 +1,53 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayOutputStream;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import static org.junit.Assert.assertEquals;
@RunWith(JUnit4.class)
public class KmlTrackExporterTest {
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Sensor data by type should only be created if present in at least on TrackPoint.
*/
@Test
public void writeCloseSegment_only_write_sensordata_if_present() {
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
"<gx:coord/>\n" +
"<ExtendedData>\n" +
"<SchemaData schemaUrl=\"#schema\">\n" +
"</SchemaData>\n" +
"</ExtendedData>\n" +
"</gx:Track>\n";
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
KMLTrackExporter kmlTrackWriter = (KMLTrackExporter) TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
kmlTrackWriter.prepare(outputStream);
kmlTrackWriter.writeTrackPoint(trackPoint);
// when
kmlTrackWriter.writeCloseSegment();
kmlTrackWriter.close();
// then
assertEquals(expected, outputStream.toString());
}
}
@@ -1,15 +1,21 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Looper;
import android.util.Log;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -19,32 +25,49 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p>
* TODO: test ignores {@link TrackStatistics} for now.
*/
@RunWith(JUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -53,37 +76,43 @@ public class ExportImportTest {
private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description";
private final List<Marker> markers = new ArrayList<>();
private final List<TrackPoint> trackPoints = new ArrayList<>();
private Track track;
private List<Marker> markers = new ArrayList<>();
private List<TrackPoint> trackPoints = new ArrayList<>();
private Track.Id trackId;
private Track.Id importTrackId;
private final Track.Id trackId = new Track.Id(System.currentTimeMillis());
@Before
public void setUp() {
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
track.first.setIcon(TRACK_ICON);
track.first.setCategory(TRACK_CATEGORY);
track.first.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.insertTrack(track.first);
contentProviderUtils.bulkInsertTrackPoint(track.second, track.first.getId());
public void setUp() throws TimeoutException {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)));
trackPoints.clear();
trackPoints.addAll(track.second);
trackId = service.startNewTrack();
for (int i = 0; i < 3; i++) {
Marker marker = new Marker(trackId, track.second.get(i).getLocation());
marker.setName("the marker " + i);
marker.setDescription("the marker description " + i);
marker.setCategory("the marker category" + i);
marker.setIcon("the marker icon" + i);
marker.setPhotoUrl("the photo url" + i);
contentProviderUtils.insertMarker(marker);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50), 0);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50), 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
service.pauseCurrentTrack();
markers.add(marker);
}
service.resumeCurrentTrack();
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50), 0);
service.endCurrentTrack();
assertEquals(markers.size(), contentProviderUtils.getMarkerCount(trackId));
track = contentProviderUtils.getTrack(trackId);
track.setIcon(TRACK_ICON);
track.setCategory(TRACK_CATEGORY);
track.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.updateTrack(track);
track = contentProviderUtils.getTrack(trackId);
trackPoints = contentProviderUtils.getTrackPoints(trackId);
markers = contentProviderUtils.getMarkers(trackId);
assertEquals(10, trackPoints.size());
assertEquals(2, markers.size());
}
@After
@@ -94,6 +123,7 @@ public class ExportImportTest {
}
}
@Ignore("Not implemented")
@LargeTest
@Test
public void kml_only_track() {
@@ -107,7 +137,7 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL.newTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL.createTrackExporter(context);
// when
// 1. export
@@ -130,11 +160,14 @@ public class ExportImportTest {
assertEquals(track.getIcon(), importedTrack.getIcon());
assertEquals(track.getUuid(), importedTrack.getUuid());
// 2. markers
assertMarkers();
// 2. trackpoints
assertTrackpoints(trackPoints, false, false, false, false, false);
// 3. trackpoints
assertTrackpoints(false, false, false, false, false);
// 3. trackstatistics
assertTrackStatistics(false, false);
// 4. markers
assertMarkers();
}
@LargeTest
@@ -143,7 +176,7 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.newTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
// when
// 1. export
@@ -165,11 +198,14 @@ public class ExportImportTest {
assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. markers
assertMarkers();
// 2. trackpoints
assertTrackpoints(trackPoints, true, true, true, true, true);
// 3. trackpoints
assertTrackpoints(true, true, true, true, true);
// 2. trackstatistics
assertTrackStatistics(false, true);
// 4. markers
assertMarkers();
}
@LargeTest
@@ -179,7 +215,7 @@ public class ExportImportTest {
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.newTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
// when
// 1. export
@@ -234,7 +270,7 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.GPX.newTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
// when
// 1. export
@@ -258,11 +294,20 @@ public class ExportImportTest {
//TODO exporting and importing a track icon is not yet supported by GpxTrackWriter.
//assertEquals(track.getIcon(), trackImported.getIcon());
// 2. markers
assertMarkers();
// 2. trackpoints
// The GPX exporter does not support exporting TrackPoints without lat/lng.
// Therefore, the track segmentation is changes.
List<TrackPoint> trackPointsWithCoordinates = trackPoints.stream().filter(it -> TrackPoint.Type.SEGMENT_START_AUTOMATIC.equals(it.getType()) || TrackPoint.Type.TRACKPOINT.equals(it.getType())).collect(Collectors.toList());
trackPointsWithCoordinates.get(0).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackPointsWithCoordinates.get(3).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
// 3. trackpoints
assertTrackpoints(false, true, true, true, true);
assertTrackpoints(trackPointsWithCoordinates, false, true, true, true, true);
// 3. trackstatistics
assertTrackStatistics(true, true);
// 4. markers
assertMarkers();
}
@LargeTest
@@ -272,7 +317,7 @@ public class ExportImportTest {
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.GPX.newTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
// when
// 1. export
@@ -309,23 +354,42 @@ public class ExportImportTest {
}
}
private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss) {
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss) {
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(trackPoints.size(), importedTrackPoints.size());
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i);
TrackPoint importedTrackPoint = importedTrackPoints.get(i);
assertEquals(trackPoint.getTime(), importedTrackPoint.getTime(), 0.01);
assertEquals(trackPoint.getTime(), importedTrackPoint.getTime());
TrackPoint.Type type = trackPoint.getType();
Log.e(TAG, "" + importedTrackPoint.getType().equals(type));
assertEquals("" + i, type, importedTrackPoint.getType());
// TODO Not exported for GPX/KML
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation());
if (trackPoint.hasLocation()) {
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
}
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
if (trackPoint.hasSpeed()) {
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
}
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
if (trackPoint.hasAltitude()) {
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
}
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
//TODO REMOVE
continue;
}
if (verifyHeartrate) {
assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
}
@@ -343,4 +407,59 @@ public class ExportImportTest {
}
}
}
private void assertTrackStatistics(boolean isGpx, boolean verifyElevationGainAndLoss) {
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack.getTrackStatistics());
TrackStatistics trackStatistics = track.getTrackStatistics();
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
// Time
assertTrue(trackStatistics.getStartTime().isBefore(trackStatistics.getStopTime())); //Just to be sure.
if (!isGpx) {
assertEquals(trackStatistics.getStartTime(), importedTrackStatistics.getStartTime());
assertEquals(trackStatistics.getStopTime(), importedTrackStatistics.getStopTime());
assertEquals(trackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(trackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01);
// Speed
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed(), 0.01);
assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed(), 0.01);
assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed(), 0.01);
}
// Elevation
assertEquals(trackStatistics.getMinElevation(), importedTrackStatistics.getMinElevation(), 0.01);
assertEquals(trackStatistics.getMaxElevation(), importedTrackStatistics.getMaxElevation(), 0.01);
if (verifyElevationGainAndLoss) {
assertEquals(trackStatistics.getTotalElevationGain(), importedTrackStatistics.getTotalElevationGain(), 0.01);
assertEquals(trackStatistics.getTotalElevationLoss(), importedTrackStatistics.getTotalElevationLoss(), 0.01);
} else {
assertFalse(importedTrackStatistics.hasTotalElevationGain());
assertFalse(importedTrackStatistics.hasTotalElevationLoss());
}
}
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, long speed, long altitude, float elevationGain, float heartRate, float cyclingCadence, float power) {
Location location = new Location("");
location.setTime(time);
location.setLongitude(longitude);
location.setLatitude(latitude);
location.setAccuracy(accuracy);
location.setAltitude(altitude);
location.setSpeed(speed);
TrackPoint tp = new TrackPoint(location);
tp.setHeartRate_bpm(heartRate);
tp.setCyclingCadence_rpm(cyclingCadence);
tp.setPower(power);
tp.setElevationGain(elevationGain);
return tp;
}
}
@@ -0,0 +1,157 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.StringUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
/**
* Test that legacy KML/GPX formats can still be imported.
*/
@RunWith(JUnit4.class)
public class LegacyImportTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private Track.Id importTrackId;
@After
public void tearDown() {
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
}
/**
* Check that data with statistics markers is imported and those ignored.
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
*/
@LargeTest
@Test
public void kml_with_statistics_marker() {
// given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_statistics_marker);
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("unknown", importedTrack.getCategory());
assertEquals("Test Track", importedTrack.getDescription());
assertEquals("2020-11-28 18:06", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(6, importedTrackPoints.size());
// first 3 trackpoints
assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
// created resume trackpoint with time of next valid trackpoint
assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
}
/**
* At least one valid location is required.
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
*/
@LargeTest
@Test(expected = ImportParserException.class)
public void kml_without_locations() {
// given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty);
// when
importTrackId = trackImporter.importFile(inputStream);
}
/**
* Check that data that contains pause (lat=100, lng=0) and resume (lat=200, lng=0) locations are restored to a segment break.
*/
@LargeTest
@Test
public void gpx_with_pause_resume() {
// given
GpxFileTrackImporter trackImporter = new GpxFileTrackImporter(context);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("the category", importedTrack.getCategory());
assertEquals("the description", importedTrack.getDescription());
assertEquals("2021-01-07 22:51", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(6, importedTrackPoints.size());
// first segment
assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2021-01-07T21:51:59.179Z", 14.0, 3.0, 10.0);
assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:00.653Z", 14.001, 3.0, 10.0);
assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:01.010Z", 14.002, 3.0, 10.0);
assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_END_MANUAL, "2021-01-07T21:52:02.658Z", null, null, null);
// created resume trackpoint with time of next valid trackpoint
assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-01-07T21:52:03.873Z", null, null, null);
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:04.103Z", 14.003, 3.0, 10.0);
}
private void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
assertEquals(type, trackPoint.getType());
if (longitude == null) {
assertFalse(trackPoint.hasLocation());
} else {
assertEquals(latitude, (Double) trackPoint.getLatitude());
assertEquals(longitude, (Double) trackPoint.getLongitude());
}
if (altitude == null) {
assertFalse(trackPoint.hasAltitude());
} else {
assertEquals(altitude, (Double) trackPoint.getAltitude());
}
}
}
@@ -1,93 +0,0 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.StringUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Imports a legacy kml file with a statistics marker at the beginning
*/
@RunWith(JUnit4.class)
public class LegacyKmlImportTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private Track.Id importTrackId;
@After
public void tearDown() {
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
}
@LargeTest
@Test
public void kml_legacy_track_import() {
// given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_track);
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("unknown", importedTrack.getCategory());
assertEquals("Test Track", importedTrack.getDescription());
assertEquals("2020-11-28 18:06", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(8, importedTrackPoints.size()); // 6 trackpoints + pause and resume
// first 3 trackpoints
assertTrackpoint(importedTrackPoints.get(0), "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
assertTrackpoint(importedTrackPoints.get(1), "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
assertTrackpoint(importedTrackPoints.get(2), "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
// created pause trackpoint with time of previous valid trackpoint
assertTrackpoint(importedTrackPoints.get(3), "2020-11-28T17:06:47.888Z", 0.0, 100, 0.0);
// created resume trackpoint with time of next valid trackpoint
assertTrackpoint(importedTrackPoints.get(4), "2020-11-28T17:06:55.861Z", 0.0, 200, 0.0);
// last 3 trackpoints
assertTrackpoint(importedTrackPoints.get(5), "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
assertTrackpoint(importedTrackPoints.get(6), "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
assertTrackpoint(importedTrackPoints.get(7), "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
}
private void assertTrackpoint(final TrackPoint trackPoint, final String when, final double longitude, final double latitude, final double altitude) {
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
assertEquals(latitude, trackPoint.getLatitude(), 0.001d);
assertEquals(longitude, trackPoint.getLongitude(), 0.001d);
assertEquals(altitude, trackPoint.getAltitude(), 0.001d);
}
}
@@ -20,6 +20,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.time.Duration;
import de.dennisguse.opentracks.services.handlers.AdaptiveLocationListenerPolicy;
import static org.junit.Assert.assertEquals;
@@ -32,13 +34,13 @@ import static org.junit.Assert.assertEquals;
@RunWith(JUnit4.class)
public class AdaptiveLocationListenerPolicyTest {
private static final long MIN = 1000;
private static final long MAX = 3000;
private static final Duration MIN = Duration.ofMillis(1000);
private static final Duration MAX = Duration.ofMillis(3000);
private static final int MIN_DISTANCE = 10;
private static final long NEW_IDLE_TIME_BIG = 10000;
private static final long NEW_IDLE_TIME_NORMAL = 5000;
private static final long NEW_IDLE_TIME_SMALL = 2000;
private static final long NEW_IDLE_TIME_LESS_THAN_MIN = 500;
private static final Duration NEW_IDLE_TIME_BIG = Duration.ofMillis(10000);
private static final Duration NEW_IDLE_TIME_NORMAL = Duration.ofMillis(5000);
private static final Duration NEW_IDLE_TIME_SMALL = Duration.ofMillis(2000);
private static final Duration NEW_IDLE_TIME_LESS_THAN_MIN = Duration.ofMillis(500);
private AdaptiveLocationListenerPolicy adaptiveLocationListenerPolicy;
@Before
@@ -62,7 +64,7 @@ public class AdaptiveLocationListenerPolicyTest {
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
// First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the nearest second.
assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
assertEquals(NEW_IDLE_TIME_NORMAL.dividedBy(2).getSeconds(), adaptiveLocationListenerPolicy.getDesiredPollingInterval().getSeconds());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
@@ -47,7 +47,6 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -158,7 +157,7 @@ public class TrackRecordingServiceTest {
@Test
public void testRecording_oldTracks() throws TimeoutException {
// given
createDummyTrack(trackId, -1L, false);
createDummyTrack(trackId, false);
// when
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
@@ -172,7 +171,7 @@ public class TrackRecordingServiceTest {
@Test
public void testRecording_serviceRestart_whileRecording() throws TimeoutException {
// given
createDummyTrack(trackId, -1L, true);
createDummyTrack(trackId, true);
//when
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
@@ -183,11 +182,44 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testRecording_pauseAndResume() throws TimeoutException, InterruptedException {
public void testRecording_start() throws TimeoutException {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
newTrackPoint(service);
// when
Track.Id trackId = service.startNewTrack();
// then
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(1, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
}
@MediumTest
@Test
public void testRecording_stop() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Track.Id trackId = service.startNewTrack();
// when
service.endCurrentTrack();
// then
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(2, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
}
@MediumTest
@Test
public void testRecording_pauseAndResume() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Track.Id trackId = service.startNewTrack();
// when
service.pauseCurrentTrack();
@@ -197,29 +229,28 @@ public class TrackRecordingServiceTest {
//when
service.resumeTrack(trackId);
newTrackPoint(service);
// then
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(5, trackPoints.size());
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(3).getLatitude(), 0.01);
assertEquals(3, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(2).getType());
}
@MediumTest
@Test
public void testRecording_resumeStoppedTrack() throws TimeoutException, InterruptedException {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
newTrackPoint(service);
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
service.endCurrentTrack();
assertEquals(1, contentProviderUtils.getTrackPoints(trackId).size());
assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
// when
service.resumeTrack(trackId);
@@ -231,8 +262,34 @@ public class TrackRecordingServiceTest {
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(4, trackPoints.size());
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(2).getType());
assertEquals(TrackPoint.Type.TRACKPOINT, trackPoints.get(3).getType());
}
@MediumTest
@Test
public void testRecording_stopPausedTrack() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
service.pauseCurrentTrack();
assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
// when
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(2, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
}
@Ignore("Sometimes fails on CI.")
@@ -336,12 +393,11 @@ public class TrackRecordingServiceTest {
}
// NOTE: Do not use to create a track that is currently recording.
private void createDummyTrack(Track.Id id, long stopTime, boolean isRecording) {
private void createDummyTrack(Track.Id id, boolean isRecording) {
Track dummyTrack = new Track();
dummyTrack.setId(id);
dummyTrack.setName("Dummy Track");
TrackStatistics trackStatistics = new TrackStatistics();
trackStatistics.setStopTime_ms(stopTime);
dummyTrack.setTrackStatistics(trackStatistics);
addTrack(dummyTrack, isRecording);
}
@@ -358,7 +414,7 @@ public class TrackRecordingServiceTest {
* Inserts a location and waits for 200ms.
*/
private static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException {
Location location = new Location("gps");
Location location = new Location("");
location.setLongitude(longitude);
location.setLatitude(latitude);
location.setAccuracy(accuracy);
@@ -368,8 +424,5 @@ public class TrackRecordingServiceTest {
TrackPoint trackPoint = new TrackPoint(location);
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(ApplicationProvider.getApplicationContext());
trackRecordingService.newTrackPoint(trackPoint, prefAccuracy);
//TODO Needed?
Thread.sleep(200);
}
}
@@ -4,6 +4,7 @@ import android.content.ContentProvider;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Looper;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -24,7 +25,6 @@ import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
@@ -34,6 +34,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
/**
* Tests insert location.
@@ -69,6 +70,7 @@ public class TrackRecordingServiceTestLocation {
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
tearDown();
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@@ -79,11 +81,6 @@ public class TrackRecordingServiceTestLocation {
@After
public void tearDown() {
// Reset service (if some previous test failed)
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@@ -95,12 +92,12 @@ public class TrackRecordingServiceTestLocation {
Track.Id trackId = service.startNewTrack();
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0001, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0003, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0001, 35.0, 2, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 3, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0003, 35.0, 4, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0005, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0005, 35.0, 6, 15);
service.endCurrentTrack();
@@ -108,8 +105,17 @@ public class TrackRecordingServiceTestLocation {
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(6, trackPoints.size());
assertEquals(45.0005, trackPoints.get(5).getLatitude(), 0.01);
assertEquals(8, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
new Pair<>(TrackPoint.Type.TRACKPOINT, 2),
new Pair<>(TrackPoint.Type.TRACKPOINT, 3),
new Pair<>(TrackPoint.Type.TRACKPOINT, 4),
new Pair<>(TrackPoint.Type.TRACKPOINT, 5),
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
), trackPoints);
}
@MediumTest
@@ -117,14 +123,15 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_slowMovingAccurate() throws Exception {
// given
Track.Id trackId = service.startNewTrack();
assertNotNull(trackId);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000001, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000001, 35.0, 2, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 3, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 4, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000004, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000005, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000005, 35.0, 6, 15);
service.endCurrentTrack();
@@ -132,8 +139,13 @@ public class TrackRecordingServiceTestLocation {
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(2, trackPoints.size());
assertEquals(45.000005, trackPoints.get(1).getLatitude(), 0.01);
assertEquals(4, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
), trackPoints);
}
// @MediumTest
@@ -174,10 +186,13 @@ public class TrackRecordingServiceTestLocation {
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(3, trackPoints.size());
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
assertEquals(6, trackPoints.get(2).getAccuracy(), 0.01);
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
new Pair<>(TrackPoint.Type.TRACKPOINT, 2),
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
), trackPoints);
}
@MediumTest
@@ -200,14 +215,17 @@ public class TrackRecordingServiceTestLocation {
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(4, trackPoints.size());
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
assertEquals(5, trackPoints.get(2).getAccuracy(), 0.01); //TODO Check why this trackPoint is inserted.
assertEquals(6, trackPoints.get(3).getAccuracy(), 0.01);
assertEquals(6, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
new Pair<>(TrackPoint.Type.TRACKPOINT, 2),
new Pair<>(TrackPoint.Type.TRACKPOINT, 5), //TODO Check why this trackPoint is inserted.
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
), trackPoints);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_idle_withSensorData() throws Exception {
@@ -230,12 +248,12 @@ public class TrackRecordingServiceTestLocation {
});
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 0, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 6, 0);
service.endCurrentTrack();
@@ -243,13 +261,17 @@ public class TrackRecordingServiceTestLocation {
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(6, trackPoints.size());
assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01);
assertEquals(1, trackPoints.get(1).getAccuracy(), 0.01);
assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
assertEquals(4, trackPoints.get(4).getAccuracy(), 0.01);
assertEquals(5, trackPoints.get(5).getAccuracy(), 0.01);
assertEquals(8, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
new Pair<>(TrackPoint.Type.TRACKPOINT, 2),
new Pair<>(TrackPoint.Type.TRACKPOINT, 3),
new Pair<>(TrackPoint.Type.TRACKPOINT, 4),
new Pair<>(TrackPoint.Type.TRACKPOINT, 5),
new Pair<>(TrackPoint.Type.TRACKPOINT, 6),
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
), trackPoints);
}
@MediumTest
@@ -272,12 +294,26 @@ public class TrackRecordingServiceTestLocation {
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(7, trackPoints.size());
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(4).getLatitude(), 0.01);
assertEquals(4, trackPoints.get(5).getAccuracy(), 0.01);
assertEquals(5, trackPoints.get(6).getAccuracy(), 0.01);
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
new Pair<>(TrackPoint.Type.SEGMENT_START_AUTOMATIC, 2),
new Pair<>(TrackPoint.Type.TRACKPOINT, 3),
new Pair<>(TrackPoint.Type.SEGMENT_START_AUTOMATIC, 4),
new Pair<>(TrackPoint.Type.TRACKPOINT, 5),
new Pair<>(TrackPoint.Type.SEGMENT_END_MANUAL, null)
), trackPoints);
}
private void assertTrackPoints(List<Pair<TrackPoint.Type, Object>> typeAndAccuracy, List<TrackPoint> actual) {
assertEquals(typeAndAccuracy.size(), actual.size());
for (int i = 0; i < typeAndAccuracy.size(); i++) {
assertEquals(typeAndAccuracy.get(i).first, actual.get(i).getType());
if (typeAndAccuracy.get(i).second != null) {
assertEquals((Integer) typeAndAccuracy.get(i).second, actual.get(i).getAccuracy(), 0.01);
}
}
}
}
@@ -19,6 +19,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -239,7 +240,7 @@ public class TrackRecordingServiceTestLooper {
assertEquals(trackId, track.getId());
TrackStatistics trackStatistics = track.getTrackStatistics();
assertNotNull(trackStatistics);
assertTrue(trackStatistics.getStartTime_ms() > 0);
assertTrue(trackStatistics.getStopTime_ms() >= trackStatistics.getStartTime_ms());
assertTrue(trackStatistics.getStartTime().isAfter(Instant.ofEpochMilli(0)));
assertTrue(trackStatistics.getStopTime().isAfter(trackStatistics.getStartTime()));
}
}
@@ -50,7 +50,7 @@ public class HandlerServerTest {
@Test
public void sendTrackPoint() throws InterruptedException {
// given
TrackPoint trackPoint = new TrackPoint();
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
int accuracy = 50;
// when
@@ -3,10 +3,12 @@ package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -32,6 +34,12 @@ public class LocationHandlerTest {
@InjectMocks
private LocationHandler locationHandler;
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@Before
public void setUp() {
// Let's use default values.
@@ -80,7 +88,6 @@ public class LocationHandlerTest {
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
// when
// poor latitude
locationHandler.onLocationChanged(createLocation(45f, 35f, prefAccuracy + 1, 5, System.currentTimeMillis()));
// then
@@ -20,7 +20,11 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.time.Duration;
import java.time.Instant;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/**
* Tests for {@link TrackStatistics}.
@@ -39,16 +43,40 @@ public class TrackStatisticsTest {
}
@Test
public void testMerge() {
public void testMerge_no_data() {
// given
TrackStatistics statistics2 = new TrackStatistics();
statistics.setStartTime_ms(1000L); // Resulting start time
statistics.setStopTime_ms(2500L);
statistics2.setStartTime_ms(3000L);
statistics2.setStopTime_ms(4000L); // Resulting stop time
statistics.setTotalTime(1500L);
statistics2.setTotalTime(1000L); // Result: 1500+1000
statistics.setMovingTime(700L);
statistics2.setMovingTime(600L); // Result: 700+600
// when
statistics.merge(statistics2);
// then
assertNull(statistics.getStartTime());
assertNull(statistics.getStopTime());
assertEquals(Duration.ofSeconds(0), statistics.getMovingTime());
assertEquals(Duration.ofSeconds(0), statistics.getTotalTime());
assertNull(statistics.getTotalElevationGain());
assertNull(statistics.getTotalElevationLoss());
assertEquals(Double.NEGATIVE_INFINITY, statistics.getMaxElevation(), 0.0);
assertEquals(Double.POSITIVE_INFINITY, statistics.getMinElevation(), 0.0);
assertEquals(0.0, statistics.getMaxSpeed(), 0.0);
assertEquals(0.0, statistics.getAverageSpeed(), 0.0);
assertEquals(0.0, statistics.getAverageMovingSpeed(), 0.0);
}
@Test
public void testMerge() {
// given
TrackStatistics statistics2 = new TrackStatistics();
statistics.setStartTime(Instant.ofEpochMilli(1000)); // Resulting start time
statistics.setStopTime(Instant.ofEpochMilli(2500));
statistics2.setStartTime(Instant.ofEpochMilli(3000));
statistics2.setStopTime(Instant.ofEpochMilli(4000)); // Resulting stop time
statistics.setTotalTime(Duration.ofMillis(1500));
statistics2.setTotalTime(Duration.ofMillis(1000)); // Result: 1500+1000
statistics.setMovingTime(Duration.ofMillis(700));
statistics2.setMovingTime(Duration.ofMillis(600)); // Result: 700+600
statistics.setTotalDistance(750.0);
statistics2.setTotalDistance(350.0); // Result: 750+350
statistics.setTotalElevationGain(50.0f);
@@ -60,15 +88,17 @@ public class TrackStatisticsTest {
statistics2.setMaxElevation(3575.0); // Resulting max elevation
statistics2.setMinElevation(2800.0);
// when
statistics.merge(statistics2);
assertEquals(1000L, statistics.getStartTime_ms());
assertEquals(4000L, statistics.getStopTime_ms());
assertEquals(2500L, statistics.getTotalTime());
assertEquals(1300L, statistics.getMovingTime());
// then
assertEquals(Instant.ofEpochMilli(1000), statistics.getStartTime());
assertEquals(Instant.ofEpochMilli(4000), statistics.getStopTime());
assertEquals(Duration.ofMillis(2500), statistics.getTotalTime());
assertEquals(Duration.ofMillis(1300), statistics.getMovingTime());
assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
assertEquals(900.0, statistics.getTotalElevationGain(), 0.001);
assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed(), 0.001);
assertEquals(statistics.getTotalDistance() / statistics.getMovingTime().getSeconds(), statistics.getMaxSpeed(), 0.001);
assertEquals(1200.0, statistics.getMinElevation(), 0.001);
assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
}
@@ -76,14 +106,14 @@ public class TrackStatisticsTest {
@Test
public void testGetAverageSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setTotalTime(50000); // in milliseconds
statistics.setTotalTime(Duration.ofMillis(50000));
assertEquals(20.0, statistics.getAverageSpeed(), 0.001);
}
@Test
public void testGetAverageMovingSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setMovingTime(20000); // in milliseconds
statistics.setMovingTime(Duration.ofMillis(20000));
assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001);
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.stats;
import org.junit.Test;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import static org.junit.Assert.assertEquals;
public class TrackStatisticsUpdaterTest {
@Test
public void addTrackPoint() {
// given
TestDataUtil.TrackData data = TestDataUtil.createTestingTrack(new Track.Id(1));
// when
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
data.trackPoints.forEach(it -> updater.addTrackPoint(it, 50));
// then
TrackStatistics statistics = updater.getTrackStatistics();
assertEquals(85.35, statistics.getTotalDistance(), 0.01);
assertEquals(Duration.ofMillis(13999), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(6), statistics.getMovingTime());
assertEquals(2.5, statistics.getMinElevation(), 0.01);
assertEquals(27.5, statistics.getMaxElevation(), 0.01);
assertEquals(27, statistics.getTotalElevationGain(), 0.01);
assertEquals(27.0, statistics.getTotalElevationLoss(), 0.01);
assertEquals(14.226, statistics.getMaxSpeed(), 0.01);
assertEquals(14.226, statistics.getAverageMovingSpeed(), 0.01);
assertEquals(6.566, statistics.getAverageSpeed(), 0.01);
}
}
@@ -8,6 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.R;
@@ -25,13 +26,15 @@ public class AnnouncementUtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void getAnnouncement() {
public void getAnnouncement_metric() {
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_metric));
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000);
stats.setTotalElevationGain(6000f);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", null);
@@ -41,42 +44,26 @@ public class AnnouncementUtilsTest {
}
@Test
public void getAnnouncement_withInterval() {
public void getAnnouncement_withInterval_metric() {
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_metric));
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000);
stats.setTotalElevationGain(6000f);
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000);
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
int speedId = R.plurals.voiceSpeedKilometersPerHour;
double kmPerHour = lastInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH;
String firstPartMsg = "OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour";
String rateMsg = " Lap speed of " + context.getResources().getQuantityString(speedId, getQuantityCount(kmPerHour), kmPerHour);
String msg = firstPartMsg + rateMsg;
String expected = "OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour Lap speed of 51.2 kilometers per hour";
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", lastInterval);
// then
assertEquals(msg, announcement);
}
private int getQuantityCount(double d) {
if (d == 0) {
return 0;
} else if (d == 1) {
return 1;
} else if (d == 2) {
return 2;
} else {
int count = (int) d;
return Math.max(count, 3);
}
assertEquals(expected, announcement);
}
}
@@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.GregorianCalendar;
import java.util.TimeZone;
@@ -40,55 +41,32 @@ public class StringUtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Tests {@link StringUtils#formatDateTimeIso8601(long)}.
*/
@Test
public void testFormatDateTimeIso8601() {
assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345));
}
/**
* Tests {@link StringUtils#formatElapsedTime(long)}.
* Tests {@link StringUtils#formatElapsedTime(Duration)}.
*/
@Test
public void testformatElapsedTime() {
// 1 second
assertEquals("00:01", StringUtils.formatElapsedTime(1000));
// 10 seconds
assertEquals("00:10", StringUtils.formatElapsedTime(10000));
// 1 minute
assertEquals("01:00", StringUtils.formatElapsedTime(60000));
// 10 minutes
assertEquals("10:00", StringUtils.formatElapsedTime(600000));
// 1 hour
assertEquals("1:00:00", StringUtils.formatElapsedTime(3600000));
// 10 hours
assertEquals("10:00:00", StringUtils.formatElapsedTime(36000000));
// 100 hours
assertEquals("100:00:00", StringUtils.formatElapsedTime(360000000));
assertEquals("00:01", StringUtils.formatElapsedTime(Duration.ofMillis(1000)));
assertEquals("00:10", StringUtils.formatElapsedTime(Duration.ofMillis(10000)));
assertEquals("01:00", StringUtils.formatElapsedTime(Duration.ofMillis(60000)));
assertEquals("10:00", StringUtils.formatElapsedTime(Duration.ofMillis(600000)));
assertEquals("1:00:00", StringUtils.formatElapsedTime(Duration.ofMillis(3600000)));
assertEquals("10:00:00", StringUtils.formatElapsedTime(Duration.ofMillis(36000000)));
assertEquals("100:00:00", StringUtils.formatElapsedTime(Duration.ofMillis(360000000)));
}
/**
* Tests {@link StringUtils#formatElapsedTimeWithHour(long)}.
* Tests {@link StringUtils#formatElapsedTimeWithHour(Duration)}.
*/
@Test
public void testformatElapsedTimeWithHour() {
// 1 second
assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(1000));
// 10 seconds
assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(10000));
// 1 minute
assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(60000));
// 10 minutes
assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(600000));
// 1 hour
assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(3600000));
// 10 hours
assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(36000000));
// 100 hours
assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(360000000));
assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(1000)));
assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(10000)));
assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(60000)));
assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(600000)));
assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(3600000)));
assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(36000000)));
assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(360000000)));
}
/**
@@ -116,18 +94,12 @@ public class StringUtilsTest {
assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there"));
}
/**
* Tests {@link StringUtils#parseTime(String)} with fractional seconds.
*/
@Test
public void testGetTime_fractional() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 352);
}
/**
* Tests {@link StringUtils#parseTime(String)} with time zone.
*/
@Test
public void testGetTime_timezone() {
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
@@ -140,9 +112,6 @@ public class StringUtilsTest {
assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0);
}
/**
* Tests {@link StringUtils#parseTime(String)} with fractional seconds and time zone.
*/
@Test
public void testGetTime_fractionalAndTimezone() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
@@ -170,29 +139,7 @@ public class StringUtilsTest {
// This comparision tends to be flaky (difference of 1ms)
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime)) <= 1);
}
/**
* Tests {@link StringUtils#getTimeParts(long)} with a positive number.
*/
@Test
public void testGetTimeParts_positive() {
int[] parts = StringUtils.getTimeParts(61000);
assertEquals(1, parts[0]);
assertEquals(1, parts[1]);
assertEquals(0, parts[2]);
}
/**
* Tests {@link StringUtils#getTimeParts(long)} with a negative number.
*/
@Test
public void testGetTimeParts_negative() {
int[] parts = StringUtils.getTimeParts(-61000);
assertEquals(-1, parts[0]);
assertEquals(-1, parts[1]);
assertEquals(0, parts[2]);
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toEpochMilli()) <= 1);
}
@Test
@@ -25,7 +25,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.time.Instant;
import java.util.Locale;
import de.dennisguse.opentracks.R;
@@ -42,7 +42,7 @@ import static org.junit.Assert.assertEquals;
public class TrackNameUtilsTest {
private static final Track.Id TRACK_ID = new Track.Id(1L);
private static final long START_TIME = 1288213406000L;
private static final Instant START_TIME = Instant.ofEpochMilli(1288213406000L);
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
@@ -62,7 +62,7 @@ public class TrackNameUtilsTest {
public void testTrackName_date_iso_8601() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US);
assertEquals(simpleDateFormat.format(new Date(START_TIME)), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
assertEquals(simpleDateFormat.format(START_TIME.toEpochMilli()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**
@@ -1,53 +0,0 @@
package de.dennisguse.opentracks.util;
import org.junit.Test;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.junit.Assert.assertEquals;
public class TrackPointUtilsTest {
@Test
public void fixTime_none() {
// given
long time = System.currentTimeMillis();
TrackPoint trackPoint = new TrackPoint();
trackPoint.setTime(time);
// when
TrackPointUtils.fixTime(trackPoint);
// then
assertEquals(time, trackPoint.getTime());
}
@Test
public void fixTime_0() {
// given
long time = System.currentTimeMillis();
TrackPoint trackPoint = new TrackPoint();
trackPoint.setTime(0);
// when
TrackPointUtils.fixTime(trackPoint);
// then
assertEquals(time, trackPoint.getTime(), 1000);
}
@Test
public void fixTime_gpsWeekRollover() {
// given
long time = System.currentTimeMillis();
TrackPoint trackPoint = new TrackPoint();
trackPoint.setTime(time - 1024 * UnitConversions.ONE_WEEK_MS);
// when
TrackPointUtils.fixTime(trackPoint);
// then
assertEquals(time, trackPoint.getTime());
}
}
@@ -8,6 +8,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import de.dennisguse.opentracks.R;
@@ -23,19 +25,12 @@ public class AggregatedStatisticsTest {
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Create a TrackStatistics object.
*
* @param totalDistance distance in meters.
* @param totalTime total time in milliseconds.
* @return TrackStatistics object.
*/
private static Track createTrack(Context context, long totalDistance, long totalTime, String category) {
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime_ms(1000L); // Resulting start time
statistics.setStopTime_ms(1000L + totalTime);
statistics.setTotalTime(totalTime);
statistics.setMovingTime(totalTime);
statistics.setStartTime(Instant.ofEpochMilli(1000L)); // Resulting start time
statistics.setStopTime(Instant.ofEpochMilli(1000L + totalTime));
statistics.setTotalTime(Duration.ofMillis(totalTime));
statistics.setMovingTime(Duration.ofMillis(totalTime));
statistics.setTotalDistance(totalDistance);
statistics.setTotalElevationGain(50.0f);
statistics.setMaxSpeed(50.0); // Resulting max speed
@@ -68,7 +63,7 @@ public class AggregatedStatisticsTest {
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
}
@Test
@@ -122,7 +117,7 @@ public class AggregatedStatisticsTest {
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(totalTime * 2L, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
}
@Test
@@ -156,19 +151,19 @@ public class AggregatedStatisticsTest {
{
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
}
{
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
}
{
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
}
}
@@ -219,28 +214,28 @@ public class AggregatedStatisticsTest {
{
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0);
assertEquals(totalTime * 5, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime * 5), statistics2.getMovingTime());
}
// Running.
{
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(totalTime * 2, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
}
// Walking.
{
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(totalTime * 2, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
}
// Driving.
{
TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
}
// Check order
@@ -25,12 +25,12 @@ public class IntervalStatisticsTest {
}
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoints.get(0).getTime());
for (TrackPoint tp : trackPoints) {
trackStatisticsUpdater.addTrackPoint(tp, 0);
}
return trackStatisticsUpdater.getTrackStatistics();
}
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
for (TrackPoint tp : trackPoints) {
trackStatisticsUpdater.addTrackPoint(tp, 0);
}
return trackStatisticsUpdater.getTrackStatistics();
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
@@ -116,17 +116,17 @@ public class IntervalStatisticsTest {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
double totalDistance = 0d;
long totalTime = 0L;
float totalTime = 0L;
float totalGain = 0f;
for (IntervalStatistics.Interval i : intervalList) {
totalDistance += i.getDistance_m();
totalTime += ((i.getDistance_m() / i.getSpeed_ms()) * UnitConversions.S_TO_MS);
totalTime += i.getDistance_m() / i.getSpeed_ms();
totalGain += i.getGain_m();
}
// then
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01);
assertEquals(trackStatistics.getTotalTime() * UnitConversions.MS_TO_S, totalTime * UnitConversions.MS_TO_S, 0.1);
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ELEVATION_GAIN, 0.1);
for (int i = 0; i < intervalList.size() - 1; i++) {
-1
View File
@@ -1 +0,0 @@
This hidden file is here to ensure there is a res folder.
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:gpxtpx="http://www.garmin.com/xmlschemes/TrackPointExtension/v2" xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1" xmlns:topografix="http://www.topografix.com/GPX/Private/TopoGrafix/0/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.1" creator="OpenTracks (Debug)" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
<metadata>
<name><![CDATA[2021-01-07 22:51]]></name>
<desc><![CDATA[the description]]></desc>
</metadata>
<wpt lat="3" lon="14">
<ele>10</ele>
<time>2021-01-07T21:51:59.179Z</time>
<name><![CDATA[Marker 1]]></name>
<desc><![CDATA[Marker 1 desc]]></desc>
<type><![CDATA[Marker 1 category]]></type>
</wpt>
<wpt lat="3" lon="14.002">
<ele>10</ele>
<time>2021-01-07T21:52:01.010Z</time>
<name><![CDATA[Marker 2]]></name>
<desc><![CDATA[Marker 2 desc]]></desc>
<type><![CDATA[Marker 2 category]]></type>
</wpt>
<trk>
<name><![CDATA[2021-01-07 22:51]]></name>
<desc><![CDATA[the description]]></desc>
<type><![CDATA[the category]]></type>
<extensions>
<topografix:color>c0c0c0</topografix:color>
<opentracks:trackid>7002101e-4198-4613-8c24-544e01ca3981</opentracks:trackid>
</extensions>
<trkseg>
<trkpt lat="3" lon="14">
<ele>10</ele>
<time>2021-01-07T21:51:59.179Z</time>
</trkpt>
<trkpt lat="3" lon="14.001">
<ele>10</ele>
<time>2021-01-07T21:52:00.653Z</time>
</trkpt>
<trkpt lat="3" lon="14.002">
<ele>10</ele>
<time>2021-01-07T21:52:01.010Z</time>
</trkpt>
<trkpt lat="100" lon="0">
<time>2021-01-07T21:52:02.658Z</time>
</trkpt>
<trkpt lat="200" lon="0">
<time>2021-01-07T21:52:03.873Z</time>
</trkpt>
<trkpt lat="3" lon="14.003">
<ele>10</ele>
<time>2021-01-07T21:52:04.103Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
<Document>
<open>1</open>
<visibility>1</visibility>
<name><![CDATA[2020-08-24 20:57]]></name>
<atom:generator><![CDATA[OpenTracks]]></atom:generator>
<Style id="track">
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
<IconStyle>
<scale>1.3</scale>
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
</IconStyle>
</Style>
<Style id="start"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="end"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="waypoint"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Schema id="schema">
<gx:SimpleArrayField name="speed" type="float">
<displayName><![CDATA[Speed (m/s)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="power" type="float">
<displayName><![CDATA[Power (W)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="cadence" type="float">
<displayName><![CDATA[Cadence (rpm)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="heart_rate" type="float">
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
</gx:SimpleArrayField>
</Schema>
<Placemark>
<name><![CDATA[2020-08-24 20:57]]></name>
<description><![CDATA[]]></description>
<icon><![CDATA[UNKNOWN]]></icon>
<opentracks:trackid>9316e527-a5b8-47c5-bdbd-954b4be7d80e</opentracks:trackid>
<styleUrl>#track</styleUrl>
<ExtendedData>
<Data name="type"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
</gx:MultiTrack>
</Placemark>
</Document>
</kml>
@@ -0,0 +1,193 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
<Document>
<open>1</open>
<visibility>1</visibility>
<name><![CDATA[2020-11-28 18:06]]></name>
<atom:generator><![CDATA[OpenTracks (Debug)]]></atom:generator>
<Style id="track">
<LineStyle>
<color>7f0000ff</color>
<width>4</width>
</LineStyle>
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href>
</Icon>
</IconStyle>
</Style>
<Style id="start">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href>
</Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels" />
</IconStyle>
</Style>
<Style id="end">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href>
</Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels" />
</IconStyle>
</Style>
<Style id="waypoint">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels" />
</IconStyle>
</Style>
<Schema id="schema">
<gx:SimpleArrayField name="speed" type="float">
<displayName><![CDATA[Speed (m/s)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="power" type="float">
<displayName><![CDATA[Power (W)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="cadence" type="float">
<displayName><![CDATA[Cadence (rpm)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="heart_rate" type="float">
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
</gx:SimpleArrayField>
</Schema>
<Folder>
<name><![CDATA[Sun, 28 Nov 2020 18:06 Markers]]></name>
<open>1</open>
<Placemark>
<name><![CDATA[Split 0]]></name>
<description><![CDATA[Total distance: 0.00 km (0.0 mi)
Total time: 00:00
Moving time: 00:00
Average speed: 0.00 km/h (0.0 mi/h)
Average moving speed: 0.00 km/h (0.0 mi/h)
Max speed: 0.00 km/h (0.0 mi/h)
Average pace: 0:00 min/km (0:00 min/mi)
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>
<TimeStamp>
<when>1970-01-01T00:00:00Z</when>
</TimeStamp>
<styleUrl>#waypoint</styleUrl>
<Point>
<coordinates>180.0,100.0</coordinates>
</Point>
</Placemark>
</Folder>
<Placemark>
<name><![CDATA[2020-11-28 18:06 (Start)]]></name>
<description />
<TimeStamp>
<when>2020-11-28T17:06:22.401Z</when>
</TimeStamp>
<styleUrl>#start</styleUrl>
<Point>
<coordinates>1.234156,12.340097,469.286376953125</coordinates>
</Point>
</Placemark>
<Placemark>
<name><![CDATA[2020-11-28 18:06]]></name>
<description><![CDATA[Test Track]]></description>
<icon><![CDATA[UNKNOWN]]></icon>
<opentracks:trackid>2f170875-55d7-445d-b2b8-22a573c81dbc</opentracks:trackid>
<styleUrl>#track</styleUrl>
<ExtendedData>
<Data name="type">
<value><![CDATA[unknown]]></value>
</Data>
</ExtendedData>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
<gx:Track>
<when>2020-11-28T17:06:22.401Z</when>
<gx:coord>1.234156 12.340097 469.286376953125</gx:coord>
<when>2020-11-28T17:06:25.448Z</when>
<gx:coord>1.23415 12.340036 439.1626281738281</gx:coord>
<when>2020-11-28T17:06:47.888Z</when>
<gx:coord>1.23405 12.340057 421.8070983886719</gx:coord>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<gx:SimpleArrayData name="speed">
<gx:value>0.5391469</gx:value>
<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>
<gx:Track>
<when>2020-11-28T17:06:55.861Z</when>
<gx:coord>1.23405 12.340057 419.93902587890625</gx:coord>
<when>2020-11-28T17:06:56.905Z</when>
<gx:coord>1.23405 12.340057 419.9036560058594</gx:coord>
<when>2020-11-28T17:07:20.870Z</when>
<gx:coord>1.234046 12.340082 417.99432373046875</gx:coord>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<gx:SimpleArrayData name="speed">
<gx:value>0.0</gx:value>
<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>
</gx:MultiTrack>
</Placemark>
<Placemark>
<name><![CDATA[2020-11-28 18:06 (End)]]></name>
<description><![CDATA[OpenTracks (Debug)
Name: 2020-11-28 18:06
Activity type: unknown
Description: -
Total distance: 0.01 km (0.0 mi)
Total time: 01:04
Moving time: 00:00
Average speed: 0.49 km/h (0.3 mi/h)
Average moving speed: 0.00 km/h (0.0 mi/h)
Max speed: 0.00 km/h (0.0 mi/h)
Average pace: 122:30 min/km (197:08 min/mi)
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>
</TimeStamp>
<styleUrl>#end</styleUrl>
<Point>
<coordinates>1.234046,12.340082,417.99432373046875</coordinates>
</Point>
</Placemark>
</Document>
</kml>
-173
View File
@@ -1,173 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
<Document>
<open>1</open>
<visibility>1</visibility>
<name><![CDATA[2020-11-28 18:06]]></name>
<atom:generator><![CDATA[OpenTracks (Debug)]]></atom:generator>
<Style id="track">
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
<IconStyle>
<scale>1.3</scale>
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
</IconStyle>
</Style>
<Style id="start"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="end"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="waypoint"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Schema id="schema">
<gx:SimpleArrayField name="speed" type="float">
<displayName><![CDATA[Speed (m/s)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="power" type="float">
<displayName><![CDATA[Power (W)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="cadence" type="float">
<displayName><![CDATA[Cadence (rpm)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="heart_rate" type="float">
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
</gx:SimpleArrayField>
</Schema>
<Folder>
<name><![CDATA[Sun, 28 Nov 2020 18:06 Markers]]></name>
<open>1</open>
<Placemark>
<name><![CDATA[Split 0]]></name>
<description><![CDATA[Total distance: 0.00 km (0.0 mi)
Total time: 00:00
Moving time: 00:00
Average speed: 0.00 km/h (0.0 mi/h)
Average moving speed: 0.00 km/h (0.0 mi/h)
Max speed: 0.00 km/h (0.0 mi/h)
Average pace: 0:00 min/km (0:00 min/mi)
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>
<TimeStamp><when>1970-01-01T00:00:00Z</when></TimeStamp>
<styleUrl>#waypoint</styleUrl>
<Point>
<coordinates>180.0,100.0</coordinates>
</Point>
</Placemark>
</Folder>
<Placemark>
<name><![CDATA[2020-11-28 18:06 (Start)]]></name>
<description><![CDATA[]]></description>
<TimeStamp><when>2020-11-28T17:06:22.401Z</when></TimeStamp>
<styleUrl>#start</styleUrl>
<Point>
<coordinates>1.234156,12.340097,469.286376953125</coordinates>
</Point>
</Placemark>
<Placemark>
<name><![CDATA[2020-11-28 18:06]]></name>
<description><![CDATA[Test Track]]></description>
<icon><![CDATA[UNKNOWN]]></icon>
<opentracks:trackid>2f170875-55d7-445d-b2b8-22a573c81dbc</opentracks:trackid>
<styleUrl>#track</styleUrl>
<ExtendedData>
<Data name="type"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
<gx:Track>
<when>2020-11-28T17:06:22.401Z</when>
<gx:coord>1.234156 12.340097 469.286376953125</gx:coord>
<when>2020-11-28T17:06:25.448Z</when>
<gx:coord>1.23415 12.340036 439.1626281738281</gx:coord>
<when>2020-11-28T17:06:47.888Z</when>
<gx:coord>1.23405 12.340057 421.8070983886719</gx:coord>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<gx:SimpleArrayData name="speed">
<gx:value>0.5391469</gx:value>
<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>
<gx:Track>
<when>2020-11-28T17:06:55.861Z</when>
<gx:coord>1.23405 12.340057 419.93902587890625</gx:coord>
<when>2020-11-28T17:06:56.905Z</when>
<gx:coord>1.23405 12.340057 419.9036560058594</gx:coord>
<when>2020-11-28T17:07:20.870Z</when>
<gx:coord>1.234046 12.340082 417.99432373046875</gx:coord>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<gx:SimpleArrayData name="speed">
<gx:value>0.0</gx:value>
<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>
</gx:MultiTrack>
</Placemark>
<Placemark>
<name><![CDATA[2020-11-28 18:06 (End)]]></name>
<description><![CDATA[OpenTracks (Debug)
Name: 2020-11-28 18:06
Activity type: unknown
Description: -
Total distance: 0.01 km (0.0 mi)
Total time: 01:04
Moving time: 00:00
Average speed: 0.49 km/h (0.3 mi/h)
Average moving speed: 0.00 km/h (0.0 mi/h)
Max speed: 0.00 km/h (0.0 mi/h)
Average pace: 122:30 min/km (197:08 min/mi)
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></TimeStamp>
<styleUrl>#end</styleUrl>
<Point>
<coordinates>1.234046,12.340082,417.99432373046875</coordinates>
</Point>
</Placemark>
</Document>
</kml>
+1 -1
View File
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_name">OpenTracks (Debug)</string>
<string name="app_name" translatable="false">OpenTracks (Debug)</string>
</resources>
@@ -84,11 +84,9 @@ public class MarkerEditActivity extends AbstractActivity {
// Setup UI elements
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.markerEditMarkerType.setAdapter(adapter);
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> {
viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
});
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString()));
viewBinding.markerEditCancel.setOnClickListener(v -> {
viewModel.onCancel();
@@ -24,6 +24,8 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import java.time.Duration;
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
@@ -52,9 +54,8 @@ public class TrackController implements View.OnTouchListener {
private final TransitionDrawable transition;
private boolean isRecording;
private boolean isPaused;
private long totalTime = 0;
private long totalTimeTimestamp = 0;
private boolean isRecordingPaused;
private Duration totalTime;
private boolean isResumed = false;
private final Callback callback;
@@ -62,8 +63,9 @@ public class TrackController implements View.OnTouchListener {
// A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
if (isResumed && isRecording && !isPaused) {
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
if (isResumed && isRecording && !isRecordingPaused) {
updateTotalTime();
setTotalTime();
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
}
}
@@ -81,7 +83,7 @@ public class TrackController implements View.OnTouchListener {
viewBinding.trackControllerRecord.setOnTouchListener(this);
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || (isRecording && !isPaused)) {
if (buttonDelay != null || (isRecording && !isRecordingPaused)) {
return;
}
@@ -117,7 +119,7 @@ public class TrackController implements View.OnTouchListener {
//To pause a recording
if (viewBinding.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording && !isPaused) {
&& isRecording && !isRecordingPaused) {
transition.startTransition(buttonDelayDuration);
@@ -164,10 +166,11 @@ public class TrackController implements View.OnTouchListener {
public void update(boolean recording, boolean paused) {
if (!isResumed) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
isRecording = recording;
isPaused = paused;
isRecordingPaused = paused;
boolean visible = alwaysShow || isRecording;
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
@@ -176,26 +179,26 @@ public class TrackController implements View.OnTouchListener {
return;
}
viewBinding.trackControllerRecord.setImageResource(isRecording && !isPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
if (isRecording && !isRecordingPaused) {
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
}
viewBinding.trackControllerRecord.setImageResource(isRecording && !isRecordingPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isRecordingPaused ? R.string.image_pause : R.string.image_record));
viewBinding.trackControllerStop.setEnabled(isRecording);
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isRecordingPaused ? android.R.color.white : R.color.recording_text));
showStatusSetDefaultText();
}
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
totalTime = getTotalTime();
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
if (!isPaused) {
totalTimeTimestamp = System.currentTimeMillis();
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
}
updateTotalTime();
setTotalTime();
}
}
@@ -217,16 +220,21 @@ public class TrackController implements View.OnTouchListener {
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
}
/**
* Gets the total time for the current recording track.
*/
private long getTotalTime() {
private void updateTotalTime() {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
if (trackRecordingService != null) {
totalTime = trackRecordingService.getTotalTime();
}
}
private void setTotalTime() {
if (totalTime != null) {
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
}
}
private void showStatusSetDefaultText() {
viewBinding.trackControllerStatus.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
viewBinding.trackControllerStatus.setText(isRecordingPaused ? R.string.generic_paused : R.string.generic_recording);
}
public interface Callback {
@@ -44,6 +44,8 @@ import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListBinding;
@@ -224,7 +226,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
String icon = cursor.getString(iconIndex);
int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex);
@@ -14,8 +14,8 @@ import java.util.concurrent.Future;
*/
class ExecutorListViewService<T> {
private List<T> enqueueObjects = new ArrayList<>();
private ExecutorService executorService;
private final List<T> enqueueObjects = new ArrayList<>();
private final ExecutorService executorService;
public ExecutorListViewService(int numThreads) {
executorService = Executors.newFixedThreadPool(numThreads);
@@ -23,7 +23,6 @@ class ExecutorListViewService<T> {
public void shutdown() {
enqueueObjects.clear();
enqueueObjects = null;
executorService.shutdown();
}
@@ -33,7 +33,8 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
private final Activity activity;
private ExecutorListViewService executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
//TODO Should be Marker.Id
private final ExecutorListViewService executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
private boolean scroll = false;
@@ -111,8 +112,8 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
return;
}
long id = cursor.getLong(cursor.getColumnIndex(MarkerColumns._ID));
String photoUrl = cursor.getString(cursor.getColumnIndex(MarkerColumns.PHOTOURL));
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MarkerColumns._ID));
String photoUrl = cursor.getString(cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL));
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
if (hasPhoto) {
@@ -12,7 +12,7 @@ public class ScrollVisibleViews implements AbsListView.OnScrollListener {
private int from = -1;
private int to = -1;
private VisibleViewsListener visibleViewsListener;
private final VisibleViewsListener visibleViewsListener;
public ScrollVisibleViews(@NonNull VisibleViewsListener visibleViewsListener) {
this.visibleViewsListener = visibleViewsListener;
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -13,19 +12,11 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final int HEART_RATE_TYPE = 0;
public static final int CADENCE_TYPE = 1;
public static final int POWER_TYPE = 2;
private List<Pair<Integer, SensorData>> sensorDataList;
private List<SensorDataModel> sensorDataList;
private final Context context;
public SensorsAdapter(Context context) {
@@ -42,9 +33,8 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
int type = sensorDataList.get(position).first;
SensorData sensorData = sensorDataList.get(position).second;
viewHolder.setData(sensorData, type);
SensorDataModel sensorDataModel = sensorDataList.get(position);
viewHolder.setData(sensorDataModel);
}
@Override
@@ -56,12 +46,7 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
}
}
@Override
public int getItemViewType(int position) {
return sensorDataList.get(position).first;
}
public List<Pair<Integer, SensorData>> swapData(List<Pair<Integer, SensorData>> data) {
public List<SensorDataModel> swapData(List<SensorDataModel> data) {
if (sensorDataList == data) {
return null;
}
@@ -89,70 +74,18 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
unit = itemView.findViewById(R.id.stats_sensor_unit);
}
public void setData(SensorData sensorData, int type) {
switch (type) {
case HEART_RATE_TYPE:
setHeartRateSensorData((SensorDataHeartRate) sensorData);
break;
case CADENCE_TYPE:
setCadenceSensorData((SensorDataCycling.Cadence) sensorData);
break;
case POWER_TYPE:
setPowerSensorData((SensorDataCyclingPower) sensorData);
break;
default:
throw new RuntimeException("Unknown sensor type");
}
}
public void setData(SensorDataModel sensorDataModel) {
String sensorName = sensorDataModel.getSensorName();
String sensorValue = sensorDataModel.hasValue() ? sensorDataModel.getSensorValue() : context.getString(R.string.value_unknown);
private void setHeartRateSensorData(SensorDataHeartRate data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasHeartRate_bpm() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
}
this.label.setText(context.getString(sensorDataModel.getLabelId()));
if (sensorName == null) {
this.sensorValue.setVisibility(View.GONE);
} else {
this.sensorValue.setText(sensorName);
}
this.label.setText(context.getString(R.string.sensor_state_heart_rate));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_beats_per_minute);
}
private void setCadenceSensorData(SensorDataCycling.Cadence data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasCadence_rpm() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
}
}
this.label.setText(context.getString(R.string.sensor_state_cadence));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_rounds_per_minute);
}
private void setPowerSensorData(SensorDataCyclingPower data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasPower_w() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getPower_w(), 0);
}
}
this.label.setText(context.getString(R.string.sensor_state_power));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_power);
this.unit.setText(context.getString(sensorDataModel.getUnitId()));
}
}
}
@@ -35,7 +35,7 @@ public class ChartPoint {
}
timeOrDistance = distance;
} else {
timeOrDistance = trackStatistics.getTotalTime();
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
elevation = trackStatisticsUpdater.getSmoothedElevation();
@@ -38,6 +38,7 @@ import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -740,7 +741,7 @@ public class ChartView extends View {
* @param spacing the spacing between x axis and marker
*/
private void drawXAxisMarker(Canvas canvas, double value, NumberFormat numberFormat, int spacing) {
String marker = chartByDistance ? numberFormat.format(value) : StringUtils.formatElapsedTime((long) value);
String marker = chartByDistance ? numberFormat.format(value) : StringUtils.formatElapsedTime((Duration.ofMillis((long) value)));
Rect rect = getRect(xAxisMarkerPaint, marker);
canvas.drawText(marker, getX(value), topBorder + effectiveHeight + spacing + rect.height(), xAxisMarkerPaint);
}
@@ -21,6 +21,8 @@ import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -146,7 +148,7 @@ public class DescriptionGenerator {
}
// Recorded time
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime_ms())));
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
@@ -177,7 +179,7 @@ public class DescriptionGenerator {
* @param lineBreak line break string
*/
@VisibleForTesting
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
void writeTime(Duration time, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
builder.append(lineBreak);
}
@@ -40,13 +40,12 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Track data hub.
* Receives data from {@link de.dennisguse.opentracks.content.provider.CustomContentProvider} and distributes it to {@link TrackDataListener} after some processing.
*
* <p>
* {@link TrackPoint}s are filtered/downsampled with a dynamic sampling frequency.
*
* @author Rodrigo Damazio
@@ -143,7 +142,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
PreferencesUtils.register(context, this);
onSharedPreferenceChanged(null, null);
runInHandlerThread(() -> {
handler.post(() -> {
if (started) {
loadDataForAll();
}
@@ -174,7 +173,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
}
public void loadTrack(final @NonNull Track.Id trackId) {
runInHandlerThread(() -> {
handler.post(() -> {
if (trackId.equals(selectedTrackId)) {
Log.i(TAG, "Not reloading track " + trackId.getId());
return;
@@ -190,7 +189,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
* @param trackDataListener the track data listener
*/
public void registerTrackDataListener(final TrackDataListener trackDataListener, final boolean tracksTable, final boolean markersTable, final boolean trackPointsTable_SampleIn, final boolean trackPointsTable_SampleOut) {
runInHandlerThread(() -> {
handler.post(() -> {
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, markersTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
if (started) {
loadDataForListener(trackDataListener);
@@ -204,7 +203,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
* @param trackDataListener the track data listener
*/
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
runInHandlerThread(() -> trackDataManager.unregisterTrackDataListener(trackDataListener));
handler.post(() -> trackDataManager.unregisterTrackDataListener(trackDataListener));
}
/**
@@ -223,7 +222,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
runInHandlerThread(() -> {
handler.post(() -> {
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
}
@@ -314,9 +313,6 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
if (cursor != null && cursor.moveToFirst()) {
do {
Marker marker = contentProviderUtils.createMarker(cursor);
if (!LocationUtils.isValidLocation(marker.getLocation())) {
continue;
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onNewMarker(marker);
}
@@ -362,18 +358,18 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
TrackPoint.Id lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
int samplingFrequency = -1;
boolean includeNextPoint = false;
TrackPoint.Id next;
TrackPoint.Id next = null;
if (localLastSeenTrackPointIdId != null) {
next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
}
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, null)) {
TrackPoint trackPoint = null;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
while (trackPointIterator.hasNext()) {
TrackPoint trackPoint = trackPointIterator.next();
trackPoint = trackPointIterator.next();
TrackPoint.Id trackPointId = trackPoint.getId();
// Stop if past the last wanted point
@@ -390,35 +386,36 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) { //This can be split markers (not anymore supported feature)
includeNextPoint = true;
// Also include the last point if the selected track is not recording.
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(trackPoint);
}
} else {
// Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
includeNextPoint = false;
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(trackPoint);
}
} else {
for (TrackDataListener trackDataListener : sampledOutListeners) {
trackDataListener.onSampledOutTrackPoint(trackPoint);
}
for (TrackDataListener trackDataListener : sampledOutListeners) {
trackDataListener.onSampledOutTrackPoint(trackPoint);
}
}
localNumLoadedTrackPoints++;
localLastSeenTrackPointIdId = trackPointId;
}
}
if (trackPoint != null) {
localLastSeenTrackPointIdId = trackPoint.getId();
}
if (updateSamplingState) {
numLoadedPoints = localNumLoadedTrackPoints;
firstSeenTrackPointId = localFirstSeenTrackPointId;
lastSeenTrackPointId = localLastSeenTrackPointIdId;
}
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone();
if (trackPoint != null) {
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone(trackPoint);
}
}
}
@@ -430,20 +427,4 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
firstSeenTrackPointId = null;
lastSeenTrackPointId = null;
}
/**
* Run in the handler thread.
*
* @param runnable the runnable
*/
@Deprecated //TODO: Why actually catch this problem: I guess it would be better to fail hard.
@VisibleForTesting
private void runInHandlerThread(Runnable runnable) {
if (handler == null) {
// Use a Throwable to ensure the stack trace is logged.
Log.d(TAG, "handler is null.", new Throwable());
return;
}
handler.post(runnable);
}
}
@@ -16,6 +16,8 @@
package de.dennisguse.opentracks.content;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -32,6 +34,7 @@ public interface TrackDataListener {
*
* @param track the track
*/
//TODO Could be @NonNull
void onTrackUpdated(Track track);
/**
@@ -44,20 +47,20 @@ public interface TrackDataListener {
*
* @param trackPoint the trackPoint
*/
void onSampledInTrackPoint(TrackPoint trackPoint);
void onSampledInTrackPoint(@NonNull TrackPoint trackPoint);
/**
* Called when a sampled out track point is read.
*
* @param trackPoint the trackPoint
*/
void onSampledOutTrackPoint(TrackPoint trackPoint);
void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint);
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint)} and {@link #onSampledOutTrackPoint(TrackPoint)}.
*/
void onNewTrackPointsDone();
void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint);
/**
* Called to clear previously sent markers.
@@ -23,11 +23,13 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import java.util.Objects;
/**
* NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
@@ -39,43 +41,49 @@ public final class Marker {
private String category = "";
private String icon = "";
private final Track.Id trackId;
private final Instant time;
private Double latitude;
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Float bearing;
//TODO It is the distance from the track starting point; rename to something more meaningful
private double length = 0.0;
private long duration = 0;
private final Location location;
private long duration = 0; //TODO Duration
@Deprecated //TODO Make an URI instead of String
private String photoUrl = "";
public Marker(@Nullable Track.Id trackId) {
public Marker(@Nullable Track.Id trackId, Instant time) {
this.trackId = trackId;
location = null;
this.time = time;
}
@VisibleForTesting
public Marker(@NonNull Track.Id trackId, @NonNull TrackPoint trackPoint) {
this(trackId, trackPoint.getLocation());
}
@Deprecated
//TODO Used by AbstractFileImporter to create an intermediate marker before saving a new one into the database.
public Marker(@NonNull Location location) {
this(null, location);
}
public Marker(@NonNull Track.Id trackId, @NonNull Location location) {
this.location = location;
public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) {
this.trackId = trackId;
this.time = trackPoint.getTime();
if (!trackPoint.hasLocation())
throw new RuntimeException("Marker requires a trackpoint with a location.");
this.latitude = trackPoint.getLatitude();
this.longitude = trackPoint.getLongitude();
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude();
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
}
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull Location location, String photoUrl) {
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull TrackPoint trackPoint, String photoUrl) {
this(trackId, trackPoint);
this.name = name;
this.description = description;
this.category = category;
this.icon = icon;
this.trackId = trackId;
this.length = length;
this.duration = duration;
this.location = location;
this.photoUrl = photoUrl;
}
@@ -91,6 +99,10 @@ public final class Marker {
this.id = id;
}
public Instant getTime() {
return time;
}
public String getName() {
return name;
}
@@ -128,6 +140,83 @@ public final class Marker {
return trackId;
}
public boolean hasLocation() {
return latitude != null || longitude != null;
}
@Nullable
public Location getLocation() {
Location location = new Location("");
location.setTime(time.toEpochMilli());
if (hasLocation()) {
location.setLatitude(latitude);
location.setLongitude(longitude);
}
if (hasBearing()) {
location.setBearing(bearing);
}
if (hasAccuracy()) {
location.setAccuracy(accuracy);
}
if (hasAltitude()) {
location.setAltitude(altitude_m);
}
return location;
}
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
public boolean hasAccuracy() {
return accuracy != null;
}
public Float getAccuracy() {
return accuracy;
}
public void setAccuracy(Float accuracy) {
this.accuracy = accuracy;
}
public boolean hasAltitude() {
return altitude_m != null;
}
public Double getAltitude() {
return altitude_m;
}
public void setAltitude(double altitude_m) {
this.altitude_m = altitude_m;
}
public boolean hasBearing() {
return bearing != null;
}
public Float getBearing() {
return bearing;
}
public void setBearing(float bearing) {
this.bearing = bearing;
}
public double getLength() {
return length;
}
@@ -144,11 +233,6 @@ public final class Marker {
this.duration = duration;
}
@NonNull
public Location getLocation() {
return location;
}
public String getPhotoUrl() {
return photoUrl;
}
@@ -16,25 +16,78 @@
package de.dennisguse.opentracks.content.data;
import android.location.Location;
import android.location.LocationManager;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
/**
* This class extends the standard Android location with extra information.
* <p>
* NOTE: default location will be latitude=0.0 and longitude=0.0.
* For {@link TrackPoint}s with `type == null` this is not meaningful.
* <p>
* NOTE: For Locations provided by the GPS.
* We are replacing the GPS-provided time using the system time.
* Then we have the same timestamps for the user-driven events (aka start, pause, resume) and restore {@link de.dennisguse.opentracks.stats.TrackStatistics}.
* Drawbacks:
* * GPS-provided timestamp might be more precise (but also have GPS week rollover)
* * System clock might be changed (and thus non-monotonic)
* TODO: if these might be a problem, we need to store both timestamps.
*
* @author Sandor Dornbush
*/
//TODO Merge constructors by use case; we have too many.
public class TrackPoint {
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private TrackPoint.Id id;
private final Location location;
private Instant time;
private Double latitude;
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Float speed_mps;
private Float bearing;
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
TRACKPOINT(0), //Just GPS data.
SEGMENT_END_MANUAL(1); //End of a segment
public final int type_db;
Type(int type_db) {
this.type_db = type_db;
}
@Override
public String toString() {
return name() + "(" + type_db + ")";
}
public static Type getById(int id) {
for (Type e : values()) {
if (e.type_db == id) return e;
}
throw new RuntimeException("unknown id: " + id);
}
}
@NonNull
private Type type;
private Float heartRate_bpm = null;
private Float cyclingCadence_rpm = null;
@@ -42,61 +95,76 @@ public class TrackPoint {
private Float elevationGain = null;
private Float elevationLoss = null;
public TrackPoint() {
this.location = new Location("");
public TrackPoint(@NonNull Type type) {
this.type = type;
}
public TrackPoint(@NonNull Location location) {
this.location = location;
this(Type.TRACKPOINT);
this.latitude = location.getLatitude();
this.longitude = location.getLongitude();
this.altitude_m = location.getAltitude();
this.speed_mps = location.getSpeed();
this.accuracy = location.getAccuracy();
setTime(Instant.now());
}
public TrackPoint(@NonNull TrackPoint trackPoint) {
this.location = trackPoint.getLocation();
this.heartRate_bpm = trackPoint.getHeartRate_bpm();
this.cyclingCadence_rpm = trackPoint.getCyclingCadence_rpm();
this.power = trackPoint.getPower();
this.elevationGain = trackPoint.getElevationGain();
this.elevationLoss = trackPoint.getElevationLoss();
public TrackPoint(@NonNull Type type, Instant time) {
this(type);
this.time = time;
}
public TrackPoint(double latitude, double longitude, Double altitude, long time) {
location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latitude);
location.setLongitude(longitude);
if (altitude != null) {
location.setAltitude(altitude);
}
location.setTime(time);
public TrackPoint(double latitude, double longitude, Double altitude, Instant time) {
this(Type.TRACKPOINT);
this.latitude = latitude;
this.longitude = longitude;
this.altitude_m = altitude;
this.time = time;
}
@Deprecated //See #316
public static TrackPoint createPause() {
return createPauseWithTime(System.currentTimeMillis());
public static TrackPoint createSegmentStartManual() {
return createSegmentStartManualWithTime(Instant.now());
}
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_START_MANUAL, time);
}
@Deprecated //See #316
public static TrackPoint createPauseWithTime(long time) {
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
pause.setTime(time);
return new TrackPoint(pause);
public static TrackPoint createSegmentStartAutomatic() {
return createSegmentStartAutomaticWithTime(Instant.now());
}
@Deprecated //See #316
public static TrackPoint createResume() {
return createResumeWithTime(System.currentTimeMillis());
public static TrackPoint createSegmentStartAutomaticWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_START_AUTOMATIC, time);
}
@Deprecated //See #316
public static TrackPoint createResumeWithTime(long time) {
Location resume = new Location(LocationManager.GPS_PROVIDER);
resume.setLongitude(0);
resume.setLatitude(TrackPointsColumns.RESUME_LATITUDE);
resume.setTime(time);
return new TrackPoint(resume);
public static TrackPoint createSegmentEnd() {
return createSegmentEndWithTime(Instant.now());
}
public static TrackPoint createSegmentEndWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_END_MANUAL, time);
}
@NonNull
public Type getType() {
return type;
}
public void setType(@NonNull Type type) {
this.type = type;
}
public boolean isSegmentStart() {
return type == Type.SEGMENT_START_AUTOMATIC || type == Type.SEGMENT_START_MANUAL;
}
public boolean isSegmentEnd() {
return type == Type.SEGMENT_END_MANUAL;
}
/**
@@ -111,8 +179,44 @@ public class TrackPoint {
this.id = id;
}
@Nullable
public boolean hasLocation() {
return latitude != null && longitude != null;
}
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
@NonNull
public Location getLocation() {
Location location = new Location("");
location.setTime(time.toEpochMilli());
if (hasLocation()) {
location.setLatitude(latitude);
location.setLongitude(longitude);
}
if (hasBearing()) {
location.setBearing(bearing);
}
if (hasAccuracy()) {
location.setAccuracy(accuracy);
}
if (hasAltitude()) {
location.setAltitude(altitude_m);
}
return location;
}
@@ -120,7 +224,7 @@ public class TrackPoint {
return elevationGain != null;
}
public float getElevationGain() {
public Float getElevationGain() {
return elevationGain;
}
@@ -140,88 +244,86 @@ public class TrackPoint {
this.elevationLoss = elevationLoss;
}
public double getLatitude() {
return location.getLatitude();
public Instant getTime() {
return time;
}
public void setLatitude(double latitude) {
location.setLatitude(latitude);
public void setTime(Instant time) {
this.time = time;
}
public double getLongitude() {
return location.getLongitude();
public boolean isRecent() {
return Instant.now()
.isBefore(time.plus(MAX_LOCATION_AGE));
}
public void setLongitude(double longitude) {
location.setLongitude(longitude);
}
public long getTime() {
return location.getTime();
}
public void setTime(long time) {
location.setTime(time);
}
public boolean hasAltitude() {
return location.hasAltitude();
return altitude_m != null;
}
public double getAltitude() {
return location.getAltitude();
return altitude_m;
}
public void setAltitude(double altitude) {
location.setAltitude(altitude);
this.altitude_m = altitude;
}
public boolean hasSpeed() {
return location.hasSpeed();
return speed_mps != null;
}
public float getSpeed() {
return location.getSpeed();
return speed_mps;
}
public void setSpeed(float speed) {
location.setSpeed(speed);
public void setSpeed(Float speed) {
this.speed_mps = speed;
}
public boolean isMoving() {
return hasSpeed() && getSpeed() >= MAX_NO_MOVEMENT_SPEED;
}
public boolean hasBearing() {
return location.hasBearing();
return bearing != null;
}
public float getBearing() {
return location.getBearing();
return bearing;
}
public void setBearing(float bearing) {
location.setBearing(bearing);
public void setBearing(Float bearing) {
this.bearing = bearing;
}
public boolean hasAccuracy() {
return location.hasAccuracy();
return accuracy != null;
}
public float getAccuracy() {
return location.getAccuracy();
return accuracy;
}
public void setAccuracy(float horizontalAccuracy) {
location.setAccuracy(horizontalAccuracy);
this.accuracy = horizontalAccuracy;
}
public float distanceTo(@NonNull TrackPoint dest) {
return location.distanceTo(dest.getLocation());
return getLocation().distanceTo(dest.getLocation());
}
public boolean fulfillsAccuracy(int poorAccuracy) {
return hasAccuracy() && accuracy < poorAccuracy;
}
public float bearingTo(@NonNull TrackPoint dest) {
return location.bearingTo(dest.getLocation());
return getLocation().bearingTo(dest.getLocation());
}
public float bearingTo(@NonNull Location dest) {
return location.bearingTo(dest);
return getLocation().bearingTo(dest);
}
// Sensor data
@@ -268,7 +370,16 @@ public class TrackPoint {
@NonNull
@Override
public String toString() {
return "time=" + getTime() + ": lat=" + getLatitude() + " lng=" + getLongitude() + " acc=" + getAccuracy();
String result = "time=" + getTime() + " (type=" + getType() + ")";
if (!hasLocation()) {
return result;
}
result += ": lat=" + getLatitude() + " lng=" + getLongitude();
if (!hasAccuracy()) {
return result;
}
return result + " acc=" + getAccuracy();
}
public static class Id {
@@ -37,13 +37,11 @@ public interface TrackPointsColumns extends BaseColumns {
// Columns
String TRACKID = "trackid";
// See {@link TrackPoint.Type}
String TYPE = "type";
String LONGITUDE = "longitude";
String LATITUDE = "latitude";
@Deprecated
double PAUSE_LATITUDE = 100.0;
@Deprecated
double RESUME_LATITUDE = 200.0;
String TIME = "time";
String ALTITUDE = "elevation";
@@ -56,6 +54,13 @@ public interface TrackPointsColumns extends BaseColumns {
String ELEVATION_GAIN = "elevation_gain";
String ELEVATION_LOSS = "elevation_loss";
// Alias for sensor statistics
String ALIAS_AVG_HR = "avg_hr";
String ALIAS_MAX_HR = "max_hr";
String ALIAS_AVG_CADENCE = "avg_cadence";
String ALIAS_MAX_CADENCE = "max_cadence";
String ALIAS_AVG_POWER = "avg_power";
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TRACKID + " INTEGER NOT NULL, "
@@ -71,6 +76,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ SENSOR_POWER + " FLOAT, "
+ ELEVATION_GAIN + " FLOAT, "
+ ELEVATION_LOSS + " FLOAT, "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1)), "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
+ ")";
@@ -30,6 +30,7 @@ public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
Uri CONTENT_URI_SENSOR_STATS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
String DEFAULT_SORT_ORDER = _ID;
@@ -9,6 +9,7 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
*/
class CachedTrackPointsIndexes {
final int idIndex;
final int typeIndex;
final int longitudeIndex;
final int latitudeIndex;
final int timeIndex;
@@ -24,6 +25,7 @@ class CachedTrackPointsIndexes {
CachedTrackPointsIndexes(Cursor cursor) {
idIndex = cursor.getColumnIndex(TrackPointsColumns._ID);
typeIndex = cursor.getColumnIndex(TrackPointsColumns.TYPE);
longitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE);
latitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE);
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.content.provider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
@@ -29,6 +30,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -42,6 +45,7 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
@@ -63,8 +67,6 @@ public class ContentProviderUtils {
// The base URI for the app's content provider.
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
// Maximum number of markers that will be loaded at one time.
public static final int MAX_LOADED_MARKERS = 10000;
private static final String ID_SEPARATOR = ",";
private final ContentResolver contentResolver;
@@ -79,20 +81,6 @@ public class ContentProviderUtils {
this.contentResolver = contentResolver;
}
/**
* Clears a track: removes markers and trackPoints.
* Only keeps the track id.
*
* @param trackId the track id
*/
@Deprecated //TODO Figure out why we need this.
public void clearTrack(Track.Id trackId) {
deleteTrackPointsAndMarkers(trackId);
Track track = new Track();
track.setId(trackId);
updateTrack(track);
}
/**
* Creates a {@link Track} from a cursor.
*
@@ -134,19 +122,19 @@ public class ContentProviderUtils {
track.setCategory(cursor.getString(categoryIndex));
}
if (!cursor.isNull(startTimeIndex)) {
trackStatistics.setStartTime_ms(cursor.getLong(startTimeIndex));
trackStatistics.setStartTime(Instant.ofEpochMilli(cursor.getLong(startTimeIndex)));
}
if (!cursor.isNull(stopTimeIndex)) {
trackStatistics.setStopTime_ms(cursor.getLong(stopTimeIndex));
trackStatistics.setStopTime(Instant.ofEpochMilli(cursor.getLong(stopTimeIndex)));
}
if (!cursor.isNull(totalDistanceIndex)) {
trackStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
}
if (!cursor.isNull(totalTimeIndex)) {
trackStatistics.setTotalTime(cursor.getLong(totalTimeIndex));
trackStatistics.setTotalTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
}
if (!cursor.isNull(movingTimeIndex)) {
trackStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
trackStatistics.setMovingTime(Duration.ofMillis(cursor.getLong(movingTimeIndex)));
}
if (!cursor.isNull(maxSpeedIndex)) {
trackStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
@@ -199,13 +187,7 @@ public class ContentProviderUtils {
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
}
private void deleteTrackPointsAndMarkers(@NonNull Track.Id trackId) {
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, TrackPointsColumns.TRACKID + "=?", selectionArgs);
contentResolver.delete(MarkerColumns.CONTENT_URI, MarkerColumns.TRACKID + "=?", selectionArgs);
}
//TODO Only use for tests; also move to tests.
public List<Track> getTracks() {
ArrayList<Track> tracks = new ArrayList<>();
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
@@ -284,7 +266,6 @@ public class ContentProviderUtils {
ContentValues values = new ContentValues();
TrackStatistics trackStatistics = track.getTrackStatistics();
// Value < 0 indicates no id is available
if (track.getId() != null) {
values.put(TracksColumns._ID, track.getId().getId());
}
@@ -292,11 +273,15 @@ public class ContentProviderUtils {
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.CATEGORY, track.getCategory());
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime_ms());
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime_ms());
if (trackStatistics.getStartTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
}
if (trackStatistics.getStopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
}
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed());
@@ -326,27 +311,23 @@ public class ContentProviderUtils {
int bearingIndex = cursor.getColumnIndexOrThrow(MarkerColumns.BEARING);
int photoUrlIndex = cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL);
Location location = new Location("");
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
Marker marker = new Marker(trackId, Instant.ofEpochMilli(cursor.getLong(timeIndex)));
if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) {
location.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6);
location.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
}
if (!cursor.isNull(timeIndex)) {
location.setTime(cursor.getLong(timeIndex));
marker.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6);
marker.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
}
if (!cursor.isNull(altitudeIndex)) {
location.setAltitude(cursor.getFloat(altitudeIndex));
marker.setAltitude(cursor.getFloat(altitudeIndex));
}
if (!cursor.isNull(accuracyIndex)) {
location.setAccuracy(cursor.getFloat(accuracyIndex));
marker.setAccuracy(cursor.getFloat(accuracyIndex));
}
if (!cursor.isNull(bearingIndex)) {
location.setBearing(cursor.getFloat(bearingIndex));
marker.setBearing(cursor.getFloat(bearingIndex));
}
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
Marker marker = new Marker(trackId, location);
if (!cursor.isNull(idIndex)) {
marker.setId(new Marker.Id(cursor.getLong(idIndex)));
}
@@ -439,7 +420,8 @@ public class ContentProviderUtils {
return markers;
}
@Deprecated //TODO TracksColumns.MARKER_COUNT while querying for tracks
//TODO Move to testing package
@Deprecated
public int getMarkerCount(Track.Id trackId) {
String[] projection = new String[]{"count(*) AS count"};
String selection = MarkerColumns.TRACKID + "=?";
@@ -502,18 +484,17 @@ public class ContentProviderUtils {
values.put(MarkerColumns.LENGTH, marker.getLength());
values.put(MarkerColumns.DURATION, marker.getDuration());
Location location = marker.getLocation();
values.put(MarkerColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
values.put(MarkerColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
values.put(MarkerColumns.TIME, location.getTime());
if (location.hasAltitude()) {
values.put(MarkerColumns.ALTITUDE, location.getAltitude());
values.put(MarkerColumns.LONGITUDE, (int) (marker.getLongitude() * 1E6));
values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6));
values.put(MarkerColumns.TIME, marker.getTime().toEpochMilli());
if (marker.hasAltitude()) {
values.put(MarkerColumns.ALTITUDE, marker.getAltitude());
}
if (location.hasAccuracy()) {
values.put(MarkerColumns.ACCURACY, location.getAccuracy());
if (marker.hasAccuracy()) {
values.put(MarkerColumns.ACCURACY, marker.getAccuracy());
}
if (location.hasBearing()) {
values.put(MarkerColumns.BEARING, location.getBearing());
if (marker.hasBearing()) {
values.put(MarkerColumns.BEARING, marker.getBearing());
}
values.put(MarkerColumns.PHOTOURL, marker.getPhotoUrl());
@@ -544,10 +525,8 @@ public class ContentProviderUtils {
* @param indexes the cached trackPoints indexes
*/
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
TrackPoint trackPoint = new TrackPoint();
if (!cursor.isNull(indexes.longitudeIndex)) {
trackPoint.setId(new TrackPoint.Id(cursor.getInt(indexes.idIndex)));
}
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.getById(cursor.getInt(indexes.typeIndex)));
trackPoint.setId(new TrackPoint.Id(cursor.getInt(indexes.idIndex)));
if (!cursor.isNull(indexes.longitudeIndex)) {
trackPoint.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
@@ -556,7 +535,7 @@ public class ContentProviderUtils {
trackPoint.setLatitude(((double) cursor.getInt(indexes.latitudeIndex)) / 1E6);
}
if (!cursor.isNull(indexes.timeIndex)) {
trackPoint.setTime(cursor.getLong(indexes.timeIndex));
trackPoint.setTime(Instant.ofEpochMilli(cursor.getLong(indexes.timeIndex)));
}
if (!cursor.isNull(indexes.altitudeIndex)) {
trackPoint.setAltitude(cursor.getFloat(indexes.altitudeIndex));
@@ -600,24 +579,6 @@ public class ContentProviderUtils {
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
/**
* Gets the first location id for a track.
* Returns null if it doesn't exist.
*
* @param trackId the track id
*/
@Deprecated
public Track.Id getFirstTrackPointId(Track.Id trackId) {
String selection = TrackPointsColumns._ID + "=(SELECT MIN(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
return new Track.Id(cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)));
}
}
return null;
}
/**
* Gets the last location id for a track.
* Returns -1L if it doesn't exist.
@@ -696,7 +657,7 @@ public class ContentProviderUtils {
*/
@Deprecated
public TrackPoint getLastValidTrackPoint(Track.Id trackId) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TYPE + " IN (" + TrackPoint.Type.SEGMENT_START_AUTOMATIC.type_db + "," + TrackPoint.Type.TRACKPOINT.type_db + "))";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
return findTrackPointBy(selection, selectionArgs);
}
@@ -715,16 +676,19 @@ public class ContentProviderUtils {
/**
* Creates the {@link ContentValues} for a {@link TrackPoint}.
*
* @param trackPoint the trackPointstats_pace_km#87
* @param trackPoint the trackPoint
* @param trackId the track id
*/
private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId.getId());
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6));
values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
values.put(TrackPointsColumns.TIME, trackPoint.getTime());
if (trackPoint.hasLocation()) {
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6));
}
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude());
}
@@ -855,4 +819,21 @@ public class ContentProviderUtils {
public static String[] parseTrackIdsFromUri(Uri url) {
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
}
public SensorStatistics getSensorStats(@NonNull Track.Id trackId) {
SensorStatistics sensorStatistics = null;
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.getId()), null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
sensorStatistics = new SensorStatistics(
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_HR)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_HR)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_CADENCE)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_CADENCE)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_POWER)) : null
);
}
}
return sensorStatistics;
}
}
@@ -36,6 +36,7 @@ import androidx.annotation.VisibleForTesting;
import java.util.Arrays;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
@@ -56,6 +57,38 @@ public class CustomContentProvider extends ContentProvider {
private SQLiteDatabase db;
/**
* The string representing the query that compute sensor stats from trackpoints table.
* It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence.
* Finally, it ignores manual pause (SEGMENT_START_MANUAL).
*/
private final String SENSOR_STATS_QUERY =
"WITH time_select as " +
"(SELECT t1." + TrackPointsColumns.TIME + " * (t1." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")) time_value " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t1 " +
"WHERE t1." + TrackPointsColumns._ID + " > t." + TrackPointsColumns._ID + " AND t1." + TrackPointsColumns.TRACKID + " = ? ORDER BY _id LIMIT 1) " +
"SELECT " +
"SUM(t." + TrackPointsColumns.SENSOR_HEARTRATE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_HR + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_HEARTRATE + ") " + TrackPointsColumns.ALIAS_MAX_HR + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_CADENCE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_CADENCE + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_CADENCE + ") " + TrackPointsColumns.ALIAS_MAX_CADENCE + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_POWER + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + " " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
@@ -63,6 +96,7 @@ public class CustomContentProvider extends ContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
@@ -223,6 +257,10 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
case TRACKS_SENSOR_STATS:
long trackId = ContentUris.parseId(url);
Cursor cursor = db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)});
return cursor;
case MARKERS:
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
@@ -329,10 +367,8 @@ public class CustomContentProvider extends ContentProvider {
}
private Uri insertTrackPoint(Uri url, ContentValues values) {
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
if (!hasLatitude || !hasLongitude || !hasTime) {
if (!hasTime) {
throw new IllegalArgumentException("Latitude, longitude, and time values are required.");
}
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
@@ -343,10 +379,6 @@ public class CustomContentProvider extends ContentProvider {
}
private Uri insertTrack(Uri url, ContentValues contentValues) {
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
if (!hasStartTime) {
throw new IllegalArgumentException("Both start time and start id values are required.");
}
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
@@ -369,6 +401,7 @@ public class CustomContentProvider extends ContentProvider {
TRACKPOINTS_BY_TRACKID,
TRACKS,
TRACKS_BY_ID,
TRACKS_SENSOR_STATS,
MARKERS,
MARKERS_BY_ID,
MARKERS_BY_TRACKID
@@ -25,7 +25,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 29;
private static final int DATABASE_VERSION = 30;
@VisibleForTesting
public static final String DATABASE_NAME = "database.db";
@@ -221,7 +221,7 @@ public class ShareContentProvider extends CustomContentProvider {
}
}
final TrackExporter trackExporter = getTrackFileFormat(uri).newTrackExporter(getContext());
final TrackExporter trackExporter = getTrackFileFormat(uri).createTrackExporter(getContext());
PipeDataWriter<String> pipeDataWriter = (output, uri1, mimeType, opts, args) -> {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
@@ -12,6 +12,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/
//TODO Remove batching; that should be handled by the database/contentprovider (i.e., already in place as we use a cursor)!
public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private static final String TAG = TrackPointIterator.class.getSimpleName();
@@ -22,7 +23,6 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private TrackPoint.Id lastTrackPointId = null;
private Cursor cursor;
public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) {
this.contentProviderUtils = contentProviderUtils;
this.trackId = trackId;
@@ -4,28 +4,32 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
public class SensorData {
public abstract class SensorData<T> {
protected T value;
private final String sensorAddress;
private final String sensorName;
private final long timestamp_ms;
private final Instant time;
SensorData(String sensorAddress) {
this(sensorAddress, null, System.currentTimeMillis());
this(sensorAddress, null);
}
SensorData(String sensorAddress, String sensorName) {
this(sensorAddress, sensorName, System.currentTimeMillis());
this(sensorAddress, sensorName, Instant.now());
}
@VisibleForTesting
SensorData(String sensorAddress, String sensorName, long timestamp_ms) {
SensorData(String sensorAddress, String sensorName, Instant time) {
this.sensorAddress = sensorAddress;
this.sensorName = sensorName;
this.timestamp_ms = timestamp_ms;
this.time = time;
}
@NonNull
@@ -42,10 +46,19 @@ public class SensorData {
return sensorName != null ? sensorName : sensorAddress;
}
public boolean hasValue() {
return value != null;
}
public T getValue() {
return value;
}
/**
* Is the data recent considering the current time.
*/
public boolean isRecent() {
return timestamp_ms + BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS > System.currentTimeMillis();
return Instant.now()
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
}
}
@@ -4,6 +4,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import de.dennisguse.opentracks.util.UintUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -20,11 +21,10 @@ public final class SensorDataCycling {
private SensorDataCycling() {
}
public static class Cadence extends SensorData {
public static class Cadence extends SensorData<Float> {
private final Long crankRevolutionsCount; // UINT32
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
private Float cadence_rpm;
public Cadence(String sensorAddress) {
super(sensorAddress);
@@ -57,24 +57,16 @@ public final class SensorDataCycling {
return crankRevolutionsTime;
}
public boolean hasCadence_rpm() {
return cadence_rpm != null;
}
public float getCadence_rpm() {
return cadence_rpm;
}
public void compute(Cadence previous) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
cadence_rpm = null;
value = null;
} else {
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
float cadence_ms = crankDiff / timeDiff_ms;
cadence_rpm = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
value = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
}
}
}
@@ -82,7 +74,7 @@ public final class SensorDataCycling {
@NonNull
@Override
public String toString() {
return "cadence=" + cadence_rpm + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
return "cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
}
@Override
@@ -98,11 +90,10 @@ public final class SensorDataCycling {
}
}
public static class Speed extends SensorData {
public static class Speed extends SensorData<Float> {
private final Integer wheelRevolutionsCount; // UINT16
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
private Float speed_mps;
public Speed(String sensorAddress) {
super(sensorAddress);
@@ -128,24 +119,16 @@ public final class SensorDataCycling {
return wheelRevolutionsTime;
}
public boolean hasSpeed_mps() {
return speed_mps != null;
}
public float getSpeed_mps() {
return speed_mps;
}
public void compute(Speed previous, int wheel_circumference_mm) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
speed_mps = null;
value = null;
} else {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
speed_mps = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
value = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
}
}
}
@@ -153,7 +136,7 @@ public final class SensorDataCycling {
@NonNull
@Override
public String toString() {
return "speed=" + speed_mps + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
return "speed=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
}
@Override
@@ -169,23 +152,19 @@ public final class SensorDataCycling {
}
}
public static class CadenceAndSpeed extends SensorData {
private final Cadence cadence;
private final Speed speed;
public static class CadenceAndSpeed extends SensorData<Pair<Cadence, Speed>> {
public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) {
super(sensorAddress, sensorName);
this.cadence = cadence;
this.speed = speed;
this.value = new Pair<>(cadence, speed);
}
public Cadence getCadence() {
return cadence;
return this.value != null ? this.value.first : null;
}
public Speed getSpeed() {
return speed;
return this.value != null ? this.value.second : null;
}
}
}
@@ -2,31 +2,20 @@ package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
public class SensorDataCyclingPower extends SensorData {
private final Float power_w;
public class SensorDataCyclingPower extends SensorData<Float> {
public SensorDataCyclingPower(String address) {
super(address);
this.power_w = null;
}
public SensorDataCyclingPower(String name, String address, float power_w) {
super(name, address);
this.power_w = power_w;
}
public boolean hasPower_w() {
return power_w != null;
}
public float getPower_w() {
return power_w;
this.value = power_w;
}
@NonNull
@Override
public String toString() {
return "power=" + power_w;
return "power=" + value;
}
}
@@ -2,31 +2,20 @@ package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
public class SensorDataHeartRate extends SensorData {
private final Float heartRate_bpm;
public class SensorDataHeartRate extends SensorData<Float> {
public SensorDataHeartRate(String address) {
super(address);
heartRate_bpm = null;
}
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
super(name, address);
this.heartRate_bpm = heartRate_bpm;
}
public boolean hasHeartRate_bpm() {
return heartRate_bpm != null;
}
public Float getHeartRate_bpm() {
return heartRate_bpm;
this.value = heartRate_bpm;
}
@NonNull
@Override
public String toString() {
return "heart=" + heartRate_bpm;
return "heart=" + value;
}
}
@@ -33,11 +33,11 @@ public final class SensorDataSet {
return cyclingPower;
}
public void set(SensorData data) {
public void set(SensorData<?> data) {
set(data, data);
}
public void remove(SensorData type) {
public void remove(SensorData<?> type) {
set(type, null);
}
@@ -50,19 +50,19 @@ public final class SensorDataSet {
public void fillTrackPoint(TrackPoint trackPoint) {
if (heartRate != null) {
trackPoint.setHeartRate_bpm(heartRate.getHeartRate_bpm());
trackPoint.setHeartRate_bpm(heartRate.getValue());
}
if (cyclingCadence != null && cyclingCadence.hasCadence_rpm()) {
trackPoint.setCyclingCadence_rpm(cyclingCadence.getCadence_rpm());
if (cyclingCadence != null && cyclingCadence.hasValue()) {
trackPoint.setCyclingCadence_rpm(cyclingCadence.getValue());
}
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
if (cyclingSpeed != null && cyclingSpeed.hasValue()) {
trackPoint.setSpeed(cyclingSpeed.getValue());
}
if (cyclingPower != null && cyclingPower.hasPower_w()) {
trackPoint.setPower(cyclingPower.getPower_w());
if (cyclingPower != null && cyclingPower.hasValue()) {
trackPoint.setPower(cyclingPower.getValue());
}
}
@@ -75,7 +75,7 @@ public final class SensorDataSet {
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
}
private void set(@NonNull SensorData type, SensorData data) {
private void set(@NonNull SensorData<?> type, SensorData<?> data) {
if (type instanceof SensorDataHeartRate) {
this.heartRate = (SensorDataHeartRate) data;
return;
@@ -43,7 +43,6 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -57,7 +56,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private static final String KEY_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
public static Fragment newInstance(boolean chartByDistance) {
public static ChartFragment newInstance(boolean chartByDistance) {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
@@ -71,8 +70,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private TrackDataHub trackDataHub;
// Stats gathered from the received data
private TrackStatisticsUpdater trackStatisticsUpdater;
private long startTime;
private TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
private ChartBinding viewBinding;
@@ -198,7 +196,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void onTrackUpdated(Track track) {
if (isResumed()) {
if (track == null || track.getTrackStatistics() == null) {
startTime = -1L;
category = "";
return;
}
@@ -209,14 +206,13 @@ public class ChartFragment extends Fragment implements TrackDataListener {
chartView.setReportSpeed(reportSpeed);
chartView.applyReportSpeed();
}
startTime = track.getTrackStatistics().getStartTime_ms();
}
}
@Override
public void clearTrackPoints() {
if (isResumed()) {
trackStatisticsUpdater = startTime != -1L ? new TrackStatisticsUpdater(startTime) : null;
trackStatisticsUpdater = new TrackStatisticsUpdater();
pendingPoints.clear();
chartView.reset();
runOnUiThread(() -> {
@@ -228,14 +224,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
pendingPoints.add(createPendingPoint(trackPoint));
}
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
if (trackStatisticsUpdater != null) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
@@ -244,7 +240,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
if (isResumed()) {
chartView.addChartPoints(pendingPoints);
pendingPoints.clear();
@@ -261,7 +257,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override
public void onNewMarker(Marker marker) {
if (isResumed() && marker != null && LocationUtils.isValidLocation(marker.getLocation())) {
if (isResumed() && marker != null) {
chartView.addMarker(marker);
}
}
@@ -327,11 +323,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
return new ChartPoint(trackStatisticsUpdater, trackPoint, chartByDistance, chartView.getMetricUnits());
}
@VisibleForTesting
void setTrackStatisticsUpdater(long time) {
trackStatisticsUpdater = new TrackStatisticsUpdater(time);
}
@VisibleForTesting
void setChartView(ChartView view) {
chartView = view;
@@ -198,21 +198,21 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
viewModel.add(trackPoint);
}
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
viewModel.add(trackPoint);
}
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
if (isResumed()) {
runOnUiThread(this::loadIntervals);
}
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks.fragments;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
@@ -253,16 +252,11 @@ public class MarkerDetailFragment extends Fragment {
}
private void setLocation(boolean addShadow) {
Location location = marker.getLocation();
String value;
if (location == null) {
value = null;
} else {
value = "[" + getString(R.string.stats_latitude) + " "
+ StringUtils.formatCoordinate(location.getLatitude()) + ", "
+ getString(R.string.stats_longitude) + " "
+ StringUtils.formatCoordinate(location.getLongitude()) + "]";
}
String value = "[" + getString(R.string.stats_latitude) + " "
+ StringUtils.formatCoordinate(marker.getLatitude()) + ", "
+ getString(R.string.stats_longitude) + " "
+ StringUtils.formatCoordinate(marker.getLongitude()) + "]";
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerLocation, value, addShadow);
}
}
@@ -27,15 +27,23 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.adapters.SensorsAdapter;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
/**
* A fragment to display track statistics to the user for a recorded {@link Track}.
@@ -43,36 +51,10 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
public class StatisticsRecordedFragment extends Fragment {
private static final String TRACK_ID_KEY = "trackId";
private TrackStatistics trackStatistics;
private String category = "";
private Track.Id trackId;
private ContentProviderUtils contentProviderUtils;
private StatisticsRecordedBinding viewBinding;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
Bundle bundle = new Bundle();
bundle.putParcelable(TRACK_ID_KEY, trackId);
@@ -82,18 +64,71 @@ public class StatisticsRecordedFragment extends Fragment {
return fragment;
}
private TrackStatistics trackStatistics;
private SensorStatistics sensorStatistics;
private SensorsAdapter sensorsAdapter;
private String category = "";
private Track.Id trackId;
private ContentProviderUtils contentProviderUtils;
private StatisticsRecordedBinding viewBinding;
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
}
if (key != null && updateUInecessary && isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trackId = getArguments().getParcelable(TRACK_ID_KEY);
contentProviderUtils = new ContentProviderUtils(getContext());
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sensorsAdapter = new SensorsAdapter(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
loadStatistics();
}
@@ -103,7 +138,13 @@ public class StatisticsRecordedFragment extends Fragment {
super.onDestroyView();
viewBinding = null;
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
public void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
public void loadStatistics() {
@@ -112,21 +153,26 @@ public class StatisticsRecordedFragment extends Fragment {
if (isResumed()) {
Track track = contentProviderUtils.getTrack(trackId);
trackStatistics = track != null ? track.getTrackStatistics() : null;
category = track != null ? track.getCategory() : "";
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
String newCategory = track != null ? track.getCategory() : "";
if (!category.equals(newCategory)) {
category = newCategory;
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
updateUI();
updateSensorUI();
}
});
}
}
private void updateUI() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
// Set total distance
{
double totalDistance = trackStatistics == null ? Double.NaN : trackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -147,9 +193,9 @@ public class StatisticsRecordedFragment extends Fragment {
// Set average speed/pace
{
double speed = trackStatistics != null ? trackStatistics.getAverageSpeed() : Double.NaN;
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
@@ -158,9 +204,9 @@ public class StatisticsRecordedFragment extends Fragment {
{
double speed = trackStatistics == null ? Double.NaN : trackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
@@ -169,9 +215,9 @@ public class StatisticsRecordedFragment extends Fragment {
{
double speed = trackStatistics != null ? trackStatistics.getAverageMovingSpeed() : Double.NaN;
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
@@ -187,13 +233,35 @@ public class StatisticsRecordedFragment extends Fragment {
Pair<String, String> parts;
parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits);
parts = StringUtils.formatElevation(getContext(), elevationGain_m, preferenceMetricUnits);
viewBinding.statsElevationGainValue.setText(parts.first);
viewBinding.statsElevationGainUnit.setText(parts.second);
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, metricUnits);
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, preferenceMetricUnits);
viewBinding.statsElevationLossValue.setText(parts.first);
viewBinding.statsElevationLossUnit.setText(parts.second);
}
}
private void updateSensorUI() {
if (sensorStatistics == null) {
return;
}
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorStatistics.hasHeartRate()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_max, R.string.sensor_unit_beats_per_minute, sensorStatistics.getMaxHeartRate()));
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_avg, R.string.sensor_unit_beats_per_minute, sensorStatistics.getAvgHeartRate()));
}
if (sensorStatistics.hasCadence()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_max, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getMaxCadence()));
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_avg, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getAvgCadence()));
}
if (sensorStatistics.hasPower()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_power_avg, R.string.sensor_unit_power, sensorStatistics.getAvgPower()));
}
if (sensorDataList.size() > 0) {
sensorsAdapter.swapData(sensorDataList);
}
}
}
@@ -10,10 +10,12 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -25,18 +27,17 @@ import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
/**
* A fragment to display track statistics to the user for a currently recording {@link Track}.
@@ -44,54 +45,78 @@ import de.dennisguse.opentracks.util.UnitConversions;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
//TODO isRecording should not be relevant anymore as we now have StatisticRecordedFragment.
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
public class StatisticsRecordingFragment extends Fragment implements TrackDataListener {
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND_MS;
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private TrackDataHub trackDataHub;
private Handler handlerUpdateUI;
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private TrackPoint lastTrackPoint;
private TrackStatistics lastTrackStatistics;
private String category = "";
private StatisticsRecordingBinding viewBinding;
private SensorsAdapter sensorsAdapter;
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private boolean preferenceShowElevation;
private boolean preferenceShowCoordinate;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_elevation_key, key)) {
updateUInecessary = true;
preferenceShowElevation = PreferencesUtils.isShowStatsElevation(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_coordinate_key, key)) {
updateUInecessary = true;
preferenceShowCoordinate = PreferencesUtils.isStatsShowCoordinate(sharedPreferences, getContext());
}
if (key != null && updateUInecessary && isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTrackStatistics != null) {
synchronized (this) {
if (lastTrackPoint != null && lastTrackPoint.hasLocation() && !lastTrackPoint.isRecent()) {
lastTrackPoint = null;
setLocationValues();
}
}
updateTotalTime();
updateSensorDataUI();
}
@@ -102,27 +127,38 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
};
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handlerUpdateUI = new Handler();
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sensorsAdapter = new SensorsAdapter(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(getContext());
updateUIeachSecond.run();
handlerUpdateUI.post(updateUIeachSecond);
}
@@ -130,7 +166,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
public void onPause() {
super.onPause();
pauseTrackDataHub();
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
}
@@ -151,6 +188,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection = null;
sharedPreferences = null;
}
@Override
@@ -159,7 +197,11 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
getActivity().runOnUiThread(() -> {
if (isResumed()) {
lastTrackStatistics = track != null ? track.getTrackStatistics() : null;
category = track != null ? track.getCategory() : "";
String newCategory = track != null ? track.getCategory() : "";
if (!category.equals(newCategory)) {
category = newCategory;
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
updateUI();
}
});
@@ -172,33 +214,32 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
// We don't care.
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint newLastTrackPoint) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastTrackPoint = null;
}
synchronized (this) {
this.lastTrackPoint = newLastTrackPoint;
TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now.
if (trackPoint != null) {
boolean hasFix = !LocationUtils.isLocationOld(trackPoint.getLocation());
if (!hasFix) {
lastTrackPoint = null;
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
this.lastTrackPoint = null;
}
if (this.lastTrackPoint != null && this.lastTrackPoint.hasLocation() && !this.lastTrackPoint.isRecent()) {
this.lastTrackPoint = null;
}
setLocationValues();
}
setLocationValues();
}
});
}
@@ -271,15 +312,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
} else {
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
if (sensorDataSet != null) {
List<Pair<Integer, SensorData>> sensorDataList = new ArrayList<>();
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorDataSet.getHeartRate() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.HEART_RATE_TYPE, sensorDataSet.getHeartRate()));
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
}
if (sensorDataSet.getCyclingCadence() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.CADENCE_TYPE, sensorDataSet.getCyclingCadence()));
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
}
if(sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.POWER_TYPE, sensorDataSet.getCyclingPower()));
if (sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
}
sensorsAdapter.swapData(sensorDataList);
setSpeedSensorData(sensorDataSet);
@@ -292,8 +333,6 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set elevation gain
private void setTotalElevationGain(Float elevationGain_m) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
Float totalElevationGain = elevationGain_m;
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationGain()) {
@@ -304,15 +343,13 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, preferenceMetricUnits);
viewBinding.statsElevationGainValue.setText(parts.first);
viewBinding.statsElevationGainUnit.setText(parts.second);
}
// Set elevation loss
private void setTotalElevationLoss(Float elevationLoss_m) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
Float totalElevationLoss = elevationLoss_m;
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationLoss()) {
@@ -323,7 +360,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, preferenceMetricUnits);
viewBinding.statsElevationLossValue.setText(parts.first);
viewBinding.statsElevationLossUnit.setText(parts.second);
}
@@ -332,8 +369,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
if (data.hasSpeed_mps() && data.isRecent()) {
setSpeed(data.getSpeed_mps());
if (data.hasValue() && data.isRecent()) {
setSpeed(data.getValue());
}
}
}
@@ -341,14 +378,10 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private void updateStats() {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
boolean isRecording = isSelectedTrackRecording();
// Set total distance
{
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -356,7 +389,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set activity type
{
viewBinding.statsActivityTypeIcon.setEnabled(isRecording);
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
}
@@ -369,9 +402,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set average speed/pace
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
@@ -380,9 +413,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
{
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
@@ -391,85 +424,65 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
// Set elevation gain and loss
{
// Make elevation visible?
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
}
}
private void updateTotalTime() {
long totalTime;
Duration totalTime = lastTrackStatistics.getTotalTime();
if (isSelectedTrackRecording()) {
totalTime = calculateTotalTime();
} else {
totalTime = lastTrackStatistics.getTotalTime();
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
totalTime = trackRecordingService.getTotalTime();
}
}
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
}
/**
* Return time from service.
* If service isn't bound then use lastTrackStatistics for calculate it.
*/
private long calculateTotalTime() {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
return trackRecordingService.getTotalTime();
} else {
return System.currentTimeMillis() - lastTrackStatistics.getStopTime_ms() + lastTrackStatistics.getTotalTime();
}
}
private void setLocationValues() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
// Set speed/pace
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
setSpeed(speed);
// Set elevation
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
if (showElevation) {
if (preferenceShowElevation) {
// Current elevation
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsElevationCurrentValue.setText(parts.first);
viewBinding.statsElevationCurrentUnit.setText(parts.second);
}
// Set coordinate
boolean showCoordinate = PreferencesUtils.isStatsShowCoordinate(getContext());
viewBinding.statsCoordinateGroup.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
if (showCoordinate) {
double latitude = lastTrackPoint != null ? lastTrackPoint.getLatitude() : Double.NaN;
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
viewBinding.statsCoordinateGroup.setVisibility(preferenceShowCoordinate ? View.VISIBLE : View.GONE);
if (preferenceShowCoordinate) {
String latitudeText = getContext().getString(R.string.value_unknown);
String longitudeText = getContext().getString(R.string.value_unknown);
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLongitude());
}
viewBinding.statsLatitudeValue.setText(latitudeText);
double longitude = lastTrackPoint != null ? lastTrackPoint.getLongitude() : Double.NaN;
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
viewBinding.statsLongitudeValue.setText(longitudeText);
}
}
private void setSpeed(double speed) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
viewBinding.statsSpeedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsSpeedValue.setText(parts.first);
viewBinding.statsSpeedUnit.setText(parts.second);
}

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