Compare commits

...

76 Commits

Author SHA1 Message Date
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
Dennis Guse 163af2dab2 Release: v3.14.3 2021-01-24 22:08:26 +01:00
Dennis Guse 393e2022a1 Bugfix: database downgrade from 30 to 29 removed required elevation_gain column. 2021-01-24 22:06:48 +01:00
Dennis Guse 793928a5a6 Release: v3.14.2 2021-01-24 21:27:25 +01:00
Dennis Guse 50bb0b14b4 Segments: database upgrade/downgrade functionality for version 30; version 29 is still used.
This will store segment data in TrackPoint.type.
This commit will enable downgrading to previous version.

#316.
2021-01-24 21:14:50 +01:00
Román Martínez 8a08e7b2b5 Creates thumbnail from marker's photos for marker's list.
These thumbnails are created and loaded async using cache from free memory available.
Fixes #545.
2021-01-24 17:43:18 +01:00
FTno e9e9fdcc3f Translated using Weblate (Norwegian Bokmål)
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/nb_NO/
2021-01-20 07:52:54 +01:00
FTno 83c471ccb6 Translated using Weblate (Norwegian Bokmål)
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/nb_NO/
2021-01-20 07:52:54 +01:00
FTno 17479c438b Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.6% (514 of 516 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-01-20 07:52:54 +01:00
FTno 943c80f9ec Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.6% (514 of 516 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-01-20 07:52:54 +01:00
FTno 23751306a4 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.4% (513 of 516 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-01-20 07:52:54 +01:00
dependabot[bot] 78b712a291 Bump gradle from 4.1.1 to 4.1.2
Bumps gradle from 4.1.1 to 4.1.2.

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-20 07:52:48 +01:00
dependabot[bot] f8432adaa0 Bump mockito-android from 3.7.0 to 3.7.7
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.7.0 to 3.7.7.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.7.0...v3.7.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-18 17:27:56 +01:00
Dennis Guse 80b26f2a04 MarkerDetail accepts Marker.Id also as long.
Fixes #567.
2021-01-17 17:54:07 +01:00
Dennis Guse 41e6e80cfc Statistics: fix position of moving time. 2021-01-16 23:09:15 +01:00
dependabot[bot] 7e72b29c3f Bump espresso-core from 3.4.0-alpha02 to 3.4.0-alpha03
Bumps espresso-core from 3.4.0-alpha02 to 3.4.0-alpha03.

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-14 12:28:38 +01:00
Román Martínez 860df0197e ImportActivity offers to open track option (when only one track is imported). Fixes #558. 2021-01-10 20:40:14 +01:00
Román Martínez f1d791a62c Import/Export summary messages improved (only show useful data). 2021-01-10 20:40:14 +01:00
Dennis Guse e9551bea36 Introduced TrackPoint.Id. 2021-01-10 20:25:59 +01:00
Dennis Guse 577b978a5c Cleanup. 2021-01-10 20:25:59 +01:00
Dennis Guse a689b6eb2c Cleanup: removed descending iterator for TrackPoints. 2021-01-10 20:25:59 +01:00
Dennis Guse ccd93dd17d Track.Id must now always be valid (if present). 2021-01-10 20:25:59 +01:00
Dennis Guse f4fe1d8d97 Marker.Id must now always be valid (if present). 2021-01-10 20:25:59 +01:00
Dennis Guse d1290c8f22 Prevent re-import should be translatable. 2021-01-09 12:51:12 +01:00
dependabot[bot] a340cb57de Bump mockito-android from 3.6.28 to 3.7.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.6.28 to 3.7.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.6.28...v3.7.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-06 18:56:18 +01:00
196 changed files with 5695 additions and 2842 deletions
+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
+7 -7
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
classpath 'com.android.tools.build:gradle:4.1.2'
}
}
@@ -51,8 +51,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3799
versionName "v3.14.1"
versionCode 3875
versionName "v3.15.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -86,18 +86,18 @@ android {
}
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02'
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.6.28'
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,10 @@
v3.14.2: OpenTracks
Changes:
- Downgrade database from upcoming release v3.15.0
- Import/Export UI improved
- Import offers to show imported track
- Marker list performance improved
Developer:
- Track.Id/Marker.Id: use null
@@ -0,0 +1,4 @@
v3.14.3: OpenTracks
Bugfix:
- Downgrading database from upcoming release v3.15.0
@@ -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
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,14 @@
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;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.FileUtils;
@@ -36,19 +38,65 @@ public class TestDataUtil {
* @param trackId the trackId of the track
* @param numPoints the trackPoints number in the track
*/
public static Pair<Track, TrackPoint[]> createTrack(Track.Id trackId, int numPoints) {
@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);
TrackPoint[] trackPoints = new TrackPoint[numPoints];
List<TrackPoint> trackPoints = new ArrayList<>(numPoints);
for (int i = 0; i < numPoints; i++) {
trackPoints[i] = createTrackPoint(i);
trackPoints.add(createTrackPoint(i));
}
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, TrackPoint[]> pair = createTrack(trackId, numPoints);
Pair<Track, List<TrackPoint>> pair = createTrack(trackId, numPoints);
insertTrackWithLocations(contentProviderUtils, pair.first, pair.second);
@@ -61,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);
@@ -77,31 +125,29 @@ 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.
*
* @param track track to be inserted
* @param trackPoints trackPoints to be inserted
*/
public static void insertTrackWithLocations(ContentProviderUtils contentProviderUtils, Track track, TrackPoint[] trackPoints) {
public static void insertTrackWithLocations(ContentProviderUtils contentProviderUtils, Track track, List<TrackPoint> trackPoints) {
contentProviderUtils.insertTrack(track);
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);
}
}
@@ -33,10 +33,12 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
@@ -64,6 +66,7 @@ import static org.mockito.Mockito.when;
*/
@RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsTest {
private static final String NAME_PREFIX = "test name";
private static final String MOCK_DESC = "Mock Next Marker Desc!";
private static final String TEST_DESC = "Test Desc!";
@@ -87,50 +90,37 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_noPoints() {
testIterator(new Track.Id(1), 0, 1, false);
testIterator(new Track.Id(1), 0, 1);
}
@Test
public void testLocationIterator_noBatchAscending() {
testIterator(new Track.Id(1), 50, 100, false);
testIterator(new Track.Id(2), 50, 50, false);
}
@Test
public void testLocationIterator_noBatchDescending() {
testIterator(new Track.Id(1), 50, 100, true);
testIterator(new Track.Id(2), 50, 50, true);
testIterator(new Track.Id(1), 50, 100);
testIterator(new Track.Id(2), 50, 50);
}
@Test
public void testLocationIterator_batchAscending() {
testIterator(new Track.Id(1), 50, 11, false);
testIterator(new Track.Id(2), 50, 25, false);
}
@Test
public void testLocationIterator_batchDescending() {
testIterator(new Track.Id(1), 50, 11, true);
testIterator(new Track.Id(2), 50, 25, true);
testIterator(new Track.Id(1), 50, 11);
testIterator(new Track.Id(2), 50, 25);
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(new Track.Id(1), 20000, 2000, false);
testIterator(new Track.Id(1), 20000, 2000);
}
private void testIterator(Track.Id trackId, int numPoints, int batchSize, boolean descending) {
private void testIterator(Track.Id trackId, int numPoints, int batchSize) {
long lastPointId = initializeTrack(trackId, numPoints);
contentProviderUtils.setDefaultCursorBatchSize(batchSize);
List<TrackPoint> locations = new ArrayList<>(numPoints);
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending)) {
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
while (it.hasNext()) {
TrackPoint loc = it.next();
assertNotNull(loc);
locations.add(loc);
TrackPoint trackPoint = it.next();
assertNotNull(trackPoint);
locations.add(trackPoint);
// Make sure the IDs are returned in the right order.
assertEquals(descending ? lastPointId - locations.size() + 1
: lastPointId - numPoints + locations.size(), it.getTrackPointId());
assertEquals(lastPointId - numPoints + locations.size(), trackPoint.getId().getId());
}
assertEquals(numPoints, locations.size());
}
@@ -144,24 +134,24 @@ public class CustomContentProviderUtilsTest {
track = contentProviderUtils.getTrack(id);
assertNotNull(track);
TrackPoint[] trackPoints = new TrackPoint[numPoints];
List<TrackPoint> trackPoints = new ArrayList<>(numPoints);
for (int i = 0; i < numPoints; ++i) {
Location loc = new Location("test");
loc.setLatitude(37.0 + (double) i / 10000.0);
loc.setLongitude(57.0 - (double) i / 10000.0);
loc.setAccuracy((float) i / 100.0f);
loc.setAltitude(i * 2.5);
trackPoints[i] = new TrackPoint(loc);
trackPoints.add(new TrackPoint(loc));
}
contentProviderUtils.bulkInsertTrackPoint(trackPoints, id);
// Load all inserted trackPoints.
long lastPointId = -1;
int counter = 0;
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(id, -1L, false)) {
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(id, null)) {
while (it.hasNext()) {
it.next();
lastPointId = it.getTrackPointId();
TrackPoint trackPoint = it.next();
lastPointId = trackPoint.getId().getId();
counter++;
}
}
@@ -243,7 +233,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();
@@ -282,7 +272,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);
@@ -294,7 +284,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.
@@ -325,12 +315,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);
@@ -433,16 +423,14 @@ public class CustomContentProviderUtilsTest {
@Test
public void testCreateContentValues_marker() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
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);
@@ -452,7 +440,7 @@ public class CustomContentProviderUtilsTest {
track.first.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track.first);
Marker marker = new Marker(trackId, track.second[0]);
Marker marker = new Marker(trackId, track.second.get(0));
marker.setDescription(TEST_DESC);
contentProviderUtils.insertMarker(marker);
@@ -539,7 +527,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.
@@ -673,7 +661,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)));
@@ -708,7 +696,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)));
@@ -742,9 +730,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);
@@ -767,20 +755,20 @@ public class CustomContentProviderUtilsTest {
}
/**
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(TrackPoint[], Track.Id)}.
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(List, Track.Id)}.
*/
@Test
public void testBulkInsertTrackPoint() {
// given
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track.first, track.second);
// when / then
contentProviderUtils.bulkInsertTrackPoint(track.second, trackId);
assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
contentProviderUtils.bulkInsertTrackPoint(Arrays.copyOfRange(track.second, 0, 8), trackId);
assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, null, 1000).getCount());
contentProviderUtils.bulkInsertTrackPoint(track.second.subList(0, 8), trackId);
assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, null, 1000).getCount());
}
/**
@@ -821,7 +809,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());
}
@@ -875,104 +863,45 @@ public class CustomContentProviderUtilsTest {
checkLocation(9, lastTrackPoint.getLocation());
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointCursor(Track.Id, long, int, boolean)} in descending.
*/
@Test
public void testGetTrackPointCursor_desc() {
// given
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
}
// when
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true);
// then
assertEquals(2, cursor.getCount());
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointCursor(Track.Id, long, int, boolean)} in ascending.
*/
@Test
public void testGetTrackPointCursor_asc() {
// given
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
}
List<TrackPoint.Id> trackpointIds = track.second.stream()
.map(it -> ContentUris.parseId(contentProviderUtils.insertTrackPoint(it, track.first.getId())))
.map(TrackPoint.Id::new).collect(Collectors.toList());
// when
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false);
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds.get(8), 5);
// then
assertEquals(2, cursor.getCount());
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(Track.Id, long, boolean)} in descending.
*/
@Test
public void testGetTrackPointLocationIterator_desc() {
// given
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
}
long startTrackPointId = trackpointIds[9];
// when
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true);
// then
for (int i = 0; i < trackpointIds.length; i++) {
assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
assertEquals(startTrackPointId - i, trackPointIterator.getTrackPointId());
checkLocation((trackpointIds.length - 1) - i, trackPoint.getLocation());
}
assertFalse(trackPointIterator.hasNext());
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(Track.Id, long, boolean)} in ascending.
*/
@Test
public void testGetTrackPointLocationIterator_asc() {
// given
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
}
List<TrackPoint.Id> trackpointIds = track.second.stream()
.map(it -> ContentUris.parseId(contentProviderUtils.insertTrackPoint(it, track.first.getId())))
.map(TrackPoint.Id::new).collect(Collectors.toList());
long startTrackPointId = trackpointIds[0];
TrackPoint.Id startTrackPointId = trackpointIds.get(0);
// when
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false);
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, trackpointIds.get(0));
// then
for (int i = 0; i < trackpointIds.length; i++) {
for (int i = 0; i < trackpointIds.size(); i++) {
assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
assertEquals(startTrackPointId + i, trackPointIterator.getTrackPointId());
assertEquals(startTrackPointId.getId() + i, trackPoint.getId().getId());
checkLocation(i, trackPoint.getLocation());
}
@@ -5,6 +5,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -14,7 +15,9 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.dennisguse.opentracks.content.data.MarkerColumns;
@@ -165,6 +168,101 @@ public class CustomSQLiteOpenHelperTest {
}
}
@Test
public void upgrade_data_to_30() {
// given: a track in version 29
createVersion23();
try (SQLiteDatabase db29 = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 29).getWritableDatabase()) {
db29.beginTransaction();
db29.execSQL("INSERT INTO tracks (_id) VALUES (1)");
// Record -> stop
db29.execSQL("INSERT INTO tracks (_id) VALUES (2)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (2, 2.1, 2.1)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (2, 2.2, 2.2)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (2, 2.3, 2.3)");
// Record -> pause -> stop
db29.execSQL("INSERT INTO tracks (_id) VALUES (3)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (3, 3.1, 3.1)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (3, 3.2, 3.2)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (3, 0.0, 100)"); //manual PAUSE
// Record -> segment marker (distance to previous) -> pause -> resume -> stop
db29.execSQL("INSERT INTO tracks (_id) VALUES (4)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.1, 4.1)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.2, 4.2)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 0.0, 100 * 1E6)"); //SegmentEndMarker; will be deleted
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.3, 4.3)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.4, 4.4)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 0.0, 100 * 1E6)"); //manual PAUSE
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 0.0, 200 * 1E6)"); //manual RESUME
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.5, 4.5)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.6, 4.6)");
db29.setTransactionSuccessful();
db29.endTransaction();
}
// when / then
try (SQLiteDatabase db30 = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 30).getWritableDatabase()) {
{
// Track 1
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("trackpoints");
queryBuilder.appendWhere("trackid = 1");
try (Cursor cursor = queryBuilder.query(db30, null, null, null, null, null, "_id")) {
assertEquals(0, cursor.getCount());
}
}
{
// Track 2
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("trackpoints");
queryBuilder.appendWhere("trackid = 2");
try (Cursor cursor = queryBuilder.query(db30, null, null, null, null, null, "_id")) {
assertEquals(3, cursor.getCount());
}
}
{
// Track 3
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("trackpoints");
queryBuilder.appendWhere("trackid = 3");
try (Cursor cursor = queryBuilder.query(db30, null, null, null, null, null, "_id")) {
assertEquals(3, cursor.getCount());
}
}
{
// Track 4
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("trackpoints");
queryBuilder.appendWhere("trackid = 4");
try (Cursor cursor = queryBuilder.query(db30, null, null, null, null, null, "_id")) {
assertEquals(8, cursor.getCount());
List<Integer> types = new ArrayList<>();
List<Double> latitude = new ArrayList<>();
cursor.moveToFirst();
do {
types.add(cursor.getInt(cursor.getColumnIndexOrThrow("type")));
if (!cursor.isNull(cursor.getColumnIndexOrThrow("latitude"))) {
latitude.add(cursor.getDouble(cursor.getColumnIndexOrThrow("latitude")));
} else {
latitude.add(-1.0);
}
} while (cursor.moveToNext());
assertEquals(List.of(0, 0, -1, 0, 1, -2, 0, 0), types);
assertEquals(List.of(4.1, 4.2, 4.3, 4.4, -1.0, -1.0, 4.5, 4.6), latitude);
}
}
}
}
private void createVersion23() {
// Manually create database schema with version 23 (base version)
SQLiteDatabase dbBase = new SQLiteOpenHelper(context, DATABASE_NAME, null, 23) {
@@ -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;
@@ -18,34 +24,50 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
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);
@@ -54,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, 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(Arrays.asList(track.second));
trackId = service.startNewTrack();
for (int i = 0; i < 3; i++) {
Marker marker = new Marker(trackId, track.second[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
@@ -95,6 +123,7 @@ public class ExportImportTest {
}
}
@Ignore("Not implemented")
@LargeTest
@Test
public void kml_only_track() {
@@ -108,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
@@ -131,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
@@ -144,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
@@ -166,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
@@ -180,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
@@ -235,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
@@ -259,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
@@ -273,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
@@ -310,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);
}
@@ -344,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.")
@@ -328,7 +385,7 @@ public class TrackRecordingServiceTest {
}
private void addTrack(Track track, boolean isRecording) {
assertTrue(track.getId().isValid());
assertNotNull(track.getId());
contentProviderUtils.insertTrack(track);
assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId().getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
@@ -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;
@@ -203,7 +204,7 @@ public class TrackRecordingServiceTestLooper {
// Start a track.
Track.Id trackId = service.startNewTrack();
assertTrue(trackId.isValid());
assertNotNull(trackId);
assertTrue(service.isRecording());
Track track = contentProviderUtils.getTrack(trackId);
assertNotNull(track);
@@ -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,7 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.R;
@@ -26,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);
@@ -42,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 = Arrays.asList(TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second);
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)));
}
/**
@@ -170,29 +148,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
@@ -1,12 +1,9 @@
package de.dennisguse.opentracks.viewmodels;
import android.util.Pair;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -24,17 +21,16 @@ public class IntervalStatisticsTest {
private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) {
Pair<Track, TrackPoint[]> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints);
return Arrays.asList(pair.second);
return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second;
}
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.
@@ -120,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>
@@ -58,7 +58,15 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
Object bundleMarkerId = getIntent().getExtras().get(EXTRA_MARKER_ID);
Marker.Id markerId = null;
if (bundleMarkerId instanceof Marker.Id) {
markerId = (Marker.Id) bundleMarkerId;
}
if (bundleMarkerId instanceof Long) {
//Incoming Intent via Dashboard API.
markerId = new Marker.Id((Long) bundleMarkerId);
}
if (markerId == null) {
Log.d(TAG, "invalid marker id");
finish();
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
@@ -30,11 +29,12 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import de.dennisguse.opentracks.adapters.MarkerResourceCursorAdapter;
import de.dennisguse.opentracks.adapters.ScrollVisibleViews;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Track;
@@ -44,8 +44,6 @@ import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -81,7 +79,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
};
private Track track;
private ResourceCursorAdapter resourceCursorAdapter;
private MarkerResourceCursorAdapter resourceCursorAdapter;
private MarkerListBinding viewBinding;
@@ -123,33 +121,17 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
viewBinding.markerList.setEmptyView(viewBinding.markerListEmpty);
viewBinding.markerList.setOnItemClickListener((parent, view, position, id) -> {
resourceCursorAdapter.markerInvalid(id);
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Marker.Id(id));
startActivity(intent);
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int nameIndex = cursor.getColumnIndex(MarkerColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TIME);
int categoryIndex = cursor.getColumnIndex(MarkerColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(MarkerColumns.DESCRIPTION);
int photoUrlIndex = cursor.getColumnIndex(MarkerColumns.PHOTOURL);
int iconId = MarkerUtils.ICON_ID;
String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex);
String category = cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl);
}
};
resourceCursorAdapter = new MarkerResourceCursorAdapter(this, R.layout.list_item);
ScrollVisibleViews scrollVisibleViews = new ScrollVisibleViews(resourceCursorAdapter);
viewBinding.markerList.setOnScrollListener(scrollVisibleViews);
viewBinding.markerList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
loadData(getIntent());
}
@Override
@@ -163,6 +145,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
protected void onResume() {
super.onResume();
this.invalidateOptionsMenu();
loadData(getIntent());
}
@Override
@@ -171,6 +154,16 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
resourceCursorAdapter.clear();
viewBinding = null;
resourceCursorAdapter = null;
contentProviderUtils = null;
sharedPreferences = null;
}
@Override
protected View getRootView() {
viewBinding = MarkerListBinding.inflate(getLayoutInflater());
@@ -237,6 +230,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
if (itemId == R.id.list_context_menu_edit) {
if (markerIds.length == 1) {
resourceCursorAdapter.markerInvalid(markerIds[0].getId());
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent);
@@ -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 {
@@ -58,7 +58,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
trackRecordingServiceConnection = new TrackRecordingServiceConnection();
Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
if (!trackId.isValid()) {
if (trackId == null) {
Log.e(TAG, "invalid trackId");
finish();
return;
@@ -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);
@@ -233,7 +235,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingTrackPaused,
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
startTime, true, category, description, null);
startTime, true, category, description, false);
}
};
viewBinding.trackList.setAdapter(resourceCursorAdapter);
@@ -0,0 +1,79 @@
package de.dennisguse.opentracks.adapters;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* ExecutorService wrap that avoid the execution of the same object in a ListView.
*
* @param <T> the type of the object that will be used to identified the Runnable.
*/
class ExecutorListViewService<T> {
private List<T> enqueueObjects = new ArrayList<>();
private ExecutorService executorService;
public ExecutorListViewService(int numThreads) {
executorService = Executors.newFixedThreadPool(numThreads);
}
public void shutdown() {
enqueueObjects.clear();
enqueueObjects = null;
executorService.shutdown();
}
/**
* Execute the runnable for the object.
*
* @param object the Object.
* @param runnable the Runnable.
*/
public void execute(T object, Runnable runnable) {
if (!preExecute(object)) {
return;
}
new Thread(() -> {
Future future = executorService.submit(runnable);
try {
future.get();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
postExecute(object);
}).start();
}
/**
* Before execution it must checks if the object is already enqueued.
*
* @param object the object to check.
* @return true if it can be executed or false otherwise.
*/
private boolean preExecute(T object) {
synchronized (enqueueObjects) {
if (!enqueueObjects.contains(object)) {
enqueueObjects.add(object);
return true;
} else {
return false;
}
}
}
/**
* After execution remove the object from the queue.
*
* @param object the object to be removed from the queue.
*/
private void postExecute(T object) {
synchronized (enqueueObjects) {
enqueueObjects.remove(object);
}
}
}
@@ -0,0 +1,185 @@
package de.dennisguse.opentracks.adapters;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.collection.LruCache;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import java.io.IOException;
import java.io.InputStream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implements ScrollVisibleViews.VisibleViewsListener {
private static final String TAG = MarkerResourceCursorAdapter.class.getSimpleName();
private static final int LIST_PREFERRED_ITEM_HEIGHT_DEFAULT = 128;
private final Activity activity;
//TODO Should be Marker.Id
private final ExecutorListViewService executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
private boolean scroll = false;
// Cache size is in bytes.
private final LruCache<String, Bitmap> memoryCache;
public MarkerResourceCursorAdapter(Activity activity, int layout) {
super(activity, layout, null, 0);
this.activity = activity;
// Get max available VM memory, exceeding this amount will throw an OutOfMemory exception.
final long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
final int cacheSize = (int) (Runtime.getRuntime().maxMemory() - usedMemory) / 8;
memoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap bitmap) {
return bitmap.getByteCount();
}
};
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
int idIndex = cursor.getColumnIndex(MarkerColumns._ID);
int nameIndex = cursor.getColumnIndex(MarkerColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TIME);
int categoryIndex = cursor.getColumnIndex(MarkerColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(MarkerColumns.DESCRIPTION);
int photoUrlIndex = cursor.getColumnIndex(MarkerColumns.PHOTOURL);
long id = cursor.getLong(idIndex);
int iconId = MarkerUtils.ICON_ID;
String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex);
String category = cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
view.setTag(String.valueOf(id));
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
ImageView imageView = view.findViewById(R.id.list_item_photo);
ImageView textGradient = view.findViewById(R.id.list_item_text_gradient);
imageView.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
imageView.setImageBitmap(null);
if (hasPhoto) {
int height = getPhotoHeight(activity);
ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.height = height;
imageView.setLayoutParams(params);
if (getBitmapFromMemCache(String.valueOf(id)) != null || !scroll) {
asyncLoadPhoto(view, imageView, photoUrl, id);
}
}
ListItemUtils.setListItem(activity, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, hasPhoto);
}
public void clear() {
executorService.shutdown();
}
@Override
public void onViewVisible(View view, int position) {
scroll = true;
Cursor cursor = getCursor();
if (!cursor.moveToPosition(position)) {
return;
}
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MarkerColumns._ID));
String photoUrl = cursor.getString(cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL));
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
if (hasPhoto) {
ImageView imageView = view.findViewById(R.id.list_item_photo);
asyncLoadPhoto(view, imageView, photoUrl, id);
}
}
public void markerInvalid(long id) {
memoryCache.remove(String.valueOf(id));
scroll = false;
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
synchronized (memoryCache) {
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap);
}
}
}
private Bitmap getBitmapFromMemCache(String key) {
return memoryCache.get(key);
}
/**
* It loads the photoUrl in the imageView from view.
* It takes the photo from cache or from storage if isn't in the cache.
*
* @param view item's view.
* @param imageView view object where photo will be loaded.
* @param photoUrl photo's url.
* @param id marker's id where photo belong.
*/
private void asyncLoadPhoto(View view, ImageView imageView, String photoUrl, long id) {
Bitmap photo = getBitmapFromMemCache(String.valueOf(id));
imageView.setImageBitmap(photo);
if (photo == null) {
executorService.execute(id, () -> {
try (InputStream inputStream = activity.getContentResolver().openInputStream(Uri.parse(photoUrl))) {
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
int height = getPhotoHeight(activity);
Log.d(TAG, "Width : " + (bitmap.getWidth() / (bitmap.getHeight() / height)) + " | Height: " + height);
final Bitmap finalPhoto = ThumbnailUtils.extractThumbnail(bitmap, bitmap.getWidth() / (bitmap.getHeight() / height), height);
addBitmapToMemoryCache(String.valueOf(id), finalPhoto);
if (view.getTag().equals(String.valueOf(id))) {
activity.runOnUiThread(() -> imageView.setImageBitmap(finalPhoto));
}
} catch (IOException e) {
Log.e(TAG, "Failed to image " + e);
}
});
}
}
/**
* Gets the photo height.
*
* @param context the context
*/
private static int getPhotoHeight(Context context) {
int[] attrs = new int[]{android.R.attr.listPreferredItemHeight};
TypedArray typeArray = context.obtainStyledAttributes(attrs);
int height = typeArray.getDimensionPixelSize(0, LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
typeArray.recycle();
return 2 * height;
}
}
@@ -0,0 +1,42 @@
package de.dennisguse.opentracks.adapters;
import android.view.View;
import android.widget.AbsListView;
import androidx.annotation.NonNull;
/**
* AbsListView.OnScrollListener class that can be used to know what views in a ListView are currently visible while scrolling.
*/
public class ScrollVisibleViews implements AbsListView.OnScrollListener {
private int from = -1;
private int to = -1;
private VisibleViewsListener visibleViewsListener;
public ScrollVisibleViews(@NonNull VisibleViewsListener visibleViewsListener) {
this.visibleViewsListener = visibleViewsListener;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
if (from >= 0 && to >= 0) {
for (int i = from; i < to; i++) {
View viewChild = view.getChildAt(i - from);
visibleViewsListener.onViewVisible(viewChild, i);
}
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
from = firstVisibleItem;
to = firstVisibleItem + visibleItemCount;
}
interface VisibleViewsListener {
void onViewVisible(View view, int position);
}
}
@@ -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;
}
@@ -76,10 +61,10 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
}
private class ViewHolder extends RecyclerView.ViewHolder {
TextView label;
TextView sensorValue;
TextView value;
TextView unit;
final TextView label;
final TextView sensorValue;
final TextView value;
final TextView unit;
public ViewHolder(@NonNull View itemView) {
super(itemView);
@@ -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
@@ -83,8 +82,8 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
// Track points sampling state
private int numLoadedPoints;
private long firstSeenTrackPointId;
private long lastSeenTrackPointId;
private TrackPoint.Id firstSeenTrackPointId;
private TrackPoint.Id lastSeenTrackPointId;
// Registered listeners
private ContentObserver tracksTableObserver;
@@ -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);
}
@@ -351,52 +347,53 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
}
int localNumLoadedTrackPoints = updateSamplingState ? numLoadedPoints : 0;
long localFirstSeenTrackPointId = updateSamplingState ? firstSeenTrackPointId : -1L;
long localLastSeenTrackPointIdId = updateSamplingState ? lastSeenTrackPointId : -1L;
long maxPointId = updateSamplingState ? -1L : lastSeenTrackPointId;
TrackPoint.Id localFirstSeenTrackPointId = updateSamplingState ? firstSeenTrackPointId : null;
TrackPoint.Id localLastSeenTrackPointIdId = updateSamplingState ? lastSeenTrackPointId : null;
TrackPoint.Id maxPointId = updateSamplingState ? null : lastSeenTrackPointId;
if (selectedTrackId == null) {
Log.w(TAG, "This should not happen, but it does"); //TODO
return;
}
long lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
TrackPoint.Id lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
int samplingFrequency = -1;
boolean includeNextPoint = false;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenTrackPointIdId + 1, false)) {
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, next)) {
while (trackPointIterator.hasNext()) {
TrackPoint trackPoint = trackPointIterator.next();
long trackPointId = trackPointIterator.getTrackPointId();
TrackPoint.Id trackPointId = trackPoint.getId();
// Stop if past the last wanted point
if (maxPointId != -1L && trackPointId > maxPointId) {
if (maxPointId != null && trackPointId.getId() > maxPointId.getId()) {
break;
}
if (localFirstSeenTrackPointId == -1) {
if (localFirstSeenTrackPointId == null) {
localFirstSeenTrackPointId = trackPointId;
}
if (samplingFrequency == -1) {
long numTotalPoints = Math.max(0L, lastTrackPointId - localFirstSeenTrackPointId);
long numTotalPoints = Math.max(0L, lastTrackPointId.getId() - localFirstSeenTrackPointId.getId()); //TODO That is an assumption; should be derived from the DB.
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);
}
}
@@ -421,23 +418,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
*/
private void resetSamplingState() {
numLoadedPoints = 0;
firstSeenTrackPointId = -1L;
lastSeenTrackPointId = -1L;
}
/**
* 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);
firstSeenTrackPointId = null;
lastSeenTrackPointId = null;
}
}
@@ -23,13 +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;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* 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
*/
@@ -41,51 +41,57 @@ 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;
}
/**
* May be null if the it was not loaded from the database.
*/
public @Nullable
Id getId() {
@Nullable
public Id getId() {
return id;
}
@@ -93,6 +99,10 @@ public final class Marker {
this.id = id;
}
public Instant getTime() {
return time;
}
public String getName() {
return name;
}
@@ -125,11 +135,88 @@ public final class Marker {
this.icon = icon;
}
public @NonNull
Track.Id getTrackId() {
@NonNull
public Track.Id getTrackId() {
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;
}
@@ -146,11 +233,6 @@ public final class Marker {
this.duration = duration;
}
public @NonNull
Location getLocation() {
return location;
}
public String getPhotoUrl() {
return photoUrl;
}
@@ -180,11 +262,6 @@ public final class Marker {
return id;
}
@Deprecated //TODO Use a Id of null instead
public boolean isValid() {
return id != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -26,7 +26,6 @@ import java.util.Objects;
import java.util.UUID;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* A track.
@@ -53,8 +52,8 @@ public class Track {
/**
* May be null if the track was not loaded from the database.
*/
public @Nullable
Id getId() {
@Nullable
public Id getId() {
return id;
}
@@ -127,11 +126,6 @@ public class Track {
return id;
}
@Deprecated //TODO Use a Track.Id of null instead
public boolean isValid() {
return id != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -16,18 +16,72 @@
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.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
*/
public class TrackPoint {
private final Location location;
private TrackPoint.Id id;
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;
@@ -35,65 +89,128 @@ 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);
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 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 createSegmentStartAutomatic() {
return createSegmentStartAutomaticWithTime(Instant.now());
}
public static TrackPoint createSegmentStartAutomaticWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_START_AUTOMATIC, time);
}
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;
}
/**
* May be null if the track was not loaded from the database.
*/
@Nullable
public TrackPoint.Id getId() {
return id;
}
public void setId(TrackPoint.Id id) {
this.id = id;
}
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);
}
location.setTime(time);
}
@Deprecated //See #316
public static TrackPoint createPause() {
return createPauseWithTime(System.currentTimeMillis());
}
@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);
}
@Deprecated //See #316
public static TrackPoint createResume() {
return createResumeWithTime(System.currentTimeMillis());
}
@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 @NonNull
Location getLocation() {
return location;
}
@@ -101,7 +218,7 @@ public class TrackPoint {
return elevationGain != null;
}
public float getElevationGain() {
public Float getElevationGain() {
return elevationGain;
}
@@ -121,88 +238,72 @@ 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 double getLongitude() {
return location.getLongitude();
}
public void setLongitude(double longitude) {
location.setLongitude(longitude);
}
public long getTime() {
return location.getTime();
}
public void setTime(long time) {
location.setTime(time);
public void setTime(Instant time) {
this.time = 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 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 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
@@ -249,6 +350,52 @@ 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 {
private final long id;
public Id(long id) {
this.id = id;
}
protected Id(Parcel in) {
id = in.readLong();
}
//TOOD Limit visibility to TrackRecordingService / ContentProvider
public long getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TrackPoint.Id id1 = (TrackPoint.Id) o;
return id == id1.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@NonNull
@Override
public String toString() {
return String.valueOf(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";
@@ -71,6 +69,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"
+ ")";
@@ -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);
@@ -23,13 +23,14 @@ import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
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;
@@ -43,6 +44,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;
@@ -64,8 +66,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;
@@ -80,20 +80,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.
*
@@ -135,19 +121,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));
@@ -200,13 +186,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)) {
@@ -224,7 +204,7 @@ public class ContentProviderUtils {
* @param trackId the track id.
*/
public Track getTrack(Track.Id trackId) {
if (trackId == null || !trackId.isValid()) {
if (trackId == null) {
return null;
}
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())}, null)) {
@@ -285,19 +265,22 @@ public class ContentProviderUtils {
ContentValues values = new ContentValues();
TrackStatistics trackStatistics = track.getTrackStatistics();
// Value < 0 indicates no id is available
if (track.getId() != null && track.getId().isValid()) {
if (track.getId() != null) {
values.put(TracksColumns._ID, track.getId().getId());
}
values.put(TracksColumns.UUID, UUIDUtils.toBytes(track.getUuid()));
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());
@@ -327,27 +310,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)));
}
@@ -385,10 +364,7 @@ public class ContentProviderUtils {
/**
* @return -1 if not able to get the next marker number.
*/
public int getNextMarkerNumber(Track.Id trackId) {
if (!trackId.isValid()) {
return -1;
}
public int getNextMarkerNumber(@NonNull Track.Id trackId) {
String[] projection = {MarkerColumns._ID};
String selection = MarkerColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
@@ -400,10 +376,7 @@ public class ContentProviderUtils {
return -1;
}
public Marker getMarker(Marker.Id markerId) {
if (!markerId.isValid()) {
return null;
}
public Marker getMarker(@NonNull Marker.Id markerId) {
try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.getId())}, MarkerColumns._ID, 1)) {
if (cursor != null && cursor.moveToFirst()) {
return createMarker(cursor);
@@ -419,14 +392,10 @@ public class ContentProviderUtils {
* @param minMarkerId the minimum marker id. null to ignore
* @param maxCount the maximum number of markers to return. -1 for no limit
*/
public Cursor getMarkerCursor(Track.Id trackId, @Nullable Marker.Id minMarkerId, int maxCount) {
if (!trackId.isValid()) {
return null;
}
public Cursor getMarkerCursor(@NonNull Track.Id trackId, @Nullable Marker.Id minMarkerId, int maxCount) {
String selection;
String[] selectionArgs;
if (minMarkerId != null && minMarkerId.isValid()) {
if (minMarkerId != null) {
selection = MarkerColumns.TRACKID + "=? AND " + MarkerColumns._ID + ">=?";
selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(minMarkerId.getId())};
} else {
@@ -450,12 +419,9 @@ 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) {
if (!trackId.isValid()) {
return 0;
}
String[] projection = new String[]{"count(*) AS count"};
String selection = MarkerColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
@@ -506,7 +472,7 @@ public class ContentProviderUtils {
ContentValues createContentValues(@NonNull Marker marker) {
ContentValues values = new ContentValues();
if (marker.getId() != null && marker.getId().isValid()) {
if (marker.getId() != null) {
values.put(MarkerColumns._ID, marker.getId().getId());
}
values.put(MarkerColumns.NAME, marker.getName());
@@ -517,18 +483,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());
@@ -559,7 +524,8 @@ public class ContentProviderUtils {
* @param indexes the cached trackPoints indexes
*/
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
TrackPoint trackPoint = new TrackPoint();
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);
@@ -568,7 +534,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));
@@ -603,22 +569,6 @@ public class ContentProviderUtils {
return trackPoint;
}
/**
* Inserts multiple trackPoints.
*
* @param trackPoints an array of trackPoints
* @param trackId the trackPoints id
* @return the number of trackPoints inserted
*/
//TODO Only used for testing; might be better to replace it.
public int bulkInsertTrackPoint(TrackPoint[] trackPoints, Track.Id trackId) {
ContentValues[] values = new ContentValues[trackPoints.length];
for (int i = 0; i < values.length; i++) {
values[i] = createContentValues(trackPoints[i], trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
//TODO Only used for file import; might be better to replace it.
public int bulkInsertTrackPoint(List<TrackPoint> trackPoints, Track.Id trackId) {
ContentValues[] values = new ContentValues[trackPoints.size()];
@@ -636,9 +586,6 @@ public class ContentProviderUtils {
*/
@Deprecated
public Track.Id getFirstTrackPointId(Track.Id trackId) {
if (!trackId.isValid()) {
return null;
}
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)) {
@@ -656,19 +603,15 @@ public class ContentProviderUtils {
* @param trackId the track id
*/
@Deprecated
public long getLastTrackPointId(Track.Id trackId) {
if (trackId == null || !trackId.isValid()) {
Log.w(TAG, "Fix callers who do this.");
return -1L;
}
public TrackPoint.Id getLastTrackPointId(@NonNull Track.Id trackId) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + 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 cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID));
return new TrackPoint.Id(cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)));
}
}
return -1L;
return null;
}
/**
@@ -678,18 +621,15 @@ public class ContentProviderUtils {
* @param location the location
* @return trackPoint id if the location is in the track. -1L otherwise.
*/
public long getTrackPointId(Track.Id trackId, Location location) {
if (!trackId.isValid()) {
return -1L;
}
public TrackPoint.Id getTrackPointId(Track.Id trackId, Location location) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(location.getTime())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID));
return new TrackPoint.Id(cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)));
}
}
return -1L;
return null;
}
/**
@@ -705,31 +645,22 @@ public class ContentProviderUtils {
* Creates a location cursor. The caller owns the returned cursor and is responsible for closing it.
*
* @param trackId the track id
* @param startTrackPointId the starting trackPoint id. -1L to ignore
* @param maxLocations maximum number of locations to return. -1 for no limit
* @param descending true to sort the result in descending order (latest location first)
* @param startTrackPointId the starting trackPoint id. `null` to ignore
* @param maxLocations maximum number of locations to return. `null` for no limit
*/
public Cursor getTrackPointCursor(Track.Id trackId, long startTrackPointId, int maxLocations, boolean descending) {
if (!trackId.isValid()) {
return null;
}
public Cursor getTrackPointCursor(Track.Id trackId, TrackPoint.Id startTrackPointId, Integer maxLocations) {
String selection;
String[] selectionArgs;
if (startTrackPointId >= 0) {
String comparison = descending ? "<=" : ">=";
selection = TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns._ID + comparison + "?";
selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(startTrackPointId)};
if (startTrackPointId != null) {
selection = TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns._ID + ">=?";
selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(startTrackPointId.getId())};
} else {
selection = TrackPointsColumns.TRACKID + "=?";
selectionArgs = new String[]{Long.toString(trackId.getId())};
}
String sortOrder = TrackPointsColumns._ID;
if (descending) {
sortOrder += " DESC";
}
if (maxLocations >= 0) {
String sortOrder = TrackPointsColumns.DEFAULT_SORT_ORDER;
if (maxLocations != null) {
sortOrder += " LIMIT " + maxLocations;
}
return getTrackPointCursor(null, selection, selectionArgs, sortOrder);
@@ -743,10 +674,7 @@ public class ContentProviderUtils {
*/
@Deprecated
public TrackPoint getLastValidTrackPoint(Track.Id trackId) {
if (!trackId.isValid()) {
return null;
}
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);
}
@@ -765,16 +693,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());
}
@@ -816,11 +747,10 @@ public class ContentProviderUtils {
* When done with iteration, {@link TrackPointIterator#close()} must be called.
*
* @param trackId the track id
* @param startTrackPointId the starting trackPoint id. -1L to ignore
* @param descending true to sort the result in descending order (latest location first)
* @param startTrackPointId the starting trackPoint id. `null` to ignore
*/
public TrackPointIterator getTrackPointLocationIterator(final Track.Id trackId, final long startTrackPointId, final boolean descending) {
return new TrackPointIterator(this, trackId, startTrackPointId, descending);
public TrackPointIterator getTrackPointLocationIterator(final Track.Id trackId, final TrackPoint.Id startTrackPointId) {
return new TrackPointIterator(this, trackId, startTrackPointId);
}
private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) {
@@ -848,7 +778,7 @@ public class ContentProviderUtils {
public List<TrackPoint> getTrackPoints(Track.Id trackId) {
List<TrackPoint> trackPoints = null;
try (Cursor trackPointCursor = getTrackPointCursor(trackId, -1L, -1, false)) {
try (Cursor trackPointCursor = getTrackPointCursor(trackId, null, null)) {
if (trackPointCursor != null) {
trackPointCursor.moveToFirst();
trackPoints = new ArrayList<>(trackPointCursor.getCount());
@@ -906,4 +836,32 @@ public class ContentProviderUtils {
public static String[] parseTrackIdsFromUri(Uri url) {
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
}
public SensorStatistics getSensorStats(Track.Id trackId) {
if (trackId == null) {
return null;
}
String[] projection = {
"MAX(" + TrackPointsColumns.SENSOR_HEARTRATE + ") max_hr",
"AVG(" + TrackPointsColumns.SENSOR_HEARTRATE + ") avg_hr",
"MAX(" + TrackPointsColumns.SENSOR_CADENCE + ") max_cadence",
"AVG(" + TrackPointsColumns.SENSOR_CADENCE + ") avg_cadence",
"AVG(" + TrackPointsColumns.SENSOR_POWER + ") avg_power"
};
String selection = TrackPointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
SensorStatistics sensorStatistics = null;
try (Cursor cursor = getTrackPointCursor(projection, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
sensorStatistics = new SensorStatistics(
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("max_hr")) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_hr")) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("max_cadence")) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_cadence")) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_power")) : null
);
}
}
return sensorStatistics;
}
}
@@ -329,10 +329,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 +341,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();
@@ -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";
@@ -79,6 +79,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 29:
upgradeFrom28to29(db);
break;
case 30:
upgradeFrom29to30(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -109,6 +112,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 28:
downgradeFrom29to28(db);
break;
case 29:
downgradeFrom30to29(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
@@ -330,4 +336,62 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.setTransactionSuccessful();
db.endTransaction();
}
/**
* Move TrackPoint type (segment start vs. segment end) into separate column.
*/
private void upgradeFrom29to30(SQLiteDatabase db) {
db.beginTransaction();
// TrackPoints
db.execSQL("ALTER TABLE trackpoints ADD COLUMN type TEXT CHECK(type IN (-2, -1, 0, 1))");
db.execSQL("UPDATE trackpoints SET type = -2, latitude = NULL, longitude = NULL WHERE latitude = 200 * 1E6");
db.execSQL("UPDATE trackpoints SET type = 1, latitude = NULL, longitude = NULL WHERE latitude = 100 * 1E6");
db.execSQL("UPDATE trackpoints SET type = 0 WHERE type IS NULL");
// PAUSE markers without RESUME were inserted automatically as segment markers if the distance between subsequent trackpoints was too great.
// Only stored data is time (local device time); not meaningful as trackpoints were stored with GPS time.
// 1. Mark there successors as SEGMENT_START_AUTOMATIC
db.execSQL(
"UPDATE trackpoints " +
"SET type = -1 " +
"WHERE type = 0 AND 1 = " +
"(SELECT type FROM trackpoints AS T1 " +
"WHERE trackpoints._id > t1._id " +
"AND trackpoints.trackid = t1.trackid " +
"ORDER BY t1._id DESC " +
"LIMIT 1)");
// 2. Delete old PAUSE trackpoint
db.execSQL(
"DELETE FROM trackpoints" +
" WHERE type = 1 AND -1 = " +
"(SELECT type FROM trackpoints AS T1 " +
"WHERE trackpoints._id < t1._id " +
"AND trackpoints.trackid = t1.trackid " +
"ORDER BY t1._id ASC " +
"LIMIT 1)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom30to29(SQLiteDatabase db) {
db.beginTransaction();
// TrackPoints
//TODO That does not restore deleted trackpoints
db.execSQL("UPDATE trackpoints SET latitude = 200 * 1E6, longitude = NULL WHERE type = -2");
db.execSQL("UPDATE trackpoints SET latitude = 100 * 1E6, longitude = NULL WHERE type = 1");
// TrackPoints; identical to upgradeFrom27to28()
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
}
@@ -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,22 +12,20 @@ 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();
private final ContentProviderUtils contentProviderUtils;
private final Track.Id trackId;
private final boolean descending;
private final CachedTrackPointsIndexes indexes;
private long lastTrackPointId = -1L;
private TrackPoint.Id lastTrackPointId = null;
private Cursor cursor;
public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, long startTrackPointId, boolean descending) {
public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) {
this.contentProviderUtils = contentProviderUtils;
this.trackId = trackId;
this.descending = descending;
cursor = getCursor(startTrackPointId);
indexes = cursor != null ? new CachedTrackPointsIndexes(cursor)
@@ -39,25 +37,21 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
*
* @param trackPointId the starting track point id
*/
private Cursor getCursor(long trackPointId) {
return contentProviderUtils.getTrackPointCursor(trackId, trackPointId, contentProviderUtils.getDefaultCursorBatchSize(), descending);
private Cursor getCursor(TrackPoint.Id trackPointId) {
return contentProviderUtils.getTrackPointCursor(trackId, trackPointId, contentProviderUtils.getDefaultCursorBatchSize());
}
/**
* Advances the cursor to the next batch. Returns true if successful.
*/
private boolean advanceCursorToNextBatch() {
long trackPointId = lastTrackPointId == -1L ? -1L : lastTrackPointId + (descending ? -1 : 1);
TrackPoint.Id trackPointId = lastTrackPointId == null ? null : new TrackPoint.Id(lastTrackPointId.getId() + 1);
Log.d(TAG, "Advancing track point id: " + trackPointId);
cursor.close();
cursor = getCursor(trackPointId);
return cursor != null;
}
public long getTrackPointId() {
return lastTrackPointId;
}
@Override
public boolean hasNext() {
if (cursor == null) {
@@ -85,7 +79,7 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
throw new NoSuchElementException();
}
}
lastTrackPointId = cursor.getLong(indexes.idIndex);
lastTrackPointId = new TrackPoint.Id(cursor.getLong(indexes.idIndex));
return ContentProviderUtils.fillTrackPoint(cursor, indexes);
}
@@ -4,25 +4,29 @@ 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 class SensorData<T> {
protected T value;
private final String sensorAddress;
private final String sensorName;
private final long timestamp_ms;
private final Instant timestamp_ms;
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 timestamp_ms) {
this.sensorAddress = sensorAddress;
this.sensorName = sensorName;
this.timestamp_ms = timestamp_ms;
@@ -42,10 +46,18 @@ 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(timestamp_ms.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;
}
}
@@ -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());
}
}
@@ -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(() -> {
@@ -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;
@@ -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}.
@@ -49,6 +57,8 @@ public class StatisticsRecordedFragment extends Fragment {
private static final String TRACK_ID_KEY = "trackId";
private TrackStatistics trackStatistics;
private SensorStatistics sensorStatistics;
private SensorsAdapter sensorsAdapter;
private String category = "";
private Track.Id trackId;
private ContentProviderUtils contentProviderUtils;
@@ -67,12 +77,6 @@ public class StatisticsRecordedFragment extends Fragment {
}
};
@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,6 +86,22 @@ public class StatisticsRecordedFragment extends Fragment {
return fragment;
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
sensorsAdapter = new SensorsAdapter(getContext());
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -112,8 +132,10 @@ public class StatisticsRecordedFragment extends Fragment {
if (isResumed()) {
Track track = contentProviderUtils.getTrack(trackId);
trackStatistics = track != null ? track.getTrackStatistics() : null;
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
category = track != null ? track.getCategory() : "";
updateUI();
updateSensorUI();
}
});
}
@@ -196,4 +218,26 @@ public class StatisticsRecordedFragment extends Fragment {
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);
}
}
}
@@ -14,6 +14,7 @@ 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,7 +26,6 @@ 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;
@@ -37,6 +37,7 @@ 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}.
@@ -191,8 +192,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
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 (trackPoint != null && trackPoint.hasLocation()) {
boolean hasFix = !LocationUtils.isTrackPointOld(trackPoint);
if (!hasFix) {
lastTrackPoint = null;
@@ -271,15 +272,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()));
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
}
sensorsAdapter.swapData(sensorDataList);
setSpeedSensorData(sensorDataSet);
@@ -332,8 +333,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());
}
}
}
@@ -407,26 +408,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
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();
}
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
}
private void setLocationValues() {
@@ -453,12 +443,13 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
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);
String latitudeText = getContext().getString(R.string.value_unknown);
String longitudeText = getContext().getString(R.string.value_unknown);
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
}
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);
}
}
@@ -6,12 +6,10 @@ import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.FileTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.GpxTrackWriter;
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
import de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.io.file.exporter.TrackWriter;
/**
* Definition of all possible track formats.
@@ -20,8 +18,8 @@ public enum TrackFileFormat {
KML_ONLY_TRACK {
@Override
public TrackWriter newTrackWriter(Context context) {
return new KmlTrackWriter(context, false, false, false);
public TrackExporter createTrackExporter(Context context) {
return new KMLTrackExporter(context, false, false, false);
}
@Override
@@ -35,8 +33,8 @@ public enum TrackFileFormat {
},
KML_WITH_TRACKDETAIL {
@Override
public TrackWriter newTrackWriter(Context context) {
return new KmlTrackWriter(context, true, false, false);
public TrackExporter createTrackExporter(Context context) {
return new KMLTrackExporter(context, true, false, false);
}
@Override
@@ -50,8 +48,8 @@ public enum TrackFileFormat {
},
KML_WITH_TRACKDETAIL_AND_SENSORDATA {
@Override
public TrackWriter newTrackWriter(Context context) {
return new KmlTrackWriter(context, true, true, false);
public TrackExporter createTrackExporter(Context context) {
return new KMLTrackExporter(context, true, true, false);
}
@Override
@@ -64,16 +62,12 @@ public enum TrackFileFormat {
}
},
KMZ_ONLY_TRACK {
private static final boolean exportPhotos = false;
@Override
public TrackWriter newTrackWriter(Context context) {
return new KmlTrackWriter(context, false, false, exportPhotos);
}
public TrackExporter newTrackExporter(Context context) {
return newKmzTrackExporter(context, this.newTrackWriter(context), exportPhotos);
public TrackExporter createTrackExporter(Context context) {
KMLTrackExporter exporter = new KMLTrackExporter(context, false, false, exportPhotos);
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
}
@Override
@@ -95,12 +89,9 @@ public enum TrackFileFormat {
private static final boolean exportPhotos = false;
@Override
public TrackWriter newTrackWriter(Context context) {
return new KmlTrackWriter(context, true, false, exportPhotos);
}
public TrackExporter newTrackExporter(Context context) {
return newKmzTrackExporter(context, this.newTrackWriter(context), exportPhotos);
public TrackExporter createTrackExporter(Context context) {
KMLTrackExporter exporter = new KMLTrackExporter(context, true, false, exportPhotos);
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
}
@Override
@@ -123,8 +114,9 @@ public enum TrackFileFormat {
private static final boolean exportPhotos = false;
@Override
public TrackWriter newTrackWriter(Context context) {
return new KmlTrackWriter(context, true, true, exportPhotos);
public TrackExporter createTrackExporter(Context context) {
KMLTrackExporter exporter = new KMLTrackExporter(context, true, true, exportPhotos);
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
}
@Override
@@ -132,10 +124,6 @@ public enum TrackFileFormat {
return MIME_KMZ;
}
public TrackExporter newTrackExporter(Context context) {
return newKmzTrackExporter(context, this.newTrackWriter(context), exportPhotos);
}
public String getExtension() {
return "kmz";
}
@@ -152,8 +140,9 @@ public enum TrackFileFormat {
private static final boolean exportPhotos = true;
@Override
public TrackWriter newTrackWriter(Context context) {
return new KmlTrackWriter(context, true, true, exportPhotos);
public TrackExporter createTrackExporter(Context context) {
KMLTrackExporter exporter = new KMLTrackExporter(context, true, true, exportPhotos);
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
}
@Override
@@ -161,10 +150,6 @@ public enum TrackFileFormat {
return MIME_KMZ;
}
public TrackExporter newTrackExporter(Context context) {
return newKmzTrackExporter(context, newTrackWriter(context), exportPhotos);
}
public String getExtension() {
return "kmz";
}
@@ -177,8 +162,8 @@ public enum TrackFileFormat {
},
GPX {
@Override
public TrackWriter newTrackWriter(Context context) {
return new GpxTrackWriter(context.getString(R.string.app_name));
public TrackExporter createTrackExporter(Context context) {
return new GPXTrackExporter(new ContentProviderUtils(context), context.getString(R.string.app_name));
}
@Override
@@ -195,31 +180,17 @@ public enum TrackFileFormat {
private static final String MIME_KML = "application/vnd.google-earth.kml+xml";
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, boolean exportPhotos) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter);
return new KmzTrackExporter(context, contentProviderUtils, fileTrackExporter, exportPhotos);
}
/**
* Returns the mime type for each format.
*/
public abstract String getMimeType();
public TrackExporter newTrackExporter(Context context) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = newTrackWriter(context);
return new FileTrackExporter(contentProviderUtils, trackWriter);
}
/**
* Creates a new track writer for the format.
*
* @param context the context
*/
public abstract TrackWriter newTrackWriter(Context context);
public abstract TrackExporter createTrackExporter(Context context);
/**
* Returns the file extension for each format.
@@ -234,7 +205,7 @@ public enum TrackFileFormat {
}
/**
* Returns the name of for each format.
* Returns the name for each format.
*/
public String getName() {
return this.name().toLowerCase(Locale.US);
@@ -268,11 +268,18 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
viewBinding.exportProgressTotal.setText("" + trackExportTotalCount);
viewBinding.exportProgressBar.setProgress((int) ((float) done / (float) trackExportTotalCount * 100f));
viewBinding.exportProgressSummary.setText(getString(R.string.export_progress_review, getTotalDone(), trackExportSuccessCount, trackExportOverwrittenCount, trackExportSkippedCount, trackExportErrorCount));
viewBinding.exportProgressSummaryNew.setText(String.valueOf(trackExportSuccessCount));
viewBinding.exportProgressSummaryOverwrite.setText(String.valueOf(trackExportOverwrittenCount));
viewBinding.exportProgressSummarySkip.setText(String.valueOf(trackExportSkippedCount));
viewBinding.exportProgressSummaryErrors.setText(String.valueOf(trackExportErrorCount));
viewBinding.exportProgressSummaryNewGroup.setVisibility(trackExportSuccessCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummaryOverwriteGroup.setVisibility(trackExportOverwrittenCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummarySkipGroup.setVisibility(trackExportSkippedCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummaryErrorsGroup.setVisibility(trackExportErrorCount > 0 ? View.VISIBLE : View.GONE);
}
private void onExportCompleted(Track.Id trackId) {
trackIds.remove(trackId);
trackIds.remove(trackId);
setProgress();
if (trackIds.isEmpty()) {
onExportEnded();

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