Compare commits

...

56 Commits

Author SHA1 Message Date
Dennis Guse a7f7e3fca1 Release: v3.28.0 2022-05-08 10:59:27 +02:00
Dennis Guse a52beffef5 Translations: fix quantities. 2022-05-06 06:36:49 +02:00
Yaxi Ye e6f6d61b84 Translated using Weblate (Chinese (Simplified))
Currently translated at 64.7% (323 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/zh_Hans/
2022-05-04 20:18:51 +02:00
Vinicius 4a167c9335 Translated using Weblate (Portuguese (Brazil))
Currently translated at 82.3% (70 of 85 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/pt_BR/
2022-05-04 20:18:51 +02:00
Vinicius 16857a9968 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (498 of 498 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2022-05-04 20:18:51 +02:00
Dennis Guse 094a20c379 Vertical accuracy: process and import/export.
Fixes #78.
2022-05-04 20:18:45 +02:00
Dennis Guse 0d3cf4ce0d Configurable filename for export: used for all automatic exports after recording, exporting all, and sharing tracks.
Fixes #512.
2022-05-03 20:06:29 +02:00
dependabot[bot] 8a2d1c42a8 Bump reactivecircus/android-emulator-runner from 2.23.0 to 2.24.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.23.0 to 2.24.0.
- [Release notes](https://github.com/reactivecircus/android-emulator-runner/releases)
- [Changelog](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/CHANGELOG.md)
- [Commits](https://github.com/reactivecircus/android-emulator-runner/compare/v2.23.0...v2.24.0)

---
updated-dependencies:
- dependency-name: reactivecircus/android-emulator-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-02 07:16:58 +02:00
Dennis Guse a5b44175a3 Convert metricUnits boolean into an enum.
Fixes #1199.
2022-04-28 22:35:38 +02:00
Dennis Guse db5c9479df Implement SpeedFormatter. 2022-04-28 22:35:38 +02:00
Dennis Guse 16f238f34b Refactoring: DistanceFormatter handles metricUnits. 2022-04-28 22:35:38 +02:00
Dennis Guse a57b5cd6ee From notification, open TrackRecordingActivity.
Fixes #1205.
2022-04-26 18:31:14 +02:00
Román Ginés 5e54cb8fda Bugfixed: intervals dropdown shows incorrect unit when metric units is selected.
Fixes #1201.
2022-04-24 19:23:44 +02:00
Dennis Guse 0934e7f10c Release: v3.27.0 2022-04-24 17:28:23 +02:00
Dennis Guse 6741ef4d9f Bugfix: notification opens TrackListActivity instead of TrackRecordingActivity.
If a recording is running, we need to pass the trackId to TrackRecordingActivity as otherwise it crashes when needing it.

Fixes #1156.
2022-04-24 16:58:36 +02:00
Dennis Guse f2ac1edc4b Create DistanceFormatter.
Fixes #1197
2022-04-23 20:12:43 +02:00
Dennis Guse b44bab1ab5 For testing: add rule to mock timezone.
Used by ExportImportTest.csv_export_only()
2022-04-23 15:03:05 +02:00
Dennis Guse 574bd38a35 Github Actions: upgrade from api from 23 to 24 to use BufferedReader during tests. 2022-04-23 15:03:05 +02:00
Dennis Guse 374959f340 Add CSV as export format (TrackPoints only).
Fixes #999.
2022-04-23 15:03:05 +02:00
Dennis Guse 3815ee6c59 Voice announcements and Interval: add sub km/miles options.
Fixes #1195.
2022-04-23 14:16:00 +02:00
Dennis Guse 5e4924a5e1 Translations: fix some quantities. 2022-04-23 13:22:15 +02:00
Egor Ermakov 8123104f39 Translated using Weblate (Russian)
Currently translated at 84.5% (71 of 84 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/ru/
2022-04-23 12:49:10 +02:00
Egor Ermakov a9e0c4916d Translated using Weblate (Russian)
Currently translated at 99.6% (500 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2022-04-23 12:49:10 +02:00
Evgeniy 58803c975a Translated using Weblate (Russian)
Currently translated at 82.1% (69 of 84 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/ru/
2022-04-23 12:49:10 +02:00
mondstern d7dc59e547 Translated using Weblate (Czech)
Currently translated at 99.6% (500 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2022-04-23 12:49:10 +02:00
Dennis Guse 5cf8b67954 Import/Export: horizontal accuracy.
Fixes #683.
2022-04-23 12:49:07 +02:00
Brian J. Murrell fb70486bdc Include GPS accuracy in GPX export
Add a new "accuracy" tag to the opentracks extension as there was no
other extension providing this tag that could be found.

Signed-off-by: Brian J. Murrell <brian@interlinx.bc.ca>
2022-04-23 12:49:07 +02:00
Dennis Guse 18b21a6a3e Bugfix: prevent race condition in TrackPointCreator.
This race condition leads to a crash in TrackStatisticsUpdater as the time might appear to move backwards if the created TrackPoints are not stored in order. Might happen if multiple sensors (BLE or GPS) are used in simultaneously.

Every method affected by a race condition is now synchronized.

Problem was introduced in c4b8082efc

Fixes #1151.
2022-04-23 12:48:19 +02:00
Dennis Guse 71288cd9be Bugfix: onDestroy() first shutdown sensors.
Fixes #1189.
2022-04-22 06:51:15 +02:00
dependabot[bot] 220545da9f Bump mockito-android from 4.5.0 to 4.5.1
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-android
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-22 06:32:41 +02:00
dependabot[bot] 43e9c42915 Bump mockito-android from 4.4.0 to 4.5.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.4.0...v4.5.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-android
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-20 07:00:32 +02:00
Dennis Guse 28944eff07 Upgrade Gradle 7.3. 2022-04-11 21:58:47 +02:00
Dennis Guse 92f9f94423 Workaround: Wahoo Tickr X repords cadence in SPM instead of RPM.
Fixes #1164.
2022-04-11 21:30:25 +02:00
dependabot[bot] 8ae268fab2 Bump actions/upload-artifact from 2 to 3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-11 21:14:28 +02:00
dependabot[bot] 346264025b Bump actions/setup-java from 2 to 3
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 3.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-11 21:03:25 +02:00
Dennis Guse 0a810e7123 Bugfix: NPE in GPXTrackExporter.
Introduced in 0f489f1cc9

Fixes #1176.
2022-04-11 20:43:11 +02:00
Dennis Guse 02f3fc1940 Merge remote-tracking branch 'weblate/main' 2022-04-10 19:31:51 +02:00
Dennis Guse dd1c5667a0 Adhere to recording distance interval for TrackPoint storage.
Part of #1059.
2022-04-10 12:57:12 +02:00
Brian J. Murrell 0f489f1cc9 Use Cluetrust GPX extension to add total distance
To each trackpiont written to the GPX file.

Signed-off-by: Brian J. Murrell <brian@interlinx.bc.ca>
2022-04-08 07:31:52 +02:00
dependabot[bot] a07f11589b Bump gradle from 7.1.2 to 7.1.3
Bumps gradle from 7.1.2 to 7.1.3.

---
updated-dependencies:
- dependency-name: com.android.tools.build:gradle
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-08 07:16:49 +02:00
Dennis Guse 209a320433 UI: while recording clock shows seconds.
Fixes #1168.
2022-04-07 20:25:22 +02:00
Mario 8ab9ed0e31 Translated using Weblate (Spanish)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2022-04-07 11:13:12 +02:00
Dennis Guse 80ca4e647a Bug: crash earlier if in IntervalsFragment if the trackId is null.
Belongs to #1156.
2022-04-05 20:22:07 +02:00
Mario a8de9cb216 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2022-04-05 19:03:23 +02:00
Max Schallert 12c21b7940 Translated using Weblate (German)
Currently translated at 90.0% (452 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2022-04-04 18:58:51 +02:00
Dennis Guse 4c5ff546a4 Translated using Weblate (German)
Currently translated at 90.0% (452 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2022-04-04 18:58:51 +02:00
mm4c 2e0835b2ec Translated using Weblate (Dutch)
Currently translated at 86.9% (73 of 84 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nl/
2022-04-02 16:24:48 +02:00
mm4c 18ea8fea87 Translated using Weblate (Dutch)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2022-04-02 16:24:48 +02:00
mondstern 02a69c8817 Translated using Weblate (Czech)
Currently translated at 99.6% (500 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2022-04-02 16:24:48 +02:00
mm4c cab70d665c Translated using Weblate (Dutch)
Currently translated at 84.3% (70 of 83 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nl/
2022-04-02 16:24:48 +02:00
mm4c c42f833703 Translated using Weblate (Dutch)
Currently translated at 81.0% (407 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2022-04-02 16:24:48 +02:00
Xosé M 39838232b5 Translated using Weblate (Galician)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2022-04-02 16:24:48 +02:00
Brian J. Murrell 2b346b3f18 Add information about Wahoo Tickr X
Cadence appears to be reported in SPM rather than RPM.
2022-04-02 16:24:42 +02:00
Dennis Guse 7c5115fc56 UI: only update once every second.
Accidentally introduced in 78519883c0.
2022-03-26 00:35:44 +01:00
Román Ginés cb71c40350 Release: v3.26.1 2022-03-22 13:37:35 +01:00
Román Ginés 22bde4af3c Bug fixed: contextual action bar didn't overlap toolbar in night theme. 2022-03-21 12:59:45 +01:00
147 changed files with 2452 additions and 952 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
- name: checkout
uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
@@ -26,7 +26,7 @@ jobs:
- name: Archive
if: always()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: OpenTracks-nightly.apk
path: ./build/outputs/apk/nightly/*.apk
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
+4 -4
View File
@@ -10,20 +10,20 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
api-level: [ 23, 30 ]
api-level: [ 24, 30 ]
target: [ default ]
fail-fast: false
steps:
- name: checkout
uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2.23.0
uses: reactivecircus/android-emulator-runner@v2.24.0
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
@@ -33,7 +33,7 @@ jobs:
- name: Archive
if: always()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: code-coverage-report
path: build/reports/androidTests/
-1
View File
@@ -60,7 +60,6 @@ captures/
.idea/encodings.xml
.idea/jsLibraryMappings.xml
.idea/libraries/
.idea/misc.xml
.idea/modules.xml
.idea/scopes/scope_settings.xml
.idea/shelf/
+1
View File
@@ -59,6 +59,7 @@ We do not support type of movement (i.e., walking vs running).
* Polar Stride
* Stryd (incl. power)
* Wahoo Tickr X (Cadence appears to be reported in SPM rather than RPM)
## Tested Barometric Sensor Smartphones
+4 -4
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath 'com.android.tools.build:gradle:7.1.3'
}
}
@@ -59,8 +59,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4773
versionName "v3.26.0"
versionCode 4816
versionName "v3.27.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -130,7 +130,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'org.mockito:mockito-android:4.4.0'
androidTestImplementation 'org.mockito:mockito-android:4.5.1'
androidTestUtil 'androidx.test:orchestrator:1.4.1'
}
+22
View File
@@ -29,4 +29,26 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="accuracy_horizontal"
type="xsd:float">
<xsd:annotation>
<xsd:documentation xml:lang="en">
Horizontal accuracy in meters of the current TrackPoint; 68% chance that the actual
location is
within this radius around the measured location.
Only used in GPX.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="accuracy_vertical"
type="xsd:float">
<xsd:annotation>
<xsd:documentation xml:lang="en">
Vertical accuracy in meters of the current TrackPoint; 68% chance that the actual
location is
within this radius around the measured location.
Only used in GPX.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:schema>
@@ -0,0 +1,4 @@
v3.26.1: OpenTracks
Bugfix:
- Contextual action bar didn't overlap toolbar in night theme.
@@ -0,0 +1,17 @@
v3.27.0: OpenTracks
Changes:
- Voice annoucements: add 100m, 200m, and 500m
- Interval: add 100m and 500m
- Clock shows now also seconds
- GPX, KML, KMZ: export and import horizontal accuracy
- GPX: export cumulative distance via clustrust extension
- CSV: basic export (TrackPoints only; not for backup)
- Workaround for Tickr X as it reports SPM instead of RPM
Bugfixes:
- Adhere to recording distance interval;
TrackPoints were recorded although they were too close
- Prevent crash on stop recording (race condition)
- Prevent crash when using multiple sensors (race condition)
- Prevent crash when opening OpenTracks via notification
@@ -0,0 +1,6 @@
v3.28.0: OpenTracks
Changes:
- Configurable filename for export/sharing (default: {starttime}_{name}.{suffix})
- Vertical GPS accuracy is now stored and can be exported/imported
(API level >= 26).
@@ -0,0 +1,11 @@
v3.25.0: OpenTracks
Veranderingen:
- GPS is nu optioneel incl. waarschuwing GPS uitgeschakeld
- PublicAPI: andere apps kunnen nu opgenomen gegevens ontvangen als ze een opname starten (geïmplementeerd voor Gadgetbridge)
- Nieuwe activiteiten: zwemmen, buiten zwemmen, en training
- Verwijderd: share pic functie
Bugfix:
- UI: snelheid/hoogte met wisselingen tussen 0,0 en de werkelijke waarde
- Tracks met emoji's kunnen worden geëxporteerd
@@ -0,0 +1,5 @@
v3.25.1: OpenTracks
Verbetering:
- Automatische export: hopelijk voorkomen van stille mislukkingen
- Automatische export: voorkom crash
@@ -0,0 +1,8 @@
v3.26.0: OpenTracks
Wijzigingen:
- Gesproken meldingen zijn instelbaar
- Gesproken meldingen van hartslag
Bugfix:
- Crash due to race condition
@@ -0,0 +1,4 @@
v3.26.1: OpenTracks
Bugfix:
- Contextuele actie balk overlapte de werkbalk niet in het nachtthema.
@@ -1,11 +1,22 @@
OpenTracks is een sport tracking applicatie die uw privacy volledig respecteert.
Een sport tracker maatje met respect voor privacy.
Kenmerken:
* Sport tracking: volg uw sport en outdoor activiteiten
* Foto's en markeringen: markeer interessante locaties tijdens het volgen
* Hoogte winst en verlies: via barometrische sensor
* Hoogte weergegeven in EGM2008 (boven gemiddeld zeeniveau); geëxporteerd als WGS84
* Spraakaankondigingen
* Ondersteunt Bluetooth LE-sensoren: hartslag, snelheid en afstand (fietsen), cadans (fietsen), en vermogensmeter (fietsen)
* Exporteer gegevens: exporteer tracks als KMZ (incl. foto's), KML, of GPX
* Geen toegang tot internet: Internet wordt niet gebruikt
Sport en outdoor activiteiten geven geluk, vaardigheid en zelfvertrouwen.
Geef je gezondheid waarde door de conditie bij te houden.
Registreert terwijl je hardloopt of wandelt, en geeft je een fietscomputer met een groter scherm voor op de fiets.
Markeer interessante locaties langs de weg met foto's.
Bewaar geregistreerde statistieken tot in detail voor analyse.
Deel alleen de gegevens die je aan anderen wilt laten zien.
* Spraakaankondigingen.
* Ondersteunt Bluetooth LE sensoren: hartslag, snelheid en afstand (fietsen), cadans (fietsen), en vermogensmeter (fietsen).
* Hoogtewinst en -verlies: via barometrische sensor.
* Hoogte weergegeven in EGM2008 (boven gemiddeld zeeniveau); geëxporteerd als WGS84.
* Gegevens exporteren als tracks, hetzij als KMZ (incl. foto's), KML, of GPX.
* Geen Internet toegang of extra toegangsrechten.
* Donker en licht thema, met inachtneming van de systeeminstellingen.
* Geen advertenties.
Libre software / Vrije Software / Open Source
wat betekent dat je de broncode mag gebruiken, bestuderen, veranderen en delen.
Gelicenseerd Apache 2.0
@@ -1 +1 @@
Een sport tracker die uw privacy volledig respecteert.
Een sport tracker maatje met respect voor privacy.
@@ -0,0 +1,16 @@
v3.27.0: OpenTracks
Mudanças:
- Anúncios de voz: adicionado 100m, 200m e 500m
- Intervalo: adicionado 100m e 500m
- O relógio mostra agora também os segundos
- GPX, KML, KMZ: uma maior precisão horizontal de exportação e importação
- GPX: exporta a distância acumulada via extensão clustrust
- CSV: exportação básica (apenas TrackPoints; não para backup)
- Solução alternativa para o Tickr X, pois relata SPM em vez de RPM
Correções de bugs:
- Foi Aderido ao intervalo de distância de gravação; Os TrackPoints foram registrados embora estivessem muito próximos
- Prevenir travamento ao parar de gravar (condição de corrida)
- Evite acidentes ao usar vários sensores (condição de corrida)
- Evite travamentos ao abrir o OpenTracks via notificação
@@ -1 +1 @@
Um rastreador esportivo que respeita totalmente a sua privacidade.
Um rastreador esportivo que respeita a sua privacidade.
@@ -0,0 +1,8 @@
v3.26.0: OpenTracks
Изменения:
- Голосовые оповещения теперь настраиваются
- Голосовые оповещения о частоте сердечных сокращений
Исправление:
- Вылет из-за состояния гонки
@@ -0,0 +1,4 @@
v3.26.1: OpenTracks
Исправление:
- Контекстная панель действий не перекрывала панель инструментов в ночной теме.
@@ -1,11 +1,22 @@
OpenTracks - это приложение для спортивного слежения, которое полностью уважает вашу конфиденциальность.
Спортивный трекер уважающий вашу приватность.
Особенности:
* Спортивный трекинг: отслеживайте свои занятия спортом и активный отдых.
* Фотографии и маркеры: отмечайте интересные места во время отслеживания.
* Набор и потеря высоты: через барометрический датчик.
* Высота отображается в EGM2008 (над средним уровнем моря); экспортируется в WGS84.
* Голосовые уведомления
* Поддержка датчиков Bluetooth LE: пульс, скорость и расстояние (велоспорт), каденция (велоспорт) и измеритель мощности (велоспорт)
* Экспорт данных: экспорт треков в формате KMZ (включая фотографии), KML или GPX.
* Нет доступа к Интернету: Интернет не используется
Спорт и иные активности дарят радость, мастерство и уверенность в себе.
Цените свое здоровье, следя за тренировками.
Записывает, как вы бегаете или идете, и предоставляет вам функционал велокомпьютера с большим экраном для езды на велосипеде.
Отмечайте интересные места на своем пути с помощью фотографий.
Храните записанные данные в мельчайших подробностях для анализа.
Делитесь только теми данными, которые хотите, чтобы другие имели доступ к ним.
* Голосовые уведомления.
* Поддержка датчиков Bluetooth LE: пульс, скорость и дистанция (для велосипеда), частота вращения педалей (для велосипеда) и измеритель мощности (для велосипеда).
* Набор и потеря высоты: с помощью барометрического датчика.
* Высота указана в EGM2008 (над средним уровнем моря); экспортируется как WGS84.
* Экспорт данных в виде треков формата KMZ (включая фотографии), KML или GPX.
* Не требует соединения с Интернетом и лишних разрешений.
* Темная и светлая темы с учетом системных настроек.
* Без рекламы.
Libre software / Свободное программное обеспечение / Открытый исходный код
означает, что вы можете использовать, изучать, изменять и делиться исходным кодом.
Лицензия Apache 2.0
@@ -1 +1 @@
Спортивный трекер, который полностью соблюдает вашу конфиденциальность.
Спортивный трекер, уважающий вашу конфиденциальность.
Binary file not shown.
+2 -3
View File
@@ -1,6 +1,5 @@
#Wed Jul 28 23:10:13 CEST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+159 -110
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,101 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+3 -18
View File
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,21 +64,6 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
@@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
@@ -5,12 +5,13 @@ import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
public class PreferenceMetricUnitRule implements TestRule {
private final boolean metricUnits;
public class PreferenceUnitSystemRule implements TestRule {
private final UnitSystem unit;
public PreferenceMetricUnitRule(boolean metricUnits) {
this.metricUnits = metricUnits;
public PreferenceUnitSystemRule(UnitSystem unit) {
this.unit = unit;
}
@Override
@@ -18,13 +19,13 @@ public class PreferenceMetricUnitRule implements TestRule {
return new Statement() {
@Override
public void evaluate() throws Throwable {
final boolean previousMetricUnits = PreferencesUtils.isMetricUnits();
final UnitSystem previousUnitSystem = PreferencesUtils.getUnitSystem();
try {
PreferencesUtils.setMetricUnits(metricUnits);
PreferencesUtils.setUnit(unit);
base.evaluate();
} finally {
PreferencesUtils.setMetricUnits(previousMetricUnits);
PreferencesUtils.setUnit(previousUnitSystem);
}
}
};
@@ -0,0 +1,36 @@
package de.dennisguse.opentracks;
import androidx.annotation.NonNull;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.util.TimeZone;
public class TimezoneRule implements TestRule {
private final TimeZone mTimeZone;
private TimeZone mDeviceTimeZone;
public TimezoneRule(@NonNull TimeZone timeZone) {
mTimeZone = timeZone;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
mDeviceTimeZone = TimeZone.getDefault();
TimeZone.setDefault(mTimeZone);
base.evaluate();
} finally {
TimeZone.setDefault(mDeviceTimeZone);
}
}
};
}
}
@@ -14,6 +14,7 @@ import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(AndroidJUnit4.class)
@@ -26,7 +27,7 @@ public class ChartPointTest {
statistics.setTotalTime(Duration.ofSeconds(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, false);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, UnitSystem.IMPERIAL);
// then
assertEquals(1000000, (long) point.getTimeOrDistance());
@@ -39,7 +40,7 @@ public class ChartPointTest {
statistics.setTotalDistance(Distance.of(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, UnitSystem.METRIC);
// then
assertEquals(1, (long) point.getTimeOrDistance());
@@ -51,7 +52,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
// then
assertEquals(50, point.getAltitude(), 0.01);
@@ -65,7 +66,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
// then
assertNull(point.getHeartRate());
@@ -84,7 +85,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, true);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
// then
assertEquals(100.0, point.getHeartRate(), 0.01);
@@ -0,0 +1,45 @@
package de.dennisguse.opentracks.data.models;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.settings.UnitSystem;
@RunWith(AndroidJUnit4.class)
public class DistanceFormatterTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void testFormatDistance_metric() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.METRIC)
.build(context);
// A large number in metric
assertEquals("5.00 km", formatter.formatDistance(Distance.of(5000)));
// A small number in metric
assertEquals("100.00 m", formatter.formatDistance(Distance.of(100)));
}
@Test
public void testFormatDistance_imperial() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.IMPERIAL)
.build(context);
// A large number in imperial
assertEquals("3.11 mi", formatter.formatDistance(Distance.of(5000)));
// A small number in imperial
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
}
}
@@ -0,0 +1,82 @@
package de.dennisguse.opentracks.data.models;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.settings.UnitSystem;
@RunWith(AndroidJUnit4.class)
public class SpeedFormatterTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void testGetSpeedParts_pace_metric() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(false)
.build(context);
assertEquals("4:59", formatter.getSpeedParts(Speed.of(3.34)).first);
assertEquals("5:00", formatter.getSpeedParts(Speed.of(3.33)).first);
assertEquals("min/km", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_pace_imperial() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.IMPERIAL)
.setReportSpeedOrPace(false)
.build(context);
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.34)).first);
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.33)).first);
assertEquals("min/mi", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_speed_metric() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(true)
.build(context);
assertEquals("11.9", formatter.getSpeedParts(Speed.of(3.31)).first);
assertEquals("km/h", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_speed_imperial() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.IMPERIAL)
.setReportSpeedOrPace(true)
.build(context);
assertEquals("7.5", formatter.getSpeedParts(Speed.of(3.34)).first);
assertEquals("mph", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testFormatSpeed() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.METRIC)
.setReportSpeedOrPace(false)
.build(context);
assertEquals("4:59 min/km", formatter.formatSpeed(Speed.of(3.34)));
}
}
@@ -0,0 +1,60 @@
package de.dennisguse.opentracks.io.file;
import static org.junit.Assert.assertEquals;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.TimeZone;
import java.util.UUID;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TimezoneRule;
import de.dennisguse.opentracks.data.models.Track;
@RunWith(Parameterized.class)
public class TrackFilenameGeneratorTest {
@Rule
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
@Parameterized.Parameters
public static Collection<String[]> data() {
return Arrays.asList(new String[][]{
{"{uuid}_{name}", "0000fee0_Best Track.gpx" },
{"{name}_{uuid}", "Best Track_0000fee0.gpx" },
{"{time}_{name}", "2020-02-02T02_02_02Z_Best Track.gpx" },
{ApplicationProvider.getApplicationContext().getString(R.string.export_filename_format_default), "2020-02-02T02_02_02Z_Best Track.gpx" },
});
}
private final TrackFilenameGenerator subject;
private final String expected;
public TrackFilenameGeneratorTest(String template, String expected) {
this.subject = new TrackFilenameGenerator(template);
this.expected = expected;
}
@Test
public void testFilenameTemplate() {
// given
Track track = new Track();
track.setName("Best Track");
track.setUuid(UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"));
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
// when
String filename = subject.format(track, TrackFileFormat.GPX);
// then
assertEquals(expected, filename);
}
}
@@ -0,0 +1,44 @@
package de.dennisguse.opentracks.io.file;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Track;
//TODO Merge with TrackFilenameGeneratorTest whenever Junit5 gets available.
//https://github.com/android/android-test/issues/224
@RunWith(Parameterized.class)
public class TrackFilenameGeneratorTest2 {
@Parameterized.Parameters
public static Collection<String> data() {
return Arrays.asList(
"{name}_{starime}",
"{name",
"name}");
}
private final TrackFilenameGenerator subject;
public TrackFilenameGeneratorTest2(String template) {
this.subject = new TrackFilenameGenerator(template);
}
@Test(expected = TrackFilenameGenerator.TemplateInvalidException.class)
public void testFilenameTemplate() {
// given
Track track = new Track();
track.setName("Best Track");
track.setUuid(UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"));
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
// when
String filename = subject.format(track, TrackFileFormat.GPX);
}
}
@@ -9,6 +9,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import androidx.annotation.NonNull;
@@ -16,28 +17,36 @@ import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TimezoneRule;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Cadence;
@@ -67,12 +76,18 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(AndroidJUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
//For csv_export_only() as we the timezone is hardcoded in the expectation.
@Rule
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
@@ -132,7 +147,7 @@ public class ExportImportTest {
Distance sensorDistance = Distance.of(10); // recording distance interval
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 15, 10, 1);
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
// A sensor-only TrackPoint
@@ -143,7 +158,7 @@ public class ExportImportTest {
trackPointCreator.setClock("2020-02-02T02:02:05Z");
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 13, 15, 10, 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:06Z");
@@ -153,11 +168,11 @@ public class ExportImportTest {
trackPointCreator.setClock("2020-02-02T02:02:20Z");
service.resumeCurrentTrack();
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:02:24Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
@@ -172,7 +187,7 @@ public class ExportImportTest {
track = contentProviderUtils.getTrack(trackId);
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
markers = contentProviderUtils.getMarkers(trackId);
assertEquals(11, trackPoints.size());
assertEquals(10, trackPoints.size());
assertEquals(2, markers.size());
}
@@ -206,8 +221,7 @@ public class ExportImportTest {
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. trackpoints
TrackPointAssert a = new TrackPointAssert()
.noAccuracy();
TrackPointAssert a = new TrackPointAssert();
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
a.assertEquals(trackPoints, actual);
@@ -320,8 +334,7 @@ public class ExportImportTest {
// Therefore, the track segmentation is changes.
TrackPointAssert a = new TrackPointAssert()
.setDelta(0.05)
.noAccuracy(); // speed is not fully
.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, Instant.parse("2020-02-02T02:02:03Z"))
@@ -330,7 +343,8 @@ public class ExportImportTest {
.setAltitude(10)
.setSpeed(Speed.of(15))
.setAltitudeLoss(1f)
.setAltitudeGain(1f),
.setAltitudeGain(1f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
.setLatitude(3)
.setLongitude(14.001)
@@ -341,21 +355,24 @@ public class ExportImportTest {
.setSensorDistance(Distance.of(12))
.setHeartRate(66f)
.setPower(50f)
.setCadence(3f),
.setCadence(3f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z"))
.setLatitude(3)
.setLongitude(14.002)
.setAltitude(10)
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setSpeed(Speed.of(15)),
.setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:22Z"))
.setLatitude(3)
.setLongitude(16)
.setAltitude(10)
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setSpeed(Speed.of(15)),
.setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:23Z"))
.setLatitude(3)
.setLongitude(16.001)
@@ -363,6 +380,7 @@ public class ExportImportTest {
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10))
), actual);
// 3. trackstatistics
@@ -423,6 +441,37 @@ public class ExportImportTest {
assertNull(trackImported);
}
@Ignore(value = "TODO Fails on CI; works on API24 and API30 locally")
@LargeTest
@Test
public void csv_export_only() throws TimeoutException, IOException {
setUp();
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.CSV.createTrackExporter(context);
// when
// 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// then
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.csv_export);
String expectedText = new BufferedReader(new InputStreamReader(expected, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
InputStream actual = context.getContentResolver().openInputStream(tmpFileUri);
String actualText = new BufferedReader(new InputStreamReader(actual, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
assertEquals(expectedText, actualText);
}
private void assertMarkers() {
assertEquals(markers.size(), contentProviderUtils.getMarkers(importTrackId).size());
@@ -469,7 +518,7 @@ public class ExportImportTest {
return sensorDataSet;
}
});
trackPointCreator.onChange(null);
trackPointCreator.onChange(new SensorDataSet());
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
@@ -478,11 +527,14 @@ public class ExportImportTest {
altitudeSumManager.setAltitudeLoss_m(altitudeGain);
}
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain) {
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, float altitude, float altitudeGain) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
location.setVerticalAccuracyMeters(verticalAccuracy);
}
location.setSpeed(speed);
location.setAltitude(altitude);
@@ -8,8 +8,6 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
public class TrackPointAssert {
private boolean assertAccuracy = true;
private double delta = 0.001;
public TrackPointAssert() {
@@ -45,13 +43,13 @@ public class TrackPointAssert {
Assert.assertEquals(expected.getSpeed().toMPS(), actual.getSpeed().toMPS(), delta);
}
if (assertAccuracy) {
Assert.assertEquals(expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
if (expected.hasHorizontalAccuracy()) {
Assert.assertEquals(expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
}
} else {
Assert.assertFalse(actual.hasHorizontalAccuracy());
Assert.assertEquals(expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
if (expected.hasHorizontalAccuracy()) {
Assert.assertEquals(expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
}
Assert.assertEquals(expected.hasVerticalAccuracy(), actual.hasVerticalAccuracy());
if (expected.hasVerticalAccuracy()) {
Assert.assertEquals(expected.getVerticalAccuracy().toM(), actual.getVerticalAccuracy().toM(), delta);
}
Assert.assertEquals(expected.hasSensorDistance(), actual.hasSensorDistance());
@@ -96,11 +94,6 @@ public class TrackPointAssert {
}
}
public TrackPointAssert noAccuracy() {
this.assertAccuracy = false;
return this;
}
public TrackPointAssert setDelta(double delta) {
this.delta = delta;
return this;
@@ -19,6 +19,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(MockitoJUnitRunner.class)
@@ -48,7 +49,7 @@ public class TrackRecordingServiceNotificationManagerTest {
.thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
subject.setMetricUnits(true);
subject.setUnitSystem(UnitSystem.METRIC);
// when
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
@@ -612,6 +612,55 @@ public class TrackRecordingServiceTestRecording {
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_movement_non_idle() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// when - will be ignored
String gps2 = "2020-02-02T02:02:04Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 15);
// when
String gps3 = "2020-02-02T02:02:05Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0001, 35.0, 1, 15);
// when
String stopTime = "2020-02-02T02:02:12Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setSpeed(Speed.of(15)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setSpeed(Speed.of(15)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
@@ -796,15 +845,12 @@ public class TrackRecordingServiceTestRecording {
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor5)) //TODO No need to store this TrackPoint, data could be merged into the next one
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(4)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45.001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(0)),
.setSensorDistance(Distance.of(4.0)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps4))
.setLatitude(45.001)
.setLongitude(35)
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -54,7 +55,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
@@ -76,7 +77,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
@@ -92,7 +93,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement);
@@ -114,7 +115,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, lastInterval, null).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
@@ -130,7 +131,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, null, null).toString();
// then
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Speed 11.4 miles per hour.", announcement);
@@ -152,7 +153,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, lastInterval, null).toString();
// then
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
@@ -168,7 +169,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, null, null).toString();
// then
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
@@ -190,7 +191,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, lastInterval, null).toString();
// then
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
@@ -215,7 +216,7 @@ public class VoiceAnnouncementUtilsTest {
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, sensorStatistics).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
@@ -22,7 +22,6 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(MockitoJUnitRunner.class)
@@ -62,7 +61,7 @@ public class GPSHandlerTest {
locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis()));
// then
verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class));
verify(trackPointCreator, times(1)).onChange(any(Location.class));
}
/**
@@ -78,7 +77,7 @@ public class GPSHandlerTest {
locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis()));
// then
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class));
verify(trackPointCreator, times(0)).onChange(any(Location.class));
}
/**
@@ -94,7 +93,7 @@ public class GPSHandlerTest {
// then
// no newTrackPoint called
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class));
verify(trackPointCreator, times(0)).onChange(any(Location.class));
}
@Test
@@ -107,7 +106,7 @@ public class GPSHandlerTest {
locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
// then
verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class));
verify(trackPointCreator, times(1)).onChange(any(Location.class));
}
/**
@@ -47,8 +47,6 @@ import java.util.TimeZone;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
/**
* Tests for {@link StringUtils}.
@@ -91,21 +89,6 @@ public class StringUtilsTest {
assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(360000000)));
}
/**
* Tests {@link StringUtils#formatDistance(android.content.Context, Distance, boolean)}.
*/
@Test
public void testFormatDistance() {
// A large number in metric
assertEquals("5.00 km", StringUtils.formatDistance(context, Distance.of(5000), true));
// A large number in imperial
assertEquals("3.11 mi", StringUtils.formatDistance(context, Distance.of(5000), false));
// A small number in metric
assertEquals("100.00 m", StringUtils.formatDistance(context, Distance.of(100), true));
// A small number in imperial
assertEquals("328.08 ft", StringUtils.formatDistance(context, Distance.of(100), false));
}
/**
* Tests {@link StringUtils#formatCData(String)}.
*/
@@ -183,27 +166,6 @@ public class StringUtilsTest {
assertEquals("1.0", StringUtils.formatDecimal(0.99, 1));
}
@Test
public void testGetSpeedParts() {
assertEquals("4:59", StringUtils.getSpeedParts(context, Speed.of(3.34), true, false).first);
assertEquals("5:00", StringUtils.getSpeedParts(context, Speed.of(3.33), true, false).first);
assertEquals("11.9", StringUtils.getSpeedParts(context, Speed.of(3.31), true, true).first);
assertEquals("7.5", StringUtils.getSpeedParts(context, Speed.of(3.34), false, true).first);
assertEquals("min/km", StringUtils.getSpeedParts(context, Speed.zero(), true, false).second);
assertEquals("min/mi", StringUtils.getSpeedParts(context, Speed.zero(), false, false).second);
}
@Test
public void testFormatSpeed() {
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, Speed.of(3.34), true, false));
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, Speed.of(3.34), false, false));
assertEquals("12.0 km/h", StringUtils.formatSpeed(context, Speed.of(3.34), true, true));
assertEquals("7.5 mph", StringUtils.formatSpeed(context, Speed.of(3.34), false, true));
}
@Test
public void testFormatDateTodayRelative() {
// given
@@ -23,6 +23,7 @@ import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
@@ -61,7 +62,7 @@ public class StatisticDataBuilderTest extends TestCase {
layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), false, true, false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true);
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
// then
assertEquals(statisticDataList.size(), 3);
@@ -100,7 +101,7 @@ public class StatisticDataBuilderTest extends TestCase {
layout.addField(context.getString(R.string.stats_custom_layout_power_key), context.getString(R.string.stats_sensors_power), true, true, false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true);
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
// then
assertEquals(statisticDataList.size(), 18);
+11
View File
@@ -0,0 +1,11 @@
#time,trackpoint_type,latitude,longitude,altitude,accuracy_horizontal,accuracy_vertical,speed,altitude_gain,altitude_loss,sensor_distance,heartrate,cadence,power
"2020-02-02T03:02:02+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
"2020-02-02T03:02:03+01:00","TRACKPOINT",3,3,10,10,,54,1,1,,,,
"2020-02-02T03:02:04+01:00","SENSORPOINT",,,,,,54,1,1,10,66,3,50
"2020-02-02T03:02:05+01:00","TRACKPOINT",3,3,10,10,,18,0,0,2,66,3,50
"2020-02-02T03:02:06+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
"2020-02-02T03:02:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
"2020-02-02T03:02:21+01:00","TRACKPOINT",3,3,10,10,,54,0,0,,,,
"2020-02-02T03:02:22+01:00","SEGMENT_START_AUTOMATIC",3,3,10,10,,54,0,0,,,,
"2020-02-02T03:02:23+01:00","TRACKPOINT",3,3,10,10,,54,0,0,,,,
"2020-02-02T03:02:24+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
1 #time trackpoint_type latitude longitude altitude accuracy_horizontal accuracy_vertical speed altitude_gain altitude_loss sensor_distance heartrate cadence power
2 2020-02-02T03:02:02+01:00 SEGMENT_START_MANUAL
3 2020-02-02T03:02:03+01:00 TRACKPOINT 3 3 10 10 54 1 1
4 2020-02-02T03:02:04+01:00 SENSORPOINT 54 1 1 10 66 3 50
5 2020-02-02T03:02:05+01:00 TRACKPOINT 3 3 10 10 18 0 0 2 66 3 50
6 2020-02-02T03:02:06+01:00 SEGMENT_END_MANUAL
7 2020-02-02T03:02:20+01:00 SEGMENT_START_MANUAL
8 2020-02-02T03:02:21+01:00 TRACKPOINT 3 3 10 10 54 0 0
9 2020-02-02T03:02:22+01:00 SEGMENT_START_AUTOMATIC 3 3 10 10 54 0 0
10 2020-02-02T03:02:23+01:00 TRACKPOINT 3 3 10 10 54 0 0
11 2020-02-02T03:02:24+01:00 SEGMENT_END_MANUAL
@@ -4,8 +4,6 @@ import android.app.Application;
import android.os.StrictMode;
import android.util.Log;
import java.util.Locale;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
@@ -28,9 +26,7 @@ public class Startup extends Application {
PreferencesUtils.initPreferences(this, getResources());
// Set default values of preferences on first start.
PreferencesUtils.resetPreferences(this, false);
if (PreferencesUtils.getString( R.string.stats_units_key, "").equals("")) {
PreferencesUtils.setMetricUnits(!Locale.US.equals(Locale.getDefault()));
}
PreferencesUtils.applyDefaultUnit();
PreferencesUtils.applyNightMode();
@@ -49,6 +49,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListBinding;
@@ -58,6 +59,7 @@ import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.share.ShareUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.AggregatedStatisticsActivity;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
@@ -87,7 +89,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack();
// Preferences
private boolean metricUnits = true;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private GpsStatusValue gpsStatusValue = TrackRecordingService.STATUS_GPS_DEFAULT;
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
@@ -111,7 +113,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits();
unitSystem = PreferencesUtils.getUnitSystem();
}
if (key != null) {
runOnUiThread(() -> {
@@ -190,7 +192,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, Distance.of(cursor.getDouble(totalDistanceIndex)), metricUnits);
String totalDistance = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(TrackListActivity.this).formatDistance(Distance.of(cursor.getDouble(totalDistanceIndex)));
int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex);
int startTimeOffset = cursor.getInt(startTimeOffsetIndex);
@@ -39,6 +39,7 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
@@ -77,9 +78,9 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits();
if (metricUnits != viewBinding.chartView.getMetricUnits()) {
viewBinding.chartView.setMetricUnits(metricUnits);
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
if (unitSystem != viewBinding.chartView.getUnitSystem()) {
viewBinding.chartView.setUnitSystem(unitSystem);
runOnUiThread(() -> {
if (isResumed()) {
viewBinding.chartView.requestLayout();
@@ -189,7 +190,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
if (isResumed()) {
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getMetricUnits());
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getUnitSystem());
pendingPoints.add(point);
}
}
@@ -8,6 +8,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
public class ChartPoint {
@@ -28,20 +29,20 @@ public class ChartPoint {
this.altitude = altitude;
}
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, boolean metricUnits) {
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, UnitSystem unitSystem) {
if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
if (smoothedAltitude != null) {
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(metricUnits);
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(unitSystem);
}
if (smoothedSpeed != null) {
speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
speed = smoothedSpeed.to(unitSystem);
pace = smoothedSpeed.toPace(unitSystem).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
}
if (trackPoint.hasHeartRate()) {
heartRate = (double) trackPoint.getHeartRate().getBPM();
@@ -27,6 +27,7 @@ import androidx.annotation.NonNull;
import java.text.NumberFormat;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
/**
@@ -217,11 +218,15 @@ abstract class ChartValueSeries {
return maxMarkerValue;
}
/**
* Gets the title id.
*/
int getTitleId(boolean metricUnits) {
return metricUnits ? metricTitleId : imperialTitleId;
int getTitleId(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return metricTitleId;
case IMPERIAL:
return imperialTitleId;
default:
throw new RuntimeException("Not implemented");
}
}
/**
@@ -46,6 +46,7 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
@@ -116,7 +117,7 @@ public class ChartView extends View {
private int effectiveHeight = 0;
private boolean chartByDistance = false;
private boolean metricUnits = true;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean reportSpeed = true;
private boolean showPointer = false;
@@ -371,17 +372,12 @@ public class ChartView extends View {
this.chartByDistance = chartByDistance;
}
public boolean getMetricUnits() {
return metricUnits;
public UnitSystem getUnitSystem() {
return unitSystem;
}
/**
* Sets metric units.
*
* @param value true to use metric units
*/
public void setMetricUnits(boolean value) {
metricUnits = value;
public void setUnitSystem(UnitSystem value) {
unitSystem = value;
}
public boolean getReportSpeed() {
@@ -677,7 +673,7 @@ public class ChartView extends View {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
count++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
Paint paint = chartValueSeries.getTitlePaint();
int x = (int) (0.5 * width) + getScrollX();
int y = topBorder - spacer - (lines - count) * (lineHeight + spacer);
@@ -696,7 +692,7 @@ public class ChartView extends View {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
lines++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
Rect rect = getRect(chartValueSeries.getTitlePaint(), title);
if (rect.height() > lineHeight) {
lineHeight = rect.height();
@@ -731,7 +727,14 @@ public class ChartView extends View {
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
return metricUnits ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile);
switch (unitSystem) {
case METRIC:
return context.getString(R.string.unit_kilometer);
case IMPERIAL:
return context.getString(R.string.unit_mile);
default:
throw new RuntimeException("Not implemented");
}
} else {
return context.getString(R.string.description_time);
}
@@ -986,7 +989,7 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) {
if (chartByDistance) {
return marker.getLength().toKM_Miles(metricUnits);
return marker.getLength().toKM_Miles(unitSystem);
} else {
return marker.getDuration().toMillis();
}
@@ -15,6 +15,7 @@ class CachedTrackPointsIndexes {
final int timeIndex;
final int altitudeIndex;
final int accuracyIndex;
final int accuracyVerticalIndex;
final int speedIndex;
final int bearingIndex;
final int sensorHeartRateIndex;
@@ -32,6 +33,7 @@ class CachedTrackPointsIndexes {
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE);
accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.HORIZONTAL_ACCURACY);
accuracyVerticalIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.VERTICAL_ACCURACY);
speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
@@ -573,6 +573,9 @@ public class ContentProviderUtils {
if (!cursor.isNull(indexes.accuracyIndex)) {
trackPoint.setHorizontalAccuracy(Distance.of(cursor.getFloat(indexes.accuracyIndex)));
}
if (!cursor.isNull(indexes.accuracyVerticalIndex)) {
trackPoint.setVerticalAccuracy(Distance.of(cursor.getFloat(indexes.accuracyVerticalIndex)));
}
if (!cursor.isNull(indexes.speedIndex)) {
trackPoint.setSpeed(Speed.of(cursor.getFloat(indexes.speedIndex)));
}
@@ -28,7 +28,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 33;
private static final int DATABASE_VERSION = 34;
public CustomSQLiteOpenHelper(Context context) {
this(context, ((Startup) context.getApplicationContext()).getDatabaseName());
@@ -91,6 +91,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 33:
upgradeFrom32to33(db);
break;
case 34:
upgradeFrom33to34(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -133,6 +136,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 32:
downgradeFrom33to32(db);
break;
case 33:
downgradeFrom34to33(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
@@ -206,7 +212,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.beginTransaction();
db.execSQL("ALTER TABLE tracks ADD COLUMN uuid BLOB");
try (Cursor cursor = db.query("tracks", new String[]{"_id"}, null, null, null, null, null)) {
try (Cursor cursor = db.query("tracks", new String[]{"_id" }, null, null, null, null, null)) {
if (cursor.moveToFirst()) {
int trackIdIndex = cursor.getColumnIndexOrThrow("_id");
do {
@@ -396,7 +402,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.beginTransaction();
// TrackPoints
//TODO That does not restore deleted trackpoints
// Info: this does not restore deleted trackpoints
db.execSQL("UPDATE trackpoints SET latitude = 200 * 1E6, longitude = NULL WHERE type = -2");
db.execSQL("UPDATE trackpoints SET latitude = 100 * 1E6, longitude = NULL WHERE type = 1");
@@ -481,7 +487,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
ZoneRules zoneRules = ZoneOffset.systemDefault().getRules();
try (Cursor cursor = db.query("tracks", new String[]{"_id", "starttime"}, null, null, null, null, null)) {
try (Cursor cursor = db.query("tracks", new String[]{"_id", "starttime" }, null, null, null, null, null)) {
if (cursor.moveToFirst()) {
int trackIdIndex = cursor.getColumnIndexOrThrow("_id");
int startTimeIndex = cursor.getColumnIndexOrThrow("starttime");
@@ -513,6 +519,33 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.execSQL("CREATE UNIQUE INDEX tracks_uuid_index ON tracks(uuid)");
db.setTransactionSuccessful();
db.endTransaction();
}
/**
* Add accuracy_vertical
*/
private void upgradeFrom33to34(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints ADD COLUMN accuracy_vertical FLOAT");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom34to33(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)), sensor_distance 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 FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
@@ -53,8 +53,7 @@ public class ShareContentProvider extends CustomContentProvider {
private static final int URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA = 6;
private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7;
private static final int URI_SHARE_PICTURE = 8;
private static final int URI_CSV = 8;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final String TRACKID_DELIMITER = "_";
@@ -66,6 +65,7 @@ public class ShareContentProvider extends CustomContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.CSV.getName() + "/*/*", URI_CSV);
}
/**
@@ -146,6 +146,9 @@ public class ShareContentProvider extends CustomContentProvider {
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
case URI_CSV:
return TrackFileFormat.CSV;
default:
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
}
@@ -1,9 +1,12 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import de.dennisguse.opentracks.settings.UnitSystem;
public class Distance {
public static Distance of(double distance_m) {
@@ -14,6 +17,14 @@ public class Distance {
return of(Float.parseFloat(distance_m));
}
@Nullable
public static Distance ofOrNull(Double distance_m) {
if (distance_m == null) {
return null;
}
return of(distance_m);
}
public static Distance ofMile(double distance_mile) {
return of(distance_mile * UnitConversions.MI_TO_M);
}
@@ -34,11 +45,14 @@ public class Distance {
return of(0.1 * distance_dm);
}
public static Distance one(boolean metricUnit) {
if (metricUnit) {
return Distance.ofKilometer(1);
} else {
return Distance.ofMile(1);
public static Distance one(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return Distance.ofKilometer(1);
case IMPERIAL:
return Distance.ofMile(1);
default:
throw new RuntimeException("Not implemented");
}
}
@@ -100,12 +114,26 @@ public class Distance {
return toKM() * UnitConversions.KM_TO_MI;
}
public double toKM_Miles(boolean metricUnit) {
return metricUnit ? toKM() : toMI();
public double toKM_Miles(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toKM();
case IMPERIAL:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
}
public double toM_FT(boolean metricUnit) {
return metricUnit ? toM() : toFT();
public double toM_FT(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toM();
case IMPERIAL:
return toFT();
default:
throw new RuntimeException("Not implemented");
}
}
@Override
@@ -0,0 +1,105 @@
package de.dennisguse.opentracks.data.models;
import android.content.Context;
import android.content.res.Resources;
import android.util.Pair;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
public class DistanceFormatter {
private final Resources resources;
private final int decimalCount;
private final UnitSystem unitSystem;
private DistanceFormatter(Resources resources, int decimalCount, UnitSystem unitSystem) {
this.resources = resources;
this.decimalCount = decimalCount;
this.unitSystem = unitSystem;
assert unitSystem != null;
}
public String formatDistance(Distance distance) {
if (distance.isInvalid()) {
return resources.getString(R.string.value_unknown);
}
Pair<String, String> distanceParts = getDistanceParts(distance);
return resources.getString(R.string.distance_with_unit, distanceParts.first, distanceParts.second);
}
/**
* Get the formatted distance with unit.
*
* @return the formatted distance (or null) and it's unit as {@link Pair}
*/
public Pair<String, String> getDistanceParts(Distance distance) {
if (distance.isInvalid()) {
switch (unitSystem) {
case METRIC:
return new Pair<>(null, resources.getString(R.string.unit_meter));
case IMPERIAL:
return new Pair<>(null, resources.getString(R.string.unit_feet));
default:
throw new RuntimeException("Not implemented");
}
}
switch (unitSystem) {
case METRIC:
if (distance.greaterThan(Distance.of(500))) {
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
}
case IMPERIAL:
if (distance.greaterThan(Distance.ofMile(0.5))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
default:
throw new RuntimeException("Not implemented");
}
}
public static Builder Builder() {
return new Builder();
}
public static class Builder {
private int decimalCount;
private UnitSystem unitSystem;
public Builder() {
decimalCount = 2;
}
public Builder setDecimalCount(int decimalCount) {
this.decimalCount = decimalCount;
return this;
}
public Builder setUnit(@Nullable UnitSystem unitSystem) {
this.unitSystem = unitSystem;
return this;
}
public DistanceFormatter build(Resources resource) {
return new DistanceFormatter(resource, decimalCount, unitSystem);
}
public DistanceFormatter build(Context context) {
return build(context.getResources());
}
}
}
@@ -6,6 +6,7 @@ import java.time.Duration;
import java.util.Objects;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
public class Speed {
@@ -92,35 +93,37 @@ public class Speed {
return toKMH() * UnitConversions.KM_TO_MI;
}
public Duration toPace(boolean metricUnit) {
public Duration toPace(UnitSystem unitSystem) {
if (isZero()) {
return Duration.ofSeconds(0);
}
double distance = speed_mps * (metricUnit ? UnitConversions.M_TO_KM : UnitConversions.M_TO_MI);
double distance = speed_mps;
switch (unitSystem) { //TODO Can we use to(UnitSystem)?
case METRIC:
distance *= UnitConversions.M_TO_KM;
break;
case IMPERIAL:
distance *= UnitConversions.M_TO_MI;
break;
default:
throw new RuntimeException("Not implemented");
}
return Duration.ofSeconds(Math.round(1 / distance));
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KMH : Unit.MPH);
}
public double to(Unit unit) {
switch (unit) {
case KMH:
public double to(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toKMH();
case MPH:
case IMPERIAL:
return toMPH();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KMH,
MPH,
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -0,0 +1,111 @@
package de.dennisguse.opentracks.data.models;
import android.content.Context;
import android.content.res.Resources;
import android.util.Pair;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
public class SpeedFormatter {
private final Resources resources;
private final int decimalCount;
private final UnitSystem unitSystem;
private final boolean reportSpeedOrPace;
private SpeedFormatter(Resources resources, int decimalCount, UnitSystem unitSystem, boolean reportSpeedOrPace) {
this.resources = resources;
this.decimalCount = decimalCount;
this.unitSystem = unitSystem;
this.reportSpeedOrPace = reportSpeedOrPace;
}
public String formatSpeed(Speed speed) {
Pair<String, String> distanceParts = getSpeedParts(speed);
return resources.getString(R.string.speed_with_unit, distanceParts.first, distanceParts.second);
}
/**
* Gets the formatted speed with unit.
*
* @return the formatted speed (or null) and it's unit as {@link Pair}
*/
public Pair<String, String> getSpeedParts(Speed speed) {
int unitId;
switch (unitSystem) {
case METRIC:
unitId = reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
break;
case IMPERIAL:
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
break;
default:
throw new RuntimeException("Not implemented");
}
String unitString = resources.getString(unitId);
if (speed == null) {
speed = Speed.zero();
}
if (reportSpeedOrPace) {
return new Pair<>(StringUtils.formatDecimal(speed.to(unitSystem), 1), unitString);
}
int pace = (int) speed.toPace(unitSystem).getSeconds();
int minutes = pace / 60;
int seconds = pace % 60;
return new Pair<>(resources.getString(R.string.time, minutes, seconds), unitString);
}
public static Builder Builder() {
return new Builder();
}
public static class Builder {
private int decimalCount;
private UnitSystem unitSystem;
private boolean reportSpeedOrPace;
public Builder() {
decimalCount = 2;
reportSpeedOrPace = true;
}
public Builder setDecimalCount(int decimalCount) {
this.decimalCount = decimalCount;
return this;
}
public Builder setUnit(@NonNull UnitSystem unitSystem) {
this.unitSystem = unitSystem;
return this;
}
public Builder setReportSpeedOrPace(boolean reportSpeedOrPace) {
this.reportSpeedOrPace = reportSpeedOrPace;
return this;
}
public SpeedFormatter build(Resources resource) {
return new SpeedFormatter(resource, decimalCount, unitSystem, reportSpeedOrPace);
}
public SpeedFormatter build(Context context) {
return build(context.getResources());
}
}
}
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.data.models;
import android.location.Location;
import android.os.Build;
import android.os.Parcel;
import androidx.annotation.NonNull;
@@ -44,6 +45,7 @@ public class TrackPoint {
private Double latitude;
private Double longitude;
private Distance horizontalAccuracy;
private Distance verticalAccuracy;
private Altitude altitude;
private Speed speed;
private Float bearing;
@@ -205,6 +207,9 @@ public class TrackPoint {
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.verticalAccuracy = location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null;
}
//TODO Should we copy the bearing?
return this;
@@ -309,6 +314,19 @@ public class TrackPoint {
return this;
}
public boolean hasVerticalAccuracy() {
return verticalAccuracy != null;
}
public Distance getVerticalAccuracy() {
return verticalAccuracy;
}
public TrackPoint setVerticalAccuracy(Distance horizontalAccuracy) {
this.verticalAccuracy = horizontalAccuracy;
return this;
}
@NonNull
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
@@ -46,6 +46,7 @@ public interface TrackPointsColumns extends BaseColumns {
String TIME = "time";
String ALTITUDE = "elevation";
String HORIZONTAL_ACCURACY = "accuracy";
String VERTICAL_ACCURACY = "accuracy_vertical";
String SPEED = "speed";
String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate";
@@ -79,6 +80,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ ALTITUDE_LOSS + " FLOAT, "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1, 2)), "
+ SENSOR_DISTANCE + " FLOAT, "
+ VERTICAL_ACCURACY + " FLOAT, "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
+ ")";
@@ -37,9 +37,12 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.adapters.StatisticsAdapter;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -79,7 +82,7 @@ public class StatisticsRecordedFragment extends Fragment {
private StatisticsRecordedBinding viewBinding;
private boolean preferenceMetricUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean preferenceReportSpeed;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
@@ -87,7 +90,7 @@ public class StatisticsRecordedFragment extends Fragment {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits();
unitSystem = PreferencesUtils.getUnitSystem();
}
if (PreferencesUtils.isKey(R.string.stats_rate_key, key) && track != null) {
@@ -186,7 +189,9 @@ public class StatisticsRecordedFragment extends Fragment {
TrackStatistics trackStatistics = track.getTrackStatistics();
// Set total distance
{
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), trackStatistics.getTotalDistance(), preferenceMetricUnits);
Pair<String, String> parts = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(getContext()).getDistanceParts(trackStatistics.getTotalDistance());
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -204,11 +209,12 @@ public class StatisticsRecordedFragment extends Fragment {
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
}
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(preferenceReportSpeed).build(getContext());
// Set average speed/pace
{
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageSpeed(), preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.getAverageSpeed());
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
@@ -217,7 +223,7 @@ public class StatisticsRecordedFragment extends Fragment {
{
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getMaxSpeed(), preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.getMaxSpeed());
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
@@ -226,7 +232,7 @@ public class StatisticsRecordedFragment extends Fragment {
{
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageMovingSpeed(), preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.getAverageMovingSpeed());
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
@@ -238,11 +244,11 @@ public class StatisticsRecordedFragment extends Fragment {
Pair<String, String> parts;
parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeGain_m, preferenceMetricUnits);
parts = StringUtils.getAltitudeParts(getContext(), altitudeGain_m, unitSystem);
viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second);
parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeLoss_m, preferenceMetricUnits);
parts = StringUtils.getAltitudeParts(getContext(), altitudeLoss_m, unitSystem);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.viewmodels.StatisticData;
import de.dennisguse.opentracks.viewmodels.StatisticsDataModel;
@@ -55,14 +56,14 @@ public class StatisticsRecordingFragment extends Fragment {
private StatisticsDataModel viewModel;
private LiveData<List<StatisticData>> statisticsLiveData;
private boolean preferenceMetricUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits();
unitSystem = PreferencesUtils.getUnitSystem();
}
if (PreferencesUtils.isKey(R.string.stats_custom_layouts_key, key) || PreferencesUtils.isKey(R.string.stats_custom_layout_selected_layout_key, key)) {
@@ -157,7 +158,7 @@ public class StatisticsRecordingFragment extends Fragment {
private void updateUI() {
if (isResumed()) {
viewModel.update(recordingData, layout, preferenceMetricUnits);
viewModel.update(recordingData, layout, unitSystem);
}
}
@@ -6,6 +6,7 @@ import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.CSVTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
@@ -100,6 +101,23 @@ public enum TrackFileFormat {
public String getExtension() {
return "gpx";
}
},
CSV {
@Override
public TrackExporter createTrackExporter(Context context) {
return new CSVTrackExporter(new ContentProviderUtils(context));
}
@Override
public String getMimeType() {
return "text/csv";
}
@Override
public String getExtension() {
return "csv";
}
};
private static final String MIME_KMZ = "application/vnd.google-earth.kmz";
@@ -0,0 +1,105 @@
package de.dennisguse.opentracks.io.file;
import androidx.annotation.NonNull;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.util.FileUtils;
public class TrackFilenameGenerator {
public static final String UUID_KEY = "{uuid}";
public static final String TRACKNAME_KEY = "{name}";
public static final String CATEGORY_KEY = "{category}";
public static final String STARTTIME_KEY = "{time}";
public static String getAllOptions() {
return Stream.of(UUID_KEY, TRACKNAME_KEY, CATEGORY_KEY, STARTTIME_KEY)
.collect(Collectors.joining(", "));
}
private final String template;
public TrackFilenameGenerator(@NonNull String template) {
this.template = template;
}
public String format(@NonNull Track track, @NonNull TrackFileFormat trackFileFormat) {
Map<String, String> values = new HashMap<>();
values.put(UUID_KEY, track.getUuid().toString().substring(0, 8));
values.put(TRACKNAME_KEY, track.getName());
values.put(CATEGORY_KEY, track.getCategory());
values.put(STARTTIME_KEY, track.getStartTime().toString());
return FileUtils.sanitizeFileName(format(template, values)) + "." + trackFileFormat.getExtension();
}
private static String format(String template, Map<String, String> values) {
StringBuilder templateCompiler = new StringBuilder(template);
List<String> valueList = new ArrayList<>();
Matcher keyMatcher = Pattern
.compile("\\{(\\w+)\\}")
.matcher(template);
while (keyMatcher.find()) {
String key = keyMatcher.group();
if (!values.containsKey(key)) {
throw new TemplateInvalidException(key);
}
int index = templateCompiler.indexOf(key);
if (index != -1) {
templateCompiler.replace(index, index + key.length(), "%s");
valueList.add(values.get(key));
}
}
String templateCompiled = templateCompiler.toString();
if (templateCompiled.contains("{") || templateCompiled.contains("}")) {
throw new TemplateInvalidException(template);
}
return String.format(templateCompiled, valueList.toArray());
}
public String getTemplate() {
return template;
}
public boolean isValid() {
try {
getExample();
return !template.isEmpty();
} catch (TemplateInvalidException e) {
return false;
}
}
public String getExample() {
Track track = new Track();
track.setName("Berlin");
track.setUuid(UUID.fromString("fefefefefe-0000-1000-8000-00805f9b34fb"));
track.getTrackStatistics().setStartTime(Instant.ofEpochMilli(0));
return format(track, TrackFileFormat.GPX);
}
public static class TemplateInvalidException extends RuntimeException {
public TemplateInvalidException(String invalidTemplate) {
super(invalidTemplate);
}
}
}
@@ -0,0 +1,203 @@
/*
* Copyright 2011 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.io.file.exporter;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Exports the {@link TrackPoint} into a CSV.
* * decimal separator: .
* * column separator: ,
* <p>
* NOTE:
* * {@link Track} data is not exported.
* * {@link Marker} data is not exported.
*/
public class CSVTrackExporter implements TrackExporter {
private static final String TAG = CSVTrackExporter.class.getSimpleName();
private static final NumberFormat ALTITUDE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat HEARTRATE_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);
static {
ALTITUDE_FORMAT.setMaximumFractionDigits(1);
ALTITUDE_FORMAT.setGroupingUsed(false);
COORDINATE_FORMAT.setMaximumFractionDigits(6);
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
COORDINATE_FORMAT.setGroupingUsed(false);
SPEED_FORMAT.setMaximumFractionDigits(2);
SPEED_FORMAT.setGroupingUsed(false);
HEARTRATE_FORMAT.setMaximumFractionDigits(0);
HEARTRATE_FORMAT.setGroupingUsed(false);
CADENCE_FORMAT.setMaximumFractionDigits(0);
CADENCE_FORMAT.setGroupingUsed(false);
POWER_FORMAT.setMaximumFractionDigits(0);
POWER_FORMAT.setGroupingUsed(false);
}
private final ContentProviderUtils contentProviderUtils;
private PrintWriter printWriter;
public CSVTrackExporter(ContentProviderUtils contentProviderUtils) {
this.contentProviderUtils = contentProviderUtils;
}
@Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
List<Column> columns = Collections.unmodifiableList(Arrays.asList(
new Column("time", null),
new Column("trackpoint_type", t -> quote(t.getType().name())),
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
new Column("altitude", t -> t.hasAltitude() ? COORDINATE_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("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""),
new Column("altitude_loss", t -> t.hasAltitudeLoss() ? DISTANCE_FORMAT.format(t.getAltitudeLoss()) : ""),
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() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : "")
));
try {
prepare(outputStream);
boolean headerWritten = false;
for (Track track : tracks) {
columns.get(0).extractor = t -> quote(StringUtils.formatDateTimeIso8601(t.getTime(), track.getZoneOffset()));
if (!headerWritten) {
writeHeader(columns);
headerWritten = true;
}
writeTrackPoints(columns, track);
}
close();
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Thread interrupted", e);
return false;
}
}
private void writeTrackPoints(List<Column> columns, Track track) throws InterruptedException {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) throw new InterruptedException();
TrackPoint trackPoint = trackPointIterator.next();
switch (trackPoint.getType()) {
case SEGMENT_START_MANUAL:
case SEGMENT_END_MANUAL:
case SEGMENT_START_AUTOMATIC:
case SENSORPOINT:
case TRACKPOINT:
writeTrackPoint(columns, trackPoint);
break;
default:
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
}
}
}
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
public void writeHeader(List<Column> columns) {
if (printWriter != null) {
String columnNames = columns.stream().map(c -> c.columnName)
.reduce((s, s2) -> s + "," + s2)
.orElseThrow(() -> new RuntimeException("No columns defined"));
printWriter.println("#" + columnNames);
}
}
public void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
if (printWriter != null) {
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint))
.reduce((s, s2) -> s + "," + s2)
.orElseThrow(() -> new RuntimeException("No columns defined"));
printWriter.println(columnNames);
}
}
private static class Column {
final String columnName;
Function<TrackPoint, String> extractor;
Column(String columnName, Function<TrackPoint, String> extractor) {
this.columnName = columnName;
this.extractor = extractor;
}
}
private static String quote(String content) {
return '"' + content + '"';
}
}
@@ -34,6 +34,7 @@ import java.util.stream.Stream;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
@@ -128,6 +129,7 @@ public class GPXTrackExporter implements TrackExporter {
private void writeTrackPoints(Track track) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
Distance trackDistance = Distance.of(0);
LinkedList<TrackPoint> sensorPoints = new LinkedList<>();
@@ -159,7 +161,7 @@ public class GPXTrackExporter implements TrackExporter {
writeOpenSegment();
wroteSegment = true;
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
break;
case SENSORPOINT:
@@ -172,7 +174,7 @@ public class GPXTrackExporter implements TrackExporter {
wroteSegment = true;
}
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
break;
default:
@@ -225,6 +227,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
printWriter.println("xmlns:gpxtrkx=\"http://www.garmin.com/xmlschemas/TrackStatsExtension/v1\"");
printWriter.println("xmlns:cluetrust=\"http://www.cluetrust.com/Schemas/\"");
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
printWriter.println("xsi:schemaLocation=" +
"\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
@@ -232,6 +235,7 @@ public class GPXTrackExporter implements TrackExporter {
+ " http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd"
+ " http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd"
+ " http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
+ " http://www.cluetrust.com/Schemas http://www.cluetrust.com/Schemas/gpxdata10.xsd"
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
}
}
@@ -312,7 +316,9 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("</trkseg>");
}
public void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints) {
public Distance writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints, Distance trackDistance) {
Distance cumulativeDistance = null;
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
@@ -352,9 +358,17 @@ public class GPXTrackExporter implements TrackExporter {
trackPointExtensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
}
Double cumulativeDistance = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null);
if (trackPoint.hasHorizontalAccuracy()) {
trackPointExtensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.getHorizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
}
if (trackPoint.hasVerticalAccuracy()) {
trackPointExtensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.getVerticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
}
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null));
if (cumulativeDistance != null) {
trackPointExtensionContent += ("<opentracks:distance>" + ALTITUDE_FORMAT.format(cumulativeDistance) + "</opentracks:distance>\n");
trackPointExtensionContent += ("<opentracks:distance>" + DISTANCE_FORMAT.format(cumulativeDistance.toM()) + "</opentracks:distance>\n");
trackPointExtensionContent += ("<cluetrust:distance>" + DISTANCE_FORMAT.format(trackDistance.plus(cumulativeDistance).toM()) + "</cluetrust:distance>\n");
}
if (!trackPointExtensionContent.isEmpty()) {
@@ -366,6 +380,10 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("</trkpt>");
}
if (cumulativeDistance != null) {
return cumulativeDistance;
}
return Distance.of(0);
}
private Double cumulateSensorData(TrackPoint trackPoint, List<TrackPoint> sensorPoints, Function<TrackPoint, Double> map) {
@@ -65,6 +65,8 @@ public class KMLTrackExporter implements TrackExporter {
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";
public static final String EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL = "accuracy_horizontal";
public static final String EXTENDED_DATA_TYPE_ACCURACY_VERTICAL = "accuracy_vertical";
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
@@ -88,6 +90,8 @@ public class KMLTrackExporter implements TrackExporter {
private final List<Float> heartRateList = new ArrayList<>();
private final List<Float> altitudeGainList = new ArrayList<>();
private final List<Float> altitudeLossList = new ArrayList<>();
private final List<Float> accuracyHorizontal = new ArrayList<>();
private final List<Float> accuracyVertical = new ArrayList<>();
public KMLTrackExporter(Context context, boolean exportPhotos) {
this.context = context;
@@ -333,6 +337,8 @@ public class KMLTrackExporter implements TrackExporter {
heartRateList.clear();
altitudeGainList.clear();
altitudeLossList.clear();
accuracyHorizontal.clear();
accuracyVertical.clear();
}
}
@@ -362,6 +368,12 @@ public class KMLTrackExporter implements TrackExporter {
if (altitudeLossList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(altitudeLossList, EXTENDED_DATA_TYPE_ALTITUDE_LOSS);
}
if (accuracyHorizontal.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(accuracyHorizontal, EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL);
}
if (accuracyVertical.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(accuracyVertical, EXTENDED_DATA_TYPE_ACCURACY_VERTICAL);
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>");
@@ -387,6 +399,8 @@ public class KMLTrackExporter implements TrackExporter {
altitudeGainList.add(trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : null);
altitudeLossList.add(trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : null);
accuracyHorizontal.add(trackPoint.hasHorizontalAccuracy() ? (float) trackPoint.getHorizontalAccuracy().toM() : null);
accuracyVertical.add(trackPoint.hasVerticalAccuracy() ? (float) trackPoint.getVerticalAccuracy().toM() : null);
}
}
@@ -82,7 +82,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private static final String TAG_EXTENSION_GAIN = "opentracks:gain";
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
private static final String TAG_EXTENSION_ACCURACY_HORIZONTAL = "opentracks:accuracy_horizontal";
private static final String TAG_EXTENSION_ACCURACY_VERTICAL = "opentracks:accuracy_vertical";
private Locator locator;
private final Context context;
@@ -106,12 +107,14 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private String heartrate;
private String cadence;
private String power;
protected String markerType;
protected String photoUrl;
protected String uuid;
protected String gain;
protected String loss;
protected String sensorDistance;
private String markerType;
private String photoUrl;
private String uuid;
private String gain;
private String loss;
private String sensorDistance;
private String accuracyHorizontal;
private String accuracyVertical;
private final LinkedList<TrackPoint> currentSegment = new LinkedList<>();
@@ -240,6 +243,16 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorDistance = content.trim();
}
break;
case TAG_EXTENSION_ACCURACY_HORIZONTAL:
if (content != null) {
accuracyHorizontal = content.trim();
}
break;
case TAG_EXTENSION_ACCURACY_VERTICAL:
if (content != null) {
accuracyVertical = content.trim();
}
break;
}
content = "";
@@ -342,6 +355,20 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", sensorDistance)), e);
}
}
if (accuracyHorizontal != null) {
try {
trackPoint.setHorizontalAccuracy(Distance.of(accuracyHorizontal));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse accuracy_horizontal: %s", sensorDistance)), e);
}
}
if (accuracyVertical != null) {
try {
trackPoint.setVerticalAccuracy(Distance.of(accuracyVertical));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse accuracy_vertical: %s", sensorDistance)), e);
}
}
return trackPoint;
}
@@ -357,6 +384,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
loss = null;
sensorDistance = null;
accuracyHorizontal = null;
accuracyVertical = null;
power = null;
heartrate = null;
cadence = null;
@@ -89,6 +89,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
private final ArrayList<Float> sensorPowerList = new ArrayList<>();
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
private final ArrayList<Float> accuracyHorizontal = new ArrayList<>();
private final ArrayList<Float> accuracyVertical = new ArrayList<>();
private final ArrayList<Marker> markers = new ArrayList<>();
@@ -294,6 +296,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorPowerList.clear();
altitudeGainList.clear();
altitudeLossList.clear();
accuracyHorizontal.clear();
accuracyVertical.clear();
}
private void onTrackSegmentEnd() {
@@ -333,6 +337,12 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
if (i < altitudeLossList.size()) {
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
}
if (i < accuracyHorizontal.size() && accuracyHorizontal.get(i) != null) {
trackPoint.setHorizontalAccuracy(Distance.of(accuracyHorizontal.get(i)));
}
if (i < accuracyVertical.size() && accuracyVertical.get(i) != null) {
trackPoint.setVerticalAccuracy(Distance.of(accuracyVertical.get(i)));
}
// Update TrackPoint type for START / STOP.
TrackPoint.Type type = trackPoint.getType();
@@ -430,6 +440,12 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS:
altitudeLossList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL:
accuracyHorizontal.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL:
accuracyVertical.add(value);
break;
default:
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
}
@@ -107,7 +107,6 @@ public abstract class BluetoothConnectionManager<DataType> {
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress());
SensorData<DataType> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
@@ -165,6 +165,11 @@ public class BluetoothUtils {
index = 3;
if (valueLength - index >= 1) {
cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index));
// Hacky workaround as the Wahoo Tickr X provides cadence in SPM (steps per minute) in violation to the standard.
if (sensorName != null && sensorName.startsWith("TICKR X")) {
cadence = Cadence.of(cadence.getRPM() / 2);
}
}
index = 4;
@@ -98,14 +98,14 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
}
void pause(TrackPointCreator trackPointCreator) {
insertTrackPoint(trackPointCreator.createSegmentEnd());
insertTrackPoint(trackPointCreator.createSegmentEnd(), true);
reset();
}
void end(TrackPointCreator trackPointCreator) {
TrackPoint segmentEnd = trackPointCreator.createSegmentEnd();
insertTrackPoint(segmentEnd);
insertTrackPoint(segmentEnd, true);
trackId = null;
trackStatisticsUpdater = null;
@@ -172,12 +172,12 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
// Always insert the first segment location
if (lastStoredTrackPoint == null) {
insertTrackPoint(trackPoint);
insertTrackPoint(trackPoint, true);
return true;
}
if (trackPoint.hasLocation() && lastStoredTrackPointWithLocation == null) {
insertTrackPoint(trackPoint);
insertTrackPoint(trackPoint, true);
return true;
}
@@ -195,19 +195,19 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(trackPoint);
insertTrackPoint(trackPoint, true);
return true;
}
if (distanceToLastStoredTrackPoint.greaterOrEqualThan(recordingDistanceInterval)
&& trackPoint.isMoving()) {
insertTrackPoint(trackPoint);
insertTrackPoint(trackPoint, false);
return true;
}
if (trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) {
// Moving from non-moving to moving or vice versa; required to compute moving time correctly.
insertTrackPoint(trackPoint);
insertTrackPoint(trackPoint, true);
return true;
}
@@ -221,8 +221,8 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
return trackStatisticsUpdater.getTrackStatistics();
}
private void insertTrackPoint(@NonNull TrackPoint trackPoint) {
if (lastTrackPoint != null) {
private void insertTrackPoint(@NonNull TrackPoint trackPoint, boolean storeLastTrackPointIfUseful) {
if (storeLastTrackPointIfUseful && lastTrackPoint != null) {
if (lastStoredTrackPoint != null && lastTrackPoint.getTime().equals(lastStoredTrackPoint.getTime())) {
// Do not insert if inserted already
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid trackId point time.");
@@ -231,8 +231,8 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
// Remove the sensorDistance from trackPoint that is already going be stored with lastTrackPoint.
trackPoint.minusCumulativeSensorData(lastTrackPoint);
}
lastTrackPoint = null;
}
lastTrackPoint = null;
insertTrackPointHelper(trackPoint);
}
@@ -46,7 +46,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private static final String TAG = TrackRecordingService.class.getSimpleName();
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofMillis(100);
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
public static final RecordingStatus STATUS_DEFAULT = RecordingStatus.notRecording();
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
@@ -120,11 +120,12 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
@Override
public void onDestroy() {
trackPointCreator.stop();
trackPointCreator = null;
handler.removeCallbacksAndMessages(null); //Some tests do not finish the recording completely
handler = null;
trackPointCreator.stop();
trackPointCreator = null;
trackRecordingManager.stop();
trackRecordingManager = null;
@@ -312,7 +313,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this));
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this, recordingStatus.getTrackId()));
}
if (!isRecording() && isGpsStarted) {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
@@ -9,19 +9,24 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Manages the content of the notification shown by {@link TrackRecordingService}.
@@ -38,7 +43,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
private boolean previousLocationWasAccurate = true;
private Boolean metricUnits = null;
private UnitSystem unitSystem = null;
TrackRecordingServiceNotificationManager(Context context) {
PreferencesUtils.registerOnSharedPreferenceChangeListener(this);
@@ -79,8 +84,12 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, Distance recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
DistanceFormatter formatter = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context);
if (trackPoint.hasHorizontalAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getHorizontalAccuracy(), metricUnits);
formattedAccuracy = formatter.formatDistance(trackPoint.getHorizontalAccuracy());
boolean currentLocationWasAccurate = trackPoint.getHorizontalAccuracy().lessThan(recordingGpsAccuracy);
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
@@ -88,17 +97,18 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
previousLocationWasAccurate = currentLocationWasAccurate;
}
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, StringUtils.formatDistance(context, trackStatistics.getTotalDistance(), metricUnits)));
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, StringUtils.formatSpeed(context, trackPoint.getSpeed(), metricUnits, true)));
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(trackStatistics.getTotalDistance())));
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(trackPoint.getSpeed());
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
notificationBuilder.setOnlyAlertOnce(true);
}
Notification setRecording(Context context) {
Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class);
Notification setRecording(Context context, @NonNull Track.Id trackId) {
Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
@@ -140,8 +150,8 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
notificationManager.cancel(NOTIFICATION_ID);
}
void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
void setUnitSystem(UnitSystem unitSystem) {
this.unitSystem = unitSystem;
}
private void updateNotification() {
@@ -155,7 +165,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
setMetricUnits(PreferencesUtils.isMetricUnits());
setUnitSystem(PreferencesUtils.getUnitSystem());
}
}
}
@@ -35,6 +35,7 @@ import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -165,12 +166,12 @@ public class VoiceAnnouncement {
}
boolean isMetricUnits = PreferencesUtils.isMetricUnits();
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
boolean isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
//TODO Do not load all trackpoints for every announcement
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits));
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(unitSystem));
intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
@@ -178,7 +179,7 @@ public class VoiceAnnouncement {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), isMetricUnits, isReportSpeed, lastInterval, sensorStatistics);
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics);
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
@@ -16,6 +16,7 @@ import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -25,15 +26,37 @@ class VoiceAnnouncementUtils {
private VoiceAnnouncementUtils() {
}
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
SpannableStringBuilder builder = new SpannableStringBuilder();
Distance distance = trackStatistics.getTotalDistance();
Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
int perUnitStringId = isMetricUnits ? R.string.voice_per_kilometer : R.string.voice_per_mile;
double distanceInUnit = distance.toKM_Miles(isMetricUnits);
int distanceId = isMetricUnits ? R.plurals.voiceDistanceKilometers : R.plurals.voiceDistanceMiles;
int perUnitStringId;
int distanceId;
int speedId;
String unitDistanceTTS;
String unitSpeedTTS;
switch (unitSystem) {
case METRIC:
perUnitStringId = R.string.voice_per_kilometer;
distanceId = R.plurals.voiceDistanceKilometers;
speedId = R.plurals.voiceSpeedKilometersPerHour;
unitDistanceTTS = "kilometer";
unitSpeedTTS = "kilometer per hour";
break;
case IMPERIAL:
perUnitStringId = R.string.voice_per_mile;
distanceId = R.plurals.voiceDistanceMiles;
speedId = R.plurals.voiceSpeedMilesPerHour;
unitDistanceTTS = "mile";
unitSpeedTTS = "mile per hour";
break;
default:
throw new RuntimeException("Not implemented");
}
double distanceInUnit = distance.toKM_Miles(unitSystem);
builder.append(context.getString(R.string.total_distance));
long distanceIntegerPart = (long) distanceInUnit;
@@ -41,7 +64,7 @@ class VoiceAnnouncementUtils {
String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2);
// Units should always be english singular for TTS.
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, isMetricUnits ? "kilometer" : "mile");
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, unitDistanceTTS);
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
if (distance.isZero()) {
@@ -56,19 +79,18 @@ class VoiceAnnouncementUtils {
}
if (isReportSpeed) {
int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
double speedInUnit = distancePerTime.to(isMetricUnits);
double speedInUnit = distancePerTime.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.speed));
long speedIntegerPart = (long) speedInUnit;
// Extract the decimal part
String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, isMetricUnits ? "kilometer per hour" : "mile per hour");
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, unitSpeedTTS);
builder.append(".");
if (currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(isMetricUnits);
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) {
@@ -77,12 +99,12 @@ class VoiceAnnouncementUtils {
long currentDistanceIntegerPart = (long) currentDistancePerTimeInUnit;
// Extract the decimal part
String currentDistanceFractionalPart = String.format("%.1f", (currentDistancePerTimeInUnit - currentDistanceIntegerPart)).substring(2);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, isMetricUnits ? "kilometer per hour" : "mile per hour");
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, unitSpeedTTS);
builder.append(".");
}
}
} else {
Duration time = distancePerTime.toPace(isMetricUnits);
Duration time = distancePerTime.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.pace));
appendDuration(context, builder, time);
@@ -90,7 +112,7 @@ class VoiceAnnouncementUtils {
.append(context.getString(perUnitStringId))
.append(".");
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(isMetricUnits) : Duration.ofMillis(0);
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(unitSystem) : Duration.ofMillis(0);
if (!currentTime.isZero()) {
builder.append(" ")
.append(context.getString(R.string.lap_time));
@@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
@@ -109,25 +110,23 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
return;
}
TrackPoint trackPoint = new TrackPoint(location, trackPointCreator.createNow());
boolean isAccurate = trackPoint.fulfillsAccuracy(thresholdHorizontalAccuracy);
boolean isValid = LocationUtils.isValidLocation(location);
if (gpsStatus != null) {
// Send each update to the status; please note that this TrackPoint is not stored.
TrackPoint trackPoint = new TrackPoint(location, Instant.ofEpochMilli(location.getTime()));
gpsStatus.onLocationChanged(trackPoint);
}
if (!isValid) {
if (!LocationUtils.isValidLocation(location)) {
Log.w(TAG, "Ignore newTrackPoint. location is invalid.");
return;
}
if (!isAccurate) {
if (!LocationUtils.fulfillsAccuracy(location, thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
trackPointCreator.onNewTrackPoint(trackPoint);
trackPointCreator.onChange(location);
}
@Override
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.location.Location;
import android.util.Log;
import android.util.Pair;
@@ -47,7 +48,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
this.gpsHandler = gpsHandler;
}
public void start(@NonNull Context context) {
public synchronized void start(@NonNull Context context) {
this.context = context;
gpsHandler.onStart(context);
@@ -71,7 +72,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
gpsHandler.onStop();
}
public void reset() {
public synchronized void reset() {
if (remoteSensorManager == null || altitudeSumManager == null) {
Log.d(TAG, "No recording running and no reset necessary.");
return;
@@ -80,7 +81,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
altitudeSumManager.reset();
}
private SensorDataSet fill(TrackPoint trackPoint) {
private SensorDataSet addSensorData(TrackPoint trackPoint) {
if (!isStarted()) {
Log.w(TAG, "Not started, should not be called.");
return null;
@@ -98,7 +99,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
return sensorDataSet;
}
public void stop() {
public synchronized void stop() {
gpsHandler.onStop();
if (remoteSensorManager != null) {
@@ -114,16 +115,21 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
this.context = null;
}
public synchronized void onChange(@NonNull Location location) {
onNewTrackPoint(new TrackPoint(location, createNow()));
}
/**
* Got a new TrackPoint from Bluetooth only; contains no GPS location.
*/
@Override
public void onChange(SensorDataSet sensorDataSet) {
public synchronized void onChange(@NonNull SensorDataSet unused) {
onNewTrackPoint(new TrackPoint(TrackPoint.Type.SENSORPOINT, createNow()));
}
public synchronized void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
fill(trackPoint);
@VisibleForTesting
public void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
addSensorData(trackPoint);
boolean stored = service.newTrackPoint(trackPoint, gpsHandler.getThresholdHorizontalAccuracy());
if (stored) {
@@ -131,13 +137,13 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
}
}
public TrackPoint createSegmentStartManual() {
public synchronized TrackPoint createSegmentStartManual() {
return TrackPoint.createSegmentStartManualWithTime(createNow());
}
public TrackPoint createSegmentEnd() {
public synchronized TrackPoint createSegmentEnd() {
TrackPoint segmentEnd = TrackPoint.createSegmentEndWithTime(createNow());
fill(segmentEnd);
addSensorData(segmentEnd);
reset();
return segmentEnd;
}
@@ -158,11 +164,12 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
currentTrackPoint.setLatitude(lastStoredTrackPointWithLocation.getLatitude());
}
SensorDataSet sensorDataSet = fill(currentTrackPoint);
SensorDataSet sensorDataSet = addSensorData(currentTrackPoint);
return new Pair<>(currentTrackPoint, sensorDataSet);
}
@VisibleForTesting
Instant createNow() {
return Instant.now(clock);
}
@@ -64,10 +64,23 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
}
private void updateUnits() {
boolean metricUnits = PreferencesUtils.isMetricUnits();
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key));
String[] entries = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
int entriesId;
switch (unitSystem) {
case METRIC:
entriesId = R.array.stats_rate_metric_options;
break;
case IMPERIAL:
entriesId = R.array.stats_rate_imperial_options;
break;
default:
throw new RuntimeException("Not implemented");
}
String[] entries = getResources().getStringArray(entriesId);
statsRatePreferences.setEntries(entries);
HackUtils.invalidatePreference(statsRatePreferences);
@@ -9,7 +9,7 @@ import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
public class GpsSettingsFragment extends PreferenceFragmentCompat {
@@ -17,30 +17,34 @@ public class GpsSettingsFragment extends PreferenceFragmentCompat {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_gps);
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
final DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setUnit(unitSystem)
.build(getContext());
findPreference(getString(R.string.recording_distance_interval_key))
.setSummaryProvider(
preference -> {
boolean metricUnits = PreferencesUtils.isMetricUnits();
Distance distance = PreferencesUtils.getRecordingDistanceInterval();
return getString(R.string.settings_recording_location_frequency_summary, StringUtils.formatDistance(getContext(), distance, metricUnits));
return getString(R.string.settings_recording_location_frequency_summary, formatter.formatDistance(distance));
}
);
findPreference(getString(R.string.max_recording_distance_key))
.setSummaryProvider(
preference -> {
boolean metricUnits = PreferencesUtils.isMetricUnits();
Distance distance = PreferencesUtils.getMaxRecordingDistance();
return getString(R.string.settings_recording_max_recording_distance_summary, StringUtils.formatDistance(getContext(), distance, metricUnits));
return getString(R.string.settings_recording_max_recording_distance_summary, formatter.formatDistance(distance));
}
);
findPreference(getString(R.string.recording_gps_accuracy_key))
.setSummaryProvider(
preference -> {
boolean metricUnits = PreferencesUtils.isMetricUnits();
Distance distance = PreferencesUtils.getThresholdHorizontalAccuracy();
return getString(R.string.settings_recording_min_required_accuracy_summary, StringUtils.formatDistance(getContext(), distance, metricUnits));
return getString(R.string.settings_recording_min_required_accuracy_summary, formatter.formatDistance(distance));
}
);
@@ -4,6 +4,7 @@ import android.net.Uri;
import android.os.Bundle;
import androidx.documentfile.provider.DocumentFile;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
@@ -12,16 +13,20 @@ import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.util.IntentUtils;
public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
private static final String TAG = ImportExportSettingsFragment.class.getSimpleName();
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_import_export);
setExportTrackFileFormatOptions();
setExportDirectorySummary();
setFilenameTemplate();
}
@Override
@@ -45,7 +50,8 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA,
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
TrackFileFormat.GPX
TrackFileFormat.GPX,
TrackFileFormat.CSV
};
String[] entries = new String[trackFileFormats.length];
String[] entryValues = new String[trackFileFormats.length];
@@ -76,4 +82,19 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
return directoryUri + (directory.canWrite() ? "" : getString(R.string.export_dir_not_writable));
});
}
private void setFilenameTemplate() {
EditTextPreference preference = findPreference(getString(R.string.export_filename_format_key));
preference.setOnBindEditTextListener(t -> {
t.setHint(getString(R.string.export_filename_format_default));
});
preference.setDialogMessage(TrackFilenameGenerator.getAllOptions());
preference.setOnPreferenceChangeListener((p, newValue) -> new TrackFilenameGenerator(newValue.toString()).isValid());
preference.setSummaryProvider(p ->
PreferencesUtils.getTrackFileformatGenerator()
.getExample()
);
}
}
@@ -32,16 +32,19 @@ import androidx.preference.PreferenceManager;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -193,19 +196,30 @@ public class PreferencesUtils {
return getBoolean(R.string.publicapi_dashboard_enabled_key, resources.getBoolean(R.bool.publicapi_dashboard_enabled_default));
}
public static boolean isMetricUnits() {
final String STATS_UNIT = resources.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(R.string.stats_units_key, STATS_UNIT));
public static UnitSystem getUnitSystem() {
final String STATS_UNIT_DEFAULT = resources.getString(R.string.stats_units_default);
final String VALUE = getString(R.string.stats_units_key, STATS_UNIT_DEFAULT);
return Arrays.stream(UnitSystem.values())
.filter(d -> VALUE.equals(getString(d.getPreferenceId(), STATS_UNIT_DEFAULT)))
.findFirst()
.orElse(UnitSystem.defaultUnitSystem()); //TODO This AGAIN defines the default
}
public static void setMetricUnits(boolean metricUnits) {
String unit;
if (metricUnits) {
unit = resources.getString(R.string.stats_units_metric);
} else {
unit = resources.getString(R.string.stats_units_imperial);
public static void setUnit(UnitSystem unitSystem) {
setString(R.string.stats_units_key, unitSystem.getPreferenceId());
}
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
public static void applyDefaultUnit() {
if (getString(R.string.stats_units_key, "").equals("")) {
if (!Locale.US.equals(Locale.getDefault())) {
setUnit(UnitSystem.METRIC);
} else {
setUnit(UnitSystem.IMPERIAL);
}
}
setString(R.string.stats_units_key, unit);
}
public static boolean isReportSpeed(String category) {
@@ -292,27 +306,32 @@ public class PreferencesUtils {
}
/**
* @return Result depends on isMetricUnits
* @return Result depends on getUnitSystem
*/
public static Distance getVoiceAnnouncementDistance() {
final float DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_distance_default));
float value = getFloat(R.string.voice_announcement_distance_key, DEFAULT);
return Distance.one(isMetricUnits()).multipliedBy(value);
return Distance.one(getUnitSystem()).multipliedBy(value);
}
/**
* @return Result depends on isMetricUnits
* @return Result depends on getUnitSystem
*/
static String[] getVoiceAnnouncementDistanceEntries() {
String[] values = resources.getStringArray(R.array.voice_announcement_distance_values);
String[] options = new String[values.length];
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < values.length; i++) {
if (resources.getString(R.string.announcement_off).equals(values[i])) {
options[i] = resources.getString(R.string.value_off);
} else {
int value = Integer.parseInt(values[i]);
options[i] = resources.getString(metricUnits ? R.string.value_integer_kilometer : R.string.value_integer_mile, value);
Distance distance = Distance.one(unitSystem).multipliedBy(Double.parseDouble(values[i]));
options[i] = formatter.formatDistance(distance);
}
}
return options;
@@ -349,26 +368,34 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length];
final int recordingDistanceIntervalDefault = (int) getRecordingDistanceIntervalDefault().toM();
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setUnit(unitSystem)
.setDecimalCount(0)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
String displayValue;
if (metricUnits) {
displayValue = resources.getString(R.string.value_integer_meter, value);
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else {
entries[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
displayValue = resources.getString(R.string.value_integer_feet, feet);
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet);
} else {
entries[i] = displayValue;
}
Distance distance = Distance.of(1).multipliedBy(value);
String displayValue = formatter.formatDistance(distance);
switch (unitSystem) {
case METRIC:
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else {
entries[i] = displayValue;
}
break;
case IMPERIAL:
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
break;
default:
throw new RuntimeException("Not implemented");
}
}
@@ -385,26 +412,34 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length];
final int maxRecordingDistanceDefault = Integer.parseInt(resources.getString(R.string.max_recording_distance_default));
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
String displayValue;
if (metricUnits) {
displayValue = resources.getString(R.string.value_integer_meter, value);
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else {
entries[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
displayValue = resources.getString(R.string.value_integer_feet, feet);
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet);
} else {
entries[i] = displayValue;
}
Distance distance = Distance.of(1).multipliedBy(value);
String displayValue = formatter.formatDistance(distance);
switch (unitSystem) {
case METRIC:
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else {
entries[i] = displayValue;
}
break;
case IMPERIAL:
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
break;
default:
throw new RuntimeException("Not implemented");
}
}
@@ -449,33 +484,41 @@ public class PreferencesUtils {
final int recordingGPSAccuracyExcellent = Integer.parseInt(resources.getString(R.string.recording_gps_accuracy_excellent));
final int recordingGPSAccuracyPoor = Integer.parseInt(resources.getString(R.string.recording_gps_accuracy_poor));
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
String displayValue;
if (metricUnits) {
displayValue = resources.getString(R.string.value_integer_meter, value);
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_meter_excellent_gps, value);
} else if (value == recordingGPSAccuracyPoor) {
entries[i] = resources.getString(R.string.value_integer_meter_poor_gps, value);
} else {
entries[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
displayValue = resources.getString(R.string.value_integer_feet, feet);
Distance distance = Distance.of(1).multipliedBy(value);
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet);
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_feet_excellent_gps, feet);
} else {
entries[i] = displayValue;
}
String displayValue = formatter.formatDistance(distance);
switch (unitSystem) {
case METRIC:
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_meter_excellent_gps, value);
} else if (value == recordingGPSAccuracyPoor) {
entries[i] = resources.getString(R.string.value_integer_meter_poor_gps, value);
} else {
entries[i] = displayValue;
}
break;
case IMPERIAL:
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_feet_excellent_gps, (int) distance.toFT());
} else {
entries[i] = displayValue;
}
break;
default:
throw new RuntimeException("Not implemented");
}
}
@@ -495,24 +538,30 @@ public class PreferencesUtils {
final float idleSpeedDefault = Float.parseFloat(resources.getString(R.string.idle_speed_default));
boolean metricUnits = isMetricUnits();
UnitSystem unitSystem = getUnitSystem();
for (int i = 0; i < entryValues.length; i++) {
float value = Float.parseFloat(entryValues[i]);
if (metricUnits) {
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
} else {
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
}
} else {
double valueMPH = Speed.ofKMH(value).toMPH();
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH);
} else {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
}
switch (unitSystem) {
case METRIC:
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
} else {
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
}
break;
case IMPERIAL:
double valueMPH = Speed.ofKMH(value).toMPH();
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH);
} else {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
}
break;
default:
throw new RuntimeException("Not implemented");
}
}
@@ -525,6 +574,16 @@ public class PreferencesUtils {
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
}
public static TrackFilenameGenerator getTrackFileformatGenerator() {
String DEFAULT = getString(R.string.export_filename_format_default, null);
TrackFilenameGenerator generator = new TrackFilenameGenerator(getString(R.string.export_filename_format_key, DEFAULT));
if (generator.isValid()) {
return generator;
} else {
return new TrackFilenameGenerator(DEFAULT);
}
}
public static TrackFileFormat getExportTrackFileFormat() {
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(R.string.export_trackfileformat_default, null);
String trackFileFormatName = getString(R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
@@ -0,0 +1,23 @@
package de.dennisguse.opentracks.settings;
import de.dennisguse.opentracks.R;
public enum UnitSystem {
METRIC(R.string.stats_units_metric),
IMPERIAL(R.string.stats_units_imperial);
private final int preferenceIdentifier;
UnitSystem(int preferenceIdentifier) {
this.preferenceIdentifier = preferenceIdentifier;
}
public int getPreferenceId() {
return preferenceIdentifier;
}
@Deprecated //TODO used to initialize before loading from preferences; should be loaded first
public static UnitSystem defaultUnitSystem() {
return METRIC;
}
}
@@ -28,8 +28,10 @@ import java.time.ZoneId;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -209,8 +211,8 @@ public class DescriptionGenerator {
*/
@VisibleForTesting
void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
Pair<String, String> paceInMetrics = SpeedFormatter.Builder().setUnit(UnitSystem.METRIC).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
Pair<String, String> paceInImperial = SpeedFormatter.Builder().setUnit(UnitSystem.IMPERIAL).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
String formattedPaceMetrics = paceInMetrics.first != null ? paceInMetrics.first : context.getString(R.string.value_unknown);
String formattedPaceImperial = paceInImperial.first != null ? paceInImperial.first : context.getString(R.string.value_unknown);
@@ -15,8 +15,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.ShareContentProvider;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class ShareUtils {
@@ -54,8 +54,9 @@ public class ShareUtils {
continue;
}
String trackName = FileUtils.sanitizeFileName(track.getName());
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, PreferencesUtils.getExportTrackFileFormat());
TrackFileFormat format = PreferencesUtils.getExportTrackFileFormat();
String trackName = PreferencesUtils.getTrackFileformatGenerator().format(track, format);
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, format);
uris.add(uriAndMime.first);
mime = uriAndMime.second;
@@ -13,7 +13,10 @@ import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -97,7 +100,7 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
private final TextView maxSpeedUnit;
private final TextView maxSpeedLabel;
private boolean metricsUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean reportSpeed;
public ViewHolder(View view) {
@@ -118,15 +121,17 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
setCommonValues(aggregatedStatistic);
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed());
avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_speed));
}
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getMaxSpeed());
maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(context.getString(R.string.stats_max_speed));
@@ -134,16 +139,18 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
}
public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
setCommonValues(aggregatedStatistic);
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed());
avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_pace));
}
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getMaxSpeed());
maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(R.string.stats_fastest_pace);
@@ -155,13 +162,15 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
String category = aggregatedStatistic.getCategory();
reportSpeed = PreferencesUtils.isReportSpeed(category);
metricsUnits = PreferencesUtils.isMetricUnits();
unitSystem = PreferencesUtils.getUnitSystem();
sportIcon.setImageResource(getIcon(aggregatedStatistic));
typeLabel.setText(category);
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStatistic.getCountTracks())));
Pair<String, String> parts = StringUtils.getDistanceParts(context, aggregatedStatistic.getTrackStatistics().getTotalDistance(), metricsUnits);
Pair<String, String> parts = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context).getDistanceParts(aggregatedStatistic.getTrackStatistics().getTotalDistance());
distance.setText(parts.first);
distanceUnit.setText(parts.second);
@@ -13,6 +13,9 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@@ -20,11 +23,11 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
private List<IntervalStatistics.Interval> intervalList;
private final Context context;
private final StackMode stackMode;
private boolean metricUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean isReportSpeed;
public IntervalStatisticsAdapter(Context context, StackMode stackMode, boolean metricUnits, boolean isReportSpeed) {
this.metricUnits = metricUnits;
public IntervalStatisticsAdapter(Context context, StackMode stackMode, UnitSystem unitSystem, boolean isReportSpeed) {
this.unitSystem = unitSystem;
this.context = context;
this.stackMode = stackMode;
this.isReportSpeed = isReportSpeed;
@@ -54,12 +57,15 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
} else {
sumDistance = interval.getDistance().multipliedBy(nextPosition);
}
viewHolder.distance.setText(StringUtils.formatDistance(context, sumDistance, metricUnits));
viewHolder.distance.setText(DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context).formatDistance(sumDistance));
viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed(), metricUnits, isReportSpeed));
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(isReportSpeed).build(context);
viewHolder.rate.setText(formatter.formatSpeed(interval.getSpeed()));
viewHolder.gain.setText(StringUtils.formatAltitudeChange(context, interval.getGain_m(), metricUnits));
viewHolder.loss.setText(StringUtils.formatAltitudeChange(context, interval.getLoss_m(), metricUnits));
viewHolder.gain.setText(StringUtils.formatAltitude(context, interval.getGain_m(), unitSystem));
viewHolder.loss.setText(StringUtils.formatAltitude(context, interval.getLoss_m(), unitSystem));
}
@@ -71,8 +77,8 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
return intervalList.size();
}
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) {
this.metricUnits = metricUnits;
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, UnitSystem unitSystem, boolean isReportSpeed) {
this.unitSystem = unitSystem;
this.isReportSpeed = isReportSpeed;
intervalList = data;
@@ -22,6 +22,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.settings.UnitSystem;
/**
* This model is used to load intervals for a track.
@@ -64,14 +65,14 @@ public class IntervalStatisticsModel extends AndroidViewModel {
handler = null;
}
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) {
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(Track.Id trackId, UnitSystem unitSystem, @Nullable IntervalOption interval) {
if (intervalsLiveData == null) {
if (interval == null) {
interval = IntervalOption.OPTION_1;
}
intervalsLiveData = new MutableLiveData<>();
distanceInterval = interval.getDistance(metricUnits);
distanceInterval = interval.getDistance(unitSystem);
intervalStatistics = new IntervalStatistics(distanceInterval);
loadIntervalStatistics(trackId);
@@ -104,13 +105,13 @@ public class IntervalStatisticsModel extends AndroidViewModel {
}
}
public void update(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) {
public void update(Track.Id trackId, UnitSystem unitSystem, @Nullable IntervalOption interval) {
if (interval == null) {
interval = IntervalOption.OPTION_1;
interval = IntervalOption.DEFAULT;
}
lastTrackPointId = null;
distanceInterval = interval.getDistance(metricUnits);
distanceInterval = interval.getDistance(unitSystem);
intervalStatistics = new IntervalStatistics(distanceInterval);
loadIntervalStatistics(trackId);
}
@@ -119,6 +120,8 @@ public class IntervalStatisticsModel extends AndroidViewModel {
* Intervals length this view model support.
*/
public enum IntervalOption {
OPTION_0_1(0.1f),
OPTION_0_5(0.5f),
OPTION_1(1),
OPTION_2(2),
OPTION_3(3),
@@ -128,25 +131,26 @@ public class IntervalStatisticsModel extends AndroidViewModel {
OPTION_20(20),
OPTION_50(50);
private final int multiplier;
static IntervalOption DEFAULT = OPTION_1;
IntervalOption(int multiplier) {
private final double multiplier;
IntervalOption(double multiplier) {
this.multiplier = multiplier;
}
public Distance getDistance(boolean metricUnits) {
public Distance getDistance(UnitSystem unitSystem) {
return Distance
.one(metricUnits)
.one(unitSystem)
.multipliedBy(multiplier);
}
public double getMultiplier() {
return multiplier;
}
public boolean sameMultiplier(IntervalOption intervalOption) {
return intervalOption != null && this.multiplier == intervalOption.multiplier;
}
@Override
public String toString() {
return "" + multiplier; //TODO Somehow IntervalsFragment relies on a parsable Integer.
}
}
}
@@ -16,12 +16,15 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.List;
import java.util.Objects;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
/**
* A fragment to display the intervals from recorded track.
@@ -39,7 +42,7 @@ public class IntervalsFragment extends Fragment {
private IntervalStatisticsModel.IntervalOption selectedInterval;
private Track.Id trackId;
private boolean metricUnits;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private IntervalStatisticsAdapter adapter;
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> intervalsAdapter;
@@ -49,7 +52,7 @@ public class IntervalsFragment extends Fragment {
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.stats_units_key, key) || PreferencesUtils.isKey(R.string.stats_rate_key, key)) {
updateIntervals(PreferencesUtils.isMetricUnits(), selectedInterval);
updateIntervals(PreferencesUtils.getUnitSystem(), selectedInterval);
if (intervalsAdapter != null) {
intervalsAdapter.notifyDataSetChanged();
}
@@ -59,10 +62,11 @@ public class IntervalsFragment extends Fragment {
/**
* Creates an instance of this class.
*
* @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top.
* @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top.
* @return IntervalsFragment instance.
*/
public static Fragment newInstance(Track.Id trackId, boolean fromTopToBottom) {
public static Fragment newInstance(@NonNull Track.Id trackId, boolean fromTopToBottom) {
Objects.nonNull(trackId);
Bundle bundle = new Bundle();
bundle.putBoolean(FROM_TOP_TO_BOTTOM_KEY, fromTopToBottom);
bundle.putParcelable(TRACK_ID_KEY, trackId);
@@ -93,25 +97,30 @@ public class IntervalsFragment extends Fragment {
return viewBinding.getRoot();
}
@Deprecated //TODO This method must be re-implemented.
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, metricUnits, isReportSpeed);
adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, unitSystem, isReportSpeed);
viewBinding.intervalList.setLayoutManager(new LinearLayoutManager(getContext()));
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
viewBinding.intervalList.setAdapter(adapter);
final DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setUnit(unitSystem)
.build(getContext());
intervalsAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
TextView v = (TextView) super.getView(position, convertView, parent);
if (metricUnits) {
v.setText(getContext().getString(R.string.value_integer_kilometer, Integer.parseInt(v.getText().toString())));
} else {
v.setText(getContext().getString(R.string.value_integer_mile, Integer.parseInt(v.getText().toString())));
}
IntervalStatisticsModel.IntervalOption option = getItem(position);
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
v.setText(stringValue);
return v;
}
@@ -122,12 +131,15 @@ public class IntervalsFragment extends Fragment {
};
viewBinding.intervalsDropdown.setAdapter(intervalsAdapter);
viewBinding.intervalsDropdown.setOnItemClickListener((parent, view1, position, id) -> updateIntervals(metricUnits, IntervalStatisticsModel.IntervalOption.values()[position]));
viewBinding.intervalsDropdown.setOnItemClickListener((parent, view1, position, id) -> {
updateIntervals(unitSystem, IntervalStatisticsModel.IntervalOption.values()[position]);
viewBinding.intervalsDropdown.setText(
getContext().getString(R.string.value_integer_kilometer, Integer.parseInt(selectedInterval != null ? selectedInterval.toString() : IntervalStatisticsModel.IntervalOption.values()[0].toString())),
false
);
//TODO This duplicates the intervalAdapter code
setIntervalsDropdownText();
});
//TODO This duplicates the intervalAdapter code
setIntervalsDropdownText();
}
@Override
@@ -180,19 +192,32 @@ public class IntervalsFragment extends Fragment {
}
viewBinding.intervalRate.setText(isReportSpeed ? getString(R.string.stats_speed) : getString(R.string.stats_pace));
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(trackId, metricUnits, selectedInterval);
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, metricUnits, isReportSpeed));
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(trackId, unitSystem, selectedInterval);
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, unitSystem, isReportSpeed));
setIntervalsDropdownText();
}
private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) {
boolean update = metricUnits != this.metricUnits
private void setIntervalsDropdownText() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setUnit(unitSystem)
.build(getContext());
IntervalStatisticsModel.IntervalOption option = selectedInterval != null ? selectedInterval : IntervalStatisticsModel.IntervalOption.DEFAULT;
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
viewBinding.intervalsDropdown.setText(stringValue, false);
}
private synchronized void updateIntervals(UnitSystem unitSystem, IntervalStatisticsModel.IntervalOption selectedInterval) {
boolean update = unitSystem != this.unitSystem
|| selectedInterval == null
|| !selectedInterval.sameMultiplier(this.selectedInterval);
this.metricUnits = metricUnits;
this.unitSystem = unitSystem;
this.selectedInterval = selectedInterval;
if (update && viewModel != null) {
viewModel.update(trackId, this.metricUnits, this.selectedInterval);
viewModel.update(trackId, this.unitSystem, this.selectedInterval);
}
}
}
@@ -108,7 +108,7 @@ public class ExportUtils {
}
private static Uri getExportDocumentFileUri(Context context, Track track, TrackFileFormat trackFileFormat, DocumentFile directory) {
String exportFileName = getExportFileNameForTrack(track, trackFileFormat.getExtension());
String exportFileName = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
if (exportDocumentFileUri == null) {
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
@@ -119,10 +119,6 @@ public class ExportUtils {
return exportDocumentFileUri;
}
private static String getExportFileNameForTrack(Track track, String trackFileFormatExtension) {
return track.getUuid().toString().substring(0, 8) + "_" + FileUtils.sanitizeFileName(track.getName()) + "." + trackFileFormatExtension;
}
private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
final ContentResolver resolver = context.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
@@ -17,6 +17,8 @@ package de.dennisguse.opentracks.util;
import android.location.Location;
import de.dennisguse.opentracks.data.models.Distance;
/**
* Utility class for decimating tracks at a given level of precision.
*
@@ -40,4 +42,11 @@ public class LocationUtils {
&& Math.abs(location.getLatitude()) <= 90
&& Math.abs(location.getLongitude()) <= 180;
}
public static boolean fulfillsAccuracy(Location location, Distance thresholdHorizontalAccuracy) {
return location.hasAccuracy() &&
Distance.of(location.getAccuracy())
.lessThan(thresholdHorizontalAccuracy);
}
}
@@ -43,9 +43,10 @@ import java.time.temporal.TemporalAccessor;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.settings.UnitSystem;
/**
* Various string manipulation methods.
@@ -128,32 +129,6 @@ public class StringUtils {
return TextUtils.split(value, ":").length == 2 ? "0:" + value : value;
}
/**
* Formats the distance in meters.
*
* @param context the context
* @param distance the distance
* @param metricUnits true to use metric units. False to use imperial units
*/
public static String formatDistance(Context context, Distance distance, boolean metricUnits) {
if (distance.isInvalid()) {
return context.getString(R.string.value_unknown);
}
Pair<String, String> distanceParts = getDistanceParts(context, distance, metricUnits);
return context.getString(R.string.distance_with_unit, distanceParts.first, distanceParts.second);
}
public static String formatSpeed(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
Pair<String, String> distanceParts = getSpeedParts(context, speed, metricUnits, reportSpeed);
return context.getString(R.string.speed_with_unit, distanceParts.first, distanceParts.second);
}
private static String formatDecimal(double value) {
return StringUtils.formatDecimal(value, 2);
}
/**
* Format a decimal number while removing trailing zeros of the decimal part (if present).
@@ -187,67 +162,6 @@ public class StringUtils {
return context.getString(R.string.location_latitude_longitude, Location.convert(latitude, Location.FORMAT_DEGREES), Location.convert(longitude, Location.FORMAT_DEGREES));
}
/**
* Get the formatted distance with unit.
*
* @param context the context
* @param distance the distance
* @param metricUnits true to use metric unit
* @return the formatted distance (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getDistanceParts(Context context, Distance distance, boolean metricUnits) {
if (distance.isInvalid()) {
return new Pair<>(null, context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet));
}
if (metricUnits) {
if (distance.greaterThan(Distance.of(500))) {
return new Pair<>(formatDecimal(distance.toKM()), context.getString(R.string.unit_kilometer));
} else {
return new Pair<>(formatDecimal(distance.toM()), context.getString(R.string.unit_meter));
}
} else {
if (distance.greaterThan(Distance.ofMile(0.5))) {
return new Pair<>(formatDecimal(distance.toMI()), context.getString(R.string.unit_mile));
} else {
return new Pair<>(formatDecimal(distance.toFT()), context.getString(R.string.unit_feet));
}
}
}
/**
* Gets the formatted speed with unit.
*
* @param context the context
* @param speed the speed
* @param metricUnits true to use metric unit
* @param reportSpeed true to report speed; false for pace
* @return the formatted speed (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getSpeedParts(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
int unitId;
if (metricUnits) {
unitId = reportSpeed ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
} else {
unitId = reportSpeed ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
}
String unitString = context.getString(unitId);
if (speed == null) {
speed = Speed.zero();
}
if (reportSpeed) {
return new Pair<>(StringUtils.formatDecimal(speed.to(metricUnits), 1), unitString);
}
int pace = (int) speed.toPace(metricUnits).getSeconds();
int minutes = pace / 60;
int seconds = pace % 60;
return new Pair<>(context.getString(R.string.time, minutes, seconds), unitString);
}
public static Pair<String, String> getHeartRateParts(Context context, HeartRate heartrate) {
String value = context.getString(R.string.value_unknown);
if (heartrate != null) {
@@ -340,35 +254,18 @@ public class StringUtils {
/**
* @return the formatted altitude_m (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, boolean metricUnits) {
String formattedValue = context.getString(R.string.value_unknown);
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(1)
.setUnit(unitSystem)
.build(context);
if (altitude_m != null) {
double value = Distance.of(altitude_m).toM_FT(metricUnits);
formattedValue = StringUtils.formatDecimal(value, 1);
}
return new Pair<>(formattedValue, unit);
Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of(Double.NaN); //TODO Refactor Double.NaN
return formatter.getDistanceParts(distance);
}
public static String formatAltitude(Context context, Float altitude_m, boolean metricUnits) {
Pair<String, String> altitudeParts = getAltitudeParts(context, altitude_m, metricUnits);
return context.getString(R.string.altitude_with_unit, altitudeParts.first, altitudeParts.second);
}
/**
* @return the formatted altitudeDifference_m (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getAltitudeChangeParts(Context context, Float altitudeDifference_m, boolean metricUnits) {
String formattedValue = altitudeDifference_m != null ? String.valueOf((int) Distance.of(altitudeDifference_m).toM_FT(metricUnits)) : context.getString(R.string.value_unknown);
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
return new Pair<>(formattedValue, unit);
}
public static String formatAltitudeChange(Context context, Float altitude_m, boolean metricUnits) {
Pair<String, String> altitudeParts = getAltitudeChangeParts(context, altitude_m, metricUnits);
public static String formatAltitude(Context context, Float altitude_m, UnitSystem unitSystem) {
Pair<String, String> altitudeParts = getAltitudeParts(context, altitude_m, unitSystem);
return context.getString(R.string.altitude_with_unit, altitudeParts.first, altitudeParts.second);
}
@@ -12,10 +12,13 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
@@ -24,17 +27,17 @@ import de.dennisguse.opentracks.util.StringUtils;
public class StatisticDataBuilder {
public static List<StatisticData> fromRecordingData(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull Layout layout, boolean metricUnits) {
public static List<StatisticData> fromRecordingData(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull Layout layout, UnitSystem unitSystem) {
List<StatisticData> statisticDataList = layout.getFields().stream()
.filter(DataField::isVisible)
.map(field -> build(context, recordingData, field.getKey(), field.isPrimary(), metricUnits))
.map(field -> build(context, recordingData, field.getKey(), field.isPrimary(), unitSystem))
.filter(Objects::nonNull)
.collect(Collectors.toList());
statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, metricUnits));
statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, unitSystem));
return statisticDataList;
}
private static StatisticData build(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull String fieldKey, boolean isPrimary, boolean metricUnits) {
private static StatisticData build(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull String fieldKey, boolean isPrimary, UnitSystem unitSystem) {
final TrackPoint latestTrackPoint = recordingData.getLatestTrackPoint();
final SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
final TrackStatistics trackStatistics = recordingData.getTrackStatistics();
@@ -46,6 +49,10 @@ public class StatisticDataBuilder {
final String sensorUnknown = context.getString(R.string.value_unknown);
SpeedFormatter speedFormatterPace = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(false).build(context);
SpeedFormatter speedFormatterSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context);
if (fieldKey.equals(context.getString(R.string.stats_custom_layout_total_time_key))) {
valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()), null);
title = context.getString(R.string.stats_total_time);
@@ -53,48 +60,51 @@ public class StatisticDataBuilder {
valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(trackStatistics.getMovingTime()), null);
title = context.getString(R.string.stats_moving_time);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_distance_key))) {
valueAndUnit = StringUtils.getDistanceParts(context, trackStatistics.getTotalDistance(), metricUnits);
valueAndUnit = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context).getDistanceParts(trackStatistics.getTotalDistance());
title = context.getString(R.string.stats_distance);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_speed_key)) || fieldKey.equals(context.getString(R.string.stats_custom_layout_pace_key))) {
boolean reportSpeed = fieldKey.equals("speed");
title = reportSpeed ? context.getString(R.string.stats_speed) : context.getString(R.string.stats_pace);
SpeedFormatter localSpeedFormatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
if (sensorDataSet != null && sensorDataSet.getSpeed() != null) {
valueAndUnit = StringUtils.getSpeedParts(context, sensorDataSet.getSpeed().first, metricUnits, reportSpeed);
valueAndUnit = localSpeedFormatter.getSpeedParts(sensorDataSet.getSpeed().first);
description = sensorDataSet.getSpeed().second;
} else {
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
valueAndUnit = StringUtils.getSpeedParts(context, speed, metricUnits, reportSpeed);
valueAndUnit = localSpeedFormatter.getSpeedParts(speed);
description = context.getString(R.string.description_speed_source_gps);
}
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_moving_speed_key))) {
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getAverageMovingSpeed(), metricUnits, true);
valueAndUnit = speedFormatterSpeed.getSpeedParts(trackStatistics.getAverageMovingSpeed());
title = context.getString(R.string.stats_average_moving_speed);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_speed_key))) {
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getAverageSpeed(), metricUnits, true);
valueAndUnit = speedFormatterSpeed.getSpeedParts(trackStatistics.getAverageSpeed());
title = context.getString(R.string.stats_average_speed);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_max_speed_key))) {
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getMaxSpeed(), metricUnits, true);
valueAndUnit = speedFormatterSpeed.getSpeedParts(trackStatistics.getMaxSpeed());
title = context.getString(R.string.stats_max_speed);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_moving_pace_key))) {
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getAverageMovingSpeed(), metricUnits, false);
valueAndUnit = speedFormatterPace.getSpeedParts(trackStatistics.getAverageMovingSpeed());
title = context.getString(R.string.stats_average_moving_pace);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_pace_key))) {
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getAverageSpeed(), metricUnits, false);
valueAndUnit = speedFormatterPace.getSpeedParts(trackStatistics.getAverageSpeed());
title = context.getString(R.string.stats_average_pace);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_fastest_pace_key))) {
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getMaxSpeed(), metricUnits, true);
valueAndUnit = speedFormatterPace.getSpeedParts(trackStatistics.getMaxSpeed());
title = context.getString(R.string.stats_fastest_pace);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_altitude_key))) {
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
title = context.getString(R.string.stats_altitude);
description = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? context.getString(latestTrackPoint.getAltitude().getLabelId()) : null;
valueAndUnit = StringUtils.getAltitudeParts(context, altitude, metricUnits);
valueAndUnit = StringUtils.getAltitudeParts(context, altitude, unitSystem);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_gain_key))) {
valueAndUnit = StringUtils.getAltitudeChangeParts(context, trackStatistics.getTotalAltitudeGain(), metricUnits);
valueAndUnit = StringUtils.getAltitudeParts(context, trackStatistics.getTotalAltitudeGain(), unitSystem);
title = context.getString(R.string.stats_gain);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_loss_key))) {
valueAndUnit = StringUtils.getAltitudeChangeParts(context, trackStatistics.getTotalAltitudeLoss(), metricUnits);
valueAndUnit = StringUtils.getAltitudeParts(context, trackStatistics.getTotalAltitudeLoss(), unitSystem);
title = context.getString(R.string.stats_loss);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_coordinates_key))) {
title = context.getString(R.string.stats_coordinates);
@@ -153,20 +163,20 @@ public class StatisticDataBuilder {
/**
* Builds a list of StatisticData with sensors configured but not in statisticDataList.
*/
private static List<StatisticData> getSensorStatsDataIfNeeded(Context context, RecordingData recordingData, List<StatisticData> statisticDataList, boolean metricUnits) {
private static List<StatisticData> getSensorStatsDataIfNeeded(Context context, RecordingData recordingData, List<StatisticData> statisticDataList, UnitSystem unitSystem) {
List<StatisticData> sensorDataList = new ArrayList<>();
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
if (sensorDataSet == null) {
return sensorDataList;
}
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_heart_rate))) && sensorDataSet.getHeartRate() != null) {
sensorDataList.add(build(context, recordingData, "heart_rate", true, metricUnits));
sensorDataList.add(build(context, recordingData, "heart_rate", true, unitSystem));
}
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_cadence))) && sensorDataSet.getCadence() != null) {
sensorDataList.add(build(context, recordingData, "cadence", true, metricUnits));
sensorDataList.add(build(context, recordingData, "cadence", true, unitSystem));
}
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_power))) && sensorDataSet.getCyclingPower() != null && sensorDataSet.getCyclingPower().hasValue()) {
sensorDataList.add(build(context, recordingData, "power", true, metricUnits));
sensorDataList.add(build(context, recordingData, "power", true, unitSystem));
}
return sensorDataList;
@@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData;
import java.util.List;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
public class StatisticsDataModel extends AndroidViewModel {
@@ -26,9 +27,9 @@ public class StatisticsDataModel extends AndroidViewModel {
return statsData;
}
public void update(RecordingData recordingData, Layout layout, boolean metricUnit) {
public void update(RecordingData recordingData, Layout layout, UnitSystem unitSystem) {
new Thread(() -> {
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, metricUnit);
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, unitSystem);
statsData.postValue(statisticDataList);
}).start();
}
+3 -1
View File
@@ -14,10 +14,12 @@
android:layout_height="wrap_content"
style="@style/TextAppearance.OpenTracks.PrimaryValue"
android:value="@string/value_unknown"
android:format12Hour="hh:mm:ss a"
android:format24Hour="HH:mm:ss"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="00:00" />
tools:text="00:00:00" />
<TextView
android:id="@+id/stats_description_main"
-4
View File
@@ -258,16 +258,12 @@ limitations under the License.
<string name="unit_minute_per_kilometer">د/كم</string>
<string name="unit_minute_per_mile">دقيقة/ميل</string>
<!-- Value -->
<string name="value_integer_feet">%1$d قدم</string>
<string name="value_integer_feet_excellent_gps">%1$d قدم (إشارة نظام تحديد المواقع ممتازة)</string>
<string name="value_integer_feet_recommended">%1$d قدم (موصى به)</string>
<string name="value_integer_kilometer">%1$d كم</string>
<string name="value_integer_kilometer_hour">%1$d‏ كم/ساعة</string>
<string name="value_integer_meter">%1$d م</string>
<string name="value_integer_meter_excellent_gps">%1$d م (إشارة نظام تحديد المواقع ممتازة)</string>
<string name="value_integer_meter_poor_gps">%1$d م (إشارة نظام تحديد المواقع سيئة)</string>
<string name="value_integer_meter_recommended">%1$d م (موصى به)</string>
<string name="value_integer_mile">%1$d ميل</string>
<string name="value_integer_mile_hour">%1$d‏‏ ميل/ساعة</string>
<string name="value_integer_minute">%1$d دقيقة</string>
<string name="value_integer_second">%1$d ث</string>
-4
View File
@@ -211,16 +211,12 @@
<string name="unit_mile_per_hour">mi/h</string>
<string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/mi</string>
<string name="value_integer_feet">%1$d ft</string>
<string name="value_integer_feet_excellent_gps">%1$d ft (excellent GPS)</string>
<string name="value_integer_feet_recommended">%1$d ft (recommended)</string>
<string name="value_integer_kilometer">%1$d km</string>
<string name="value_integer_kilometer_hour">%1$d km/h</string>
<string name="value_integer_meter">%1$d m</string>
<string name="value_integer_meter_excellent_gps">%1$d m (excellent GPS)</string>
<string name="value_integer_meter_poor_gps">%1$d m (poor GPS)</string>
<string name="value_integer_meter_recommended">%1$d m (recommended)</string>
<string name="value_integer_mile">%1$d mi</string>
<string name="value_integer_mile_hour">%1$d mph</string>
<string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d s</string>
+18 -4
View File
@@ -214,16 +214,12 @@
<string name="unit_mile_per_hour">millas/hora</string>
<string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/milla</string>
<string name="value_integer_feet">%1$d pies</string>
<string name="value_integer_feet_excellent_gps">%1$d pies (señal de GPS excelente)</string>
<string name="value_integer_feet_recommended">%1$d pies (recomendado)</string>
<string name="value_integer_kilometer">%1$d km</string>
<string name="value_integer_kilometer_hour">%1$d km/h</string>
<string name="value_integer_meter">%1$d m</string>
<string name="value_integer_meter_excellent_gps">%1$d m (señal de GPS excelente)</string>
<string name="value_integer_meter_poor_gps">%1$d m (señal de GPS deficiente)</string>
<string name="value_integer_meter_recommended">%1$d m (recomendado)</string>
<string name="value_integer_mile">%1$d millas</string>
<string name="value_integer_mile_hour">%1$d mi/h</string>
<string name="value_integer_minute">%1$d mín</string>
<string name="value_integer_second">%1$d s</string>
@@ -537,4 +533,22 @@
<string name="settings_public_api_dashboard_enabled_title">Transferencia automática de datos</string>
<string name="settings_public_api_dashboard_enabled_summary_off">Los datos grabados no se compartirán automáticamente con otras aplicaciones.</string>
<string name="settings_public_api_dashboard_enabled_summary_on">Una aplicación que ha iniciado una grabación puede también acceder a los datos grabados.</string>
<string name="voice_per_kilometer">por kilometro</string>
<string name="voice_per_mile">por milla</string>
<string name="lap_time">Tiempo de vuelta</string>
<string name="pace">Ritmo</string>
<string name="lap_speed">Velocidad de vuelta</string>
<string name="speed">Velocidad</string>
<string name="total_distance">Distancia total</string>
<plurals name="voiceDistanceKilometers">
<item quantity="one">1 kilómetro</item>
<item quantity="other">%1$.2f kilómetros</item>
</plurals>
<plurals name="voiceDistanceMiles">
<item quantity="one">1 milla</item>
<item quantity="other">%1$.2f millas</item>
</plurals>
<string name="average_heart_rate">Ritmo cardíaco promedio</string>
<string name="current_heart_rate">Ritmo cardíaco actual</string>
<string name="settings_announcements_report_heart_rate">Anunciar ritmo cardíaco</string>
</resources>
-4
View File
@@ -259,16 +259,12 @@ limitations under the License.
<string name="unit_minute_per_kilometer">мин/км</string>
<string name="unit_minute_per_mile">мин/мил</string>
<!-- Value -->
<string name="value_integer_feet">%1$d фт</string>
<string name="value_integer_feet_excellent_gps">%1$d фт (отличен GPS)</string>
<string name="value_integer_feet_recommended">%1$d фт (препоръчително)</string>
<string name="value_integer_kilometer">%1$d км</string>
<string name="value_integer_kilometer_hour">%1$d км/ч</string>
<string name="value_integer_meter">%1$d м</string>
<string name="value_integer_meter_excellent_gps">%1$d м (отличен GPS)</string>
<string name="value_integer_meter_poor_gps">%1$d м (слаб GPS)</string>
<string name="value_integer_meter_recommended">%1$d м (препоръчително)</string>
<string name="value_integer_mile">%1$d мил</string>
<string name="value_integer_mile_hour">%1$d мил/ч</string>
<string name="value_integer_minute">%1$d мин</string>
<string name="value_integer_second">%1$d сек</string>
-4
View File
@@ -257,16 +257,12 @@ limitations under the License.
<string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/milles</string>
<!-- Value -->
<string name="value_integer_feet">%1$d peus</string>
<string name="value_integer_feet_excellent_gps">%1$d peus (senyal de GPS excel·lent)</string>
<string name="value_integer_feet_recommended">%1$d peus (recomanat)</string>
<string name="value_integer_kilometer">%1$d km</string>
<string name="value_integer_kilometer_hour">%1$d km/h</string>
<string name="value_integer_meter">%1$d m</string>
<string name="value_integer_meter_excellent_gps">%1$d m (GPS amb bona senyal)</string>
<string name="value_integer_meter_poor_gps">%1$d m (GPS amb poca senyal)</string>
<string name="value_integer_meter_recommended">%1$d m (recomanat)</string>
<string name="value_integer_mile">%1$d milles</string>
<string name="value_integer_mile_hour">%1$d mi/h</string>
<string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d s</string>
+9 -7
View File
@@ -324,16 +324,12 @@ limitations under the License.
<string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/míle</string>
<!-- Value -->
<string name="value_integer_feet">%1$d stop</string>
<string name="value_integer_feet_excellent_gps">%1$d stop (vynikající signál GPS)</string>
<string name="value_integer_feet_recommended">%1$d stop (doporučeno)</string>
<string name="value_integer_kilometer">%1$d km</string>
<string name="value_integer_kilometer_hour">%1$d km/h</string>
<string name="value_integer_meter">%1$d m</string>
<string name="value_integer_meter_excellent_gps">%1$d m (vynikající signál GPS)</string>
<string name="value_integer_meter_poor_gps">%1$d m (špatný signál GPS)</string>
<string name="value_integer_meter_recommended">%1$d m (doporučeno)</string>
<string name="value_integer_mile">%1$d mil</string>
<string name="value_integer_mile_hour">%1$d mil/h</string>
<string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d s</string>
@@ -589,9 +585,9 @@ limitations under the License.
<string name="speed">Rychlost</string>
<string name="total_distance">Celková vzdálenost</string>
<plurals name="voiceDistanceKilometers">
<item quantity="one"></item>
<item quantity="few"></item>
<item quantity="other"></item>
<item quantity="one"/>
<item quantity="few"/>
<item quantity="other"/>
</plurals>
<string name="average_heart_rate">Průměrný srdeční tep</string>
<string name="current_heart_rate">Aktuální srdeční tep</string>
@@ -610,4 +606,10 @@ limitations under the License.
<string name="settings_public_api_dashboard_enabled_title">Automatický přenos dat</string>
<string name="settings_public_api_dashboard_enabled_summary_on">Aplikace, která zahájila nahrávání, může také přistupovat k nahraným datům.</string>
<string name="settings_public_api_dashboard_enabled_summary_off">Zaznamenaná data nebudou automaticky sdílena s jinými aplikacemi.</string>
<string name="pace">Tempo</string>
<plurals name="voiceDistanceMiles">
<item quantity="one"></item>
<item quantity="few"></item>
<item quantity="other"></item>
</plurals>
</resources>
-4
View File
@@ -329,16 +329,12 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="unit_minute_per_kilometer">min./km</string>
<string name="unit_minute_per_mile">min./mil</string>
<!-- Value -->
<string name="value_integer_feet">%1$d fod</string>
<string name="value_integer_feet_excellent_gps">%1$d fod (fremragende GPS)</string>
<string name="value_integer_feet_recommended">%1$d fod (anbefales)</string>
<string name="value_integer_kilometer">%1$d km</string>
<string name="value_integer_kilometer_hour">%1$d km/t</string>
<string name="value_integer_meter">%1$d m</string>
<string name="value_integer_meter_excellent_gps">%1$d m (fremragende GPS)</string>
<string name="value_integer_meter_poor_gps">%1$d m (dårlig GPS)</string>
<string name="value_integer_meter_recommended">%1$d m (anbefalet)</string>
<string name="value_integer_mile">%1$d mil</string>
<string name="value_integer_mile_hour">%1$d mil/t</string>
<string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d sek</string>

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