Compare commits

..

80 Commits

Author SHA1 Message Date
Dennis Guse ae4d2da262 Heartrate: ignore "0" if sent from sensor as this is an invalid value.
On import "0" is ignored.
2026-02-10 18:42:14 +01:00
Dennis Guse 708cbf0b2a Refactored Sensor subsystem 2026-02-10 18:30:15 +01:00
Dennis Guse 60e2fe57f9 Bugfix: SensorHandler may receive a connectionLost from BluetoothDriver without being connected first. 2026-02-10 18:13:13 +01:00
Dennis Guse 14cebf3fec Aggregator: there are no default values anymore. 2026-02-10 11:13:57 +01:00
Dennis Guse b651a8ca6a GPS is now optional: data source (internal, none) be selected in settings.
Migrated to GPSHandler.

Fixes #1279.
2026-02-10 10:07:41 +01:00
Dennis Guse d75472ef11 BluetoothLeSensorPreference was not adhering to configured default value. 2026-02-09 18:15:37 +01:00
Dennis Guse 0b849c3d55 CyclingDistanceSpeedHandler: uses wheel size (again).
Skipped while to SensorHandler: a5dcbec3961957dfc651cf4ffd62ec075324c0ba
2026-02-09 18:15:37 +01:00
Dennis Guse 1c767c41a7 DriverObserver: renamed methods and all drivers properly use onConnected, onConnectionLost and onDisconnected. 2026-02-09 18:15:37 +01:00
Dennis Guse dfa80d61d7 Restored functionality: sensor name/address is shown AFTER connecting. 2026-02-09 18:15:37 +01:00
Dennis Guse fa558c9ab7 Cleanup: SensorManager avoids instanceof for testing. 2026-02-09 18:15:37 +01:00
Dennis Guse a1a363698f SensorHandler now interacts with Aggregator. 2026-02-09 18:15:37 +01:00
Dennis Guse b79acfb942 Cleanup: SensorManager provides context and handler. 2026-02-09 18:15:37 +01:00
Dennis Guse 24ea718ff2 Cleanup: renamed GainManager to AltitudeChangeHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 6d187fa66d Cleanup: removed DriverObserver0 and moved sensor drivers to separate package. 2026-02-09 18:15:37 +01:00
Dennis Guse ea27db13fa Created RunningSpeedCadenceDistanceHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 29d2e7c78c Created CyclingPowerHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 4b7badbecc Created CyclingDistanceSpeedHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 0dcb8010e2 Created CyclingCadenceHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 0a9d4b207c Created TemperatureHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse c6cb388ce4 Created HeartRateHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse e6e009ba14 GainManager should only re-connect if sensor should be changed. 2026-02-09 18:15:37 +01:00
Dennis Guse f8aa456549 Extract SensorHandler from GainManager for re-use. 2026-02-09 18:15:37 +01:00
Dennis Guse 167d90332e Extract Driver for internal GPS to GpsInternal. 2026-02-09 18:15:37 +01:00
Dennis Guse 719a052f34 Cleanup: merged BluetoothRemoteSensorManager into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 244ca26210 Cleanup: created new DriverObserver interface. 2026-02-09 18:15:37 +01:00
Dennis Guse 476f3a517b Cleanup: GainManager now processes AggregatorBarometer directly (instead of SensorManager). 2026-02-09 18:15:37 +01:00
Dennis Guse 289e93eb5d Cleanup: moved AggregatorGPS to GPSManager. 2026-02-09 18:15:37 +01:00
Dennis Guse 18ff96733e Cleanup: moved Raw into Aggregator.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 0f2c5adf26 Cleanup: SensorManager.SensorDataChangedObserver inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 74954df600 Cleanup: SensorManager.SensorDataChangedObserver inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 1a14fed287 Cleanup: SensorDataAggregator inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 744853ee91 Cleanup: removed SensorConnector interface.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 597e1b2c7f Release: v4.25.2 2026-02-09 07:51:06 +01:00
jimkats 8156993921 Translated using Weblate (Greek)
Currently translated at 51.0% (278 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-02-08 22:49:35 +01:00
Dennis Guse 9890dc28bd Dashboard API: provide version 2 again (required by OSMDashboard until v6.4.1).
Part of #2265.
2026-02-08 22:48:43 +01:00
Dennis Guse b65d83010d Bugfix: ShareContentProvider should report contentType for all Uris (not only TrackFileFormat).
Part of #2265.
2026-02-08 18:11:52 +01:00
Dennis Guse 1a5d06f9c6 Cleanup: renamed IntentDashboardUtils.java to DataProvider. 2026-02-07 19:20:48 +01:00
Dennis Guse 723c21d7ec Dashboard API: add tests for column names.
Part of #2265.
2026-02-07 09:39:23 +01:00
Dennis Guse 8fe0c3be7d Bugfix: update data for UI immediately after starting recording. 2026-02-06 18:54:32 +01:00
Dennis Guse e64792bf3f Resetting preferences requires clearing and naming each preference file explicitly. 2026-02-06 18:54:18 +01:00
Dennis Guse 21bafff761 TrackRecordingService should start UI timer at the end of startRecording. 2026-02-06 18:54:06 +01:00
Dennis Guse c33c2de51d Cleanup: EspressoUI should always lookup UI elements. 2026-02-06 18:53:55 +01:00
Dennis Guse a6b11c1c37 Release: v4.25.1 2026-02-01 22:05:05 +01:00
Dennis Guse 315788308b Bumping DashboardAPI to version 3.
Fixes #2255.
2026-02-01 20:15:24 +01:00
Dennis Guse 547c1c5c65 Rename MarkerColumns.
Fixes #2255.
2026-02-01 20:15:24 +01:00
tace16 69efd6fb82 Translated using Weblate (Tamil)
Currently translated at 100.0% (545 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/ta/
2026-01-31 18:08:56 +01:00
ghose 3a0524f96d Translated using Weblate (Galician)
Currently translated at 100.0% (545 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2026-01-31 18:08:56 +01:00
Priit Jõerüüt 05456b44ef Translated using Weblate (Estonian)
Currently translated at 100.0% (545 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-01-31 18:08:56 +01:00
tace16 b449dad619 Translated using Weblate (Tamil)
Currently translated at 100.0% (179 of 179 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/ta/
2026-01-31 18:08:56 +01:00
SomeTr ccefe36489 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (545 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2026-01-31 18:08:56 +01:00
ovl-005 90e9ab6d2a Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.1% (535 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2026-01-31 18:08:56 +01:00
Outbreak2096 d1061fa6cd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 88.8% (159 of 179 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/zh_Hans/
2026-01-31 18:08:56 +01:00
Outbreak2096 c0c813001f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (545 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2026-01-31 18:08:56 +01:00
Outbreak2096 51498bbcf9 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (542 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2026-01-31 18:08:56 +01:00
CI/CD Bot by pstorch 4ef1fcefbc Update dependency gradle to v9.3.1 2026-01-29 15:35:43 +00:00
CI/CD Bot by pstorch 9a572130a7 Update dependency androidx.work:work-runtime to v2.11.1 2026-01-29 00:34:18 +00:00
Dennis Guse e931f5ab7c Reproducible build: for naming the APK use the actual versionCode.
Fixes #2252.
2026-01-23 15:11:37 +01:00
Dennis Guse 9affe28fad Release: v4.25.0 2026-01-21 18:33:40 +01:00
Dennis Guse 4905a37951 Build scripts: add some error handling. 2026-01-21 18:33:20 +01:00
Dennis Guse 439671d0a4 Build scripts: add some error handling. 2026-01-18 15:49:41 +01:00
Dennis Guse 825e8dda6e Revert "Translated using Weblate (English (en_001))"
This reverts commit 99a9248b27.
2026-01-18 10:13:00 +01:00
Dennis Guse 661c839c78 Update README_TESTED_SENSORS.md
Assioma Pro MX-2

Fixes #2246.
2026-01-18 10:08:42 +01:00
Dennis Guse 8eb42a7ecb Upgrade to Android Gradle Plugin 9.0: removing applicationVariants 2026-01-17 22:58:17 +01:00
CI/CD Bot by pstorch 660a55ff5a Update dependency gradle to v9.3.0 2026-01-16 12:45:31 +00:00
CI/CD Bot by pstorch 958697bcb3 Update dependency com.android.tools.build:gradle to v9 2026-01-15 17:42:07 +00:00
ntd 6029eb0317 Translated using Weblate (Italian)
Currently translated at 100.0% (542 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/it/
2026-01-10 21:48:12 +01:00
ntd 8b56d53a6a Translated using Weblate (Italian)
Currently translated at 3.9% (7 of 178 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/it/
2026-01-10 21:48:12 +01:00
Dennis Guse e9deebea6c Refactor: RecordingLayout. 2026-01-10 21:43:54 +01:00
Dennis Guse 54844b9a9a Sensor: add temperature.
Fixes #2241.
2026-01-10 21:43:54 +01:00
Dennis Guse 738201c1c2 Cleanup. 2026-01-04 09:57:05 +01:00
discollizard 48b6be0bd1 record button moved to the middle of the screen
Part of #2199
2026-01-03 18:40:17 -03:00
Priit Jõerüüt 6b924cd936 Translated using Weblate (Estonian)
Currently translated at 100.0% (542 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-01-02 19:56:27 +01:00
discollizard 5f083de530 adjusted distance time and markers to be horizontally consistent
formatter

initial date and time are always visible in the list while recording
2026-01-02 15:31:42 -03:00
Matt Votava 278f6323f1 track import: get extension from file name, not URI
When importing tracks from nextcloud, the URI for a file may be something like

content://org.nextcloud.documents/tree/7keh3ot7zmiaxecoktv6bfn6gdygt2lg%2F707/document/7keh3ot7zmiaxecoktv6bfn6gdygt2lg%2F745

The URI contains no ".", so getExtension() would return the whole URI as the extension, resulting in an "Unsupported file format" error.

To fix this, also pass DocumentFile.getName() to the ImportWorker.
2025-12-28 01:30:10 -08:00
Dennis Guse 5baa8fa21e Cleanup: KMLTrackImporter doesn't need Position at first. 2025-12-28 08:59:19 +01:00
Dennis Guse 707f4add71 Cleanup: GpsStatusManager only needs Position. 2025-12-28 08:59:19 +01:00
Dennis Guse b583f8f23d Sticker: added printable PDF 2025-12-27 08:38:01 +01:00
Priit Jõerüüt c847f622e4 Translated using Weblate (Estonian)
Currently translated at 91.3% (495 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2025-12-27 07:35:12 +01:00
Priit Jõerüüt 70dd58f484 Translated using Weblate (Estonian)
Currently translated at 78.9% (428 of 542 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2025-12-27 07:35:12 +01:00
Dennis Guse 015352c989 TrackPoint is now a record. 2025-12-24 17:15:19 +01:00
166 changed files with 4808 additions and 3245 deletions
+4 -1
View File
@@ -44,7 +44,10 @@ However, often only one value is provided.
We only support _Instantaneous Power_.
For cycling (incl. cadence):
* Assioma Duo Powermeter
* Assioma
* Duo Powermeter
* Pro MX-2
* Elite drivo II
* QUARQ Red DZero Powermeter
* Tacx Satori Smart
+2 -5
View File
@@ -11,9 +11,6 @@ CHANGELOG_FILE="$CHANGELOG_DIR/$VERSIONID.txt"
git diff --exit-code || echo -e "\nWARNING WARNING WARNING WARNING WARNING: this branch contains uncommit changes."
#TODO Execute all tests: we are destroying data, running on a real device.
# ./gradlew connectedCheck
# Changelog
echo -e "\nPrevious changelog\n------------"
PREVIOUS_CHANGELOG=$(find "$CHANGELOG_DIR" -iname "[[:digit:]]**.txt" | sort -n | tail -1)
@@ -34,8 +31,8 @@ gedit -w "$CHANGELOG_FILE"
git add "$CHANGELOG_FILE"
echo "Updating build.gradle"
sed -i s/versionCode\ .*/versionCode\ "$VERSIONID"/g build.gradle
sed -i s/versionName\ .*/versionName\ "\"$VERSIONNAME\""/g build.gradle
sed -i -e "s/versionCode\ [0-9]\{1,\}/versionCode\ $VERSIONID/g" build.gradle
sed -i -e "s/versionName\ \"v[0-9\.]\{1,\}\"/versionName\ \"$VERSIONNAME\"/g" build.gradle
git diff
+7 -2
View File
@@ -6,8 +6,13 @@ export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64/
JAVA_VERSION=`java --version`
if [[ $JAVA_VERSION != "openjdk 21."* ]]; then
echo "OpenJDK version should be 21.X"
exit -1;
echo -e "OpenJDK version should be 21.X"
fi
git diff --exit-code || echo -e "\nWARNING WARNING WARNING WARNING WARNING: this branch contains uncommit changes."
if [[ $(git log "$(git describe --tags --abbrev=0)..HEAD" --no-merges --oneline | wc -l) -gt 0 ]]; then
echo -e "No version tag for this commit; please use RELEASE.sh";
fi
RELEASE_STORE_PASSWORD=
+17 -22
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.2'
classpath 'com.android.tools.build:gradle:9.0.0'
}
}
@@ -21,7 +21,7 @@ def getVersionName = { ->
return 'git describe --tags'.execute().text.trim()
}
def getVersionCode = { ->
def getVersionCodeNightly = { ->
return Integer.valueOf('git rev-list HEAD --count'.execute().text.trim())
}
@@ -38,6 +38,7 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
resValues = true
}
androidResources {
@@ -46,8 +47,8 @@ android {
defaultConfig {
applicationId 'de.dennisguse.opentracks'
versionCode 6516
versionName "v4.24.2"
versionCode 6564
versionName "v4.25.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -106,12 +107,19 @@ android {
nightly {
dimension 'version'
applicationId 'de.dennisguse.opentracks.nightly'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.nightly'
versionCode getVersionCodeNightly()
versionName getVersionName()
base.archivesName = "de.dennisguse.opentracks.nightly_${getVersionCodeNightly()}"
signingConfig = signingConfigs.nightly
}
irreproducible {
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
dimension 'version'
applicationId 'de.dennisguse.opentracks'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks'
versionNameSuffix 'irreproducible'
}
reproducible {
@@ -120,27 +128,14 @@ android {
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
dimension 'version'
applicationId 'de.dennisguse.opentracks.playstore'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.playstore'
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
signingConfig = signingConfigs.release
}
}
applicationVariants.configureEach { variant ->
variant.resValue 'string', 'applicationId', variant.applicationId
variant.outputs.configureEach {
if (variant.flavorName == 'nightly') {
setVersionCodeOverride(getVersionCode())
setVersionNameOverride(getVersionName())
}
if (variant.flavorName == 'reproducible') {
outputFileName = "${applicationId}_${variant.versionName}.apk"
} else {
outputFileName = "${applicationId}_${variant.versionCode}.apk"
}
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
@@ -163,7 +158,7 @@ dependencies {
implementation 'androidx.core:core-splashscreen:1.2.0'
implementation 'androidx.mediarouter:mediarouter:1.8.1'
implementation 'androidx.core:core-location-altitude:1.0.0-beta01'
implementation 'androidx.work:work-runtime:2.11.0'
implementation 'androidx.work:work-runtime:2.11.1'
androidTestImplementation 'androidx.test:core:1.7.0'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
@@ -0,0 +1,13 @@
v4.25.0: OpenTracks
Changes:
- Record button is centered
- Support for BLE thermometer (Environmental Sensing)
Bugfix:
- On import: get type from filename (not URI)
Developer:
- TrackPoint is a record
- Update to Android Gradle Plugin 9.0
@@ -0,0 +1,7 @@
v4.25.1: OpenTracks
Bugfix:
- Since v4.24.0, the API for transferring data to OSM Dashboard was broken.
Sadly, the changes in OpenTracks could not be reverted.
Thus, the protocol version was bumped to 3.
This requires updating OSM Dashboard to a version AFTER v6.5.7.
@@ -0,0 +1,8 @@
v4.25.2: OpenTracks
Changes:
- Restored compatibility with OSMDashboard (Dashboard API protocol version 2)
Bugfix:
- Update UI immediately after resuming a track
@@ -4,7 +4,7 @@ Modifiche:
- Velocità degli annunci vocali configurabile
- API <26: L'icona del lanciatore omette lo sfondo
Bugfix:
Correzioni:
- UI di registrazione: mostra i valori corretti
- Registrazione UI: mostra (di nuovo) l'altitudine in EGM2008
@@ -1,4 +1,4 @@
v4.22.1: OpenTracks
Modiciche:
Modifiche:
- Primo rilascio da Codeberg
@@ -1,4 +1,4 @@
v4.22.2: OpenTracks
Correzione di errori:
Correzioni:
- L'impostazione degli annunci vocali per l'ora e l'unità di misura è stata collegata
@@ -3,7 +3,7 @@
Modifiche:
-
Correzioni di errori:
Correzioni:
-
Sviluppatore:
@@ -1 +1 @@
Un tracker sportivo che rispetta completamente la tua privacy.
Un tracker sportivo che rispetta la tua privacy.
@@ -0,0 +1,15 @@
v4.24.0: OpenTracks
மாற்றங்கள்:
- PublicAPI: தரவு இருந்தால் ஸ்டார்ட் டிராக் மட்டுமே புலத்தை அமைக்கும்
- மார்க்கர் புகைப்படங்களை மீண்டும் பகிரலாம்
- புகைப்படங்கள் இல்லாமல் KMZ அகற்றப்பட்டது
- இதயத் துடிப்பு 0.0 தவறானதாகக் கருதப்படுகிறது (சென்சார் ஸ்டாடிஸ்டிக்ஸ் தவிர)
- சிறப்பு ஒருங்கிணைப்புகளுக்கான இறக்குமதி தீர்வு நீக்கப்பட்டது (100/200)
(சிறப்பு டிராக் பாயிண்ட்களை ஏற்றுமதி/இறக்குமதி செய்ய MyTracks பயன்படுத்தியது)
டெவலப்பர்:
- ட்ராக் மற்றும் மார்க்கர் பதிவுகள்
- திரட்டப்பட்ட புள்ளிவிவரங்கள் தரவுத்தளத்தில் கணக்கிடப்படுகின்றன
- TrackStatisticsUpdater மற்றும் SegmentStatisticUpdater எனப் பிரிக்கவும்
- அறிமுகப்படுத்தப்பட்ட இட்டரேட்டர் வகுப்புகள் (கர்சர் அல்லது பட்டியலைக் கடந்து செல்வதற்குப் பதிலாக)
@@ -0,0 +1,5 @@
v4.24.1: OpenTracks
பிழைத்திருத்தம்:
- OSM டாஷ்போர்டு v6.4.1 மற்றும் அதற்கு முந்தையவற்றுடன் இணக்கத்தன்மையை மீட்டெடுக்கவும் (v4.24.0 இல் அறிமுகப்படுத்தப்பட்டது)
- எண்ணின்படி இயல்புநிலை ட்ராக் பெயரைப் பயன்படுத்தும்போது செயலிழக்கிறது (v4.24.0 இல் அறிமுகப்படுத்தப்பட்டது)
@@ -0,0 +1,4 @@
v4.24.2: OpenTracks
பிழைத்திருத்தம்:
- அனைத்தையும் தேர்ந்தெடுக்கும்போது செயலிழக்க (v4.24.0 இல் அறிமுகப்படுத்தப்பட்டது)
@@ -0,0 +1,12 @@
v4.25.0: OpenTracks
மாற்றங்கள்:
- பதிவுப் பொத்தான் மையமாக உள்ளது
- BLE தெர்மோமீட்டருக்கான ஆதரவு (சுற்றுச்சூழல் உணர்தல்)
பிழைத்திருத்தம்:
- இறக்குமதியில்: கோப்பு பெயரிலிருந்து வகையைப் பெறுங்கள் (URI அல்ல)
டெவலப்பர்:
- TrackPoint ஒரு பதிவு
- Android Gradle செருகுநிரல் 9.0 க்கு புதுப்பிக்கவும்
@@ -17,6 +17,6 @@
* 深色和浅色主题,跟随系统设置。
* 无广告。
‌自由软件/开放源代码
自由软件/开放源代码
意味着您可以使用、研究、修改和分享源代码。
采用 Apache 2.0 许可
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Binary file not shown.
@@ -36,7 +36,8 @@ public class EspressoDeleteTrackTest {
@Test
public void espressoDeleteTrackTest() {
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action)).perform(click());
onView(withId(R.id.track_list_fab_action))
.perform(click());
// TrackRecordingActivity
onView(withId(R.id.track_recording_fab_action))
@@ -46,10 +47,12 @@ public class EspressoDeleteTrackTest {
.perform(longClick());
// TrackStoppedActivity
onView(withId(R.id.finish_button)).perform(click());
onView(withId(R.id.finish_button))
.perform(click());
// select track
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick());
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0)))
.perform(longClick());
// open menu and delete selected track
onView(
@@ -62,9 +65,11 @@ public class EspressoDeleteTrackTest {
isDisplayed()))
.perform(click());
onView(withText("Delete")).perform(click());
onView(withText("Delete"))
.perform(click());
onView(withText("OK")).perform(click());
onView(withText("OK"))
.perform(click());
// tracklist is empty now
onView(allOf(withText("Start recording your next adventure here"), isDisplayed()));
@@ -15,7 +15,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@@ -45,41 +44,43 @@ public class EspressoEditTrackRecordingTest {
public void espressoEditTrackRecordingTest() {
{
// TrackListActivity: start recording
ViewInteraction fabRecordButton = onView(withId(R.id.track_list_fab_action));
fabRecordButton.perform(click());
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
// wait; stay recording
trackControllerStopButton.perform(waitFor(4000));
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(4000));
// open menu
openContextualActionModeOverflowMenu();
// Click the item.
onView(withText(R.string.menu_edit)).perform(click());
onView(withText(R.string.menu_edit))
.perform(click());
// change name for "New Name"
ViewInteraction textInputEditText = onView(withId(R.id.track_edit_name));
textInputEditText.perform(scrollTo(), replaceText("New Name"));
onView(withId(R.id.track_edit_name))
.perform(scrollTo(), replaceText("New Name"));
ViewInteraction textInputEditText2 = onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()));
textInputEditText2.perform(closeSoftKeyboard());
onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()))
.perform(closeSoftKeyboard());
// save edition
ViewInteraction appCompatButton = onView(withId(R.id.track_edit_save));
appCompatButton.perform(click());
onView(withId(R.id.track_edit_save))
.perform(click());
// stop;
trackControllerStopButton.perform(longClick());
onView(withId(R.id.track_recording_fab_action))
.perform(longClick());
// it's on track stopped activity and there are two buttons
ViewInteraction resumeButton = onView(withId(R.id.resume_button));
resumeButton.check(matches(isDisplayed()));
ViewInteraction finishButton = onView(withId(R.id.finish_button));
finishButton.check(matches(isDisplayed()));
onView(withId(R.id.resume_button))
.check(matches(isDisplayed()));
onView(withId(R.id.finish_button))
.check(matches(isDisplayed()));
}
}
}
@@ -37,8 +37,8 @@ public class EspressoUITest {
public void record_stop_resume_stop_finish() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
trackControllerRecordButton.perform(click());
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
{
// TrackRecordingActivity: wait to record some time and then stop
@@ -66,13 +66,12 @@ public class EspressoUITest {
public void record_move_through_tabs() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
trackControllerRecordButton.perform(click());
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
tabLayout.perform(selectTabAtIndex(1));
tabLayout.perform(waitFor(1000));
@@ -87,7 +86,8 @@ public class EspressoUITest {
tabLayout.perform(waitFor(1000));
// stop
trackControllerStopButton.perform(longClick());
onView(withId(R.id.track_recording_fab_action)).
perform(longClick());
}
}
@@ -17,9 +17,13 @@ import de.dennisguse.opentracks.data.MarkerIterator;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
@@ -92,9 +96,9 @@ public class TestDataUtil {
);
List<Marker> markers = List.of(
new Marker(null, trackId, "Marker 1", "Marker description 1", "Marker typeLocalized 3", trackPoints.get(1).getPosition(), null),
new Marker(null, trackId, "Marker 2", "Marker description 2", "Marker typeLocalized 3", trackPoints.get(4).getPosition(), null),
new Marker(null, trackId, "Marker 3", "Marker description 3", "Marker typeLocalized 3", trackPoints.get(5).getPosition(), null)
new Marker(null, trackId, "Marker 1", "Marker description 1", "Marker typeLocalized 3", trackPoints.get(1).position(), null),
new Marker(null, trackId, "Marker 2", "Marker description 2", "Marker typeLocalized 3", trackPoints.get(4).position(), null),
new Marker(null, trackId, "Marker 3", "Marker description 3", "Marker typeLocalized 3", trackPoints.get(5).position(), null)
);
return new TrackData(track, trackPoints, markers);
@@ -112,8 +116,10 @@ public class TestDataUtil {
return pair.first;
}
public static TrackPoint createTrackPoint(int i) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT,
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type, AltitudeGainLoss altitudeGainLoss) {
return new TrackPoint(
null,
type,
new Position(
Instant.ofEpochSecond(i),
INITIAL_LATITUDE + (double) i / 10000.0,
@@ -123,21 +129,25 @@ public class TestDataUtil {
null,
null,
Speed.of(5f + (i / 10f))
));
trackPoint.setHeartRate(100f + i % 80);
trackPoint.setCadence(300f + i);
trackPoint.setPower(400f + i);
trackPoint.setAltitudeGainLoss(ALTITUDE_GAIN, ALTITUDE_LOSS);
return trackPoint;
),
null,
HeartRate.of(100f + i % 80),
null,
Cadence.of(300f + i),
Power.of(400f + i),
altitudeGainLoss
);
}
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type) {
TrackPoint trackPoint = createTrackPoint(i);
trackPoint.setType(type);
return trackPoint;
return createTrackPoint(i, TrackPoint.Type.TRACKPOINT, new AltitudeGainLoss(ALTITUDE_GAIN, ALTITUDE_LOSS));
}
public static TrackPoint createTrackPoint(int i) {
return createTrackPoint(i, TrackPoint.Type.TRACKPOINT);
}
/**
* Inserts a track with locations into the database.
*
@@ -164,7 +174,7 @@ public class TestDataUtil {
"Marker name",
description,
"Marker typeLocalized",
trackPoint.getPosition(),
trackPoint.position(),
photoUri);
}
@@ -6,8 +6,12 @@ import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
@@ -20,7 +24,9 @@ public class TestSensorDataUtil {
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
int i = trackPointList.size() + 1;
TrackPoint tp = new TrackPoint(type,
TrackPoint tp = new TrackPoint(
null,
type,
new Position(
time,
TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0,
@@ -30,18 +36,15 @@ public class TestSensorDataUtil {
null,
null,
Speed.of(5f + (i / 10f))
)
),
null,
HeartRate.ofOrNull(hr),
null,
cadence != null ? Cadence.of(cadence) : null,
power != null ? Power.of(power) : null,
new AltitudeGainLoss(3, 3)
);
if (hr != null) {
tp.setHeartRate(hr);
}
if (cadence != null) {
tp.setCadence(cadence);
}
if (power != null) {
tp.setPower(power);
}
tp.setAltitudeGainLoss(3, 3);
trackPointList.add(tp);
}
@@ -54,19 +54,22 @@ import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.MarkerBuilder;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -124,7 +127,7 @@ public class CustomContentProviderUtilsTest {
assertNotNull(trackPoint);
locations.add(trackPoint);
// Make sure the IDs are returned in the right order.
assertEquals(lastPointId.id() - numPoints + locations.size(), trackPoint.getId().id());
assertEquals(lastPointId.id() - numPoints + locations.size(), trackPoint.id().id());
}
assertEquals(numPoints, locations.size());
}
@@ -167,7 +170,7 @@ public class CustomContentProviderUtilsTest {
try (TrackPointIterator it = contentProviderUtils.getTrackPointIterator(id, null)) {
while (it.hasNext()) {
TrackPoint trackPoint = it.next();
lastPointId = trackPoint.getId();
lastPointId = trackPoint.id();
counter++;
}
}
@@ -237,7 +240,7 @@ public class CustomContentProviderUtilsTest {
"",
"",
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -300,7 +303,7 @@ public class CustomContentProviderUtilsTest {
"",
"",
"",
getLastValidTrackPoint(trackId1).getPosition(),
getLastValidTrackPoint(trackId1).position(),
null
));
@@ -473,7 +476,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -553,7 +556,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
Marker.Id marker2Id = contentProviderUtils.insertMarker(new Marker(
@@ -562,7 +565,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -589,7 +592,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
}
@@ -613,7 +616,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -636,7 +639,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -647,7 +650,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC_NEW,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -803,20 +806,27 @@ public class CustomContentProviderUtilsTest {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackPoint trackPoint = TestDataUtil.createTrackPoint(5);
trackPoint.setHeartRate(1F);
trackPoint.setCadence(2F);
trackPoint.setPower(3F);
TrackPoint trackPoint = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
TestDataUtil.createTrackPoint(5).position(),
null,
HeartRate.of(1),
null,
Cadence.of(2),
Power.of(3),
null
);
// when
contentProviderUtils.insertTrackPoint(trackPoint, trackId);
// then
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertTrue(trackPoints.get(10).hasHeartRate());
assertEquals(trackPoint.getHeartRate(), trackPoints.get(10).getHeartRate());
assertEquals(trackPoint.getCadence(), trackPoints.get(10).getCadence());
assertEquals(trackPoint.getPower(), trackPoints.get(10).getPower());
assertNotNull(trackPoints.get(10).heartRate());
assertEquals(trackPoint.heartRate(), trackPoints.get(10).heartRate());
assertEquals(trackPoint.cadence(), trackPoints.get(10).cadence());
assertEquals(trackPoint.power(), trackPoints.get(10).power());
}
@Test
@@ -857,9 +867,9 @@ public class CustomContentProviderUtilsTest {
for (int i = 0; i < trackpointIds.size(); i++) {
assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
assertEquals(startTrackPointId.id() + i, trackPoint.getId().id());
assertEquals(startTrackPointId.id() + i, trackPoint.id().id());
Position position = trackPoint.getPosition();
Position position = trackPoint.position();
assertEquals(TestDataUtil.INITIAL_LATITUDE + i / 10000.0, position.latitude(), 0.01);
assertEquals(TestDataUtil.INITIAL_LONGITUDE - i / 10000.0, position.longitude(), 0.01);
assertEquals(i / 100.0, position.horizontalAccuracy().distance_m(), 0.01);
@@ -879,11 +889,18 @@ public class CustomContentProviderUtilsTest {
public void testGetSensorStats_noSensorData() {
// given
List<TrackPoint> trackPointList = new ArrayList<>();
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
trackPoint.setPower(null);
trackPoint.setCadence(null);
trackPoint.setHeartRate(null);
TrackPoint trackPoint = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
TestDataUtil.createTrackPoint(1).position(),
null,
null,
null,
null,
null,
null
);
trackPointList.add(trackPoint);
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Track track = TestDataUtil.createTrack(trackId);
@@ -1217,7 +1234,7 @@ public class CustomContentProviderUtilsTest {
return trackpoints.reversed()
.stream()
.filter(it -> List.of(TrackPoint.Type.TRACKPOINT, TrackPoint.Type.SEGMENT_START_AUTOMATIC).contains(it.getType()))
.filter(it -> List.of(TrackPoint.Type.TRACKPOINT, TrackPoint.Type.SEGMENT_START_AUTOMATIC).contains(it.type()))
.findFirst().orElseThrow();
}
@@ -13,7 +13,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
public class TrackPointTest {
@Test
public void distanceToPrevious() {
public void distanceToPrevious_gps() {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
@@ -38,20 +38,123 @@ public class TrackPointTest {
// without sensor distance
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_first() {
TrackPoint tp1 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null
);
TrackPoint tp2 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null));
// tp1 has sensor distance
tp1.setSensorDistance(Distance.of(5));
tp1.setSensorDistance(null);
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_second() {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null));
TrackPoint tp2 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null);
// tp2 has sensor distance
tp1.setSensorDistance(null);
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_both() {
TrackPoint tp1 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null),
Distance.of(10),
null,
null,
null,
null,
null
);
TrackPoint tp2 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null);
// tp1 and tp2 have sensor distance
tp1.setSensorDistance(Distance.of(10));
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
}
@@ -26,7 +26,6 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.io.BufferedReader;
import java.io.File;
@@ -61,22 +60,24 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
@@ -88,7 +89,7 @@ import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
public class ExportImportTest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(50, TimeUnit.SECONDS); //TODO 5s
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@@ -97,13 +98,15 @@ public class ExportImportTest {
@Rule
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
private static final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
private final Context context = ApplicationProvider.getApplicationContext();
PreferencesUtils.resetPreferences(context, true);
}
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -123,11 +126,13 @@ public class ExportImportTest {
private TrackImporter trackImporter;
@Before
public void fileSetup() throws IOException {
public void fileSetup() throws IOException, TimeoutException {
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
tmpFileUri = Uri.fromFile(tmpFile);
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(200), true);
setUp();
}
@After
@@ -158,20 +163,28 @@ public class ExportImportTest {
"Marker 1",
"Marker 1 desc",
"Marker 1 typeLocalized",
service.getLastStoredTrackPointWithLocation().getPosition(),
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.1234567, 14.0014567,
Distance.of(10),
Altitude.WGS84.of(1020.25),
Distance.of(13),
null,
Speed.of(15)
),
null
));
// A sensor-only TrackPoint
trackPointCreator.setClock("2020-02-02T02:02:04Z");
mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f);
mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f, 27);
trackPointCreator.setClock("2020-02-02T02:02:14Z");
mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null);
trackPointCreator.setClock("2020-02-02T02:02:14Z"); //ignored
mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null, 28);
trackPointCreator.setClock("2020-02-02T02:02:15Z");
mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null);
mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null, 29);
trackPointCreator.setClock("2020-02-02T02:02:16Z");
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null); // Distance will be added to next TrackPoint
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null, 30); //Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 0f);
contentProviderUtils.insertMarker(
@@ -181,12 +194,12 @@ public class ExportImportTest {
"Marker 2",
"Marker 2 desc",
"Marker 2 typeLocalized",
service.getLastStoredTrackPointWithLocation().getPosition(),
service.getLastStoredTrackPointWithLocation().position(),
null
));
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet(trackPointCreator);
trackPointCreator.getSensorManager().clear();
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -201,7 +214,7 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3.1234567, 16.001, 10, 27, 15, 999.123, 0f);
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet(trackPointCreator);
trackPointCreator.getSensorManager().clear();
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
@@ -218,13 +231,134 @@ public class ExportImportTest {
@LargeTest
@Test
public void track() throws TimeoutException {
setUp();
Track track = contentProviderUtils.getTrack(trackId);
Statistics trackStatistics = track.statistics();
assertEquals(ZoneOffset.of("+01:00"), track.zoneOffset());
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:04Z"),
null, null, null,
null, null,
null,
Speed.of(15)),
Distance.of(10),
HeartRate.of(66),
Temperature.of(27),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.parse("2020-02-02T02:02:15Z")),
null,
HeartRate.of(68),
Temperature.of(29),
Cadence.of(3),
Power.of(50),
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(5)),
Distance.of(2),
HeartRate.of(69),
Temperature.of(30),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.IDLE,
Position.of(Instant.parse("2020-02-02T02:03:30Z")),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null, Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
assertEquals(new Statistics(
Instant.parse("2020-02-02T02:02:02Z"),
Instant.parse("2020-02-02T02:04:00Z"),
@@ -238,77 +372,12 @@ public class ExportImportTest {
null
),
trackStatistics);
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Instant.parse("2020-02-02T02:02:04Z"))
.setSensorDistance(Distance.of(10))
.setSpeed(Speed.of(15))
.setHeartRate(HeartRate.of(66))
.setCadence(3)
.setPower(50).setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:15Z"))
.setHeartRate(HeartRate.of(68))
.setCadence(3)
.setPower(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(5)))
.setSensorDistance(Distance.of(2))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(69))
.setCadence(3)
.setPower(50),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z"))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null, Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
}
//TODO Does not test marker images
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp();
public void kmz_with_trackdetail_and_sensordata() throws IOException {
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -360,9 +429,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException, IOException {
setUp();
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws IOException {
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
@@ -387,8 +454,6 @@ public class ExportImportTest {
@LargeTest
@Test
public void gpx() throws TimeoutException, IOException {
setUp();
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -421,51 +486,86 @@ public class ExportImportTest {
.setDelta(0.05); // speed is not fully
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456d, Distance.of(10),
Altitude.WGS84.of(1020.2), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.2), null,
null,
Speed.of(5)))
.setAltitudeGainLoss(1, 1)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
.setCadence(3f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(5)),
Distance.of(12),
HeartRate.of(69),
Temperature.of(30),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(10)))
.setAltitudeGainLoss(0, 0)
.setSpeed(Speed.of(15))
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), actual);
// 3. trackstatistics
@@ -492,9 +592,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
setUp();
public void gpx_duplicate_trackUUID() throws IOException {
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
@@ -520,9 +618,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void csv_export_only() throws TimeoutException, IOException {
setUp();
public void csv_export_only() throws IOException {
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -566,17 +662,17 @@ public class ExportImportTest {
}
}
private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain) {
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
private static void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain, float temperature) {
SensorManager sensorManager = trackPointCreator.getSensorManager();
AggregatorCyclingPower cyclingPower = new AggregatorCyclingPower("", "");
cyclingPower.add(new Raw<>(trackPointCreator.createNow(), new BluetoothHandlerManagerCyclingPower.Data(Power.of(power), null)));
sensorDataSet.add(cyclingPower);
cyclingPower.add(trackPointCreator.getNow(), new CyclingPowerBluetooth.Data(Power.of(power), null));
sensorManager.setAggregator(cyclingPower);
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
avgHeartRate.add(new Raw<>(trackPointCreator.createNow(), HeartRate.of(heartRate)));
sensorDataSet.add(avgHeartRate);
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(heartRate));
sensorManager.setAggregator(avgHeartRate);
AggregatorCyclingCadence cyclingCadence = new AggregatorCyclingCadence("", "") {
@NonNull
@@ -590,33 +686,65 @@ public class ExportImportTest {
return true;
}
};
sensorDataSet.add(cyclingCadence);
sensorManager.setAggregator(cyclingCadence);
if (distance != null && speed != null) {
AggregatorCyclingDistanceSpeed distanceSpeed = Mockito.mock(AggregatorCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasReceivedData()).thenReturn(true);
Mockito.when(distanceSpeed.getAggregatedValue(Mockito.any())).thenReturn(new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed)));
sensorDataSet.add(distanceSpeed);
AggregatorCyclingDistanceSpeed aggregatorCyclingDistanceSpeed = new AggregatorCyclingDistanceSpeed("", "") {
@NonNull
@Override
public Data getAggregatedValue(Instant now) {
return new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed));
}
@Override
public boolean hasReceivedData() {
return true;
}
};
sensorManager.setAggregator(aggregatorCyclingDistanceSpeed);
} else {
sensorDataSet.add(new AggregatorCyclingDistanceSpeed("", ""));
sensorManager.setAggregator(new AggregatorCyclingDistanceSpeed("", ""));
}
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.onChange(sensorDataSet);
sensorManager.setAggregator(new AggregatorTemperature("", "'") {
@NonNull
@Override
public Temperature getAggregatedValue(Instant now) {
return Temperature.of(temperature);
}
@Override
public boolean hasReceivedData() {
return true;
}
});
trackPointCreator.onChange();
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
private static void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
SensorManager sensorManager = trackPointCreator.getSensorManager();
if (altitudeGain != null) {
AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class);
Mockito.when(barometer.hasReceivedData()).thenReturn(true);
Mockito.when(barometer.getAggregatedValue(Mockito.any())).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
sensorDataSet.add(barometer);
} else {
sensorDataSet.add(new AggregatorBarometer("test", null));
if (altitudeGain == null) {
sensorManager.getAltitudeChangeHandler().setAggregator(new AggregatorBarometer("test", null));
return;
}
AggregatorBarometer aggregatorBarometer = new AggregatorBarometer("", "") {
@NonNull
@Override
public AltitudeGainLoss getAggregatedValue(Instant now) {
return new AltitudeGainLoss(altitudeGain, altitudeGain);
}
@Override
public boolean hasReceivedData() {
return true;
}
};
sensorManager.getAltitudeChangeHandler().setAggregator(aggregatorBarometer);
}
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
@@ -631,6 +759,6 @@ public class ExportImportTest {
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
}
}
@@ -27,8 +27,12 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
@@ -193,51 +197,86 @@ public class GPXTrackImporterTest {
TrackPointAssert a = new TrackPointAssert()
.setDelta(0.05); // speed is not fully
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3d, 14d, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3d, 14.001, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(5)))
.setAltitudeGainLoss(1, 1)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
.setCadence(3f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(5)),
Distance.of(12),
HeartRate.of(69),
null,
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3d, 14.002, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
3d, 16d, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3d, 16.001, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(10)))
.setAltitudeGainLoss(0, 0)
.setSpeed(Speed.of(15))
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), actual);
}
@@ -247,7 +286,7 @@ public class GPXTrackImporterTest {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
try(InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone)) {
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// when
@@ -16,57 +16,47 @@ public class TrackPointAssert {
public void assertEquals(TrackPoint expected, TrackPoint actual) {
Assert.assertEquals("time", expected.getTime(), actual.getTime());
Assert.assertEquals("type", expected.getType(), actual.getType());
Assert.assertEquals("type", expected.type(), actual.type());
Assert.assertEquals("has location,", expected.hasLocation(), actual.hasLocation());
if (expected.hasLocation()) {
Assert.assertEquals("latitude", expected.getPosition().latitude(), actual.getPosition().latitude(), 0.001);
Assert.assertEquals("longitude", expected.getPosition().longitude(), actual.getPosition().longitude(), 0.001);
Assert.assertEquals("has location,", expected.position().hasLocation(), actual.position().hasLocation());
if (expected.position().hasLocation()) {
Assert.assertEquals("latitude", expected.position().latitude(), actual.position().latitude(), 0.001);
Assert.assertEquals("longitude", expected.position().longitude(), actual.position().longitude(), 0.001);
}
Assert.assertEquals("has altitude", expected.hasAltitude(), actual.hasAltitude());
if (expected.hasAltitude()) {
Assert.assertEquals("altitude", expected.getAltitude().toM(), actual.getAltitude().toM(), delta);
Assert.assertEquals("has altitude", expected.position().hasAltitude(), actual.position().hasAltitude());
if (expected.position().hasAltitude()) {
Assert.assertEquals("altitude", expected.position().altitude().toM(), actual.position().altitude().toM(), delta);
}
Assert.assertEquals("has altitudeGainLoss", expected.hasAltitudeGainLoss(), actual.hasAltitudeGainLoss());
if (expected.hasAltitudeGainLoss()) {
Assert.assertEquals("altitudeGainLoss", expected.getAltitudeGainLoss(), actual.getAltitudeGainLoss());
Assert.assertEquals("altitudeGainLoss", expected.altitudeGainLoss(), actual.altitudeGainLoss());
Assert.assertEquals("has speed", expected.position().hasSpeed(), actual.position().hasSpeed());
if (expected.position().hasSpeed()) {
Assert.assertEquals("speed", expected.position().speed().toMPS(), actual.position().speed().toMPS(), delta);
}
Assert.assertEquals("has speed", expected.hasSpeed(), actual.hasSpeed());
if (expected.hasSpeed()) {
Assert.assertEquals("speed", expected.getSpeed().toMPS(), actual.getSpeed().toMPS(), delta);
Assert.assertEquals("has horizontalAccuracy", expected.position().hasHorizontalAccuracy(), actual.position().hasHorizontalAccuracy());
if (expected.position().hasHorizontalAccuracy()) {
Assert.assertEquals("horizontalAccuracy", expected.position().horizontalAccuracy().toM(), actual.position().horizontalAccuracy().toM(), delta);
}
Assert.assertEquals("has verticalAccuracy", expected.position().hasVerticalAccuracy(), actual.position().hasVerticalAccuracy());
if (expected.position().hasVerticalAccuracy()) {
Assert.assertEquals("verticalAccuracy", expected.position().verticalAccuracy().toM(), actual.position().verticalAccuracy().toM(), delta);
}
Assert.assertEquals("has horizontalAccuracy", expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
if (expected.hasHorizontalAccuracy()) {
Assert.assertEquals("horizontalAccuracy", expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
}
Assert.assertEquals("has verticalAccuracy", expected.hasVerticalAccuracy(), actual.hasVerticalAccuracy());
if (expected.hasVerticalAccuracy()) {
Assert.assertEquals("verticalAccuracy", expected.getVerticalAccuracy().toM(), actual.getVerticalAccuracy().toM(), delta);
Assert.assertEquals("has sensorDistance", expected.sensorDistance() != null, actual.sensorDistance() != null);
if (expected.sensorDistance() != null) {
Assert.assertEquals("sensorDistance", expected.sensorDistance().toM(), actual.sensorDistance().toM(), delta);
}
Assert.assertEquals("has sensorDistance", expected.hasSensorDistance(), actual.hasSensorDistance());
if (expected.hasSensorDistance()) {
Assert.assertEquals("sensorDistance", expected.getSensorDistance().toM(), actual.getSensorDistance().toM(), delta);
}
Assert.assertEquals("heartRate", expected.heartRate(), actual.heartRate());
Assert.assertEquals("has heartrate", expected.hasHeartRate(), actual.hasHeartRate());
if (expected.hasHeartRate()) {
Assert.assertEquals("heartrate", expected.getHeartRate(), actual.getHeartRate());
}
Assert.assertEquals("temperature", expected.temperature(), actual.temperature());
Assert.assertEquals("has power", expected.hasPower(), actual.hasPower());
if (expected.hasPower()) {
Assert.assertEquals("power", expected.getPower(), actual.getPower());
}
Assert.assertEquals("power", expected.power(), actual.power());
Assert.assertEquals("has cadence", expected.hasCadence(), actual.hasCadence());
if (expected.hasCadence()) {
Assert.assertEquals("cadence", expected.getCadence(), actual.getCadence());
}
Assert.assertEquals("cadence", expected.cadence(), actual.cadence());
}
public void assertEquals(List<TrackPoint> expected, List<TrackPoint> actual) {
@@ -0,0 +1,224 @@
package de.dennisguse.opentracks.publicapi;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@RunWith(AndroidJUnit4.class)
public class DataProviderTest {
private final ContentResolver resolver = ApplicationProvider.getApplicationContext().getContentResolver();
@Test
public void supportsVersion2_tracks() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = new String[]{
"_id",
"name",
"description",
"category",
"activity_type_localized",
"starttime",
"stoptime",
"totaltime",
"movingtime",
"totaldistance",
"maxspeed",
"avgspeed",
"avgmovingspeed",
"minelevation",
"maxelevation",
"elevationgain",
"elevationloss"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTracksColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.track", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTracksColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion2_trackpoints() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
"latitude",
"longitude",
"time",
"type",
"speed"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTrackpointsColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTrackpointsColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion2_markers() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
"description",
"category",
"trackid",
"longitude",
"latitude",
"photoUrl",
"icon"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedMarkersColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.waypoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedMarkersColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_tracks() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = {
"_id",
"name",
"description",
"activity_type",
"activity_type_localized",
"time_start",
"time_stop",
"duration_total",
"duration_moving",
"distance",
"speed_max",
"altitude_min",
"altitude_max",
"altitude_gain",
"altitude_loss"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTracksColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.track", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTracksColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_trackpoints() {
//given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
"latitude",
"longitude",
"time",
"type",
"speed"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTrackpointsColumns,
null,
null,
null
)) {
//then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTrackpointsColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_markers() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
"description",
"marker_type_localized",
"trackid",
"latitude",
"longitude",
"photoUrl"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedMarkersColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.waypoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedMarkersColumns, cursor.getColumnNames());
}
}
}
@@ -28,7 +28,6 @@ import java.util.List;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(AndroidJUnit4.class)
public class GpsStatusTest {
@@ -82,16 +81,16 @@ public class GpsStatusTest {
// when / then
subject.start();
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX, GPS_SIGNAL_BAD), statusList);
}
@@ -106,13 +105,13 @@ public class GpsStatusTest {
// when / then
subject.start();
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now().minusMillis(1000))));
subject.onNewTrackPoint(Position.of(ok, Instant.now().minusMillis(1000)));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX), statusList);
subject.determineGpsStatusByTime(Instant.now());
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_FIX), statusList);
}
@@ -133,13 +132,13 @@ public class GpsStatusTest {
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX), statusList);
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_BAD), statusList);
Thread.sleep(100);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -13,11 +13,11 @@ public class BluetoothHandlerBarometricPressureTest {
@Test
public void parseEnvironmentalSensing_Pa() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BarometerBluetooth.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
// when
AtmosphericPressure pressure = BluetoothHandlerBarometricPressure.parseEnvironmentalSensing(characteristic);
AtmosphericPressure pressure = BarometerBluetooth.parseEnvironmentalSensing(characteristic);
// then
assertEquals(AtmosphericPressure.ofHPA(1001.65f), pressure);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -8,16 +8,16 @@ import org.junit.Test;
import de.dennisguse.opentracks.data.models.HeartRate;
public class BluetoothHandlerManagerHeartRateTest {
public class BluetoothHandlerHeartRateTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(HeartRateBluetooth.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
HeartRate heartRate = HeartRateBluetooth.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(60), heartRate);
@@ -26,11 +26,11 @@ public class BluetoothHandlerManagerHeartRateTest {
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(HeartRateBluetooth.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
HeartRate heartRate = HeartRateBluetooth.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(257), heartRate);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -8,14 +8,14 @@ import android.util.Pair;
import org.junit.Test;
public class BluetoothHandlerCyclingDistanceSpeedTest {
public class CyclingDistanceSpeedBluetoothTest {
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertNull(sensor.first);
@@ -24,11 +24,11 @@ public class BluetoothHandlerCyclingDistanceSpeedTest {
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertEquals(65535 + 16777216, sensor.first.wheelRevolutionsCount());
@@ -37,11 +37,11 @@ public class BluetoothHandlerCyclingDistanceSpeedTest {
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertEquals(200 + 16777216, sensor.first.wheelRevolutionsCount());
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -6,15 +6,15 @@ import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
public class BluetoothHandlerCyclingPowerTest {
public class CyclingPowerBluetoothTest {
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingPowerBluetooth.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
CyclingPowerBluetooth.Data powerCadence = CyclingPowerBluetooth.parseCyclingPower(characteristic);
// then
assertEquals(40, powerCadence.power().getW(), 0.01);
@@ -22,11 +22,11 @@ public class BluetoothHandlerCyclingPowerTest {
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingPowerBluetooth.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
CyclingPowerBluetooth.Data powerCadence = CyclingPowerBluetooth.parseCyclingPower(characteristic);
// then
assertEquals(0, powerCadence.power().getW(), 0.01);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -10,15 +10,15 @@ import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
public class BluetoothHandlerRunningSpeedAndCadenceTest {
public class RunningSpeedAndCadenceBluetoothTest {
@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(RunningSpeedAndCadenceBluetooth.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
// when
BluetoothHandlerRunningSpeedAndCadence.Data sensor = BluetoothHandlerRunningSpeedAndCadence.parseRunningSpeedAndCadence("sensorName", characteristic);
RunningSpeedAndCadenceBluetooth.Data sensor = RunningSpeedAndCadenceBluetooth.parseRunningSpeedAndCadence("sensorName", characteristic);
// then
assertEquals(Speed.of(5), sensor.speed());
@@ -11,7 +11,7 @@ public class AggregatorBarometerTest {
private static void addSensorValue(AggregatorBarometer aggregatorBarometer, float[] values) {
for (float f : values) {
aggregatorBarometer.add(new Raw<>(Instant.MIN, AtmosphericPressure.ofHPA(f)));
aggregatorBarometer.add(Instant.MIN, AtmosphericPressure.ofHPA(f));
}
}
@@ -24,7 +24,7 @@ public class AggregatorBarometerTest {
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
// then
Assert.assertEquals(0f, subject.aggregatedValue.gain_m(), 0.01);
Assert.assertEquals(15f, subject.aggregatedValue.loss_m(), 0.01);
Assert.assertEquals(0f, subject.output.gain_m(), 0.01);
Assert.assertEquals(15f, subject.output.loss_m(), 0.01);
}
}
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.sensors.sensorData;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -12,10 +13,9 @@ import java.time.Instant;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.UintUtils;
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
@RunWith(AndroidJUnit4.class)
public class SensorDataCyclingTest {
@@ -25,8 +25,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 2048));
// then
assertEquals(60, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -37,8 +37,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 6184)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 8016)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 6184));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 8016));
// then
assertEquals(33.53, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -49,8 +49,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 2048));
// then
assertEquals(Cadence.of(0), current.getAggregatedValue(Instant.MIN));
@@ -62,8 +62,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 1024)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 1024));
// then
assertFalse(current.hasReceivedData()); //TODO Cadence should be 0?
@@ -74,8 +74,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 0)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, UintUtils.UINT16_MAX - 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 0));
// then
assertEquals(60, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -87,13 +87,13 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(0, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(UintUtils.UINT32_MAX - 1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(0, 2048));
// then
// TODO See #953
// assertEquals(60, current.getValue().getRPM(), 0.01);
assertEquals(Cadence.of(0), current.getAggregatedValue(Instant.MIN));
assertNull(current.getAggregatedValue(Instant.MIN));
}
@Test
@@ -102,8 +102,8 @@ public class SensorDataCyclingTest {
current.setWheelCircumference(Distance.ofMM(2150));
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016)));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(1, 6184));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(2, 8016));
// then
assertEquals(2.15, current.getAggregatedValue(Instant.MIN).distance().toM(), 0.01);
@@ -117,14 +117,13 @@ public class SensorDataCyclingTest {
current.setWheelCircumference(Distance.ofMM(2000));
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(0, 2048)));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(UintUtils.UINT32_MAX - 1, 1024));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(0, 2048));
// then
// TODO See #953
// assertEquals(2, current.getValue().getDistance().toM(), 0.01);
// assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
assertEquals(new AggregatorCyclingDistanceSpeed.Data(Distance.ZERO, Distance.ZERO, Speed.ZERO), current.getAggregatedValue(Instant.MIN));
assertNull(current.getAggregatedValue(Instant.MIN));
}
}
@@ -6,7 +6,6 @@ import static org.junit.Assert.assertFalse;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationManager;
import android.os.Looper;
import androidx.annotation.NonNull;
@@ -35,6 +34,7 @@ import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
@@ -43,12 +43,12 @@ import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -126,8 +126,16 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -145,7 +153,8 @@ public class TrackRecordingServiceRecordingTest {
Track.Id trackId = service.startNewTrack();
//We do not want the real GPS to interfere.
LocationManagerCompat.removeUpdates((LocationManager) context.getSystemService(Context.LOCATION_SERVICE), trackPointCreator.getSensorManager().getGpsManager());
GpsInternal gpsInternal = (GpsInternal)trackPointCreator.getSensorManager().getGpsHandler().driver;
LocationManagerCompat.removeUpdates(gpsInternal.locationManager, gpsInternal.locationListenerCompat);
String gps1 = "2020-02-02T02:02:03Z";
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
@@ -201,8 +210,16 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
//when
@@ -216,8 +233,17 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -249,8 +275,16 @@ public class TrackRecordingServiceRecordingTest {
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(starTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), trackPoints);
}
@@ -278,8 +312,17 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -296,16 +339,21 @@ public class TrackRecordingServiceRecordingTest {
mockAltitudeChange(trackPointCreator, 0);
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataSet.add(new AggregatorHeartRate("", ""));
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(sensor1, HeartRate.of(5))); //Should be ignored
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
sensorManager.setAggregator(avgHeartRate);
//Should be ignored
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(5));
sensorManager.onChange();
String sensor3 = "2020-02-02T02:02:13Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(sensor3, HeartRate.of(7)));
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(7));
sensorManager.onChange();
String stopTime = "2020-02-02T02:02:15Z";
trackPointCreator.setClock(stopTime);
@@ -314,12 +362,28 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor3))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(7)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(7))
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.parse(sensor3)),
null,
HeartRate.of(7),
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
HeartRate.of(7),
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -369,32 +433,65 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps2),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -441,30 +538,54 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.00002, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse(stopTime),
45.00002, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -499,24 +620,47 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -559,8 +703,16 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -603,24 +755,48 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse(gps2),
45.1, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -637,19 +813,22 @@ public class TrackRecordingServiceRecordingTest {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
AggregatorRunning aggregatorRunning = new AggregatorRunning("", "");
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataSet.add(new AggregatorRunning("", ""));
sensorManager.sensorDataSet.barometer = null;
sensorManager.setAggregator(aggregatorRunning);
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(sensor1, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.ZERO))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(1), Distance.ZERO));
sensorManager.onChange();
// when
String sensor2 = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(sensor2);
sensorManager.onChanged(new Raw<>(sensor2, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(2))));
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(2), Distance.of(2)));
sensorManager.onChange();
// when
String gps1 = "2020-02-02T02:02:05Z";
@@ -658,12 +837,15 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor3 = "2020-02-02T02:02:06Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(sensor3, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(12))));
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(3), Distance.of(12)));
sensorManager.onChange();
// when
String sensor4 = "2020-02-02T02:02:07Z";
trackPointCreator.setClock(sensor4);
sensorManager.onChanged(new Raw<>(sensor4, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(14)))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(4), Distance.of(14)));
sensorManager.onChange();
// when
String gps2 = "2020-02-02T02:02:08Z";
@@ -672,7 +854,9 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor5 = "2020-02-02T02:02:10Z";
trackPointCreator.setClock(sensor5);
sensorManager.onChanged(new Raw<>(sensor5, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(16)))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(5), Distance.of(16)));
sensorManager.onChange();
// when
String gps3 = "2020-02-02T02:02:12Z";
@@ -691,49 +875,107 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor2)) //First moving TrackPoint: store as the time might be interesting.
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint( //First moving TrackPoint: stored as the time might be interesting.
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(sensor2),
null, null, null,
null, null,
null,
Speed.of(5)),
Distance.of(2),
null,
null,
Cadence.of(2),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.ZERO),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor3))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(5)),
Distance.ZERO,
null,
null,
Cadence.of(2),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(sensor3),
null, null, null,
null, null,
null,
Speed.of(5)),
Distance.of(10),
null,
null,
Cadence.of(3),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.of(4.0)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(5)),
Distance.of(4.0),
null,
null,
Cadence.of(5),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps4),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.ZERO),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL,
Speed.of(5)),
Distance.ZERO,
null,
null,
Cadence.of(5),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse(stopTime),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.ZERO)) //Sensor data is now outdated, but we do not fall back to GPS.
.setSensorDistance(Distance.ZERO)
Speed.ZERO), //Sensor data is now outdated, but we do not fall back to GPS.
Distance.ZERO,
null,
null,
Cadence.of(0), //TODO This could be null, right?
null,
null
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
trackPointCreator.getSensorManager().sensorDataSet.barometer = new AggregatorBarometer("", "") {
trackPointCreator.getSensorManager().getAltitudeChangeHandler().setAggregator(new AggregatorBarometer("", "") {
@Override
public boolean hasReceivedData() {
return true;
@@ -744,7 +986,7 @@ public class TrackRecordingServiceRecordingTest {
public AltitudeGainLoss getAggregatedValue(Instant now) {
return new AltitudeGainLoss(altitudeGain, altitudeGain);
}
};
});
}
private static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
@@ -756,6 +998,6 @@ public class TrackRecordingServiceRecordingTest {
location.setSpeed(speed);
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
}
}
@@ -45,13 +45,13 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
/**
* Testing the states of TrackRecordingService.
* As states are checked against MutableLiveData that are updated via `postValue()` (is asynchronous to be inform other Threads), Thread.sleep() is required.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceStateMachineTest {
@@ -60,7 +60,7 @@ public class TrackRecordingServiceStateMachineTest {
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
public GrantPermissionRule mRuntimePermissionRule = TestUtil.createGrantPermissionRule();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@@ -117,13 +117,12 @@ public class TrackRecordingServiceStateMachineTest {
@MediumTest
@Test
public void gps_startStop() throws InterruptedException {
public void gps_startStop() {
// given
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when
service.tryStartSensors();
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -132,7 +131,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.stopSensors();
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -142,13 +140,12 @@ public class TrackRecordingServiceStateMachineTest {
@MediumTest
@Test
public void recording_startStopResume_no_data() throws InterruptedException {
public void recording_startStopResume_no_data() {
// given
assertFalse(service.isRecording());
// when
Track.Id trackId = service.startNewTrack();
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
@@ -160,7 +157,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.resumeTrack(trackId);
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
@@ -171,7 +167,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.endCurrentTrack();
Thread.sleep(1000);
// then
assertFalse(service.isRecording());
@@ -182,12 +177,10 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.resumeTrack(trackId);
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
}
@@ -213,7 +206,7 @@ public class TrackRecordingServiceStateMachineTest {
@MediumTest
@Test
public void cannotEnd_without_starting() throws InterruptedException {
public void cannotEnd_without_starting() {
// given
assertFalse(service.isRecording());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -222,7 +215,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.endCurrentTrack();
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -233,33 +225,30 @@ public class TrackRecordingServiceStateMachineTest {
@Ignore("TODO Bug: GPS can be stopped although the current track is recording")
@MediumTest
@Test
public void recording_stopGPS_noop() throws InterruptedException {
public void recording_stopGPS_noop() {
// given
Track.Id trackId = service.startNewTrack();
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
// when
service.stopSensors(); //TODO Should be ignored as service is recording
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
}
@MediumTest
@Test
public void recording_startRecording_alreadyRecording() throws InterruptedException {
public void recording_startRecording_alreadyRecording() {
// given
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
// when
Track.Id newTrackId = service.startNewTrack();
Thread.sleep(1000);
// then
assertNotNull(trackId);
@@ -15,7 +15,6 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.time.Duration;
import java.time.Instant;
@@ -34,10 +33,11 @@ import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatisticsUpdater;
//Due to DateTimeFormatter using NBSP, these tests only work in SDK34+
@@ -84,8 +84,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -117,8 +124,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -150,8 +164,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -183,8 +204,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -198,8 +226,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, null).toString();
@@ -231,8 +266,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, false, null, null).toString();
@@ -246,8 +288,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, false, data.second, null).toString();
@@ -279,8 +328,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
@@ -312,8 +368,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
@@ -345,8 +408,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_METER, true, null, null).toString();
@@ -378,8 +448,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -393,8 +470,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.IMPERIAL_FEET, true, data.second, null).toString();
@@ -426,8 +510,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, false, null, null).toString();
@@ -441,8 +532,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.IMPERIAL_FEET, false, data.second, null).toString();
@@ -461,8 +559,15 @@ public class VoiceAnnouncementUtilsTest {
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
@@ -487,8 +592,15 @@ public class VoiceAnnouncementUtilsTest {
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
@@ -60,8 +60,7 @@ public class PreferencesOpenHelperTest {
// when update to version 2
PreferencesOpenHelper.newInstance(2).check();
String updatedOldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";"
+ PreferencesUtils.getLayoutColumnsByDefault() + ";distance,1,1,0;speed,1,1,0;";
String updatedOldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";2;distance,1,1,0;speed,1,1,0;";
// then there should be one layout with old custom layout that has the new CSV value.
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
@@ -110,7 +109,7 @@ public class PreferencesOpenHelperTest {
PreferencesOpenHelper.newInstance(1).check();
// then custom layout should be equals to default layout.
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsv(PreferencesUtils.buildDefaultLayout(), resources);
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsvLine(PreferencesUtils.buildDefaultLayout(), resources);
List<RecordingLayout> customRecordingLayout = PreferencesUtils.getAllCustomLayouts();
assertEquals(1, customRecordingLayout.size());
@@ -210,7 +210,7 @@ public class PreferencesUtilsTest {
String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layouts_key), null);
assertNotNull(csv);
assertEquals(csv,
"road cycling;" + PreferencesUtils.getLayoutColumnsByDefault() + ";"
"road cycling;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1,0;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1,0;"
@@ -258,8 +258,8 @@ public class PreferencesUtilsTest {
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
List<RecordingLayout> layoutsToBeUpdated = new ArrayList<>();
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsv(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsv(runningProfile, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsvLine(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsvLine(runningProfile, resources));
PreferencesUtils.updateCustomLayouts(layoutsToBeUpdated);
@@ -303,7 +303,7 @@ public class PreferencesUtilsTest {
+ context.getString(R.string.stats_custom_layout_distance_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
RecordingLayout recordingLayoutToBeUpdated = RecordingLayoutIO.fromCsv(cyclingProfileUpdated, resources);
RecordingLayout recordingLayoutToBeUpdated = RecordingLayoutIO.fromCsvLine(cyclingProfileUpdated, resources);
PreferencesUtils.updateCustomLayout(recordingLayoutToBeUpdated);
// then only updated profile is modified in the custom layouts
@@ -89,8 +89,7 @@ public class TrackStatisticsUpdaterTest {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3));
@@ -103,15 +102,18 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_distance_from_GPS_moving_and_sensor_moving() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(Distance.of(10f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
TrackPoint tp4 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), Distance.of(5));
TrackPoint tp5 = new TrackPoint(null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.ofEpochMilli(5000)),
Distance.of(10f),
null,
null,
null,
null,
null);
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3));
@@ -131,13 +133,10 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSensorDistance(Distance.of(5f));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), Distance.of(5));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), null);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
// when
@@ -158,10 +157,8 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_maxSpeed_multiple_segments() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), Speed.of(2), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), Speed.of(2), null),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(4))
));
assertEquals(Speed.of(2), subject.getTrackStatistics().maxSpeed());
@@ -169,10 +166,8 @@ public class TrackStatisticsUpdaterTest {
// when
List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(5)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6))
.setSpeed(Speed.of(1f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7))
.setSpeed(Speed.of(1f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6), Speed.of(1), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7), Speed.of(1), null),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(8))
).forEach(subject::addTrackPoint);
@@ -185,16 +180,32 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), Speed.of(2), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), Speed.of(2), null),
new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setHeartRate(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setHeartRate(50),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
createTrackPoint(0, 1, Altitude.WGS84.of(0), Instant.ofEpochSecond(50)),
createTrackPoint(0, 2, Altitude.WGS84.of(0), Instant.ofEpochSecond(55)),
@@ -210,21 +221,43 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30))
.setSensorDistance(Distance.ofKilometer(1)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setHeartRate(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setHeartRate(50),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(50))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(55))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), null, Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), null, Distance.of(10)),
new TrackPoint(
null,
TrackPoint.Type.IDLE,
Position.of(Instant.ofEpochSecond(30)),
Distance.ofKilometer(1),
null,
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(50), null, Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(55), null, Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(60))
));
@@ -240,8 +273,8 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
subject.addTrackPoint(
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(10))
.setSensorDistance(Distance.of(10)));
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(10), null, Distance.of(10))
);
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)));
// then
@@ -252,8 +285,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setSensorDistance(Distance.ZERO));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
Distance.ZERO,
null,
null,
null,
null,
null
)
);
// then
assertTrue(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -262,8 +305,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setSensorDistance(Distance.of(1)));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
Distance.of(1),
null,
null,
null,
null,
null
)
);
// then
assertTrue(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -272,8 +325,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(50))
.setSensorDistance(Distance.of(10)));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(50)),
Distance.of(10),
null,
null,
null,
null,
null
)
);
// then
assertFalse(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -294,15 +357,12 @@ public class TrackStatisticsUpdaterTest {
public void copy_constructor() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), null);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
TrackStatisticsUpdater initial = new TrackStatisticsUpdater(List.of(tp1, tp2,tp3, tp4));
TrackStatisticsUpdater initial = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3, tp4));
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(initial, tp5);
@@ -335,7 +395,13 @@ public class TrackStatisticsUpdaterTest {
}
public TrackPoint createTrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
return new TrackPoint(TrackPoint.Type.TRACKPOINT,
return createTrackPoint(latitude, longitude, altitude, time, null, null);
}
public TrackPoint createTrackPoint(double latitude, double longitude, Altitude altitude, Instant time, Speed speed, Distance sensorDistance) {
return new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
time,
latitude,
@@ -344,7 +410,14 @@ public class TrackStatisticsUpdaterTest {
altitude,
null,
null,
null
));
speed
),
sensorDistance,
null,
null,
null,
null,
null
);
}
}
@@ -14,7 +14,6 @@ import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -30,6 +29,7 @@ import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -47,7 +47,7 @@ public class EspressoAggregatedFilterTest {
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Before
public void setUp() {
@@ -60,28 +60,34 @@ public class EspressoAggregatedFilterTest {
@Test
public void espressoAggregatedFilterTest() {
// open AggregatedStatisticsActivity through toolbar's menu item
onView(ViewMatchers.withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_stats_button))));
onView(withId(R.id.aggregated_stats_button)).perform(click());
onView(ViewMatchers.withId(R.id.bottom_app_bar))
.check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar))
.check(matches(hasDescendant(withId(R.id.aggregated_stats_button))));
onView(withId(R.id.aggregated_stats_button))
.perform(click());
// open FilterDialogFragment through toolbar's menu item
onView(withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
onView(withId(R.id.aggregated_statistics_filter)).perform(click());
onView(withId(R.id.bottom_app_bar))
.check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar))
.check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
onView(withId(R.id.aggregated_statistics_filter))
.perform(click());
// check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text
ViewInteraction checkBox = onView(
allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
withParent(allOf(withId(R.id.filter_items),
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
isDisplayed()));
checkBox.perform(waitFor(2000));
onView(allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
withParent(allOf(withId(R.id.filter_items),
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
isDisplayed()))
.perform(waitFor(2000));
// check there's an edit text for "from date"
onView(withId(R.id.filter_date_edit_text_from)).check(matches(isDisplayed()));
onView(withId(R.id.filter_date_edit_text_from))
.check(matches(isDisplayed()));
// check there's an edit text for "to date"
onView(withId(R.id.filter_date_edit_text_to)).check(matches(isDisplayed()));
onView(withId(R.id.filter_date_edit_text_to))
.check(matches(isDisplayed()));
}
}
@@ -28,7 +28,7 @@ public class RecordingLayoutTest extends TestCase {
String csv = "running;2;" + context.getString(R.string.stats_custom_layout_speed_key) + ",1,1,0;" + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;" + context.getString(R.string.stats_custom_layout_altitude_key) + ",0,1,0;" + context.getString(R.string.stats_custom_layout_gain_key) + ",0,0,0;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -60,13 +60,12 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;speed,1,1;distance,0,0;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(0, recordingLayout.getFields().size());
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@Test
@@ -75,13 +74,13 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(0, recordingLayout.getFields().size());
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@Test
@@ -90,7 +89,7 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;speed,distance,total time;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -105,7 +104,7 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;speed,1,0;distance;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -145,6 +144,6 @@ public class RecordingLayoutTest extends TestCase {
String csv = recordingLayout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;" + PreferencesUtils.getLayoutColumnsByDefault() + ";key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
}
}
@@ -104,8 +104,7 @@ public class IntervalStatisticsUpdaterTest {
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
for (int i = 0; i < numberOfPoints; i++) {
trackPoints.add(TestDataUtil.createTrackPoint(i)
.setAltitudeGainLoss(null));
trackPoints.add(TestDataUtil.createTrackPoint(i, TrackPoint.Type.TRACKPOINT, null));
}
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoints);
@@ -47,6 +47,7 @@ import java.util.Objects;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.services.MissingPermissionException;
import de.dennisguse.opentracks.services.RecordingStatus;
@@ -62,7 +63,6 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.RecyclerViewSwipeDeleteCallback;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@@ -367,7 +367,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
if (itemId == R.id.list_context_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, false, trackIds);
DataProvider.showTrackOnMap(this, false, trackIds);
return true;
}
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
@@ -45,7 +46,6 @@ import de.dennisguse.opentracks.share.ShareUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
/**
@@ -167,7 +167,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, false, trackId);
DataProvider.showTrackOnMap(this, false, trackId);
return true;
}
@@ -38,17 +38,18 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
/**
@@ -266,7 +267,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, true, trackId);
DataProvider.showTrackOnMap(this, true, trackId);
return true;
}
@@ -278,15 +279,17 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
Intent intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId)
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.getPosition().toLocation());
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.position().toLocation());
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_menu_select_layout) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
List<String> layoutNames = PreferencesUtils.getAllCustomLayoutNames();
builder.setTitle(getString(R.string.custom_layout_select_layout)).setItems(layoutNames.toArray(new String[0]), (dialog, which) -> PreferencesUtils.setDefaultLayout(layoutNames.get(which)));
List<String> layoutNames = PreferencesUtils.getAllCustomLayouts().stream()
.map(RecordingLayout::getName)
.toList();
builder.setTitle(getString(R.string.custom_layout_select_layout)).setItems(layoutNames.toArray(new String[0]), (dialog, which) -> PreferencesUtils.setSelectedLayoutName(layoutNames.get(which)));
builder.create().show();
return true;
}
@@ -35,11 +35,11 @@ import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
/**
* A fragment to display track chart to the user.
@@ -204,7 +204,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull Statistics trackStatistics) {
if (isResumed()) {
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.getSpeed(), chartByDistance, viewBinding.chartView.getUnitSystem());
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.position().speed(), chartByDistance, viewBinding.chartView.getUnitSystem());
pendingPoints.add(point);
}
}
@@ -27,8 +27,8 @@ public record ChartPoint(
chartByDistance
? trackStatistics.totalDistance().toKM_Miles(unitSystem)
: trackStatistics.totalDuration().toMillis(),
trackPoint.hasAltitude()
? Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem)
trackPoint.position().hasAltitude()
? Distance.of(trackPoint.position().altitude().toM()).toM_FT(unitSystem)
: null,
smoothedSpeed != null
? smoothedSpeed.to(unitSystem)
@@ -36,14 +36,14 @@ public record ChartPoint(
smoothedSpeed != null
? smoothedSpeed.toPace(unitSystem).toSeconds() / 60d
: null,
trackPoint.hasHeartRate()
? (double) trackPoint.getHeartRate().getBPM()
trackPoint.heartRate() != null
? (double) trackPoint.heartRate().getBPM()
: null,
trackPoint.hasCadence()
? (double) trackPoint.getCadence().getRPM()
trackPoint.cadence() != null
? (double) trackPoint.cadence().getRPM()
: null,
trackPoint.hasPower()
? (double) trackPoint.getPower().getW()
trackPoint.power() != null
? (double) trackPoint.power().getW()
: null
);
}
@@ -49,12 +49,13 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.util.FileUtils;
@@ -68,9 +69,11 @@ public class ContentProviderUtils {
private static final String TAG = ContentProviderUtils.class.getSimpleName();
//TODO Move to CustomContentProvider
// The authority (the first part of the URI) for the app's content provider.
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
//TODO Move to CustomContentProvider
// The base URI for the app's content provider.
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
@@ -538,13 +541,19 @@ public class ContentProviderUtils {
int speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
int bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
int sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
int temperatureIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_TEMPERATURE);
int sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
int sensorDistanceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_DISTANCE);
int sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
int altitudeGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_GAIN);
int altitudeLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_LOSS);
TrackPoint trackPoint = new TrackPoint(
AltitudeGainLoss altitudeGainLoss = null;
if (!cursor.isNull(altitudeGainIndex) && !cursor.isNull(altitudeLossIndex)) {
altitudeGainLoss = new AltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex));
}
return new TrackPoint(
new TrackPoint.Id(cursor.getInt(idIndex)),
TrackPoint.Type.getById(cursor.getInt(typeIndex)),
new Position(
@@ -556,26 +565,14 @@ public class ContentProviderUtils {
!cursor.isNull(accuracyVerticalIndex) ? Distance.of(cursor.getFloat(accuracyVerticalIndex)) : null,
!cursor.isNull(bearingIndex) ? cursor.getFloat(bearingIndex) : null,
!cursor.isNull(speedIndex) ? Speed.of(cursor.getFloat(speedIndex)) : null
));
if (!cursor.isNull(sensorHeartRateIndex)) {
trackPoint.setHeartRate(cursor.getFloat(sensorHeartRateIndex));
}
if (!cursor.isNull(sensorCadenceIndex)) {
trackPoint.setCadence(cursor.getFloat(sensorCadenceIndex));
}
if (!cursor.isNull(sensorDistanceIndex)) {
trackPoint.setSensorDistance(Distance.of(cursor.getFloat(sensorDistanceIndex)));
}
if (!cursor.isNull(sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(sensorPowerIndex));
}
if (!cursor.isNull(altitudeGainIndex) && !cursor.isNull(altitudeLossIndex)) {
trackPoint.setAltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex));
}
return trackPoint;
),
!cursor.isNull(sensorDistanceIndex) ? Distance.of(cursor.getFloat(sensorDistanceIndex)) : null,
!cursor.isNull(sensorHeartRateIndex) ? HeartRate.of(cursor.getFloat(sensorHeartRateIndex)) : null,
!cursor.isNull(temperatureIndex) ? Temperature.of(cursor.getFloat(temperatureIndex)) : null,
!cursor.isNull(sensorCadenceIndex) ? Cadence.of(cursor.getFloat(sensorCadenceIndex)) : null,
!cursor.isNull(sensorPowerIndex) ? Power.of(cursor.getFloat(sensorPowerIndex)) : null,
altitudeGainLoss
);
}
//TODO Only used for file import; might be better to replace it.
@@ -637,42 +634,45 @@ public class ContentProviderUtils {
private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId.id());
values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
values.put(TrackPointsColumns.TYPE, trackPoint.type().type_db);
if (trackPoint.hasLocation()) {
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getPosition().latitude() * 1E6));
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getPosition().longitude() * 1E6));
if (trackPoint.position().hasLocation()) {
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.position().latitude() * 1E6));
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.position().longitude() * 1E6));
}
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude().toM());
if (trackPoint.position().hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.position().altitude().toM());
}
if (trackPoint.hasHorizontalAccuracy()) {
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.getHorizontalAccuracy().toM());
if (trackPoint.position().hasHorizontalAccuracy()) {
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.position().horizontalAccuracy().toM());
}
if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed().toMPS());
if (trackPoint.position().hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.position().speed().toMPS());
}
if (trackPoint.hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
if (trackPoint.position().hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.position().bearing());
}
if (trackPoint.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate().getBPM());
if (trackPoint.heartRate() != null) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.heartRate().getBPM());
}
if (trackPoint.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCadence().getRPM());
if (trackPoint.temperature() != null) {
values.put(TrackPointsColumns.SENSOR_TEMPERATURE, trackPoint.temperature().getCelsius());
}
if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM());
if (trackPoint.cadence() != null) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.cadence().getRPM());
}
if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower().getW());
if (trackPoint.sensorDistance() != null) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.sensorDistance().toM());
}
if (trackPoint.power() != null) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.power().getW());
}
if (trackPoint.hasAltitudeGainLoss()) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.getAltitudeGainLoss().gain_m());
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.getAltitudeGainLoss().loss_m());
if (trackPoint.altitudeGainLoss() != null) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.altitudeGainLoss().gain_m());
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.altitudeGainLoss().loss_m());
}
return values;
}
@@ -38,6 +38,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
@@ -94,7 +95,6 @@ public class CustomContentProvider extends ContentProvider {
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
public CustomContentProvider() {
//TODO Add tests for used URIs (incl. Dashboard API)
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath() + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal());
@@ -108,6 +108,10 @@ public class CustomContentProvider extends ContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, UrlType.DASHBOARD_TRACKS_BY_IDS.getPath() + "*", UrlType.DASHBOARD_TRACKS_BY_IDS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.getPath() + "*", UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.getPath() + "*", UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.ordinal());
}
@Override
@@ -170,12 +174,21 @@ public class CustomContentProvider extends ContentProvider {
@Override
public String getType(@NonNull Uri url) {
String contentType = getUrlType(url).contentType;
if (contentType != null) {
return contentType;
}
//TODO replace with getUrlType(url).getContentType()
return switch (getUrlType(url)) {
case TRACKPOINTS -> TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_BY_TRACKID -> TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS -> TracksColumns.CONTENT_TYPE;
case MARKERS -> MarkerColumns.CONTENT_TYPE;
case MARKERS_BY_TRACKID -> MarkerColumns.CONTENT_ITEMTYPE;
case DASHBOARD_TRACKS_BY_IDS -> UrlType.DASHBOARD_TRACKS_BY_IDS.contentType;
case DASHBOARD_TRACKPOINTS_BY_TRACKIDS -> UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.contentType;
case DASHBOARD_MARKERS_BY_TRACKIDS -> UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.contentType;
default -> throw new IllegalArgumentException("Unknown URL " + url);
};
}
@@ -225,6 +238,7 @@ public class CustomContentProvider extends ContentProvider {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String sortOrder = null;
String groupBy = null;
//TODO queryBuilder.setStrict(true);
//TODO queryBuilder.appendWhere() should use selectionArgs?
switch (getUrlType(url)) {
case TRACKPOINTS -> {
@@ -239,6 +253,11 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
}
case DASHBOARD_TRACKPOINTS_BY_TRACKIDS -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKPOINTS);
}
case TRACKS -> {
if (projection != null && Arrays.asList(projection).contains(TracksColumns.MARKER_COUNT)) {
queryBuilder.setTables(TracksColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT " + MarkerColumns.TRACKID + " AS markerTrackId, COUNT(*) AS " + TracksColumns.MARKER_COUNT + " FROM " + MarkerColumns.TABLE_NAME + " GROUP BY " + MarkerColumns.TRACKID + ") ON (" + TracksColumns.TABLE_NAME + "." + TracksColumns._ID + "= markerTrackId)");
@@ -251,6 +270,11 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
}
case DASHBOARD_TRACKS_BY_IDS -> {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKS);
}
case TRACKS_SENSOR_STATS -> {
//TODO Replace with SQLQueryBuilder?
long trackId = ContentUris.parseId(url);
@@ -278,9 +302,14 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.appendWhere(MarkerColumns._ID + "=" + ContentUris.parseId(url));
}
case MARKERS_BY_TRACKID -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT 'unused' AS ICON)"); //TODO Workaround: Column icon was removed in v4.24.0, but is still required for OSM Dashboard v6.4.1.
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
}
case DASHBOARD_MARKERS_BY_TRACKIDS -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_MARKERS);
}
default -> throw new IllegalArgumentException("Unknown url " + url);
}
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder);
@@ -372,9 +401,11 @@ public class CustomContentProvider extends ContentProvider {
throw new SQLException("Failed to insert a marker " + url);
}
private enum UrlType {
//TODO Add ContentType as attribute
public enum UrlType {
TRACKPOINTS,
TRACKPOINTS_BY_ID,
//TODO Check if still used
TRACKPOINTS_BY_TRACKID,
TRACKS,
TRACKS_BY_ID,
@@ -382,6 +413,36 @@ public class CustomContentProvider extends ContentProvider {
TRACKS_AGGREGATED_STATISTICS,
MARKERS,
MARKERS_BY_ID,
MARKERS_BY_TRACKID
@Deprecated //TODO Should not be used anymore
MARKERS_BY_TRACKID,
DASHBOARD_TRACKS_BY_IDS(Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/"), TracksColumns.CONTENT_TYPE),
DASHBOARD_TRACKPOINTS_BY_TRACKIDS(Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/"), TrackPointsColumns.CONTENT_TYPE),
DASHBOARD_MARKERS_BY_TRACKIDS(Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/"), MarkerColumns.CONTENT_TYPE);
private final Uri uri;
private final String contentType;
//TODO Add Uri as attribute:
//TODO Add ContentType as attribute:
@Deprecated()
UrlType() {
this.uri = null;
this.contentType = null;
}
UrlType(Uri uri, String contentType) {
this.uri = uri;
this.contentType = contentType;
}
public Uri getUri() {
return uri;
}
String getPath() {
return uri.getPath();
}
}
}
@@ -29,7 +29,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 40;
private static final int DATABASE_VERSION = 43;
private final Context context;
@@ -84,6 +84,9 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 38 -> upgradeFrom37to38(db);
case 39 -> upgradeFrom38to39(db);
case 40 -> upgradeFrom39to40(db);
case 41 -> upgradeFrom40to41(db);
case 42 -> upgradeFrom41to42(db);
case 43 -> upgradeFrom42to43(db);
default -> throw new RuntimeException("Not implemented: upgrade to " + toVersion);
}
}
@@ -111,6 +114,9 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 37 -> downgradeFrom38to37(db);
case 38 -> downgradeFrom39to38(db);
case 39 -> downgradeFrom40to39(db);
case 40 -> downgradeFrom41to40(db);
case 41 -> downgradeFrom42to41(db);
case 42 -> downgradeFrom43to42(db);
default -> throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
}
@@ -734,4 +740,74 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.setTransactionSuccessful();
db.endTransaction();
}
private void upgradeFrom40to41(SQLiteDatabase db) {
db.beginTransaction();
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_temperature FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1, 3)), sensor_distance FLOAT, accuracy_vertical 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, null, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance, accuracy_vertical FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom41to40(SQLiteDatabase db) {
db.beginTransaction();
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, type TEXT CHECK(type IN (-2, -1, 0, 1, 3)), sensor_distance FLOAT, accuracy_vertical 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, type, sensor_distance, accuracy_vertical FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void upgradeFrom41to42(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE markers RENAME TO markers_old");
db.execSQL("CREATE TABLE markers (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, name TEXT, description TEXT, marker_type_localized TEXT, photoUrl TEXT, longitude INTEGER, latitude INTEGER, time INTEGER, altitude FLOAT, accuracy FLOAT, bearing FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO markers SELECT _id, trackid, name, description, category, photoUrl, longitude, latitude, time, elevation, accuracy, bearing FROM markers_old");
db.execSQL("DROP TABLE markers_old");
db.execSQL("CREATE INDEX markers_trackid_index ON markers(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom42to41(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE markers RENAME TO markers_old");
db.execSQL("CREATE TABLE markers (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO markers SELECT _id, name, description, marker_type_localized, trackid, longitude, latitude, time, altitude, accuracy, bearing, photoUrl FROM markers_old");
db.execSQL("DROP TABLE markers_old");
db.execSQL("CREATE INDEX markers_trackid_index ON markers(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void upgradeFrom42to43(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("UPDATE trackpoints SET heartrate = null WHERE heartrate = 0.0");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom43to42(SQLiteDatabase db) {
// Downgrade not possible: invalid data replaced with null.
}
}
@@ -89,11 +89,10 @@ public class ShareContentProvider extends CustomContentProvider {
trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getPreferenceId() + "/" + trackIdBuilder + "/" + Uri.encode(filename));
String mime = getTypeMime(uri);
Log.d(TAG, "Created uri " + uri + " with MIME " + mime);
Log.d(TAG, "Created uri " + uri + " with MIME " + trackFileFormat.getMimeType());
return new Pair<>(uri, mime);
return new Pair<>(uri, trackFileFormat.getMimeType());
}
static Set<Track.Id> parseURI(Uri uri) {
@@ -126,6 +125,7 @@ public class ShareContentProvider extends CustomContentProvider {
}
}
@Nullable
private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) {
return switch (uriMatcher.match(uri)) {
case URI_GPX -> TrackFileFormat.GPX;
@@ -134,16 +134,10 @@ public class ShareContentProvider extends CustomContentProvider {
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES ->
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
case URI_CSV -> TrackFileFormat.CSV;
default ->
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
default -> null;
};
}
@Nullable
private static String getTypeMime(@NonNull Uri uri) {
return getTrackFileFormat(uri).getMimeType();
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
if (uriMatcher.match(uri) == -1) {
@@ -182,9 +176,9 @@ public class ShareContentProvider extends CustomContentProvider {
@Nullable
@Override
public String getType(@NonNull Uri uri) {
String mime = getTypeMime(uri);
if (mime != null) {
return mime;
TrackFileFormat trackFileFormat = getTrackFileFormat(uri);
if (trackFileFormat != null) {
return trackFileFormat.getMimeType();
}
return super.getType(uri);
@@ -33,12 +33,12 @@ import java.util.Set;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
/**
* Track data hub.
@@ -288,14 +288,14 @@ public class TrackDataHub {
}
trackPoint = trackPointIterator.next();
TrackPoint.Id trackPointId = trackPoint.getId();
TrackPoint.Id trackPointId = trackPoint.id();
// Stop if past the last wanted point
if (maxPointId != null && trackPointId.id() > maxPointId.id()) {
break;
}
egm2008Correction.correctAltitude(context, trackPoint);
trackPoint = egm2008Correction.correctAltitude(context, trackPoint);
if (localFirstSeenTrackPointId == null) {
localFirstSeenTrackPointId = trackPointId;
@@ -319,7 +319,7 @@ public class TrackDataHub {
}
if (trackPoint != null) {
localLastSeenTrackPointIdId = trackPoint.getId();
localLastSeenTrackPointIdId = trackPoint.id();
}
if (updateSamplingState) {
@@ -1,5 +1,7 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.Nullable;
public record HeartRate(float value) {
public static HeartRate INVALID = HeartRate.of(0.0f);
@@ -7,6 +9,15 @@ public record HeartRate(float value) {
return new HeartRate(value);
}
@Nullable
public static HeartRate ofOrNull(Float value) {
if (value == null) return null;
HeartRate rate = of(value);
if (rate.isValid()) return rate;
return null;
}
public float getBPM() {
return value;
}
@@ -51,10 +51,10 @@ public final class MarkerBuilder {
this.typeLocalized = "";
this.photoUrl = null;
if (!trackPoint.hasLocation())
if (!trackPoint.position().hasLocation())
throw new RuntimeException("Marker requires a trackpoint with a location.");
this.position = trackPoint.getPosition();
this.position = trackPoint.position();
}
public MarkerBuilder(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint, String name, String description, String typeLocalized, Uri photoUrl) {
@@ -17,8 +17,9 @@ public record Position(
@Nullable Float bearing,
@Nullable Speed speed
) {
@Deprecated
public static Position empty() {
return of((Instant) null);
return of(null);
}
public static Position of(@NonNull Instant time) {
@@ -34,10 +35,6 @@ public record Position(
);
}
public static Position of(@NonNull Location location) {
return of(location, Instant.ofEpochMilli(location.getTime()));
}
public static Position of(@NonNull Location location, @NonNull Instant time) {
return new Position(
time,
@@ -132,21 +129,12 @@ public record Position(
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
//TODO Use double
public Position withCoordinates(Double latitude, Double longitude) {
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
public Position withCoordinates(Position position) {
return new Position(time, position.latitude, position.longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
//TODO Use float
public Position withBearing(Float bearing) {
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
public Position withHorizontalAccuracy(Distance horizontalAccuracy) {
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
public Position withVerticalAccuracy(Distance verticalAccuracy) {
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
}
}
@@ -0,0 +1,18 @@
package de.dennisguse.opentracks.data.models;
public record Temperature(
float value_celsius
) {
public static Temperature of(float value_celsius) {
return new Temperature(value_celsius);
}
public static Temperature ofCentiCelsius(int value_centi_celsius) {
return Temperature.of((float) (value_centi_celsius / 100.0));
}
public float getCelsius() {
return value_celsius;
}
}
@@ -22,23 +22,188 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Instant;
import java.util.Optional;
/**
* Sensor and/or location information for a specific point in time.
* <p>
* Time is created using the {@link de.dennisguse.opentracks.services.handlers.MonotonicClock}, because system time jump backwards.
* GPS time is ignored as for non-GPS events, we could not create GPS-based timestamps.
* GPS time is ignored as for non-GPS events, we cannot create GPS-based timestamps.
*/
//TODO Should be a record (with final properties)
public class TrackPoint {
public record TrackPoint(
@Nullable
private final TrackPoint.Id id;
@Nullable
TrackPoint.Id id,
@NonNull
TrackPoint.Type type,
//Requires: position.time must be non-null
@NonNull
Position position,
Distance sensorDistance,
HeartRate heartRate,
Temperature temperature,
Cadence cadence,
Power power,
AltitudeGainLoss altitudeGainLoss
) {
public TrackPoint(Type type, Position position) {
this(
null,
type,
position,
null,
null,
null,
null,
null,
null
);
}
@Deprecated //TOOD Private?
public TrackPoint(Type type, Instant now) {
this(
null,
type,
Position.of(now),
null,
null,
null,
null,
null,
null
);
}
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
return new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, time);
}
public static TrackPoint with(TrackPoint trackPoint, Altitude.EGM2008 correctAltitude) {
return new TrackPoint(
trackPoint.id,
trackPoint.type,
new Position(
trackPoint.position.time(),
trackPoint.position.latitude(),
trackPoint.position.longitude(),
trackPoint.position.horizontalAccuracy(),
correctAltitude,
trackPoint.position.verticalAccuracy(),
trackPoint.position.bearing(),
trackPoint.position.speed()
),
trackPoint.sensorDistance,
trackPoint.heartRate,
null,
trackPoint.cadence,
trackPoint.power,
trackPoint.altitudeGainLoss
);
}
public TrackPoint with(Type newType) {
return new TrackPoint(
id,
newType,
position,
sensorDistance,
heartRate,
null,
cadence,
power,
altitudeGainLoss
);
}
public TrackPoint removeHeartRate() {
return new TrackPoint(
id,
type,
position,
sensorDistance,
null,
cadence,
power,
altitudeGainLoss
);
}
public TrackPoint with(Position newPosition) {
return new TrackPoint(
id,
type,
newPosition,
sensorDistance,
heartRate,
null,
cadence,
power,
altitudeGainLoss
);
}
//Requires: position.time must be non-null
@NonNull
private Position position;
public Instant getTime() {
return position.time();
}
public TrackPoint minusCumulativeSensorData(@NonNull TrackPoint lastTrackPoint) {
Distance newSensorDistance = sensorDistance;
if (sensorDistance != null && lastTrackPoint.sensorDistance != null) {
newSensorDistance = sensorDistance.minus(lastTrackPoint.sensorDistance);
}
AltitudeGainLoss newAltitudeGainLoss = altitudeGainLoss;
if (altitudeGainLoss != null && lastTrackPoint.altitudeGainLoss() != null) {
newAltitudeGainLoss = new AltitudeGainLoss(
altitudeGainLoss.gain_m() - lastTrackPoint.altitudeGainLoss.gain_m(),
altitudeGainLoss.loss_m() - lastTrackPoint.altitudeGainLoss.loss_m());
}
return new TrackPoint(
id,
type,
position,
newSensorDistance,
heartRate,
null,
cadence,
power,
newAltitudeGainLoss
);
}
@NonNull
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (sensorDistance() != null) {
return sensorDistance();
}
return distanceToPreviousFromLocation(previous);
}
@NonNull
public Distance distanceToPreviousFromLocation(@NonNull TrackPoint previous) {
if (!position.hasLocation() || !previous.position().hasLocation()) {
throw new RuntimeException("Cannot compute distance.");
}
return Distance.of(position().toLocation().distanceTo(previous.position().toLocation()));
}
public boolean isIdleTriggered() {
return type == TrackPoint.Type.IDLE;
}
public boolean isSegmentManualStart() {
return type == TrackPoint.Type.SEGMENT_START_MANUAL;
}
public boolean isSegmentManualEnd() {
return type == TrackPoint.Type.SEGMENT_END_MANUAL;
}
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
@@ -75,298 +240,6 @@ public class TrackPoint {
}
}
@NonNull
private Type type;
private Distance sensorDistance;
private HeartRate heartRate = null;
private Cadence cadence = null;
private Power power = null;
private AltitudeGainLoss altitudeGainLoss = null;
public TrackPoint(@Nullable TrackPoint.Id id, @NonNull Type type, @NonNull Position position) {
this.id = id;
this.type = type;
this.position = position;
}
public TrackPoint(@NonNull Type type, @NonNull Instant time) {
this(null, type, Position.of(time));
}
public TrackPoint(@NonNull Type type, @NonNull Position position) {
this(null, type, position);
}
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_START_MANUAL, time);
}
public static TrackPoint createSegmentEndWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_END_MANUAL, time);
}
@NonNull
public Type getType() {
return type;
}
@Deprecated //Should not be needed.
public TrackPoint setType(@NonNull Type type) {
this.type = type;
return this;
}
public boolean isIdleTriggered() {
return type == Type.IDLE;
}
public boolean isSegmentManualStart() {
return type == Type.SEGMENT_START_MANUAL;
}
public boolean isSegmentManualEnd() {
return type == Type.SEGMENT_END_MANUAL;
}
//TODO Check if naming is correct.
public boolean wasCreatedManually() {
return hasLocation() || hasSpeed();
}
/**
* May be null if the track was not loaded from the database.
*/
@Nullable
public TrackPoint.Id getId() {
return id;
}
public boolean hasLocation() {
return position.hasLocation();
}
@NonNull
public Position getPosition() {
return position;
}
public TrackPoint setPosition(Position position) {
this.position = position.with(this.position.time());
return this;
}
public boolean hasAltitudeGainLoss() {
return altitudeGainLoss != null;
}
public AltitudeGainLoss getAltitudeGainLoss() {
return altitudeGainLoss;
}
public TrackPoint setAltitudeGainLoss(AltitudeGainLoss altitudeGainLoss) {
this.altitudeGainLoss = altitudeGainLoss;
return this;
}
public TrackPoint setAltitudeGainLoss(float altitudeGain_m, float altitudeLoss_m) {
setAltitudeGainLoss(new AltitudeGainLoss(altitudeGain_m, altitudeLoss_m));
return this;
}
@NonNull
public Instant getTime() {
return position.time();
}
public boolean hasAltitude() {
return position.hasAltitude();
}
public Altitude getAltitude() {
return position.altitude();
}
@Deprecated
public TrackPoint setAltitude(Altitude altitude) {
position = position.with(altitude);
return this;
}
public boolean hasSpeed() {
return position.hasSpeed();
}
public Speed getSpeed() {
return position.speed();
}
@Deprecated
public TrackPoint setSpeed(Speed speed) {
this.position = position.with(speed);
return this;
}
public boolean hasBearing() {
return position.hasBearing();
}
public float getBearing() {
return position.bearing();
}
public TrackPoint setBearing(Float bearing) {
this.position = this.position.withBearing(bearing);
return this;
}
public boolean hasHorizontalAccuracy() {
return position.hasHorizontalAccuracy();
}
public Distance getHorizontalAccuracy() {
return position.horizontalAccuracy();
}
@Deprecated
public TrackPoint setHorizontalAccuracy(Distance horizontalAccuracy) {
this.position = this.position.withHorizontalAccuracy(horizontalAccuracy);
return this;
}
public boolean hasVerticalAccuracy() {
return position.hasVerticalAccuracy();
}
public Distance getVerticalAccuracy() {
return position.verticalAccuracy();
}
public TrackPoint setVerticalAccuracy(Distance verticalAccuracy) {
this.position = this.position.withVerticalAccuracy(verticalAccuracy);
return this;
}
@NonNull
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
return distanceToPreviousFromLocation(previous);
}
@NonNull
public Distance distanceToPreviousFromLocation(@NonNull TrackPoint previous) {
if (!hasLocation() || hasLocation() != previous.hasLocation()) {
throw new RuntimeException("Cannot compute distance.");
}
return Distance.of(getPosition().toLocation().distanceTo(previous.getPosition().toLocation()));
}
public boolean fulfillsAccuracy(Distance thresholdHorizontalAccuracy) {
return position.fulfillsAccuracy(thresholdHorizontalAccuracy);
}
public Optional<Float> bearingTo(@NonNull TrackPoint dest) {
if (!dest.hasLocation() || !hasLocation()) {
return Optional.empty();
}
return Optional.of(getPosition().toLocation().bearingTo(dest.getPosition().toLocation()));
}
// Sensor data
public boolean hasSensorDistance() {
return sensorDistance != null;
}
public Distance getSensorDistance() {
return sensorDistance;
}
public TrackPoint setSensorDistance(Distance distance_m) {
this.sensorDistance = distance_m;
return this;
}
public TrackPoint minusCumulativeSensorData(@NonNull TrackPoint lastTrackPoint) {
if (hasSensorDistance() && lastTrackPoint.hasSensorDistance()) {
sensorDistance = sensorDistance.minus(lastTrackPoint.getSensorDistance());
}
if (hasAltitudeGainLoss() && lastTrackPoint.hasAltitudeGainLoss()) {
altitudeGainLoss = new AltitudeGainLoss(
altitudeGainLoss.gain_m() - lastTrackPoint.altitudeGainLoss.gain_m(),
altitudeGainLoss.loss_m() - lastTrackPoint.altitudeGainLoss.loss_m());
}
return this;
}
public boolean hasHeartRate() {
return heartRate != null;
}
public HeartRate getHeartRate() {
return heartRate;
}
public TrackPoint setHeartRate(HeartRate heartRate) {
this.heartRate = heartRate;
return this;
}
public TrackPoint setHeartRate(float heartRate) {
return setHeartRate(HeartRate.of(heartRate));
}
public boolean hasCadence() {
return cadence != null;
}
public Cadence getCadence() {
return cadence;
}
public TrackPoint setCadence(Cadence cadence) {
this.cadence = cadence;
return this;
}
public TrackPoint setCadence(float cadence) {
return setCadence(Cadence.of(cadence));
}
public boolean hasPower() {
return power != null;
}
public Power getPower() {
return power;
}
public TrackPoint setPower(Power power) {
this.power = power;
return this;
}
public TrackPoint setPower(float power) {
return setPower(Power.of(power));
}
@NonNull
@Override
public String toString() {
return "TrackPoint{" +
"id=" + id +
", type=" + type +
", position=" + position +
", sensorDistance=" + sensorDistance +
", heartRate=" + heartRate +
", cadence=" + cadence +
", power=" + power +
", altitudeGainLoss=" + altitudeGainLoss +
'}';
}
public record Id(long id) implements Parcelable {
@Override
@@ -154,7 +154,7 @@ public class SegmentStatisticUpdater {
Duration movingDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
if (movingDuration.isNegative()) {
throw new RuntimeException("Moving time cannot be negative");
throw new RuntimeException("Moving time cannot be negative: " + lastTrackPoint.getTime() + " is after " + trackPoint.getTime());
}
this.movingDuration = this.movingDuration.plus(movingDuration);
}
@@ -111,32 +111,32 @@ public class TrackStatisticsUpdater {
currentSegment.updateTotalTime(trackPoint.getTime());
// Process sensor data: barometer
if (trackPoint.hasAltitudeGainLoss()) {
currentSegment.addTotalAltitudeGainLoss(trackPoint.getAltitudeGainLoss());
if (trackPoint.altitudeGainLoss() != null) {
currentSegment.addTotalAltitudeGainLoss(trackPoint.altitudeGainLoss());
}
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
currentSegment.updateAltitudeExtremities(trackPoint.getAltitude());
// Update absolute (GPS-based) altitude
if (trackPoint.position().hasAltitude()) {
currentSegment.updateAltitudeExtremities(trackPoint.position().altitude());
}
// Update heart rate
if (trackPoint.hasHeartRate() && lastTrackPoint != null) {
if (trackPoint.heartRate() != null && lastTrackPoint != null) {
Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
Duration newTotalDuration = totalHeartRateDuration.plus(trackPointDuration);
averageHeartRateBPM = (totalHeartRateDuration.toMillis() * averageHeartRateBPM + trackPointDuration.toMillis() * trackPoint.getHeartRate().getBPM()) / newTotalDuration.toMillis();
averageHeartRateBPM = (totalHeartRateDuration.toMillis() * averageHeartRateBPM + trackPointDuration.toMillis() * trackPoint.heartRate().getBPM()) / newTotalDuration.toMillis();
totalHeartRateDuration = newTotalDuration;
currentSegment.setAverageHeartRate(HeartRate.of(averageHeartRateBPM));
}
// Update power
if (trackPoint.hasPower() && lastTrackPoint != null) {
if (trackPoint.power() != null && lastTrackPoint != null) {
Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
Duration newTotalDuration = totalPowerDuration.plus(trackPointDuration);
averagePowerW = (totalPowerDuration.toMillis() * averagePowerW + trackPointDuration.toMillis() * trackPoint.getPower().getW()) / newTotalDuration.toMillis();
averagePowerW = (totalPowerDuration.toMillis() * averagePowerW + trackPointDuration.toMillis() * trackPoint.power().getW()) / newTotalDuration.toMillis();
totalPowerDuration = newTotalDuration;
currentSegment.setAveragePower(Power.of(averagePowerW));
@@ -145,11 +145,11 @@ public class TrackStatisticsUpdater {
{
// Update total distance
Distance movingDistance = null;
if (trackPoint.hasSensorDistance()) {
movingDistance = trackPoint.getSensorDistance();
if (trackPoint.sensorDistance() != null) {
movingDistance = trackPoint.sensorDistance();
} else if (lastTrackPoint != null
&& lastTrackPoint.hasLocation()
&& trackPoint.hasLocation()) {
&& lastTrackPoint.position().hasLocation()
&& trackPoint.position().hasLocation()) {
// GPS-based distance/speed
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
}
@@ -173,7 +173,7 @@ public class TrackStatisticsUpdater {
}
}
if (trackPoint.hasSpeed()) {
if (trackPoint.position().hasSpeed()) {
updateSpeed(trackPoint);
}
}
@@ -205,7 +205,7 @@ public class TrackStatisticsUpdater {
* Updates a speed reading while assuming the user is moving.
*/
private void updateSpeed(@NonNull TrackPoint trackPoint) {
Speed currentSpeed = trackPoint.getSpeed();
Speed currentSpeed = trackPoint.position().speed();
if (currentSpeed.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(currentSpeed);
}
@@ -215,8 +215,8 @@ public class TrackStatisticsUpdater {
@Override
public String toString() {
return "TrackStatisticsUpdater{" +
"segmentStatisticUpdater=" + statisticsWithoutCurrentSegment +
", currentSegment=" + currentSegment +
"currentSegment=" + currentSegment +
", segmentStatisticUpdater=" + statisticsWithoutCurrentSegment +
", lastTrackPoint=" + lastTrackPoint +
'}';
}
@@ -39,32 +39,32 @@ public interface MarkerColumns extends BaseColumns {
String DEFAULT_SORT_ORDER = _ID;
// Columns
String NAME = "name"; // marker name
String DESCRIPTION = "description"; // marker description
String TYPE_LOCALIZED = "category";
String TRACKID = "trackid"; // track id
String LONGITUDE = "longitude"; // longitude
String LATITUDE = "latitude"; // latitude
String TIME = "time"; // time
String ALTITUDE = "elevation"; // altitude //TODO RENAME column
String HORIZONTAL_ACCURACY = "accuracy"; // accuracy
String BEARING = "bearing"; // bearing
String NAME = "name";
String DESCRIPTION = "description";
String TYPE_LOCALIZED = "marker_type_localized";
String TRACKID = "trackid";
String LONGITUDE = "longitude";
String LATITUDE = "latitude";
String TIME = "time";
String ALTITUDE = "altitude";
String HORIZONTAL_ACCURACY = "accuracy";
String BEARING = "bearing";
String PHOTOURL = "photoUrl"; // url for the photo
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TRACKID + " INTEGER NOT NULL, "
+ NAME + " TEXT, "
+ DESCRIPTION + " TEXT, "
+ TYPE_LOCALIZED + " TEXT, "
+ TRACKID + " INTEGER NOT NULL, "
+ PHOTOURL + " TEXT, "
+ LONGITUDE + " INTEGER, "
+ LATITUDE + " INTEGER, "
+ TIME + " INTEGER, "
+ ALTITUDE + " FLOAT, "
+ HORIZONTAL_ACCURACY + " FLOAT, "
+ BEARING + " FLOAT, "
+ PHOTOURL + " TEXT, "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
+ ")";
@@ -50,6 +50,7 @@ public interface TrackPointsColumns extends BaseColumns {
String SPEED = "speed";
String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate";
String SENSOR_TEMPERATURE = "sensor_temperature";
String SENSOR_CADENCE = "sensor_cadence";
String SENSOR_DISTANCE = "sensor_distance"; //DISTANCE from previous TrackPoint
String SENSOR_POWER = "sensor_power";
@@ -75,6 +76,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ SPEED + " FLOAT, "
+ BEARING + " FLOAT, "
+ SENSOR_HEARTRATE + " FLOAT, "
+ SENSOR_TEMPERATURE + " FLOAT, "
+ SENSOR_CADENCE + " FLOAT, "
+ SENSOR_POWER + " FLOAT, "
+ ALTITUDE_GAIN + " FLOAT, "
@@ -5,7 +5,6 @@ import android.content.res.Resources;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
@@ -31,11 +30,13 @@ public enum TrackFileFormat {
return new KMLTrackExporter(context, contentProviderUtils, false);
}
@NonNull
@Override
public String getMimeType() {
return "application/vnd.google-earth.kml+xml";
}
@NonNull
public String getExtension() {
return "kml";
}
@@ -50,11 +51,13 @@ public enum TrackFileFormat {
return new KMZTrackExporter(context, contentProviderUtils, exportPhotos);
}
@NonNull
@Override
public String getMimeType() {
return "application/vnd.google-earth.kmz";
}
@NonNull
public String getExtension() {
return "kmz";
}
@@ -72,11 +75,13 @@ public enum TrackFileFormat {
return new GPXTrackExporter(contentProviderUtils, context.getString(R.string.app_name));
}
@NonNull
@Override
public String getMimeType() {
return "application/gpx+xml";
}
@NonNull
public String getExtension() {
return "gpx";
}
@@ -88,11 +93,13 @@ public enum TrackFileFormat {
return new CSVTrackExporter(contentProviderUtils);
}
@NonNull
@Override
public String getMimeType() {
return "text/csv";
}
@NonNull
@Override
public String getExtension() {
return "csv";
@@ -115,16 +122,10 @@ public enum TrackFileFormat {
return preferenceIdLabelMap;
}
public static TrackFileFormat valueOfPreferenceId(final String preferenceId) {
return Arrays.stream(values())
.filter(trackFileFormat -> trackFileFormat.getPreferenceId().equals(preferenceId))
.findFirst()
.orElse(null);
}
/**
* Returns the mime type for each format.
*/
@NonNull
public abstract String getMimeType();
/**
@@ -137,6 +138,7 @@ public enum TrackFileFormat {
/**
* Returns the file extension for each format.
*/
@NonNull
public abstract String getExtension();
/**
@@ -149,6 +151,7 @@ public enum TrackFileFormat {
/**
* The identifier to be stored in the preferences.
*/
@NonNull
public String getPreferenceId() {
return preferenceId;
}
@@ -90,20 +90,20 @@ public class CSVTrackExporter implements TrackExporter {
public boolean writeTrack(@NonNull List<Track> tracks, @NonNull OutputStream outputStream) {
List<Column> columns = List.of(
new Column("time", null),
new Column("trackpoint_type", t -> quote(t.getType().name())),
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getPosition().latitude()) : ""),
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getPosition().longitude()) : ""),
new Column("altitude", t -> t.hasAltitude() ? ALTITUDE_FORMAT.format(t.getAltitude().toM()) : ""),
new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""),
new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""),
new Column("trackpoint_type", t -> quote(t.type().name())),
new Column("latitude", t -> t.position().hasLocation() ? COORDINATE_FORMAT.format(t.position().latitude()) : ""),
new Column("longitude", t -> t.position().hasLocation() ? COORDINATE_FORMAT.format(t.position().longitude()) : ""),
new Column("altitude", t -> t.position().hasAltitude() ? ALTITUDE_FORMAT.format(t.position().altitude().toM()) : ""),
new Column("accuracy_horizontal", t -> t.position().hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.position().horizontalAccuracy().toM()) : ""),
new Column("accuracy_vertical", t -> t.position().hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.position().verticalAccuracy().toM()) : ""),
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
new Column("altitude_gain", t -> t.hasAltitudeGainLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeGainLoss().gain_m()) : ""),
new Column("altitude_loss", t -> t.hasAltitudeGainLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeGainLoss().loss_m()) : ""),
new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""),
new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""),
new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""),
new Column("power", t -> t.hasPower() ? POWER_FORMAT.format(t.getPower().getW()) : ""));
new Column("speed", t -> t.position().hasSpeed() ? SPEED_FORMAT.format(t.position().speed().toKMH()) : ""),
new Column("altitude_gain", t -> t.altitudeGainLoss() != null ? ALTITUDE_FORMAT.format(t.altitudeGainLoss().gain_m()) : ""),
new Column("altitude_loss", t -> t.altitudeGainLoss() != null ? ALTITUDE_FORMAT.format(t.altitudeGainLoss().loss_m()) : ""),
new Column("sensor_distance", t -> t.sensorDistance() != null ? DISTANCE_FORMAT.format(t.sensorDistance().toM()) : ""),
new Column("heartrate", t -> t.heartRate() != null ? HEARTRATE_FORMAT.format(t.heartRate().getBPM()) : ""),
new Column("cadence", t -> t.cadence() != null ? CADENCE_FORMAT.format(t.cadence().getRPM()) : ""),
new Column("power", t -> t.power() != null ? POWER_FORMAT.format(t.power().getW()) : ""));
try {
prepare(outputStream);
@@ -61,6 +61,7 @@ public class GPXTrackExporter implements TrackExporter {
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat TEMPERATURE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat POWER_FORMAT = NumberFormat.getInstance(Locale.US);
@@ -82,6 +83,9 @@ public class GPXTrackExporter implements TrackExporter {
HEARTRATE_FORMAT.setMaximumFractionDigits(0);
HEARTRATE_FORMAT.setGroupingUsed(false);
TEMPERATURE_FORMAT.setMaximumFractionDigits(2);
TEMPERATURE_FORMAT.setGroupingUsed(false);
CADENCE_FORMAT.setMaximumFractionDigits(0);
CADENCE_FORMAT.setGroupingUsed(false);
@@ -140,7 +144,7 @@ public class GPXTrackExporter implements TrackExporter {
wroteTrack = true;
}
switch (trackPoint.getType()) {
switch (trackPoint.type()) {
case SEGMENT_START_MANUAL ->
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
case SEGMENT_END_MANUAL -> {
@@ -152,7 +156,7 @@ public class GPXTrackExporter implements TrackExporter {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
wroteSegment = true;
if (trackPoint.hasLocation()) {
if (trackPoint.position().hasLocation()) {
trackDistance = trackDistance.plus(writeTrackPoint(track.zoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
} else {
@@ -165,7 +169,7 @@ public class GPXTrackExporter implements TrackExporter {
writeOpenSegment();
wroteSegment = true;
}
if (trackPoint.hasLocation()) {
if (trackPoint.position().hasLocation()) {
trackDistance = trackDistance.plus(writeTrackPoint(track.zoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
} else {
@@ -176,7 +180,7 @@ public class GPXTrackExporter implements TrackExporter {
// Not supported as IDLE-TrackPoints have no location.
}
default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.type());
}
}
@@ -306,10 +310,10 @@ public class GPXTrackExporter implements TrackExporter {
private Distance writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints, Distance trackDistance) {
Distance cumulativeDistance;
printWriter.println("<trkpt " + formatLocation(trackPoint.getPosition()) + ">");
printWriter.println("<trkpt " + formatLocation(trackPoint.position()) + ">");
if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude().toM()) + "</ele>");
if (trackPoint.position().hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.position().altitude().toM()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime(), zoneOffset) + "</time>");
@@ -317,41 +321,45 @@ public class GPXTrackExporter implements TrackExporter {
{
String trackPointExtensionV2Content = "";
if (trackPoint.hasHeartRate()) {
trackPointExtensionV2Content += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate().getBPM()) + "</gpxtpx:hr>\n";
if (trackPoint.heartRate() != null) {
trackPointExtensionV2Content += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.heartRate().getBPM()) + "</gpxtpx:hr>\n";
}
if (trackPoint.hasCadence()) {
trackPointExtensionV2Content += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence().getRPM()) + "</gpxtpx:cad>\n";
if (trackPoint.temperature() != null) {
trackPointExtensionV2Content += "<gpxtpx:atemp>" + TEMPERATURE_FORMAT.format(trackPoint.temperature().getCelsius()) + "</gpxtpx:atemp>\n";
}
if (trackPoint.hasSpeed()) {
trackPointExtensionV2Content += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>\n";
if (trackPoint.cadence() != null) {
trackPointExtensionV2Content += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.cadence().getRPM()) + "</gpxtpx:cad>\n";
}
if (trackPoint.position().hasSpeed()) {
trackPointExtensionV2Content += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.position().speed().toMPS()) + "</gpxtpx:speed>\n";
}
String extensionContent = "";
if (trackPoint.hasPower()) {
extensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower().getW()) + "</pwr:PowerInWatts>\n";
if (trackPoint.power() != null) {
extensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.power().getW()) + "</pwr:PowerInWatts>\n";
}
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGainLoss() ? (double) tp.getAltitudeGainLoss().gain_m() : null);
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.altitudeGainLoss() != null ? (double) tp.altitudeGainLoss().gain_m() : null);
if (cumulativeGain != null) {
extensionContent += ("<opentracks:gain>" + ALTITUDE_FORMAT.format(cumulativeGain) + "</opentracks:gain>\n");
}
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGainLoss() ? (double) tp.getAltitudeGainLoss().loss_m() : null);
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.altitudeGainLoss() != null ? (double) tp.altitudeGainLoss().loss_m() : null);
if (cumulativeLoss != null) {
extensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
}
if (trackPoint.hasHorizontalAccuracy()) {
extensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.getHorizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
if (trackPoint.position().hasHorizontalAccuracy()) {
extensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.position().horizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
}
if (trackPoint.hasVerticalAccuracy()) {
extensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.getVerticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
if (trackPoint.position().hasVerticalAccuracy()) {
extensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.position().verticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
}
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null));
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.sensorDistance() != null ? tp.sensorDistance().toM() : null));
if (cumulativeDistance != null) {
extensionContent += ("<opentracks:distance>" + DISTANCE_FORMAT.format(cumulativeDistance.toM()) + "</opentracks:distance>\n");
extensionContent += ("<cluetrust:distance>" + DISTANCE_FORMAT.format(trackDistance.plus(cumulativeDistance).toM()) + "</cluetrust:distance>\n");
@@ -67,8 +67,9 @@ public class KMLTrackExporter implements TrackExporter {
public static final String EXTENDED_DATA_TYPE_TRACKPOINT = "trackpoint_type";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_HEARTRATE = "heartrate";
public static final String EXTENDED_DATA_TYPE_TEMPERATURE = "temperature";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_POWER = "power";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_GAIN = "elevation_gain";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_LOSS = "elevation_loss";
@@ -95,7 +96,8 @@ public class KMLTrackExporter implements TrackExporter {
private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
private final List<Float> altitudeGainList = new ArrayList<>();
private final List<Float> temperatureList = new ArrayList<>();
private final List<Float> altitudeGainList = new ArrayList<>(); //TODO Join with altitudeLossList
private final List<Float> altitudeLossList = new ArrayList<>();
private final List<Float> accuracyHorizontal = new ArrayList<>();
private final List<Float> accuracyVertical = new ArrayList<>();
@@ -167,7 +169,7 @@ public class KMLTrackExporter implements TrackExporter {
wroteTrack = true;
}
switch (trackPoint.getType()) {
switch (trackPoint.type()) {
case SEGMENT_START_MANUAL, SEGMENT_START_AUTOMATIC -> {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
@@ -190,7 +192,7 @@ public class KMLTrackExporter implements TrackExporter {
writeTrackPoint(track.zoneOffset(), trackPoint);
}
default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.type());
}
}
@@ -237,7 +239,7 @@ public class KMLTrackExporter implements TrackExporter {
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd
http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
"""); //TODO ADD xsi:schemaLocation for atom
"""); //NOTE: There is no XSD for Atom
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
@@ -254,6 +256,7 @@ public class KMLTrackExporter implements TrackExporter {
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_POWER, context.getString(R.string.description_sensor_power));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_HEARTRATE, context.getString(R.string.description_sensor_heart_rate));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_TEMPERATURE, context.getString(R.string.description_sensor_temperature));
printWriter.println("</Schema>");
}
@@ -321,6 +324,7 @@ public class KMLTrackExporter implements TrackExporter {
powerList.clear();
cadenceList.clear();
heartRateList.clear();
temperatureList.clear();
altitudeGainList.clear();
altitudeLossList.clear();
accuracyHorizontal.clear();
@@ -349,6 +353,9 @@ public class KMLTrackExporter implements TrackExporter {
if (heartRateList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(heartRateList, EXTENDED_DATA_TYPE_HEARTRATE);
}
if (temperatureList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(temperatureList, EXTENDED_DATA_TYPE_TEMPERATURE);
}
if (altitudeGainList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(altitudeGainList, EXTENDED_DATA_TYPE_ALTITUDE_GAIN);
}
@@ -370,28 +377,29 @@ public class KMLTrackExporter implements TrackExporter {
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getTime()) + "</when>");
trackpointTypeList.add(trackPoint.getType());
trackpointTypeList.add(trackPoint.type());
if (trackPoint.hasLocation()) {
printWriter.println("<coord>" + getCoordinates(trackPoint.getPosition(), " ") + "</coord>");
if (trackPoint.position().hasLocation()) {
printWriter.println("<coord>" + getCoordinates(trackPoint.position(), " ") + "</coord>");
} else {
printWriter.println("<coord/>");
}
speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null);
speedList.add(trackPoint.position().hasSpeed() ? (float) trackPoint.position().speed().toMPS() : null);
distanceList.add(trackPoint.hasSensorDistance() ? (float) trackPoint.getSensorDistance().toM() : null);
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate().getBPM() : null);
cadenceList.add(trackPoint.hasCadence() ? trackPoint.getCadence().getRPM() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower().getW() : null);
if (trackPoint.hasAltitudeGainLoss()) {
altitudeGainList.add(trackPoint.getAltitudeGainLoss().loss_m());
altitudeLossList.add(trackPoint.getAltitudeGainLoss().loss_m());
distanceList.add(trackPoint.sensorDistance() != null ? (float) trackPoint.sensorDistance().toM() : null);
heartRateList.add(trackPoint.heartRate() != null ? trackPoint.heartRate().getBPM() : null);
temperatureList.add(trackPoint.temperature() != null ? trackPoint.temperature().getCelsius() : null);
cadenceList.add(trackPoint.cadence() != null ? trackPoint.cadence().getRPM() : null);
powerList.add(trackPoint.power() != null ? trackPoint.power().getW() : null);
if (trackPoint.altitudeGainLoss() != null) {
altitudeGainList.add(trackPoint.altitudeGainLoss().loss_m());
altitudeLossList.add(trackPoint.altitudeGainLoss().loss_m());
} else {
altitudeGainList.add(null);
altitudeLossList.add(null);
}
accuracyHorizontal.add(trackPoint.hasHorizontalAccuracy() ? (float) trackPoint.getHorizontalAccuracy().toM() : null);
accuracyVertical.add(trackPoint.hasVerticalAccuracy() ? (float) trackPoint.getVerticalAccuracy().toM() : null);
accuracyHorizontal.add(trackPoint.position().hasHorizontalAccuracy() ? (float) trackPoint.position().horizontalAccuracy().toM() : null);
accuracyVertical.add(trackPoint.position().hasVerticalAccuracy() ? (float) trackPoint.position().verticalAccuracy().toM() : null);
}
private void writeSimpleArraySensorData(List<Float> list, String name) {
@@ -32,10 +32,15 @@ import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
@@ -79,6 +84,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
*/
private static final String TAG_EXTENSION_SPEED_COMPAT = "speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_TEMPERATURE = "gpxtpx:atemp";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
@@ -109,6 +115,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
private String time;
private String speed;
private String heartrate;
private String temperature;
private String cadence;
private String power;
private String markerType;
@@ -214,6 +221,11 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
heartrate = content.trim();
}
}
case TAG_EXTENSION_TEMPERATURE -> {
if (content != null) {
temperature = content.trim();
}
}
case TAG_EXTENSION_CADENCE -> {
if (content != null) {
cadence = content.trim();
@@ -265,8 +277,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
return;
}
TrackPoint first = currentSegment.getFirst();
first.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
TrackPoint first = currentSegment.removeFirst();
currentSegment.addFirst(first.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC));
trackImporter.addTrackPoints(currentSegment);
currentSegment.clear();
@@ -332,59 +344,80 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
}
TrackPoint trackPoint = new TrackPoint(
TrackPoint.Type.TRACKPOINT,
new Position(
parsedTime.toInstant(),
latitudeParsed,
longitudeParsed,
accuracyHorizontalParsed,
altitudeParsed,
accuracyVerticalParsed,
null,
speedParsed
));
if (heartrate != null) {
try {
trackPoint.setHeartRate(Float.parseFloat(heartrate));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
if (cadence != null) {
try {
trackPoint.setCadence(Float.parseFloat(cadence));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
if (gain != null && loss != null) {
try {
trackPoint.setAltitudeGainLoss(Float.parseFloat(gain), Float.parseFloat(loss));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s or los: %s", gain, loss)), e);
}
}
Distance parsedSensorDistance = null;
if (sensorDistance != null) {
try {
trackPoint.setSensorDistance(Distance.of(Float.parseFloat(sensorDistance)));
parsedSensorDistance = Distance.of(Float.parseFloat(sensorDistance));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", sensorDistance)), e);
}
}
return trackPoint;
HeartRate parsedHeartRate = null;
if (heartrate != null) {
try {
parsedHeartRate = HeartRate.ofOrNull(Float.parseFloat(heartrate));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
Temperature parsedTemperature = null;
if (temperature != null) {
try {
parsedTemperature = Temperature.of(Float.parseFloat(temperature));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse temperature: %s", temperature)), e);
}
}
Cadence parsedCadence = null;
if (cadence != null) {
try {
parsedCadence = Cadence.of(Float.parseFloat(cadence));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
Power parsedPower = null;
if (power != null) {
try {
parsedPower = Power.of(Float.parseFloat(power));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
AltitudeGainLoss altitudeGainLoss = null;
if (gain != null && loss != null) {
try {
altitudeGainLoss = new AltitudeGainLoss(Float.parseFloat(gain), Float.parseFloat(loss));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s or los: %s", gain, loss)), e);
}
}
return new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
parsedTime.toInstant(),
latitudeParsed,
longitudeParsed,
accuracyHorizontalParsed,
altitudeParsed,
accuracyVerticalParsed,
null,
speedParsed
),
parsedSensorDistance,
parsedHeartRate,
parsedTemperature,
parsedCadence,
parsedPower,
altitudeGainLoss
);
}
private void onTrackPointStart(Attributes attributes) {
@@ -400,9 +433,10 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorDistance = null;
accuracyHorizontal = null;
accuracyVertical = null;
power = null;
heartrate = null;
temperature = null;
cadence = null;
power = null;
}
private void onMarkerStart(Attributes attributes) {
@@ -425,8 +459,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
TrackPoint trackPoint = createTrackPoint();
if (!trackPoint.hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getPosition());
if (!trackPoint.position().hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.position());
return;
}
markers.add(new Marker(
@@ -435,7 +469,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
name != null ? name : "",
description != null ? description : "",
markerType != null ? markerType : "",
trackPoint.getPosition(),
trackPoint.position(),
photoUrl
));
}
@@ -25,6 +25,7 @@ public class ImportWorker extends Worker {
private static final String TAG = ImportWorker.class.getSimpleName();
static final String URI_KEY = "DIRECTORY_URI_KEY";
static final String FILENAME_KEY = "FILENAME_KEY";
static final String RESULT_SUCCESS_LIST_TRACKIDS_KEY = "RESULT_TRACK_IDS";
static final String RESULT_URI_KEY = "RESULT_URI";
@@ -32,11 +33,13 @@ public class ImportWorker extends Worker {
static final String RESULT_FAILURE_IS_DUPLICATE = "RESULT_FAILURE_IS_DUPLICATE";
private final Uri uri;
private final String filename;
public ImportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
uri = Uri.parse(getInputData().getString(URI_KEY));
filename = getInputData().getString(FILENAME_KEY);
}
@NonNull
@@ -53,7 +56,7 @@ public class ImportWorker extends Worker {
.putString(RESULT_URI_KEY, uri.toString());
String fileExtension = FileUtils.getExtension(uri);
String fileExtension = FileUtils.getExtension(filename);
try {
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new GPXTrackImporter(getApplicationContext(), trackImporter)).importFile(context, uri));
@@ -69,6 +69,7 @@ public class Importer {
WorkRequest importRequest = new OneTimeWorkRequest.Builder(ImportWorker.class)
.setInputData(new Data.Builder()
.putString(ImportWorker.URI_KEY, documentFile.getUri().toString())
.putString(ImportWorker.FILENAME_KEY, documentFile.getName())
.build())
.build();
@@ -33,10 +33,16 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
@@ -79,7 +85,6 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
private static final String TAG_PLACEMARK = "Placemark";
private static final String TAG_STYLE_URL = "styleUrl";
// private static final String TAG_VALUE = "value"; TODO
private static final String TAG_WHEN = "when";
private static final String TAG_UUID = "opentracks:trackid";
@@ -95,15 +100,17 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
private ZoneOffset zoneOffset;
private final ArrayList<Instant> whenList = new ArrayList<>();
private final ArrayList<Position> positionList = new ArrayList<>();
private final ArrayList<LatLngAltitude> positionList = new ArrayList<>();
private String dataType;
//TODO For ArrayList use specific data models; and why Float? Double would be easier.
private final ArrayList<String> trackpointTypeList = new ArrayList<>();
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
private final ArrayList<Float> sensorHeartRateList = new ArrayList<>();
private final ArrayList<Float> sensorTemperatureList = new ArrayList<>();
private final ArrayList<Float> sensorPowerList = new ArrayList<>();
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
@@ -151,8 +158,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
onTrackSegmentStart();
}
case TAG_EXTENDED_DATA, TAG_SIMPLE_ARRAY_DATA, TAG_KML22_SIMPLE_ARRAY_DATA ->
dataType = attributes.getValue(ATTRIBUTE_NAME);
case TAG_EXTENDED_DATA, TAG_SIMPLE_ARRAY_DATA, TAG_KML22_SIMPLE_ARRAY_DATA -> dataType = attributes.getValue(ATTRIBUTE_NAME);
}
}
@@ -256,13 +262,21 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
return;
}
Position position = createPosition(latitude, longitude, altitude);
if (position == null) {
LatLngAltitude latLngAltitude = createPosition(latitude, longitude, altitude);
if (latLngAltitude == null) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + latitude + " " + longitude);
return;
}
position = position.with(whenList.get(0));
Position position = new Position(
whenList.get(0),
latLngAltitude.latitude,
latLngAltitude.longitude,
null,
latLngAltitude.altitude,
null,
null,
null
);
markers.add(new Marker(
null,
null,
@@ -300,6 +314,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorSpeedList.clear();
sensorDistanceList.clear();
sensorHeartRateList.clear();
sensorTemperatureList.clear();
sensorCadenceList.clear();
sensorPowerList.clear();
altitudeGainList.clear();
@@ -316,63 +331,86 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
// Close a track segment by inserting the segment locations
for (int i = 0; i < positionList.size(); i++) {
Instant time = whenList.get(i);
Position position = positionList.get(i);
LatLngAltitude latLngAltitude = positionList.get(i);
TrackPoint trackPoint;
if (position == null) {
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(time));
} else {
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, position.with(time));
}
TrackPoint.Type type = null;
Speed speed = null;
Distance sensorDistance = null;
HeartRate heartRate = null;
Temperature temperature = null;
Cadence cadence = null;
Power power = null;
AltitudeGainLoss altitudeGainLoss = null;
Distance horizontalAccuracy = null;
Distance verticalAccuracy = null;
if (i < trackpointTypeList.size() && trackpointTypeList.get(i) != null) {
TrackPoint.Type type = TrackPoint.Type.valueOf(trackpointTypeList.get(i));
trackPoint.setType(type);
type = TrackPoint.Type.valueOf(trackpointTypeList.get(i));
}
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
trackPoint.setSpeed(Speed.of(sensorSpeedList.get(i)));
speed = Speed.of(sensorSpeedList.get(i));
}
if (i < sensorDistanceList.size() && sensorDistanceList.get(i) != null) {
trackPoint.setSensorDistance(Distance.of(sensorDistanceList.get(i)));
sensorDistance = Distance.of(sensorDistanceList.get(i));
}
if (i < sensorHeartRateList.size() && sensorHeartRateList.get(i) != null) {
trackPoint.setHeartRate(sensorHeartRateList.get(i));
if (i < sensorHeartRateList.size()) {
heartRate = HeartRate.ofOrNull(sensorHeartRateList.get(i));
}
if (i < sensorTemperatureList.size() && sensorTemperatureList.get(i) != null) {
temperature = Temperature.of(sensorTemperatureList.get(i));
}
if (i < sensorCadenceList.size() && sensorCadenceList.get(i) != null) {
trackPoint.setCadence(sensorCadenceList.get(i));
cadence = Cadence.of(sensorCadenceList.get(i));
}
if (i < sensorPowerList.size() && sensorPowerList.get(i) != null) {
trackPoint.setPower(sensorPowerList.get(i));
power = Power.of(sensorPowerList.get(i));
}
if (i < altitudeGainList.size() && i < altitudeLossList.size()) {
if (altitudeGainList.get(i) != null && altitudeLossList.get(i) != null) {
trackPoint.setAltitudeGainLoss(altitudeGainList.get(i), altitudeLossList.get(i));
}
if ((i < altitudeGainList.size() && altitudeGainList.get(i) != null)
&& (i < altitudeLossList.size() && altitudeLossList.get(i) != null)) {
altitudeGainLoss = new AltitudeGainLoss(altitudeGainList.get(i), altitudeLossList.get(i));
}
if (i < accuracyHorizontal.size() && accuracyHorizontal.get(i) != null) {
trackPoint.setHorizontalAccuracy(Distance.of(accuracyHorizontal.get(i)));
horizontalAccuracy = Distance.of(accuracyHorizontal.get(i));
}
if (i < accuracyVertical.size() && accuracyVertical.get(i) != null) {
trackPoint.setVerticalAccuracy(Distance.of(accuracyVertical.get(i)));
verticalAccuracy = Distance.of(accuracyVertical.get(i));
}
Position position = new Position(
time,
latLngAltitude != null ? latLngAltitude.latitude : null,
latLngAltitude != null ? latLngAltitude.longitude : null,
horizontalAccuracy,
latLngAltitude != null ? latLngAltitude.altitude : null,
verticalAccuracy,
null,
speed
);
// Update TrackPoint type for START / STOP.
TrackPoint.Type type = trackPoint.getType();
if (i == 0) {
//first
if (!trackPoint.wasCreatedManually()) {
type = TrackPoint.Type.SEGMENT_START_MANUAL;
} else {
if (wasCreatedAutomatically(position)) {
type = TrackPoint.Type.SEGMENT_START_AUTOMATIC;
} else {
type = TrackPoint.Type.SEGMENT_START_MANUAL;
}
} else if (i == positionList.size() - 1 && !trackPoint.wasCreatedManually()) {
} else if (i == positionList.size() - 1 && !wasCreatedAutomatically(position)) {
//last
type = TrackPoint.Type.SEGMENT_END_MANUAL;
}
trackPoint.setType(type);
trackImporter.addTrackPoint(trackPoint);
trackImporter.addTrackPoint(new TrackPoint(
null,
type != null ? type : TrackPoint.Type.TRACKPOINT,
position,
sensorDistance,
heartRate,
temperature,
cadence,
power,
altitudeGainLoss
));
}
}
@@ -391,7 +429,8 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
altitude = null;
}
private Position createPosition(String latitude, String longitude, String altitude) {
private LatLngAltitude createPosition(String latitude, String longitude, String altitude) {
//TODO Do not use Location.
if (longitude != null && latitude != null) {
Location location = new Location("import");
try {
@@ -408,7 +447,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
return Position.of(location, null);
return new LatLngAltitude(location.getLatitude(), location.getLongitude(), Altitude.WGS84.of(location.getAltitude()));
}
return null;
@@ -436,15 +475,13 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER -> sensorPowerList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEARTRATE,
EXTENDED_DATA_TYPE_HEART_RATE_LEGACY -> sensorHeartRateList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_TEMPERATURE -> sensorTemperatureList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE -> sensorCadenceList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN -> altitudeGainList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS -> altitudeLossList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL ->
accuracyHorizontal.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL ->
accuracyVertical.add(value);
default ->
Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL -> accuracyHorizontal.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL -> accuracyVertical.add(value);
default -> Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
}
}
@@ -457,6 +494,10 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
trackImporter.finish();
}
public boolean wasCreatedAutomatically(Position position) {
return position.hasLocation() || position.hasSpeed();
}
@Override
public DefaultHandler getHandler() {
return this;
@@ -471,4 +512,11 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
public void cleanImport() {
trackImporter.cleanImport();
}
private record LatLngAltitude(
double latitude,
double longitude,
Altitude altitude
) {
}
}
@@ -176,21 +176,28 @@ public class TrackImporter {
TrackPoint previous = trackPoints.get(i - 1);
TrackPoint current = trackPoints.get(i);
if (current.hasSensorDistance() || (previous.hasLocation() && current.hasLocation())) {
if (current.sensorDistance() != null || (previous.position().hasLocation() && current.position().hasLocation())) {
TrackPoint newCurrent = current;
Distance distanceToPrevious = current.distanceToPrevious(previous);
if (!current.hasSpeed()) {
if (!current.position().hasSpeed()) {
Duration timeDifference = Duration.between(previous.getTime(), current.getTime());
current.setSpeed(Speed.of(distanceToPrevious, timeDifference));
newCurrent = newCurrent.with(newCurrent.position().with(Speed.of(distanceToPrevious, timeDifference)));
}
if (!current.hasBearing()) {
previous.bearingTo(current)
.ifPresent(current::setBearing);
if (current.type().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
newCurrent = newCurrent.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
trackPoints.set(i, newCurrent);
}
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
current.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
}
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint current = trackPoints.get(i);
if (!current.heartRate().isValid()) {
current = current.removeHeartRate();
trackPoints.set(i, current);
}
}
}
@@ -0,0 +1,199 @@
package de.dennisguse.opentracks.publicapi;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Map;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.CustomContentProvider;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Create an {@link Intent} to request showing tracks on a Map or a Dashboard.
* The receiving {@link android.app.Activity} gets temporary access to the {@link TracksColumns} and the {@link TrackPointsColumns} (incl. update).
*/
public class DataProvider {
private static final String TAG = DataProvider.class.getSimpleName();
private static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard";
private static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload";
/**
* Assume "1" if not present.
*/
private static final String EXTRAS_PROTOCOL_VERSION = "PROTOCOL_VERSION";
/**
* version 1: the initial version.
* version 2: replaced pause/resume trackpoints for track segmentation (lat=100 / lat=200) by TrackPoint.Type.
* version 3: renamed track database columns.
*/
private static final int CURRENT_VERSION = 2;
private static final String EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK = "EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK";
private static final String EXTRAS_SHOULD_KEEP_SCREEN_ON = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
private static final String EXTRAS_SHOW_WHEN_LOCKED = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
private static final String EXTRAS_SHOW_FULLSCREEN = "EXTRAS_SHOULD_FULLSCREEN";
private static final int TRACK_URI_INDEX = 0;
private static final int TRACKPOINTS_URI_INDEX = 1;
private static final int MARKERS_URI_INDEX = 2;
public static final Map<String, String> DATA_PROJECTIONMAP_TRACKS = Map.ofEntries(
//V2
Map.entry("_id", TracksColumns._ID),
Map.entry("name", TracksColumns.NAME),
Map.entry("description", TracksColumns.DESCRIPTION),
Map.entry("category", TracksColumns.ACTIVITY_TYPE + " AS category"),
Map.entry("activity_type_localized", TracksColumns.ACTIVITY_TYPE_LOCALIZED),
Map.entry("starttime", TracksColumns.STARTTIME + " AS starttime"),
Map.entry("stoptime", TracksColumns.STOPTIME + " AS stoptime"),
Map.entry("totaltime", TracksColumns.TOTALTIME + " AS totaltime"),
Map.entry("movingtime", TracksColumns.MOVINGTIME + " AS movingtime"),
Map.entry("totaldistance", TracksColumns.TOTALDISTANCE + " AS totaldistance"),
Map.entry("maxspeed", TracksColumns.MAXSPEED + " AS maxspeed"),
Map.entry("avgspeed", "null AS avgspeed"),
Map.entry("avgmovingspeed", "null AS avgmovingspeed"),
Map.entry("minelevation", TracksColumns.MIN_ALTITUDE + " AS minelevation"),
Map.entry("maxelevation", TracksColumns.MAX_ALTITUDE + " AS maxelevation"),
Map.entry("elevationgain", TracksColumns.ALTITUDE_GAIN + " AS elevationgain"),
Map.entry("elevationloss", TracksColumns.ALTITUDE_LOSS + " AS elevationloss"),
//V3
// Map.entry("_id", TracksColumns._ID),
// Map.entry("name",TracksColumns.NAME),
// Map.entry("description",TracksColumns.DESCRIPTION),
Map.entry("activity_type", TracksColumns.ACTIVITY_TYPE),
// Map.entry("activity_type_localized",TracksColumns.ACTIVITY_TYPE_LOCALIZED),
Map.entry("time_start", TracksColumns.STARTTIME),
Map.entry("time_stop", TracksColumns.STOPTIME),
Map.entry("duration_total", TracksColumns.TOTALTIME),
Map.entry("duration_moving", TracksColumns.MOVINGTIME),
Map.entry("distance", TracksColumns.TOTALDISTANCE),
Map.entry("speed_max", TracksColumns.MAXSPEED),
Map.entry("altitude_min", TracksColumns.MIN_ALTITUDE),
Map.entry("altitude_max", TracksColumns.MAX_ALTITUDE),
Map.entry("altitude_gain", TracksColumns.ALTITUDE_GAIN),
Map.entry("altitude_loss", TracksColumns.ALTITUDE_LOSS)
);
public static final Map<String, String> DATA_PROJECTIONMAP_TRACKPOINTS = Map.ofEntries(
//V2 and V3
Map.entry("_id", TrackPointsColumns._ID),
Map.entry("trackid", TrackPointsColumns.TRACKID),
Map.entry("latitude", TrackPointsColumns.LATITUDE),
Map.entry("longitude", TrackPointsColumns.LONGITUDE),
Map.entry("time", TrackPointsColumns.TIME),
Map.entry("type", TrackPointsColumns.TYPE),
Map.entry("speed", TrackPointsColumns.SPEED)
);
public static final Map<String, String> DATA_PROJECTIONMAP_MARKERS = Map.ofEntries(
//V2
Map.entry("_id", MarkerColumns._ID),
Map.entry("trackid", MarkerColumns.TRACKID),
Map.entry("name", MarkerColumns.NAME),
Map.entry("description", MarkerColumns.DESCRIPTION),
Map.entry("category", MarkerColumns.TYPE_LOCALIZED + " AS category"),
Map.entry("latitude", MarkerColumns.LATITUDE),
Map.entry("longitude", MarkerColumns.LONGITUDE),
Map.entry("photoUrl", MarkerColumns.PHOTOURL),
Map.entry("icon", "null AS icon"),
//V3
// Map.entry("_id", MarkerColumns._ID),
// Map.entry("trackid",MarkerColumns.TRACKID),
// Map.entry("name",MarkerColumns.NAME),
// Map.entry("description",MarkerColumns.DESCRIPTION),
Map.entry("marker_type_localized", MarkerColumns.TYPE_LOCALIZED)
// Map.entry("latitude",MarkerColumns.LATITUDE),
// Map.entry("longitude",MarkerColumns.LONGITUDE),
// Map.entry("photoUrl",MarkerColumns.PHOTOURL)
);
private DataProvider() {
}
/**
* Send intent to show tracks on a map (needs an another app) as resource URIs.
*
* @param context the context
* @param isRecording are we currently recording?
* @param trackIds the track ids
*/
public static void showTrackOnMap(Context context, boolean isRecording, Track.Id... trackIds) {
startDashboard(context, isRecording, null, null, trackIds);
}
/**
* Send intent to show tracks on a map (needs an another app) as resource URIs.
* By providing a targetPackage and targetClass an explicit intent can be sent,
* thus bypassing the need for the user to select an app.
*
* @param context the context
* @param isRecording are we currently recording?
* @param targetPackage the target package
* @param targetClass the target class
* @param trackIds the track ids
*/
public static void startDashboard(Context context, boolean isRecording, @Nullable String targetPackage, @Nullable String targetClass, Track.Id... trackIds) {
if (trackIds.length == 0) {
return;
}
String trackIdList = ContentProviderUtils.formatIdListForUri(trackIds);
ArrayList<Uri> uris = new ArrayList<>();
uris.add(TRACK_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_TRACKS_BY_IDS.getUri(), trackIdList));
uris.add(TRACKPOINTS_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.getUri(), trackIdList));
uris.add(MARKERS_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.getUri(), trackIdList));
Intent intent = new Intent(ACTION_DASHBOARD);
intent.putExtra(EXTRAS_PROTOCOL_VERSION, CURRENT_VERSION);
intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris);
intent.putExtra(EXTRAS_SHOULD_KEEP_SCREEN_ON, PreferencesUtils.shouldKeepScreenOn());
intent.putExtra(EXTRAS_SHOW_WHEN_LOCKED, PreferencesUtils.shouldShowStatsOnLockscreen());
intent.putExtra(EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK, isRecording);
if (isRecording) {
intent.putExtra(EXTRAS_SHOW_FULLSCREEN, PreferencesUtils.shouldUseFullscreen());
}
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ClipData clipData = ClipData.newRawUri(null, uris.get(TRACK_URI_INDEX));
clipData.addItem(new ClipData.Item(uris.get(TRACKPOINTS_URI_INDEX)));
clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
intent.setClipData(clipData);
if (targetPackage != null && targetClass != null) {
Log.i(TAG, "Starting dashboard activity with explicit intent (package=" + targetPackage + ", class=" + targetClass + ")");
intent.setClassName(targetPackage, targetClass);
} else {
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
}
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Dashboard not installed; cannot start it.");
Toast.makeText(context, R.string.show_on_dashboard_not_installed, Toast.LENGTH_SHORT).show();
}
}
}
@@ -10,7 +10,6 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
public class StartRecording extends AbstractAPIActivity {
@@ -61,7 +60,7 @@ public class StartRecording extends AbstractAPIActivity {
String targetPackage = bundle.getString(EXTRA_STATS_TARGET_PACKAGE, null);
String targetClass = bundle.getString(EXTRA_STATS_TARGET_CLASS, null);
if (targetClass != null && targetPackage != null) {
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
DataProvider.startDashboard(this, true, targetPackage, targetClass, trackId);
}
}
@@ -0,0 +1,50 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.driver.BarometerBluetooth;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
/**
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/
//TODO Rename class
public class AltitudeChangeHandler extends SensorHandler<AtmosphericPressure, AltitudeGainLoss> {
public AltitudeChangeHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverInternal() {
return new BarometerInternal(this);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new BarometerBluetooth()
);
}
@NonNull
@Override
public AggregatorBarometer createAggregator(String sensorAddress, String sensorName) {
return new AggregatorBarometer(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_pressure_key;
}
}
@@ -1,55 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(BAROMETRIC_PRESSURE);
}
@Override
public Aggregator<?, ?> createEmptySensorData(String address, String name) {
return new AggregatorBarometer(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
AtmosphericPressure value = parseEnvironmentalSensing(characteristic);
if (value == null) return;
observer.onChange(new Raw<>(observer.getNow(), value));
}
/**
* Decoding:
* org.bluetooth.service.environmental_sensing.xml
* org.bluetooth.characteristic.pressure.xml
*/
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
if (raw.length < 4) {
return null;
}
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
return AtmosphericPressure.ofPA(pressure / 10f);
}
}
@@ -1,57 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import android.util.Pair;
import java.util.List;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
private static final String TAG = BluetoothHandlerCyclingCadence.class.getSimpleName();
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
BluetoothHandlerManagerCyclingPower.CYCLING_POWER,
BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return CYCLING_CADENCE;
}
@Override
public AggregatorCyclingCadence createEmptySensorData(String address, String name) {
return new AggregatorCyclingCadence(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
if (serviceMeasurementUUID.equals(BluetoothHandlerManagerCyclingPower.CYCLING_POWER)) {
BluetoothHandlerManagerCyclingPower.Data data = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
if (data != null && data.crank() != null) {
observer.onChange(new Raw<>(observer.getNow(), data.crank()));
}
return;
}
if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE)) {
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, CrankData> data = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (data != null && data.second != null) {
observer.onChange(new Raw<>(observer.getNow(), data.second));
}
return;
}
Log.e(TAG, "Don't know how to decode this payload.");
}
public record CrankData(
long crankRevolutionsCount, // UINT32
int crankRevolutionsTime // UINT16; 1/1024s
) {}
}
@@ -1,122 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
* <p>
* Note: should only be instantiated once.
* TODO: listen for Bluetooth enabled/disabled events.
* @author Sandor Dornbush
*/
public class BluetoothRemoteSensorManager implements SensorConnector, SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
private final Context context;
private final Handler handler;
private boolean started = false;
private final BluetoothConnectionManager heartRate;
private final BluetoothConnectionManager cyclingCadence;
private final BluetoothConnectionManager cyclingSpeed;
private final BluetoothConnectionManager cyclingPower;
private final BluetoothConnectionManager runningSpeedAndCadence;
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) {
this.context = context;
this.handler = handler;
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerHeartRate());
this.cyclingCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingCadence());
this.cyclingSpeed = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerCyclingPower());
this.runningSpeedAndCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerRunningSpeedAndCadence());
}
@Override
public void start(Context context, Handler handler) {
started = true;
// Triggers connection startup
onSharedPreferenceChanged(null, null);
}
@Override
public synchronized void stop(Context context) {
heartRate.disconnect();
cyclingCadence.disconnect();
cyclingSpeed.disconnect();
cyclingPower.disconnect();
runningSpeedAndCadence.disconnect();
started = false;
}
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
connectionManager.connect(context, handler, address);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences unused, @Nullable String key) {
if (!started) return;
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress();
connect(heartRate, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress();
connect(cyclingCadence, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_speed_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingSpeedSensorAddress();
connect(cyclingSpeed, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress();
connect(cyclingPower, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_running_speed_and_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothRunningSpeedAndCadenceAddress();
connect(runningSpeedAndCadence, address);
}
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
public class CyclingCadenceHandler extends SensorHandler<CyclingCadenceBluetooth.CrankData, Cadence> {
protected CyclingCadenceHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new CyclingCadenceBluetooth()
);
}
@NonNull
@Override
protected AggregatorCyclingCadence createAggregator(String sensorAddress, String sensorName) {
return new AggregatorCyclingCadence(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_cycling_cadence_key;
}
}
@@ -0,0 +1,46 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class CyclingDistanceSpeedHandler extends SensorHandler<CyclingDistanceSpeedBluetooth.WheelData, AggregatorCyclingDistanceSpeed.Data> {
protected CyclingDistanceSpeedHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new CyclingDistanceSpeedBluetooth()
);
}
@NonNull
@Override
protected AggregatorCyclingDistanceSpeed createAggregator(String sensorAddress, String sensorName) {
return new AggregatorCyclingDistanceSpeed(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_cycling_speed_key;
}
@Override
public void onDataReceived(CyclingDistanceSpeedBluetooth.WheelData value) {
//TODO Fetch once and then listen for changes.
((AggregatorCyclingDistanceSpeed) aggregator).setWheelCircumference(PreferencesUtils.getWheelCircumference());
super.onDataReceived(value);
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
public class CyclingPowerHandler extends SensorHandler<CyclingPowerBluetooth.Data, Power> {
protected CyclingPowerHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new CyclingPowerBluetooth()
);
}
@NonNull
@Override
protected AggregatorCyclingPower createAggregator(String sensorAddress, String sensorName) {
return new AggregatorCyclingPower(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_cycling_power_key;
}
}
@@ -1,77 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/
public class GainManager implements SensorConnector {
private static final String TAG = GainManager.class.getSimpleName();
private final SensorManager.SensorDataChangedObserver listener;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> connect();
private Context context;
private Handler handler;
private Driver driver;
public GainManager(SensorManager.SensorDataChangedObserver listener) {
this.listener = listener;
}
public void start(Context context, Handler handler) {
this.context = context;
this.handler = handler;
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
public void stop(Context context) {
Log.d(TAG, "Stop");
this.context = null;
this.handler = null;
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
onDisconnect();
}
private void connect() {
onDisconnect();
String address = PreferencesUtils.getBarometerSensorAddress();
switch (PreferencesUtils.getSensorType(address)) {
case NONE -> {
driver = null;
listener.onRemove(new AggregatorBarometer(null, null));
return;
}
case INTERNAL -> driver = new BarometerInternal(listener);
case REMOTE -> driver =
new BluetoothConnectionManager(
BluetoothUtils.getAdapter(context),
listener,
new BluetoothHandlerBarometricPressure()
);
default -> throw new RuntimeException("Not implemented");
}
driver.connect(context, handler, address);
}
private void onDisconnect() {
if (driver == null) return;
driver.disconnect();
listener.onDisconnect(new AggregatorBarometer("GainManager", null));
}
}
@@ -0,0 +1,120 @@
package de.dennisguse.opentracks.sensors;
import android.content.SharedPreferences;
import android.location.Location;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.sensors.driver.SensorType;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GpsHandler extends SensorHandler<Location, Position> {
private final String TAG = GpsHandler.class.getSimpleName();
//TODO Refactor to just pass information via SensorManager.
private TrackPointCreator trackPointCreator;
private GpsStatusManager gpsStatusManager;
private Distance thresholdHorizontalAccuracy;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
if (gpsStatusManager != null) {
Distance gpsMinDistance = PreferencesUtils.getRecordingDistanceInterval();
gpsStatusManager.onRecordingDistanceChanged(gpsMinDistance);
}
}
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
}
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
if (gpsStatusManager != null) {
gpsStatusManager.onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval());
}
//TODO
connect();
}
};
public GpsHandler(SensorManager sensorManager, TrackPointCreator trackPointCreator) {
super(sensorManager);
this.trackPointCreator = trackPointCreator;
gpsStatusManager = new GpsStatusManager(sensorManager.getContext(), trackPointCreator::sendGpsStatus, sensorManager.getHandler());
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
@Override
public void onConnected(String sensorAddress, String sensorName) {
super.onConnected(sensorAddress, sensorName);
gpsStatusManager.onGpsEnabled();
gpsStatusManager.start();
}
@Override
public void onDisconnected() {
super.onDisconnected();
gpsStatusManager.stop();
}
@Override
public void onSensorDeactivated() {
super.onSensorDeactivated();
gpsStatusManager.onGpsDisabled();
}
@Override
public void onDataReceived(Location location) {
// Send each update to the status; please note that this TrackPoint is not stored.
Position position = Position.of(location, trackPointCreator.getNow());
gpsStatusManager.onNewTrackPoint(position);
//TODO We could move the following check into the AggregatorGPS
if (!position.hasValidLocation()) {
Log.w(TAG, "Ignore newTrackPoint. Location is invalid.");
return;
}
if (!position.fulfillsAccuracy(thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
super.onDataReceived(location);
}
@NonNull
@Override
protected Driver createDriverInternal() {
return new GpsInternal(this);
}
@NonNull
@Override
protected AggregatorGPS createAggregator(String sensorAddress, String sensorName) {
return new AggregatorGPS(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_gps_key;
}
@Override
protected String getSensorPreferenceDefaultValue() {
return SensorType.INTERNAL.getPreferenceValue();
}
}
@@ -1,182 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.location.LocationListenerCompat;
import androidx.core.location.LocationManagerCompat;
import androidx.core.location.LocationRequestCompat;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GpsManager implements SensorConnector, LocationListenerCompat, GpsStatusManager.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
private final String TAG = GpsManager.class.getSimpleName();
private static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
private TrackPointCreator trackPointCreator;
private SensorManager.SensorDataChangedObserver observer;
private Context context;
private Handler handler;
private LocationManager locationManager;
private GpsStatusManager gpsStatusManager;
private Duration gpsInterval;
private Distance thresholdHorizontalAccuracy;
public GpsManager(TrackPointCreator trackPointCreator, SensorManager.SensorDataChangedObserver observer) {
this.trackPointCreator = trackPointCreator;
this.observer = observer;
}
public void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context;
this.handler = handler;
onSharedPreferenceChanged(null, null);
gpsStatusManager = new GpsStatusManager(context, this, handler);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
observer.onConnect(new AggregatorGPS("internal"));
registerLocationListener();
gpsStatusManager.start();
}
private boolean isStarted() {
return locationManager != null;
}
@SuppressWarnings({"MissingPermission"})
public void stop(Context context) {
if (isStarted()) {
LocationManagerCompat.removeUpdates(locationManager, this);
}
locationManager = null;
this.context = null;
handler = null;
gpsStatusManager.stop();
gpsStatusManager = null;
observer.onDisconnect(new AggregatorGPS("internal"));
observer = null;
trackPointCreator = null;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
boolean registerListener = false;
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
registerListener = true;
gpsInterval = PreferencesUtils.getMinSamplingInterval();
if (gpsStatusManager != null) {
gpsStatusManager.onMinSamplingIntervalChanged(gpsInterval);
}
}
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
}
if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
registerListener = true;
if (gpsStatusManager != null) {
Distance gpsMinDistance = PreferencesUtils.getRecordingDistanceInterval();
gpsStatusManager.onRecordingDistanceChanged(gpsMinDistance);
}
}
if (registerListener && isStarted()) {
registerLocationListener();
}
}
/**
* Checks if location is valid and builds a track point that will be send through TrackPointCreator.
*
* @param location {@link Location} object.
*/
@Override
public void onLocationChanged(@NonNull Location location) {
// Send each update to the status; please note that this TrackPoint is not stored.
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(location, trackPointCreator.createNow()));
gpsStatusManager.onNewTrackPoint(trackPoint);
if (!trackPoint.getPosition().hasValidLocation()) {
Log.w(TAG, "Ignore newTrackPoint. location is invalid.");
return;
}
if (!trackPoint.getPosition().fulfillsAccuracy(thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
observer.onChange(new Raw<>(observer.getNow(), Position.of(location)));
}
@Override
public void onStatusChanged(@NonNull String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(@NonNull String provider) {
gpsStatusManager.onGpsEnabled();
}
@Override
public void onProviderDisabled(@NonNull String provider) {
gpsStatusManager.onGpsDisabled();
}
private void registerLocationListener() {
if (!LocationManagerCompat.hasProvider(locationManager, LOCATION_PROVIDER)) {
Log.e(TAG, "Device doesn't have GPS.");
return;
}
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(gpsInterval.toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
if (PermissionRequester.GPS.hasPermission(context)) {
try {
Log.i(TAG, "Register for location updates " + context);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, this);
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
}
}
}
@Override
public void onGpsStatusChanged(GpsStatusValue currentStatus) {
trackPointCreator.sendGpsStatus(currentStatus);
}
}
@@ -12,7 +12,7 @@ import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
@@ -31,11 +31,11 @@ class GpsStatusManager {
private Duration signalLostThreshold;
private GpsStatusValue gpsStatus = GpsStatusValue.GPS_NONE;
private GpsStatusListener client;
private GpsStatusListener observer;
private final Context context;
@Nullable
private TrackPoint lastTrackPoint = null;
private Position lastPosition = null;
private Handler handler;
@@ -44,8 +44,8 @@ class GpsStatusManager {
};
public GpsStatusManager(Context context, GpsStatusListener client, Handler handler) {
this.client = client;
public GpsStatusManager(Context context, GpsStatusListener observer, Handler handler) {
this.observer = observer;
this.context = context;
this.handler = handler;
@@ -54,7 +54,7 @@ class GpsStatusManager {
}
public void start() {
client.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
observer.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
}
/**
@@ -62,9 +62,7 @@ class GpsStatusManager {
*/
public void stop() {
stopTimer();
client.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
client = null;
handler = null;
observer.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
}
/**
@@ -81,26 +79,24 @@ class GpsStatusManager {
}
/**
* This method must be called from the client every time a new trackPoint is received.
* Receive new trackPoint and calculate the new status if needed.
* This method must be called every time a new {@link Position} is received.
* Receive new position and calculate the new status if needed.
* It look for GPS changes in lastLocation if it's not null. If it's null then look for in lastValidLocation if any.
*/
public void onNewTrackPoint(@NonNull final TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
public void onNewTrackPoint(@NonNull Position position) {
lastPosition = position;
determineGpsStatusOnTrackpoint(trackPoint);
determineGpsStatusOnTrackpoint(position);
}
/**
* Checks if lastLocation has new GPS status looking up time and accuracy.
* Checks if {@link Position} has new GPS status looking up time and accuracy.
* It depends of signalLostThreshold and signalBadThreshold.
* If there is any change then it does the change.
* Also, it'll run the runnable if signal is bad or stop it if the signal is lost.
*/
//TODO use MonotonicClock instead of Instant.now()
@VisibleForTesting
void determineGpsStatusOnTrackpoint(@NonNull TrackPoint lastTrackPoint) {
if (lastTrackPoint.fulfillsAccuracy(horizontalAccuracyThreshold)) {
private void determineGpsStatusOnTrackpoint(@NonNull Position position) {
if (position.fulfillsAccuracy(horizontalAccuracyThreshold)) {
if (gpsStatus != GpsStatusValue.GPS_SIGNAL_FIX) {
setGpsStatus(GpsStatusValue.GPS_SIGNAL_FIX);
scheduleTimer(); //TODO
@@ -115,10 +111,10 @@ class GpsStatusManager {
}
void determineGpsStatusByTime(Instant now) {
if (lastTrackPoint == null) {
if (lastPosition == null) {
return;
}
if (signalLostThreshold.minus(Duration.between(lastTrackPoint.getTime(), now)).isNegative()) {
if (signalLostThreshold.minus(Duration.between(lastPosition.time(), now)).isNegative()) {
// Too much time without receiving signal -> signal lost.
if (gpsStatus != GpsStatusValue.GPS_SIGNAL_LOST) {
setGpsStatus(GpsStatusValue.GPS_SIGNAL_LOST);
@@ -155,14 +151,14 @@ class GpsStatusManager {
}
setGpsStatus(GpsStatusValue.GPS_DISABLED);
lastTrackPoint = null;
lastPosition = null;
stopTimer();
}
private void setGpsStatus(GpsStatusValue current) {
gpsStatus = current;
if (client != null) {
client.onGpsStatusChanged(current);
if (observer != null) {
observer.onGpsStatusChanged(current);
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.HeartRateBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
public class HeartRateHandler extends SensorHandler<HeartRate, HeartRate> {
protected HeartRateHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new HeartRateBluetooth()
);
}
@NonNull
@Override
protected AggregatorHeartRate createAggregator(String sensorAddress, String sensorName) {
return new AggregatorHeartRate(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_heart_rate_key;
}
}
@@ -0,0 +1,37 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
public class RunningSpeedCadenceDistanceHandler extends SensorHandler<RunningSpeedAndCadenceBluetooth.Data, AggregatorRunning.Data> {
protected RunningSpeedCadenceDistanceHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new RunningSpeedAndCadenceBluetooth()
);
}
@NonNull
@Override
protected AggregatorRunning createAggregator(String sensorAddress, String sensorName) {
return new AggregatorRunning("", null);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_running_speed_and_cadence_key;
}
}
@@ -1,11 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.os.Handler;
public interface SensorConnector {
void start(Context context, Handler handler);
void stop(Context context);
}
@@ -0,0 +1,134 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
import de.dennisguse.opentracks.sensors.driver.SensorType;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implements DriverObserver<AggregatorInput> {
private static final String TAG = SensorHandler.class.getSimpleName();
private final SensorManager sensorManager;
@VisibleForTesting
public Driver driver;
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
protected SensorHandler(SensorManager sensorManager) {
this.sensorManager = sensorManager;
}
void connect() {
disconnect();
String address = PreferencesUtils.getString(getSensorPreferenceKey(), getSensorPreferenceDefaultValue());
switch (PreferencesUtils.getSensorType(address)) {
case NONE -> {
onDisconnected();
return;
}
case INTERNAL -> {
driver = createDriverInternal();
}
case REMOTE -> {
driver = createDriverBluetooth(address);
}
default -> throw new UnsupportedOperationException();
}
driver.connect(sensorManager.getContext(), sensorManager.getHandler(), address);
}
void disconnect() {
if (driver == null) return;
driver.disconnect();
driver = null;
}
@NonNull
protected Driver createDriverInternal() {
throw new RuntimeException("Not implemented");
}
@NonNull
protected Driver createDriverBluetooth(String address) {
throw new RuntimeException("Not implemented");
}
@NonNull
protected abstract Aggregator<AggregatorInput, AggregatorOutput> createAggregator(String sensorAddress, String sensorName);
protected abstract int getSensorPreferenceKey();
protected String getSensorPreferenceDefaultValue() {
return SensorType.NONE.getPreferenceValue();
}
protected Context getContext() {
return sensorManager.getContext();
}
void resetAggregated() {
if (aggregator != null) {
aggregator.resetAggregated();
}
}
@Nullable
SensorData<AggregatorOutput> getSensorData(Instant now) {
if (aggregator == null) return null;
if (!aggregator.hasReceivedData()) return null;
return aggregator.getAggregatedValueWithSensorName(now);
}
//TODO REMOVE
@VisibleForTesting
public void setAggregator(Aggregator<AggregatorInput, AggregatorOutput> aggregator) {
this.aggregator = aggregator;
}
public void onConnected(String sensorAddress, String sensorName) {
SensorHandler.this.aggregator = createAggregator(sensorAddress, sensorName);
}
@Override
public void onConnectionLost() {
if (SensorHandler.this.aggregator == null) {
Log.d(TAG, "Connection lost, but where not connected.");
return;
}
SensorHandler.this.aggregator.resetAggregated();
}
@Override
public void onDataReceived(AggregatorInput value) {
if (aggregator == null) {
throw new RuntimeException("Received data while being disconnected.");
}
SensorHandler.this.aggregator.add(sensorManager.getNow(), value);
sensorManager.onChange();
}
@Override
public void onDisconnected() {
SensorHandler.this.aggregator = null;
}
}
@@ -6,154 +6,329 @@ import android.os.Handler;
import android.os.PowerManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
public class SensorManager implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = SensorManager.class.getSimpleName();
//TODO Should be final and not be visible for testing
@VisibleForTesting
public SensorDataSet sensorDataSet;
private Context context;
private Handler handler;
private PowerManager.WakeLock wakeLock;
private final TrackPointCreator observer;
private final SensorDataChangedObserver listener = new SensorDataChangedObserver() {
private HeartRateHandler heartRateHandler;
private TemperatureHandler temperatureHandler;
private CyclingCadenceHandler cyclingCadenceHandler;
private CyclingDistanceSpeedHandler cyclingDistanceSpeedHandler;
private CyclingPowerHandler cyclingPowerHandler;
private RunningSpeedCadenceDistanceHandler runningSpeedCadenceDistanceHandler;
@Override
public void onConnect(Aggregator<?, ?> aggregator) {
sensorDataSet.add(aggregator);
}
private AltitudeChangeHandler altitudeChangeHandler;
@Override
public void onChange(Raw<?> data) {
sensorDataSet.update(data);
observer.onChange(new SensorDataSet(sensorDataSet));
}
@Override
public void onDisconnect(Aggregator<?, ?> aggregator) {
sensorDataSet.add(aggregator);
}
@Override
public void onRemove(Aggregator<?, ?> aggregator) {
sensorDataSet.remove(aggregator);
}
@Override
public Instant getNow() {
return observer.createNow();
}
};
private BluetoothRemoteSensorManager bluetoothSensorManager;
private GainManager altitudeSumManager;
private GpsManager gpsManager;
private GpsHandler gpsHandler;
public SensorManager(TrackPointCreator observer) {
this.observer = observer;
this.sensorDataSet = new SensorDataSet(observer);
}
public void start(Context context, Handler handler) {
if (gpsManager != null) {
if (isStarted()) {
throw new RuntimeException("SensorManager cannot be started twice; stop first.");
}
wakeLock = SystemUtils.acquireWakeLock(context, wakeLock);
this.context = context;
this.handler = handler;
gpsManager = new GpsManager(observer, listener);
altitudeSumManager = new GainManager(listener);
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
gpsHandler = new GpsHandler(this, observer);
altitudeChangeHandler = new AltitudeChangeHandler(this);
heartRateHandler = new HeartRateHandler(this);
temperatureHandler = new TemperatureHandler(this);
cyclingCadenceHandler = new CyclingCadenceHandler(this);
cyclingDistanceSpeedHandler = new CyclingDistanceSpeedHandler(this);
cyclingPowerHandler = new CyclingPowerHandler(this);
runningSpeedCadenceDistanceHandler = new RunningSpeedCadenceDistanceHandler(this);
onSharedPreferenceChanged(null, null);
gpsManager.start(context, handler);
altitudeSumManager.start(context, handler);
bluetoothSensorManager.start(context, handler);
}
public void stop(Context context) {
public void stop() {
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
bluetoothSensorManager.stop(context);
bluetoothSensorManager = null;
heartRateHandler.disconnect();
heartRateHandler = null;
altitudeSumManager.stop(context);
altitudeSumManager = null;
temperatureHandler.disconnect();
temperatureHandler = null;
gpsManager.stop(context);
gpsManager = null;
cyclingCadenceHandler.disconnect();
cyclingCadenceHandler = null;
sensorDataSet.clear();
}
cyclingDistanceSpeedHandler.disconnect();
cyclingDistanceSpeedHandler = null;
public SensorDataSet fill(TrackPoint trackPoint) {
sensorDataSet.fillTrackPoint(trackPoint);
return new SensorDataSet(sensorDataSet);
cyclingPowerHandler.disconnect();
cyclingPowerHandler = null;
runningSpeedCadenceDistanceHandler.disconnect();
runningSpeedCadenceDistanceHandler = null;
altitudeChangeHandler.disconnect();
altitudeChangeHandler = null;
gpsHandler.disconnect();
gpsHandler = null;
context = null;
handler = null;
}
public void reset() {
if (bluetoothSensorManager == null || altitudeSumManager == null) {
if (gpsHandler == null || altitudeChangeHandler == null) {
Log.d(TAG, "No recording running and no reset necessary.");
return;
}
sensorDataSet.reset();
Log.i(TAG, "Resetting data");
heartRateHandler.resetAggregated();
temperatureHandler.resetAggregated();
cyclingCadenceHandler.resetAggregated();
cyclingDistanceSpeedHandler.resetAggregated();
cyclingPowerHandler.resetAggregated();
runningSpeedCadenceDistanceHandler.resetAggregated();
altitudeChangeHandler.resetAggregated();
gpsHandler.resetAggregated();
}
@VisibleForTesting
public void onChanged(Raw<?> data) {
listener.onChange(data);
}
public GpsManager getGpsManager() {
return gpsManager;
public GpsHandler getGpsHandler() {
return gpsHandler;
}
@Deprecated
@VisibleForTesting
public GainManager getAltitudeSumManager() {
return altitudeSumManager;
}
@Deprecated
@VisibleForTesting
public void setAltitudeSumManager(GainManager altitudeSumManager) {
this.altitudeSumManager = altitudeSumManager;
public AltitudeChangeHandler getAltitudeChangeHandler() {
return altitudeChangeHandler;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
if (gpsManager != null) {
gpsManager.onSharedPreferenceChanged(sharedPreferences, key);
bluetoothSensorManager.onSharedPreferenceChanged(sharedPreferences, key);
if (!isStarted()) return;
if (PreferencesUtils.isKey(gpsHandler.getSensorPreferenceKey(), key)) {
gpsHandler.connect();
}
if (PreferencesUtils.isKey(altitudeChangeHandler.getSensorPreferenceKey(), key)) {
altitudeChangeHandler.connect();
}
if (PreferencesUtils.isKey(heartRateHandler.getSensorPreferenceKey(), key)) {
heartRateHandler.connect();
}
if (PreferencesUtils.isKey(temperatureHandler.getSensorPreferenceKey(), key)) {
temperatureHandler.connect();
}
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
cyclingCadenceHandler.connect();
}
if (PreferencesUtils.isKey(cyclingDistanceSpeedHandler.getSensorPreferenceKey(), key)) {
cyclingDistanceSpeedHandler.connect();
}
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
cyclingPowerHandler.connect();
}
if (PreferencesUtils.isKey(runningSpeedCadenceDistanceHandler.getSensorPreferenceKey(), key)) {
runningSpeedCadenceDistanceHandler.connect();
}
}
public interface SensorDataChangedObserver {
public Instant getNow() {
return observer.getNow();
}
void onConnect(Aggregator<?, ?> sensorData);
void onChange(Raw<?> sensorData);
public Context getContext() {
return context;
}
void onDisconnect(Aggregator<?, ?> sensorData);
public Handler getHandler() {
return handler;
}
void onRemove(Aggregator<?, ?> sensorData);
private SensorData<HeartRate> getHeartRate(Instant now) {
SensorData<HeartRate> value = heartRateHandler.getSensorData(now);
if (value != null) {
Instant getNow();
//TODO Move this check into HeartRateHandler
if (value.data().isValid()) return value;
}
return null;
}
private SensorData<Cadence> getCadence(Instant now) {
{
SensorData<Cadence> value = cyclingCadenceHandler.getSensorData(now);
if (value != null) {
return cyclingCadenceHandler.getSensorData(now);
}
}
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
if (value != null && value.data().cadence() != null) {
return new SensorData<>(value.data().cadence(), value.sensorNameOrAddress());
}
return null;
}
private SensorData<Distance> getDistance(Instant now) {
{
SensorData<AggregatorCyclingDistanceSpeed.Data> value = cyclingDistanceSpeedHandler.getSensorData(now);
if (value != null) {
return new SensorData<>(value.data().distanceOverall(), value.sensorNameOrAddress());
}
}
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
if (value != null) {
return new SensorData<>(value.data().distance(), value.sensorNameOrAddress());
}
return null;
}
//TOOD simplify?
private SensorData<Speed> getSpeed(Instant now) {
{
SensorData<AggregatorCyclingDistanceSpeed.Data> value = cyclingDistanceSpeedHandler.getSensorData(now);
if (value != null && value.data() != null && value.data().speed() != null) {
return new SensorData<>(value.data().speed(), value.sensorNameOrAddress());
}
}
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
if (value != null && value.data() != null && value.data().speed() != null) {
return new SensorData<>(value.data().speed(), value.sensorNameOrAddress());
}
return null;
}
public void onChange() {
observer.onChange();
}
public SensorDataSet getSensorDataSet(Instant now) {
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
var a = temperatureHandler.getSensorData(now);
var b = altitudeChangeHandler.getSensorData(now);
return new SensorDataSet(
gpsSensorData != null ? gpsSensorData : new SensorData<>(Position.of(now), ""), //We always need a Position(now)
getSpeed(now),
getDistance(now),
getHeartRate(now),
temperatureHandler.getSensorData(now),
getCadence(now),
cyclingPowerHandler.getSensorData(now),
altitudeChangeHandler.getSensorData(now)
);
}
@VisibleForTesting
public void clear() {
Log.i(TAG, "Removing all aggregators");
//TODO This is to ExportImportTest alive; refactor test.
if (this.altitudeChangeHandler != null) {
this.altitudeChangeHandler.setAggregator(null);
}
if (this.heartRateHandler != null) {
this.heartRateHandler.setAggregator(null);
}
if (this.temperatureHandler != null) {
this.temperatureHandler.setAggregator(null);
}
if (this.cyclingCadenceHandler != null) {
this.cyclingCadenceHandler.setAggregator(null);
}
if (cyclingDistanceSpeedHandler != null) {
cyclingDistanceSpeedHandler.setAggregator(null);
}
if (cyclingPowerHandler != null) {
cyclingPowerHandler.setAggregator(null);
}
if (runningSpeedCadenceDistanceHandler != null) {
runningSpeedCadenceDistanceHandler.setAggregator(null);
}
if (this.gpsHandler != null) {
this.gpsHandler.setAggregator(null);
}
}
private boolean isStarted() {
return wakeLock != null;
}
@VisibleForTesting
public void setAggregator(AggregatorHeartRate data) {
heartRateHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorTemperature data) {
temperatureHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorCyclingCadence data) {
cyclingCadenceHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorCyclingDistanceSpeed data) {
cyclingDistanceSpeedHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorCyclingPower data) {
cyclingPowerHandler.setAggregator(data);
}
@VisibleForTesting
public void setAggregator(@NonNull AggregatorRunning data) {
runningSpeedCadenceDistanceHandler.setAggregator(data);
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.TemperatureBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
public class TemperatureHandler extends SensorHandler<Temperature, Temperature> {
protected TemperatureHandler(SensorManager sensorManager) {
super(sensorManager);
}
@NonNull
@Override
protected Driver createDriverBluetooth(String address) {
return new BluetoothDriver<>(
BluetoothUtils.getAdapter(getContext()),
this,
new TemperatureBluetooth()
);
}
@NonNull
@Override
protected AggregatorTemperature createAggregator(String sensorAddress, String sensorName) {
return new AggregatorTemperature(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_temperature_key;
}
}
@@ -0,0 +1,42 @@
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class BarometerBluetooth implements BluetoothDriver.BluetoothParser<AtmosphericPressure> {
static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL) //TODO extract leastSigBits
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(BAROMETRIC_PRESSURE);
}
@Override
public AtmosphericPressure parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
return parseEnvironmentalSensing(characteristic);
}
/**
* Decoding: org.bluetooth.characteristic.pressure.xml
*/
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
if (raw.length < 4) {
return null;
}
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
return AtmosphericPressure.ofPA(pressure / 10f);
}
}
@@ -12,9 +12,6 @@ import androidx.annotation.NonNull;
import java.util.concurrent.TimeUnit;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
public class BarometerInternal implements Driver {
@@ -22,9 +19,9 @@ public class BarometerInternal implements Driver {
private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5);
private final SensorManager.SensorDataChangedObserver observer;
private final DriverObserver<AtmosphericPressure> observer;
private Context context;
private android.hardware.SensorManager sensorService;
private final SensorEventListener sensorEventListener = new SensorEventListener() {
@Override
@@ -34,7 +31,7 @@ public class BarometerInternal implements Driver {
return;
}
observer.onChange(new Raw<>(observer.getNow(), AtmosphericPressure.ofHPA(event.values[0])));
observer.onDataReceived(AtmosphericPressure.ofHPA(event.values[0]));
}
@Override
@@ -43,40 +40,34 @@ public class BarometerInternal implements Driver {
}
};
public BarometerInternal(@NonNull SensorManager.SensorDataChangedObserver observer) {
public BarometerInternal(@NonNull DriverObserver<AtmosphericPressure> observer) {
this.observer = observer;
}
@Override
public void connect(Context context, Handler handler, String addressIgnored) {
sensorService = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorService.getDefaultSensor(Sensor.TYPE_PRESSURE);
android.hardware.SensorManager sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
if (pressureSensor == null) {
Log.w(TAG, "No pressure sensor available.");
if (pressureSensor != null && sensorService.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
observer.onConnected(null, null);
return;
}
if (sensorManager.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
this.context = context;
observer.onConnect(new AggregatorBarometer("internal", null));
return;
}
disconnect();
Log.w(TAG, "No pressure sensor available.");
observer.onDisconnected();
}
@Override
public boolean isConnected() {
return context != null;
return sensorService != null;
}
@Override
public void disconnect() {
if (!isConnected()) return;
android.hardware.SensorManager sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(sensorEventListener);
this.context = null;
sensorService.unregisterListener(sensorEventListener);
sensorService = null;
}
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
@@ -31,23 +31,25 @@ import android.util.Log;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
/**
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
*/
//TODO Re-act to Bluetooth disabled/enabled
@SuppressLint("MissingPermission")
public class BluetoothConnectionManager implements Driver {
public class BluetoothDriver<T> implements Driver {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private static final String TAG = BluetoothDriver.class.getSimpleName();
private final SensorManager.SensorDataChangedObserver observer;
private final DriverObserver<T> observer;
private final SensorHandlerInterface sensorHandler;
private final BluetoothParser<T> sensorHandler;
private final BluetoothAdapter bluetoothAdapter;
private BluetoothGatt bluetoothGatt;
@@ -55,27 +57,29 @@ public class BluetoothConnectionManager implements Driver {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
switch (newState) {
case BluetoothProfile.STATE_CONNECTING ->
Log.i(TAG, gatt.getDevice() + ": connecting to sensor");
case BluetoothProfile.STATE_CONNECTING -> {
Log.i(TAG, gatt.getDevice() + ": connecting to sensor");
}
case BluetoothProfile.STATE_CONNECTED -> {
Log.i(TAG, gatt.getDevice() + ": connected to sensor; discovering services");
gatt.discoverServices();
}
case BluetoothProfile.STATE_DISCONNECTING ->
Log.i(TAG, gatt.getDevice() + ": disconnecting from sensor: ");
case BluetoothProfile.STATE_DISCONNECTING -> Log.i(TAG, gatt.getDevice() + ": disconnecting from sensor: ");
case BluetoothProfile.STATE_DISCONNECTED -> {
//This is also triggered, if no connection was established (ca. 30s)
Log.i(TAG, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
if (gatt.connect()) {
Log.e(TAG, gatt.getDevice() + ": could not trigger reconnect for sensor");
}
clearData();
observer.onConnectionLost();
}
}
}
@Override
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothDevice device = gatt.getDevice();
BluetoothGattService gattService = null;
ServiceMeasurementUUID serviceMeasurement = null;
for (ServiceMeasurementUUID s : sensorHandler.getServices()) {
@@ -87,13 +91,13 @@ public class BluetoothConnectionManager implements Driver {
}
if (gattService == null) {
Log.e(TAG, gatt.getDevice() + ": could not get gattService for serviceUUID=" + serviceMeasurement);
Log.e(TAG, device + ": could not get gattService for serviceUUID=" + serviceMeasurement);
return;
}
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(serviceMeasurement.measurementUUID());
if (characteristic == null) {
Log.e(TAG, gatt.getDevice() + ": could not get BluetoothCharacteristic for serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
Log.e(TAG, device + ": could not get BluetoothCharacteristic for serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
return;
}
gatt.setCharacteristicNotification(characteristic, true);
@@ -111,6 +115,8 @@ public class BluetoothConnectionManager implements Driver {
if (!gatt.writeDescriptor(descriptor)) {
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
}
observer.onConnected(device.getAddress(), device.getName());
}
@Override
@@ -127,11 +133,15 @@ public class BluetoothConnectionManager implements Driver {
return;
}
sensorHandler.handlePayload(observer, serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
T value = sensorHandler.parsePayload(serviceMeasurementUUID.get(), gatt.getDevice().getName(), characteristic);
if (value == null) {
return;
}
observer.onDataReceived(value);
}
};
BluetoothConnectionManager(BluetoothAdapter bluetoothAdapter, SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
public BluetoothDriver(BluetoothAdapter bluetoothAdapter, DriverObserver<T> observer, BluetoothParser<T> sensorHandler) {
this.bluetoothAdapter = bluetoothAdapter;
this.observer = observer;
this.sensorHandler = sensorHandler;
@@ -144,26 +154,11 @@ public class BluetoothConnectionManager implements Driver {
return;
}
if (SensorType.NONE.getPreferenceValue().equals(address)) {
Log.w(TAG, "NONE: going to disconnect");
if (isConnected()) {
disconnect();
observer.onRemove(sensorHandler.createEmptySensorData(null, null));
}
return;
}
if (isConnected()) {
Log.w(TAG, "Already connected; ignoring.");
Log.e(TAG, "Already connected; ignoring.");
return;
}
if (isSameBluetoothDevice(address)) {
return;
} else {
disconnect();
}
BluetoothDevice device;
try {
device = bluetoothAdapter.getRemoteDevice(address);
@@ -175,37 +170,23 @@ public class BluetoothConnectionManager implements Driver {
Log.d(TAG, device + ": trying to connect");
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
observer.onConnect(sensorHandler.createEmptySensorData(device.getAddress(), device.getName()));
}
private synchronized void clearData() {
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress(), bluetoothGatt.getDevice().getName()));
}
@Override
public synchronized void disconnect() {
if (!isConnected()) {
Log.w(TAG, "Not connected; no need to re-connect.");
Log.w(TAG, "Not connected; cannot disconnect.");
return;
}
Log.i(TAG, bluetoothGatt.getDevice() + ": start disconnect");
bluetoothGatt.disconnect();
bluetoothGatt.close();
clearData();
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
bluetoothGatt = null;
observer.onDisconnected();
}
private synchronized boolean isSameBluetoothDevice(String address) {
if (bluetoothGatt == null) {
return false;
}
return address.equals(bluetoothGatt.getDevice().getAddress());
}
private boolean isBluetoothEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
@@ -214,4 +195,11 @@ public class BluetoothConnectionManager implements Driver {
public boolean isConnected() {
return bluetoothGatt != null;
}
public interface BluetoothParser<T> {
List<ServiceMeasurementUUID> getServices();
T parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic);
}
}
@@ -0,0 +1,49 @@
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import android.util.Pair;
import java.util.List;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class CyclingCadenceBluetooth implements BluetoothDriver.BluetoothParser<CyclingCadenceBluetooth.CrankData> {
private static final String TAG = CyclingCadenceBluetooth.class.getSimpleName();
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
CyclingPowerBluetooth.CYCLING_POWER,
CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return CYCLING_CADENCE;
}
@Override
public CrankData parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
if (serviceMeasurementUUID.equals(CyclingPowerBluetooth.CYCLING_POWER)) {
CyclingPowerBluetooth.Data data = CyclingPowerBluetooth.parseCyclingPower(characteristic);
if (data != null && data.crank() != null) {
return data.crank();
}
}
if (serviceMeasurementUUID.equals(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE)) {
Pair<CyclingDistanceSpeedBluetooth.WheelData, CrankData> data = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
if (data != null && data.second != null) {
return data.second;
}
}
Log.e(TAG, "Don't know how to decode this payload.");
return null;
}
public record CrankData(
long crankRevolutionsCount, // UINT32
int crankRevolutionsTime // UINT16; 1/1024s
) {}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Pair;
@@ -9,11 +9,9 @@ import androidx.annotation.VisibleForTesting;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterface {
public class CyclingDistanceSpeedBluetooth implements BluetoothDriver.BluetoothParser<CyclingDistanceSpeedBluetooth.WheelData> {
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
new UUID(0x181600001000L, 0x800000805f9b34fbL),
@@ -26,21 +24,18 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
}
@Override
public AggregatorCyclingDistanceSpeed createEmptySensorData(String address, String name) {
return new AggregatorCyclingDistanceSpeed(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> data = parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (data.first != null) {
observer.onChange(new Raw<>(observer.getNow(), data.first));
public WheelData parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
Pair<WheelData, CyclingCadenceBluetooth.CrankData> data = parseCyclingCrankAndWheel(characteristic);
if (data != null && data.first != null) {
return data.first;
}
return null;
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
public static Pair<WheelData, CyclingCadenceBluetooth.CrankData> parseCyclingCrankAndWheel(@NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
@@ -61,13 +56,13 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
index += 2;
}
BluetoothHandlerCyclingCadence.CrankData crankData = null;
CyclingCadenceBluetooth.CrankData crankData = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
crankData = new CyclingCadenceBluetooth.CrankData(crankCount, crankTime);
}
return new Pair<>(wheelData, crankData);
@@ -78,5 +73,6 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
long wheelRevolutionsCount, // UINT32
int wheelRevolutionsTime // UINT16; 1/1024s
) {}
) {
}
}

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