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 - name: checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: actions/setup-java@v2 - uses: actions/setup-java@v3
with: with:
distribution: 'adopt' distribution: 'adopt'
java-version: '11' java-version: '11'
@@ -26,7 +26,7 @@ jobs:
- name: Archive - name: Archive
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: OpenTracks-nightly.apk name: OpenTracks-nightly.apk
path: ./build/outputs/apk/nightly/*.apk path: ./build/outputs/apk/nightly/*.apk
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: actions/setup-java@v2 - uses: actions/setup-java@v3
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '11' java-version: '11'
+4 -4
View File
@@ -10,20 +10,20 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
strategy: strategy:
matrix: matrix:
api-level: [ 23, 30 ] api-level: [ 24, 30 ]
target: [ default ] target: [ default ]
fail-fast: false fail-fast: false
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: actions/setup-java@v2 - uses: actions/setup-java@v3
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '11' java-version: '11'
- name: run tests - name: run tests
uses: reactivecircus/android-emulator-runner@v2.23.0 uses: reactivecircus/android-emulator-runner@v2.24.0
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }} target: ${{ matrix.target }}
@@ -33,7 +33,7 @@ jobs:
- name: Archive - name: Archive
if: always() if: always()
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: code-coverage-report name: code-coverage-report
path: build/reports/androidTests/ path: build/reports/androidTests/
-1
View File
@@ -60,7 +60,6 @@ captures/
.idea/encodings.xml .idea/encodings.xml
.idea/jsLibraryMappings.xml .idea/jsLibraryMappings.xml
.idea/libraries/ .idea/libraries/
.idea/misc.xml
.idea/modules.xml .idea/modules.xml
.idea/scopes/scope_settings.xml .idea/scopes/scope_settings.xml
.idea/shelf/ .idea/shelf/
+1
View File
@@ -59,6 +59,7 @@ We do not support type of movement (i.e., walking vs running).
* Polar Stride * Polar Stride
* Stryd (incl. power) * Stryd (incl. power)
* Wahoo Tickr X (Cadence appears to be reported in SPM rather than RPM)
## Tested Barometric Sensor Smartphones ## Tested Barometric Sensor Smartphones
+4 -4
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { 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 { defaultConfig {
applicationId "de.dennisguse.opentracks" applicationId "de.dennisguse.opentracks"
versionCode 4773 versionCode 4816
versionName "v3.26.0" versionName "v3.27.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -130,7 +130,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.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' androidTestUtil 'androidx.test:orchestrator:1.4.1'
} }
+22
View File
@@ -29,4 +29,26 @@
</xsd:documentation> </xsd:documentation>
</xsd:annotation> </xsd:annotation>
</xsd:element> </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> </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 en outdoor activiteiten geven geluk, vaardigheid en zelfvertrouwen.
* Sport tracking: volg uw sport en outdoor activiteiten
* Foto's en markeringen: markeer interessante locaties tijdens het volgen Geef je gezondheid waarde door de conditie bij te houden.
* Hoogte winst en verlies: via barometrische sensor Registreert terwijl je hardloopt of wandelt, en geeft je een fietscomputer met een groter scherm voor op de fiets.
* Hoogte weergegeven in EGM2008 (boven gemiddeld zeeniveau); geëxporteerd als WGS84 Markeer interessante locaties langs de weg met foto's.
* Spraakaankondigingen Bewaar geregistreerde statistieken tot in detail voor analyse.
* Ondersteunt Bluetooth LE-sensoren: hartslag, snelheid en afstand (fietsen), cadans (fietsen), en vermogensmeter (fietsen) Deel alleen de gegevens die je aan anderen wilt laten zien.
* Exporteer gegevens: exporteer tracks als KMZ (incl. foto's), KML, of GPX
* Geen toegang tot internet: Internet wordt niet gebruikt * 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 distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" 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. # 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"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 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 [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 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." location of your Java installation."
fi fi
else 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. 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 Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,95 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD=$( ulimit -H -n ) ||
MAX_FD="$MAX_FD_LIMIT" warn "Could not query maximum file descriptor limit"
fi esac
ulimit -n $MAX_FD case $MAX_FD in #(
if [ $? -ne 0 ] ; then '' | soft) :;; #(
warn "Could not set maximum file descriptor limit: $MAX_FD" *)
fi ulimit -n "$MAX_FD" ||
else warn "Could not set maximum file descriptor limit to $MAX_FD"
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" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --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 # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 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" "$@" exec "$JAVACMD" "$@"
Vendored
+3 -18
View File
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 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_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,21 +64,6 @@ echo location of your Java installation.
goto fail 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 :execute
@rem Setup the command line @rem Setup the command line
@@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @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 :end
@rem End local scope for the variables with windows NT shell @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 org.junit.runners.model.Statement;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
public class PreferenceMetricUnitRule implements TestRule { public class PreferenceUnitSystemRule implements TestRule {
private final boolean metricUnits; private final UnitSystem unit;
public PreferenceMetricUnitRule(boolean metricUnits) { public PreferenceUnitSystemRule(UnitSystem unit) {
this.metricUnits = metricUnits; this.unit = unit;
} }
@Override @Override
@@ -18,13 +19,13 @@ public class PreferenceMetricUnitRule implements TestRule {
return new Statement() { return new Statement() {
@Override @Override
public void evaluate() throws Throwable { public void evaluate() throws Throwable {
final boolean previousMetricUnits = PreferencesUtils.isMetricUnits(); final UnitSystem previousUnitSystem = PreferencesUtils.getUnitSystem();
try { try {
PreferencesUtils.setMetricUnits(metricUnits); PreferencesUtils.setUnit(unit);
base.evaluate(); base.evaluate();
} finally { } 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.Distance;
import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@@ -26,7 +27,7 @@ public class ChartPointTest {
statistics.setTotalTime(Duration.ofSeconds(1000)); statistics.setTotalTime(Duration.ofSeconds(1000));
// when // 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 // then
assertEquals(1000000, (long) point.getTimeOrDistance()); assertEquals(1000000, (long) point.getTimeOrDistance());
@@ -39,7 +40,7 @@ public class ChartPointTest {
statistics.setTotalDistance(Distance.of(1000)); statistics.setTotalDistance(Distance.of(1000));
// when // 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 // then
assertEquals(1, (long) point.getTimeOrDistance()); assertEquals(1, (long) point.getTimeOrDistance());
@@ -51,7 +52,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics(); TrackStatistics statistics = new TrackStatistics();
// when // 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 // then
assertEquals(50, point.getAltitude(), 0.01); assertEquals(50, point.getAltitude(), 0.01);
@@ -65,7 +66,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics(); TrackStatistics statistics = new TrackStatistics();
// when // 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 // then
assertNull(point.getHeartRate()); assertNull(point.getHeartRate());
@@ -84,7 +85,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics(); TrackStatistics statistics = new TrackStatistics();
// when // 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 // then
assertEquals(100.0, point.getHeartRate(), 0.01); 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.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -16,28 +17,36 @@ import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule; import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule; import androidx.test.rule.ServiceTestRule;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TimezoneRule;
import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Cadence;
@@ -67,12 +76,18 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class ExportImportTest { public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
@Rule @Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule @Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION); 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 @BeforeClass
public static void preSetUp() { public static void preSetUp() {
// Prepare looper for Android's message queue // Prepare looper for Android's message queue
@@ -132,7 +147,7 @@ public class ExportImportTest {
Distance sensorDistance = Distance.of(10); // recording distance interval 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); service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
// A sensor-only TrackPoint // A sensor-only TrackPoint
@@ -143,7 +158,7 @@ public class ExportImportTest {
trackPointCreator.setClock("2020-02-02T02:02:05Z"); trackPointCreator.setClock("2020-02-02T02:02:05Z");
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint 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); service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:06Z"); trackPointCreator.setClock("2020-02-02T02:02:06Z");
@@ -153,11 +168,11 @@ public class ExportImportTest {
trackPointCreator.setClock("2020-02-02T02:02:20Z"); trackPointCreator.setClock("2020-02-02T02:02:20Z");
service.resumeCurrentTrack(); 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.setClock("2020-02-02T02:02:24Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator)); trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
@@ -172,7 +187,7 @@ public class ExportImportTest {
track = contentProviderUtils.getTrack(trackId); track = contentProviderUtils.getTrack(trackId);
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId); trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
markers = contentProviderUtils.getMarkers(trackId); markers = contentProviderUtils.getMarkers(trackId);
assertEquals(11, trackPoints.size()); assertEquals(10, trackPoints.size());
assertEquals(2, markers.size()); assertEquals(2, markers.size());
} }
@@ -206,8 +221,7 @@ public class ExportImportTest {
assertEquals(track.getIcon(), importedTrack.getIcon()); assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. trackpoints // 2. trackpoints
TrackPointAssert a = new TrackPointAssert() TrackPointAssert a = new TrackPointAssert();
.noAccuracy();
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId); List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
a.assertEquals(trackPoints, actual); a.assertEquals(trackPoints, actual);
@@ -320,8 +334,7 @@ public class ExportImportTest {
// Therefore, the track segmentation is changes. // Therefore, the track segmentation is changes.
TrackPointAssert a = new TrackPointAssert() TrackPointAssert a = new TrackPointAssert()
.setDelta(0.05) .setDelta(0.05); // speed is not fully
.noAccuracy(); // speed is not fully
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId); List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
a.assertEquals(List.of( a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:03Z")) new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:03Z"))
@@ -330,7 +343,8 @@ public class ExportImportTest {
.setAltitude(10) .setAltitude(10)
.setSpeed(Speed.of(15)) .setSpeed(Speed.of(15))
.setAltitudeLoss(1f) .setAltitudeLoss(1f)
.setAltitudeGain(1f), .setAltitudeGain(1f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z")) new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
.setLatitude(3) .setLatitude(3)
.setLongitude(14.001) .setLongitude(14.001)
@@ -341,21 +355,24 @@ public class ExportImportTest {
.setSensorDistance(Distance.of(12)) .setSensorDistance(Distance.of(12))
.setHeartRate(66f) .setHeartRate(66f)
.setPower(50f) .setPower(50f)
.setCadence(3f), .setCadence(3f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z")) new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z"))
.setLatitude(3) .setLatitude(3)
.setLongitude(14.002) .setLongitude(14.002)
.setAltitude(10) .setAltitude(10)
.setAltitudeLoss(0f) .setAltitudeLoss(0f)
.setAltitudeGain(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")) new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:22Z"))
.setLatitude(3) .setLatitude(3)
.setLongitude(16) .setLongitude(16)
.setAltitude(10) .setAltitude(10)
.setAltitudeLoss(0f) .setAltitudeLoss(0f)
.setAltitudeGain(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")) new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:23Z"))
.setLatitude(3) .setLatitude(3)
.setLongitude(16.001) .setLongitude(16.001)
@@ -363,6 +380,7 @@ public class ExportImportTest {
.setAltitudeLoss(0f) .setAltitudeLoss(0f)
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setSpeed(Speed.of(15)) .setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10))
), actual); ), actual);
// 3. trackstatistics // 3. trackstatistics
@@ -423,6 +441,37 @@ public class ExportImportTest {
assertNull(trackImported); 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() { private void assertMarkers() {
assertEquals(markers.size(), contentProviderUtils.getMarkers(importTrackId).size()); assertEquals(markers.size(), contentProviderUtils.getMarkers(importTrackId).size());
@@ -469,7 +518,7 @@ public class ExportImportTest {
return sensorDataSet; return sensorDataSet;
} }
}); });
trackPointCreator.onChange(null); trackPointCreator.onChange(new SensorDataSet());
} }
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) { private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
@@ -478,11 +527,14 @@ public class ExportImportTest {
altitudeSumManager.setAltitudeLoss_m(altitudeGain); 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 location = new Location("mock");
location.setLatitude(latitude); location.setLatitude(latitude);
location.setLongitude(longitude); location.setLongitude(longitude);
location.setAccuracy(accuracy); location.setAccuracy(accuracy);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
location.setVerticalAccuracyMeters(verticalAccuracy);
}
location.setSpeed(speed); location.setSpeed(speed);
location.setAltitude(altitude); location.setAltitude(altitude);
@@ -8,8 +8,6 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
public class TrackPointAssert { public class TrackPointAssert {
private boolean assertAccuracy = true;
private double delta = 0.001; private double delta = 0.001;
public TrackPointAssert() { public TrackPointAssert() {
@@ -45,13 +43,13 @@ public class TrackPointAssert {
Assert.assertEquals(expected.getSpeed().toMPS(), actual.getSpeed().toMPS(), delta); Assert.assertEquals(expected.getSpeed().toMPS(), actual.getSpeed().toMPS(), delta);
} }
if (assertAccuracy) {
Assert.assertEquals(expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy()); Assert.assertEquals(expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
if (expected.hasHorizontalAccuracy()) { if (expected.hasHorizontalAccuracy()) {
Assert.assertEquals(expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta); Assert.assertEquals(expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
} }
} else { Assert.assertEquals(expected.hasVerticalAccuracy(), actual.hasVerticalAccuracy());
Assert.assertFalse(actual.hasHorizontalAccuracy()); if (expected.hasVerticalAccuracy()) {
Assert.assertEquals(expected.getVerticalAccuracy().toM(), actual.getVerticalAccuracy().toM(), delta);
} }
Assert.assertEquals(expected.hasSensorDistance(), actual.hasSensorDistance()); 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) { public TrackPointAssert setDelta(double delta) {
this.delta = delta; this.delta = delta;
return this; return this;
@@ -19,6 +19,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
@@ -48,7 +49,7 @@ public class TrackRecordingServiceNotificationManagerTest {
.thenReturn(notificationCompatBuilder); .thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder); TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
subject.setMetricUnits(true); subject.setUnitSystem(UnitSystem.METRIC);
// when // when
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100)); subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
@@ -612,6 +612,55 @@ public class TrackRecordingServiceTestRecording {
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId)); ), 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 @MediumTest
@Test @Test
@@ -796,15 +845,12 @@ public class TrackRecordingServiceTestRecording {
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3)) new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3))
.setSpeed(Speed.of(5)) .setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)), .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)) new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45.001) .setLatitude(45.001)
.setLongitude(35) .setLongitude(35)
.setHorizontalAccuracy(Distance.of(1)) .setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(5)) .setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(0)), .setSensorDistance(Distance.of(4.0)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps4)) new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps4))
.setLatitude(45.001) .setLatitude(45.001)
.setLongitude(35) .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.Speed;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics; import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -54,7 +55,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f); stats.setTotalAltitudeGain(6000f);
// when // 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 // then
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement); 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 // 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 // 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); 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); stats.setTotalAltitudeGain(6000f);
// when // 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 // then
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement); 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 // 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 // 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); 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); stats.setTotalAltitudeGain(6000f);
// when // 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 // then
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Speed 11.4 miles per hour.", announcement); 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 // 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 // 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); 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); stats.setTotalAltitudeGain(6000f);
// when // 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 // then
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement); 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 // 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 // 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); 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); SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
// when // 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 // 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); 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 java.time.Instant;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
@@ -62,7 +61,7 @@ public class GPSHandlerTest {
locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis())); locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis()));
// then // 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())); locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis()));
// then // 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 // then
// no newTrackPoint called // no newTrackPoint called
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class)); verify(trackPointCreator, times(0)).onChange(any(Location.class));
} }
@Test @Test
@@ -107,7 +106,7 @@ public class GPSHandlerTest {
locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis())); locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
// then // 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 java.util.stream.Collectors;
import de.dennisguse.opentracks.LocaleRule; import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
/** /**
* Tests for {@link StringUtils}. * Tests for {@link StringUtils}.
@@ -91,21 +89,6 @@ public class StringUtilsTest {
assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(360000000))); 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)}. * Tests {@link StringUtils#formatCData(String)}.
*/ */
@@ -183,27 +166,6 @@ public class StringUtilsTest {
assertEquals("1.0", StringUtils.formatDecimal(0.99, 1)); 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 @Test
public void testFormatDateTodayRelative() { public void testFormatDateTodayRelative() {
// given // 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.Power;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.RecordingData; import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout; 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); layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), false, true, false);
// when // when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true); List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
// then // then
assertEquals(statisticDataList.size(), 3); 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); layout.addField(context.getString(R.string.stats_custom_layout_power_key), context.getString(R.string.stats_sensors_power), true, true, false);
// when // when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true); List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
// then // then
assertEquals(statisticDataList.size(), 18); 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.os.StrictMode;
import android.util.Log; import android.util.Log;
import java.util.Locale;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
/** /**
@@ -28,9 +26,7 @@ public class Startup extends Application {
PreferencesUtils.initPreferences(this, getResources()); PreferencesUtils.initPreferences(this, getResources());
// Set default values of preferences on first start. // Set default values of preferences on first start.
PreferencesUtils.resetPreferences(this, false); PreferencesUtils.resetPreferences(this, false);
if (PreferencesUtils.getString( R.string.stats_units_key, "").equals("")) { PreferencesUtils.applyDefaultUnit();
PreferencesUtils.setMetricUnits(!Locale.US.equals(Locale.getDefault()));
}
PreferencesUtils.applyNightMode(); PreferencesUtils.applyNightMode();
@@ -49,6 +49,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import de.dennisguse.opentracks.data.models.Distance; 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.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns; import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListBinding; 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.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.share.ShareUtils; import de.dennisguse.opentracks.share.ShareUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.AggregatedStatisticsActivity; import de.dennisguse.opentracks.ui.aggregatedStatistics.AggregatedStatisticsActivity;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
@@ -87,7 +89,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack(); private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack();
// Preferences // Preferences
private boolean metricUnits = true; private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private GpsStatusValue gpsStatusValue = TrackRecordingService.STATUS_GPS_DEFAULT; private GpsStatusValue gpsStatusValue = TrackRecordingService.STATUS_GPS_DEFAULT;
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT; private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
@@ -111,7 +113,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(); unitSystem = PreferencesUtils.getUnitSystem();
} }
if (key != null) { if (key != null) {
runOnUiThread(() -> { runOnUiThread(() -> {
@@ -190,7 +192,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
int iconId = TrackIconUtils.getIconDrawable(icon); int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex); String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex))); 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); int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex); long startTime = cursor.getLong(startTimeIndex);
int startTimeOffset = cursor.getInt(startTimeOffsetIndex); 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.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding; import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
@@ -77,9 +78,9 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(); UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
if (metricUnits != viewBinding.chartView.getMetricUnits()) { if (unitSystem != viewBinding.chartView.getUnitSystem()) {
viewBinding.chartView.setMetricUnits(metricUnits); viewBinding.chartView.setUnitSystem(unitSystem);
runOnUiThread(() -> { runOnUiThread(() -> {
if (isResumed()) { if (isResumed()) {
viewBinding.chartView.requestLayout(); 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) { public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
if (isResumed()) { 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); 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.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.models.UnitConversions; import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
public class ChartPoint { public class ChartPoint {
@@ -28,20 +29,20 @@ public class ChartPoint {
this.altitude = altitude; 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) { if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits); timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
} else { } else {
timeOrDistance = trackStatistics.getTotalTime().toMillis(); timeOrDistance = trackStatistics.getTotalTime().toMillis();
} }
if (smoothedAltitude != null) { if (smoothedAltitude != null) {
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(metricUnits); altitude = Distance.of(smoothedAltitude.toM()).toM_FT(unitSystem);
} }
if (smoothedSpeed != null) { if (smoothedSpeed != null) {
speed = smoothedSpeed.to(metricUnits); speed = smoothedSpeed.to(unitSystem);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN; pace = smoothedSpeed.toPace(unitSystem).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
} }
if (trackPoint.hasHeartRate()) { if (trackPoint.hasHeartRate()) {
heartRate = (double) trackPoint.getHeartRate().getBPM(); heartRate = (double) trackPoint.getHeartRate().getBPM();
@@ -27,6 +27,7 @@ import androidx.annotation.NonNull;
import java.text.NumberFormat; import java.text.NumberFormat;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.ExtremityMonitor; import de.dennisguse.opentracks.stats.ExtremityMonitor;
/** /**
@@ -217,11 +218,15 @@ abstract class ChartValueSeries {
return maxMarkerValue; return maxMarkerValue;
} }
/** int getTitleId(UnitSystem unitSystem) {
* Gets the title id. switch (unitSystem) {
*/ case METRIC:
int getTitleId(boolean metricUnits) { return metricTitleId;
return metricUnits ? metricTitleId : imperialTitleId; 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.R;
import de.dennisguse.opentracks.data.models.Marker; import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.ExtremityMonitor; import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity; import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
import de.dennisguse.opentracks.ui.markers.MarkerUtils; import de.dennisguse.opentracks.ui.markers.MarkerUtils;
@@ -116,7 +117,7 @@ public class ChartView extends View {
private int effectiveHeight = 0; private int effectiveHeight = 0;
private boolean chartByDistance = false; private boolean chartByDistance = false;
private boolean metricUnits = true; private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean reportSpeed = true; private boolean reportSpeed = true;
private boolean showPointer = false; private boolean showPointer = false;
@@ -371,17 +372,12 @@ public class ChartView extends View {
this.chartByDistance = chartByDistance; this.chartByDistance = chartByDistance;
} }
public boolean getMetricUnits() { public UnitSystem getUnitSystem() {
return metricUnits; return unitSystem;
} }
/** public void setUnitSystem(UnitSystem value) {
* Sets metric units. unitSystem = value;
*
* @param value true to use metric units
*/
public void setMetricUnits(boolean value) {
metricUnits = value;
} }
public boolean getReportSpeed() { public boolean getReportSpeed() {
@@ -677,7 +673,7 @@ public class ChartView extends View {
for (ChartValueSeries chartValueSeries : seriesList) { for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) { if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
count++; count++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits)); String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
Paint paint = chartValueSeries.getTitlePaint(); Paint paint = chartValueSeries.getTitlePaint();
int x = (int) (0.5 * width) + getScrollX(); int x = (int) (0.5 * width) + getScrollX();
int y = topBorder - spacer - (lines - count) * (lineHeight + spacer); int y = topBorder - spacer - (lines - count) * (lineHeight + spacer);
@@ -696,7 +692,7 @@ public class ChartView extends View {
for (ChartValueSeries chartValueSeries : seriesList) { for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) { if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
lines++; lines++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits)); String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
Rect rect = getRect(chartValueSeries.getTitlePaint(), title); Rect rect = getRect(chartValueSeries.getTitlePaint(), title);
if (rect.height() > lineHeight) { if (rect.height() > lineHeight) {
lineHeight = rect.height(); lineHeight = rect.height();
@@ -731,7 +727,14 @@ public class ChartView extends View {
private String getXAxisLabel() { private String getXAxisLabel() {
Context context = getContext(); Context context = getContext();
if (chartByDistance) { 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 { } else {
return context.getString(R.string.description_time); return context.getString(R.string.description_time);
} }
@@ -986,7 +989,7 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) { private double getMarkerXValue(Marker marker) {
if (chartByDistance) { if (chartByDistance) {
return marker.getLength().toKM_Miles(metricUnits); return marker.getLength().toKM_Miles(unitSystem);
} else { } else {
return marker.getDuration().toMillis(); return marker.getDuration().toMillis();
} }
@@ -15,6 +15,7 @@ class CachedTrackPointsIndexes {
final int timeIndex; final int timeIndex;
final int altitudeIndex; final int altitudeIndex;
final int accuracyIndex; final int accuracyIndex;
final int accuracyVerticalIndex;
final int speedIndex; final int speedIndex;
final int bearingIndex; final int bearingIndex;
final int sensorHeartRateIndex; final int sensorHeartRateIndex;
@@ -32,6 +33,7 @@ class CachedTrackPointsIndexes {
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME); timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE); altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE);
accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.HORIZONTAL_ACCURACY); accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.HORIZONTAL_ACCURACY);
accuracyVerticalIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.VERTICAL_ACCURACY);
speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED); speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING); bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE); sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
@@ -573,6 +573,9 @@ public class ContentProviderUtils {
if (!cursor.isNull(indexes.accuracyIndex)) { if (!cursor.isNull(indexes.accuracyIndex)) {
trackPoint.setHorizontalAccuracy(Distance.of(cursor.getFloat(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)) { if (!cursor.isNull(indexes.speedIndex)) {
trackPoint.setSpeed(Speed.of(cursor.getFloat(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 String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 33; private static final int DATABASE_VERSION = 34;
public CustomSQLiteOpenHelper(Context context) { public CustomSQLiteOpenHelper(Context context) {
this(context, ((Startup) context.getApplicationContext()).getDatabaseName()); this(context, ((Startup) context.getApplicationContext()).getDatabaseName());
@@ -91,6 +91,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 33: case 33:
upgradeFrom32to33(db); upgradeFrom32to33(db);
break; break;
case 34:
upgradeFrom33to34(db);
break;
default: default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion); throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -133,6 +136,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 32: case 32:
downgradeFrom33to32(db); downgradeFrom33to32(db);
break; break;
case 33:
downgradeFrom34to33(db);
break;
default: default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion); throw new RuntimeException("Not implemented: downgrade to " + toVersion);
} }
@@ -396,7 +402,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.beginTransaction(); db.beginTransaction();
// TrackPoints // 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 = 200 * 1E6, longitude = NULL WHERE type = -2");
db.execSQL("UPDATE trackpoints SET latitude = 100 * 1E6, longitude = NULL WHERE type = 1"); db.execSQL("UPDATE trackpoints SET latitude = 100 * 1E6, longitude = NULL WHERE type = 1");
@@ -513,6 +519,33 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.execSQL("CREATE UNIQUE INDEX tracks_uuid_index ON tracks(uuid)"); 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.setTransactionSuccessful();
db.endTransaction(); 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_AND_SENSORDATA = 6;
private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7; private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7;
private static final int URI_CSV = 8;
private static final int URI_SHARE_PICTURE = 8;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final String TRACKID_DELIMITER = "_"; 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.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.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: case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES; return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
case URI_CSV:
return TrackFileFormat.CSV;
default: default:
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri); throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
} }
@@ -1,9 +1,12 @@
package de.dennisguse.opentracks.data.models; package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects; import java.util.Objects;
import de.dennisguse.opentracks.settings.UnitSystem;
public class Distance { public class Distance {
public static Distance of(double distance_m) { public static Distance of(double distance_m) {
@@ -14,6 +17,14 @@ public class Distance {
return of(Float.parseFloat(distance_m)); 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) { public static Distance ofMile(double distance_mile) {
return of(distance_mile * UnitConversions.MI_TO_M); return of(distance_mile * UnitConversions.MI_TO_M);
} }
@@ -34,11 +45,14 @@ public class Distance {
return of(0.1 * distance_dm); return of(0.1 * distance_dm);
} }
public static Distance one(boolean metricUnit) { public static Distance one(UnitSystem unitSystem) {
if (metricUnit) { switch (unitSystem) {
case METRIC:
return Distance.ofKilometer(1); return Distance.ofKilometer(1);
} else { case IMPERIAL:
return Distance.ofMile(1); return Distance.ofMile(1);
default:
throw new RuntimeException("Not implemented");
} }
} }
@@ -100,12 +114,26 @@ public class Distance {
return toKM() * UnitConversions.KM_TO_MI; return toKM() * UnitConversions.KM_TO_MI;
} }
public double toKM_Miles(boolean metricUnit) { public double toKM_Miles(UnitSystem unitSystem) {
return metricUnit ? toKM() : toMI(); switch (unitSystem) {
case METRIC:
return toKM();
case IMPERIAL:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
} }
public double toM_FT(boolean metricUnit) { public double toM_FT(UnitSystem unitSystem) {
return metricUnit ? toM() : toFT(); switch (unitSystem) {
case METRIC:
return toM();
case IMPERIAL:
return toFT();
default:
throw new RuntimeException("Not implemented");
}
} }
@Override @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 java.util.Objects;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
public class Speed { public class Speed {
@@ -92,35 +93,37 @@ public class Speed {
return toKMH() * UnitConversions.KM_TO_MI; return toKMH() * UnitConversions.KM_TO_MI;
} }
public Duration toPace(boolean metricUnit) { public Duration toPace(UnitSystem unitSystem) {
if (isZero()) { if (isZero()) {
return Duration.ofSeconds(0); 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)); return Duration.ofSeconds(Math.round(1 / distance));
} }
public double to(boolean metricUnit) { public double to(UnitSystem unitSystem) {
return to(metricUnit ? Unit.KMH : Unit.MPH); switch (unitSystem) {
} case METRIC:
public double to(Unit unit) {
switch (unit) {
case KMH:
return toKMH(); return toKMH();
case MPH: case IMPERIAL:
return toMPH(); return toMPH();
default: default:
throw new RuntimeException("Not implemented"); throw new RuntimeException("Not implemented");
} }
} }
public enum Unit {
KMH,
MPH,
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; 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; package de.dennisguse.opentracks.data.models;
import android.location.Location; import android.location.Location;
import android.os.Build;
import android.os.Parcel; import android.os.Parcel;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -44,6 +45,7 @@ public class TrackPoint {
private Double latitude; private Double latitude;
private Double longitude; private Double longitude;
private Distance horizontalAccuracy; private Distance horizontalAccuracy;
private Distance verticalAccuracy;
private Altitude altitude; private Altitude altitude;
private Speed speed; private Speed speed;
private Float bearing; private Float bearing;
@@ -205,6 +207,9 @@ public class TrackPoint {
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null; this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null; this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : 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? //TODO Should we copy the bearing?
return this; return this;
@@ -309,6 +314,19 @@ public class TrackPoint {
return this; return this;
} }
public boolean hasVerticalAccuracy() {
return verticalAccuracy != null;
}
public Distance getVerticalAccuracy() {
return verticalAccuracy;
}
public TrackPoint setVerticalAccuracy(Distance horizontalAccuracy) {
this.verticalAccuracy = horizontalAccuracy;
return this;
}
@NonNull @NonNull
public Distance distanceToPrevious(@NonNull TrackPoint previous) { public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) { if (hasSensorDistance()) {
@@ -46,6 +46,7 @@ public interface TrackPointsColumns extends BaseColumns {
String TIME = "time"; String TIME = "time";
String ALTITUDE = "elevation"; String ALTITUDE = "elevation";
String HORIZONTAL_ACCURACY = "accuracy"; String HORIZONTAL_ACCURACY = "accuracy";
String VERTICAL_ACCURACY = "accuracy_vertical";
String SPEED = "speed"; String SPEED = "speed";
String BEARING = "bearing"; String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate"; String SENSOR_HEARTRATE = "sensor_heartrate";
@@ -79,6 +80,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ ALTITUDE_LOSS + " FLOAT, " + ALTITUDE_LOSS + " FLOAT, "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1, 2)), " + TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1, 2)), "
+ SENSOR_DISTANCE + " FLOAT, " + SENSOR_DISTANCE + " FLOAT, "
+ VERTICAL_ACCURACY + " FLOAT, "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE" + "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.TrackRecordedActivity;
import de.dennisguse.opentracks.adapters.StatisticsAdapter; import de.dennisguse.opentracks.adapters.StatisticsAdapter;
import de.dennisguse.opentracks.data.ContentProviderUtils; 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.data.models.Track;
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding; import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
@@ -79,7 +82,7 @@ public class StatisticsRecordedFragment extends Fragment {
private StatisticsRecordedBinding viewBinding; private StatisticsRecordedBinding viewBinding;
private boolean preferenceMetricUnits; private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean preferenceReportSpeed; private boolean preferenceReportSpeed;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { 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)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true; updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(); unitSystem = PreferencesUtils.getUnitSystem();
} }
if (PreferencesUtils.isKey(R.string.stats_rate_key, key) && track != null) { if (PreferencesUtils.isKey(R.string.stats_rate_key, key) && track != null) {
@@ -186,7 +189,9 @@ public class StatisticsRecordedFragment extends Fragment {
TrackStatistics trackStatistics = track.getTrackStatistics(); TrackStatistics trackStatistics = track.getTrackStatistics();
// Set total distance // 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.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second); viewBinding.statsDistanceUnit.setText(parts.second);
@@ -204,11 +209,12 @@ public class StatisticsRecordedFragment extends Fragment {
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime())); viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
} }
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(preferenceReportSpeed).build(getContext());
// Set average speed/pace // Set average speed/pace
{ {
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace); viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageSpeed(), preferenceMetricUnits, preferenceReportSpeed); Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.getAverageSpeed());
viewBinding.statsAverageSpeedValue.setText(parts.first); viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second); 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); 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.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second); 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); 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.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second); viewBinding.statsMovingSpeedUnit.setText(parts.second);
} }
@@ -238,11 +244,11 @@ public class StatisticsRecordedFragment extends Fragment {
Pair<String, String> parts; Pair<String, String> parts;
parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeGain_m, preferenceMetricUnits); parts = StringUtils.getAltitudeParts(getContext(), altitudeGain_m, unitSystem);
viewBinding.statsAltitudeGainValue.setText(parts.first); viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second); 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.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second); 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.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout; import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.viewmodels.StatisticData; import de.dennisguse.opentracks.viewmodels.StatisticData;
import de.dennisguse.opentracks.viewmodels.StatisticsDataModel; import de.dennisguse.opentracks.viewmodels.StatisticsDataModel;
@@ -55,14 +56,14 @@ public class StatisticsRecordingFragment extends Fragment {
private StatisticsDataModel viewModel; private StatisticsDataModel viewModel;
private LiveData<List<StatisticData>> statisticsLiveData; private LiveData<List<StatisticData>> statisticsLiveData;
private boolean preferenceMetricUnits; private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false; boolean updateUInecessary = false;
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true; 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)) { 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() { private void updateUI() {
if (isResumed()) { 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.R;
import de.dennisguse.opentracks.data.ContentProviderUtils; 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.GPXTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter; import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter; import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
@@ -100,6 +101,23 @@ public enum TrackFileFormat {
public String getExtension() { public String getExtension() {
return "gpx"; 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"; 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.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator; 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.Marker;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
@@ -128,6 +129,7 @@ public class GPXTrackExporter implements TrackExporter {
private void writeTrackPoints(Track track) throws InterruptedException { private void writeTrackPoints(Track track) throws InterruptedException {
boolean wroteTrack = false; boolean wroteTrack = false;
boolean wroteSegment = false; boolean wroteSegment = false;
Distance trackDistance = Distance.of(0);
LinkedList<TrackPoint> sensorPoints = new LinkedList<>(); LinkedList<TrackPoint> sensorPoints = new LinkedList<>();
@@ -159,7 +161,7 @@ public class GPXTrackExporter implements TrackExporter {
writeOpenSegment(); writeOpenSegment();
wroteSegment = true; wroteSegment = true;
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints); trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear(); sensorPoints.clear();
break; break;
case SENSORPOINT: case SENSORPOINT:
@@ -172,7 +174,7 @@ public class GPXTrackExporter implements TrackExporter {
wroteSegment = true; wroteSegment = true;
} }
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints); trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear(); sensorPoints.clear();
break; break;
default: default:
@@ -225,6 +227,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\""); printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\""); 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: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("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
printWriter.println("xsi:schemaLocation=" + printWriter.println("xsi:schemaLocation=" +
"\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" "\"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/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/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd"
+ " http://www.garmin.com/xmlschemas/TrackStatsExtension/v1" + " 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\">"); + " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
} }
} }
@@ -312,7 +316,9 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("</trkseg>"); 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) { if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">"); 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"); 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) { 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()) { if (!trackPointExtensionContent.isEmpty()) {
@@ -366,6 +380,10 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("</trkpt>"); printWriter.println("</trkpt>");
} }
if (cumulativeDistance != null) {
return cumulativeDistance;
}
return Distance.of(0);
} }
private Double cumulateSensorData(TrackPoint trackPoint, List<TrackPoint> sensorPoints, Function<TrackPoint, Double> map) { 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_POWER = "power";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_GAIN = "elevation_gain"; 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_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 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"; 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> heartRateList = new ArrayList<>();
private final List<Float> altitudeGainList = new ArrayList<>(); private final List<Float> altitudeGainList = new ArrayList<>();
private final List<Float> altitudeLossList = 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) { public KMLTrackExporter(Context context, boolean exportPhotos) {
this.context = context; this.context = context;
@@ -333,6 +337,8 @@ public class KMLTrackExporter implements TrackExporter {
heartRateList.clear(); heartRateList.clear();
altitudeGainList.clear(); altitudeGainList.clear();
altitudeLossList.clear(); altitudeLossList.clear();
accuracyHorizontal.clear();
accuracyVertical.clear();
} }
} }
@@ -362,6 +368,12 @@ public class KMLTrackExporter implements TrackExporter {
if (altitudeLossList.stream().anyMatch(Objects::nonNull)) { if (altitudeLossList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(altitudeLossList, EXTENDED_DATA_TYPE_ALTITUDE_LOSS); 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("</SchemaData>");
printWriter.println("</ExtendedData>"); printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>"); printWriter.println("</gx:Track>");
@@ -387,6 +399,8 @@ public class KMLTrackExporter implements TrackExporter {
altitudeGainList.add(trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : null); altitudeGainList.add(trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : null);
altitudeLossList.add(trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : 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_GAIN = "opentracks:gain";
private static final String TAG_EXTENSION_LOSS = "opentracks:loss"; private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance"; 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 Locator locator;
private final Context context; private final Context context;
@@ -106,12 +107,14 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private String heartrate; private String heartrate;
private String cadence; private String cadence;
private String power; private String power;
protected String markerType; private String markerType;
protected String photoUrl; private String photoUrl;
protected String uuid; private String uuid;
protected String gain; private String gain;
protected String loss; private String loss;
protected String sensorDistance; private String sensorDistance;
private String accuracyHorizontal;
private String accuracyVertical;
private final LinkedList<TrackPoint> currentSegment = new LinkedList<>(); private final LinkedList<TrackPoint> currentSegment = new LinkedList<>();
@@ -240,6 +243,16 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorDistance = content.trim(); sensorDistance = content.trim();
} }
break; 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 = ""; 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); 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; return trackPoint;
} }
@@ -357,6 +384,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
loss = null; loss = null;
sensorDistance = null; sensorDistance = null;
accuracyHorizontal = null;
accuracyVertical = null;
power = null; power = null;
heartrate = null; heartrate = null;
cadence = 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> sensorPowerList = new ArrayList<>();
private final ArrayList<Float> altitudeGainList = new ArrayList<>(); private final ArrayList<Float> altitudeGainList = new ArrayList<>();
private final ArrayList<Float> altitudeLossList = 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<>(); private final ArrayList<Marker> markers = new ArrayList<>();
@@ -294,6 +296,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorPowerList.clear(); sensorPowerList.clear();
altitudeGainList.clear(); altitudeGainList.clear();
altitudeLossList.clear(); altitudeLossList.clear();
accuracyHorizontal.clear();
accuracyVertical.clear();
} }
private void onTrackSegmentEnd() { private void onTrackSegmentEnd() {
@@ -333,6 +337,12 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
if (i < altitudeLossList.size()) { if (i < altitudeLossList.size()) {
trackPoint.setAltitudeLoss(altitudeLossList.get(i)); 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. // Update TrackPoint type for START / STOP.
TrackPoint.Type type = trackPoint.getType(); TrackPoint.Type type = trackPoint.getType();
@@ -430,6 +440,12 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS: case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS:
altitudeLossList.add(value); altitudeLossList.add(value);
break; break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL:
accuracyHorizontal.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL:
accuracyVertical.add(value);
break;
default: default:
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported."); Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
} }
@@ -107,7 +107,6 @@ public abstract class BluetoothConnectionManager<DataType> {
@Override @Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) { public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress()); Log.d(TAG, "Received data from " + gatt.getDevice().getAddress());
SensorData<DataType> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic); SensorData<DataType> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) { if (sensorData != null) {
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData); Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
@@ -165,6 +165,11 @@ public class BluetoothUtils {
index = 3; index = 3;
if (valueLength - index >= 1) { if (valueLength - index >= 1) {
cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index)); 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; index = 4;
@@ -98,14 +98,14 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
} }
void pause(TrackPointCreator trackPointCreator) { void pause(TrackPointCreator trackPointCreator) {
insertTrackPoint(trackPointCreator.createSegmentEnd()); insertTrackPoint(trackPointCreator.createSegmentEnd(), true);
reset(); reset();
} }
void end(TrackPointCreator trackPointCreator) { void end(TrackPointCreator trackPointCreator) {
TrackPoint segmentEnd = trackPointCreator.createSegmentEnd(); TrackPoint segmentEnd = trackPointCreator.createSegmentEnd();
insertTrackPoint(segmentEnd); insertTrackPoint(segmentEnd, true);
trackId = null; trackId = null;
trackStatisticsUpdater = null; trackStatisticsUpdater = null;
@@ -172,12 +172,12 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
// Always insert the first segment location // Always insert the first segment location
if (lastStoredTrackPoint == null) { if (lastStoredTrackPoint == null) {
insertTrackPoint(trackPoint); insertTrackPoint(trackPoint, true);
return true; return true;
} }
if (trackPoint.hasLocation() && lastStoredTrackPointWithLocation == null) { if (trackPoint.hasLocation() && lastStoredTrackPointWithLocation == null) {
insertTrackPoint(trackPoint); insertTrackPoint(trackPoint, true);
return true; return true;
} }
@@ -195,19 +195,19 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) { if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC); trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(trackPoint); insertTrackPoint(trackPoint, true);
return true; return true;
} }
if (distanceToLastStoredTrackPoint.greaterOrEqualThan(recordingDistanceInterval) if (distanceToLastStoredTrackPoint.greaterOrEqualThan(recordingDistanceInterval)
&& trackPoint.isMoving()) { && trackPoint.isMoving()) {
insertTrackPoint(trackPoint); insertTrackPoint(trackPoint, false);
return true; return true;
} }
if (trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) { if (trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) {
// Moving from non-moving to moving or vice versa; required to compute moving time correctly. // Moving from non-moving to moving or vice versa; required to compute moving time correctly.
insertTrackPoint(trackPoint); insertTrackPoint(trackPoint, true);
return true; return true;
} }
@@ -221,8 +221,8 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
return trackStatisticsUpdater.getTrackStatistics(); return trackStatisticsUpdater.getTrackStatistics();
} }
private void insertTrackPoint(@NonNull TrackPoint trackPoint) { private void insertTrackPoint(@NonNull TrackPoint trackPoint, boolean storeLastTrackPointIfUseful) {
if (lastTrackPoint != null) { if (storeLastTrackPointIfUseful && lastTrackPoint != null) {
if (lastStoredTrackPoint != null && lastTrackPoint.getTime().equals(lastStoredTrackPoint.getTime())) { if (lastStoredTrackPoint != null && lastTrackPoint.getTime().equals(lastStoredTrackPoint.getTime())) {
// Do not insert if inserted already // Do not insert if inserted already
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid trackId point time."); 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. // Remove the sensorDistance from trackPoint that is already going be stored with lastTrackPoint.
trackPoint.minusCumulativeSensorData(lastTrackPoint); trackPoint.minusCumulativeSensorData(lastTrackPoint);
} }
lastTrackPoint = null;
} }
lastTrackPoint = null;
insertTrackPointHelper(trackPoint); insertTrackPointHelper(trackPoint);
} }
@@ -46,7 +46,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private static final String TAG = TrackRecordingService.class.getSimpleName(); 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 RecordingStatus STATUS_DEFAULT = RecordingStatus.notRecording();
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null); public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
@@ -120,11 +120,12 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
@Override @Override
public void onDestroy() { public void onDestroy() {
trackPointCreator.stop();
trackPointCreator = null;
handler.removeCallbacksAndMessages(null); //Some tests do not finish the recording completely handler.removeCallbacksAndMessages(null); //Some tests do not finish the recording completely
handler = null; handler = null;
trackPointCreator.stop();
trackPointCreator = null;
trackRecordingManager.stop(); trackRecordingManager.stop();
trackRecordingManager = null; trackRecordingManager = null;
@@ -312,7 +313,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private void showNotification(boolean isGpsStarted) { private void showNotification(boolean isGpsStarted) {
if (isRecording()) { if (isRecording()) {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this)); startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this, recordingStatus.getTrackId()));
} }
if (!isRecording() && isGpsStarted) { if (!isRecording() && isGpsStarted) {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this)); startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
@@ -9,19 +9,24 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity; import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.TrackRecordingActivity; import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.data.models.Distance; 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.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
/** /**
* Manages the content of the notification shown by {@link TrackRecordingService}. * 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 previousLocationWasAccurate = true;
private Boolean metricUnits = null; private UnitSystem unitSystem = null;
TrackRecordingServiceNotificationManager(Context context) { TrackRecordingServiceNotificationManager(Context context) {
PreferencesUtils.registerOnSharedPreferenceChangeListener(this); PreferencesUtils.registerOnSharedPreferenceChangeListener(this);
@@ -79,8 +84,12 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, Distance recordingGpsAccuracy) { void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, Distance recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none); String formattedAccuracy = context.getString(R.string.value_none);
DistanceFormatter formatter = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context);
if (trackPoint.hasHorizontalAccuracy()) { if (trackPoint.hasHorizontalAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getHorizontalAccuracy(), metricUnits); formattedAccuracy = formatter.formatDistance(trackPoint.getHorizontalAccuracy());
boolean currentLocationWasAccurate = trackPoint.getHorizontalAccuracy().lessThan(recordingGpsAccuracy); boolean currentLocationWasAccurate = trackPoint.getHorizontalAccuracy().lessThan(recordingGpsAccuracy);
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate; boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
@@ -88,17 +97,18 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
previousLocationWasAccurate = currentLocationWasAccurate; previousLocationWasAccurate = currentLocationWasAccurate;
} }
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, StringUtils.formatDistance(context, trackStatistics.getTotalDistance(), metricUnits))); notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(trackStatistics.getTotalDistance())));
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, StringUtils.formatSpeed(context, trackPoint.getSpeed(), metricUnits, true))); 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)); notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification(); updateNotification();
notificationBuilder.setOnlyAlertOnce(true); notificationBuilder.setOnlyAlertOnce(true);
} }
Notification setRecording(Context context) { Notification setRecording(Context context, @NonNull Track.Id trackId) {
Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class); Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT; int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE; pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
@@ -140,8 +150,8 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
notificationManager.cancel(NOTIFICATION_ID); notificationManager.cancel(NOTIFICATION_ID);
} }
void setMetricUnits(boolean metricUnits) { void setUnitSystem(UnitSystem unitSystem) {
this.metricUnits = metricUnits; this.unitSystem = unitSystem;
} }
private void updateNotification() { private void updateNotification() {
@@ -155,7 +165,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, 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.Distance;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics; 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()); boolean isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
//TODO Do not load all trackpoints for every announcement //TODO Do not load all trackpoints for every announcement
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null); 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.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval(); IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null; SensorStatistics sensorStatistics = null;
@@ -178,7 +179,7 @@ public class VoiceAnnouncement {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId()); 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. // 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"); 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.R;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics; import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -25,15 +26,37 @@ class VoiceAnnouncementUtils {
private 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(); SpannableStringBuilder builder = new SpannableStringBuilder();
Distance distance = trackStatistics.getTotalDistance(); Distance distance = trackStatistics.getTotalDistance();
Speed distancePerTime = trackStatistics.getAverageMovingSpeed(); Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null; 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 perUnitStringId;
int distanceId = isMetricUnits ? R.plurals.voiceDistanceKilometers : R.plurals.voiceDistanceMiles; 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)); builder.append(context.getString(R.string.total_distance));
long distanceIntegerPart = (long) distanceInUnit; long distanceIntegerPart = (long) distanceInUnit;
@@ -41,7 +64,7 @@ class VoiceAnnouncementUtils {
String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2); String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2);
// Units should always be english singular for TTS. // Units should always be english singular for TTS.
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE // 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 // Punctuation helps introduce natural pauses in TTS
builder.append("."); builder.append(".");
if (distance.isZero()) { if (distance.isZero()) {
@@ -56,19 +79,18 @@ class VoiceAnnouncementUtils {
} }
if (isReportSpeed) { if (isReportSpeed) {
int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour; double speedInUnit = distancePerTime.to(unitSystem);
double speedInUnit = distancePerTime.to(isMetricUnits);
builder.append(" ") builder.append(" ")
.append(context.getString(R.string.speed)); .append(context.getString(R.string.speed));
long speedIntegerPart = (long) speedInUnit; long speedIntegerPart = (long) speedInUnit;
// Extract the decimal part // Extract the decimal part
String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2); 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("."); builder.append(".");
if (currentDistancePerTime != null) { if (currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(isMetricUnits); double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) { if (currentDistancePerTimeInUnit > 0) {
@@ -77,12 +99,12 @@ class VoiceAnnouncementUtils {
long currentDistanceIntegerPart = (long) currentDistancePerTimeInUnit; long currentDistanceIntegerPart = (long) currentDistancePerTimeInUnit;
// Extract the decimal part // Extract the decimal part
String currentDistanceFractionalPart = String.format("%.1f", (currentDistancePerTimeInUnit - currentDistanceIntegerPart)).substring(2); 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("."); builder.append(".");
} }
} }
} else { } else {
Duration time = distancePerTime.toPace(isMetricUnits); Duration time = distancePerTime.toPace(unitSystem);
builder.append(" ") builder.append(" ")
.append(context.getString(R.string.pace)); .append(context.getString(R.string.pace));
appendDuration(context, builder, time); appendDuration(context, builder, time);
@@ -90,7 +112,7 @@ class VoiceAnnouncementUtils {
.append(context.getString(perUnitStringId)) .append(context.getString(perUnitStringId))
.append("."); .append(".");
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(isMetricUnits) : Duration.ofMillis(0); Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(unitSystem) : Duration.ofMillis(0);
if (!currentTime.isZero()) { if (!currentTime.isZero()) {
builder.append(" ") builder.append(" ")
.append(context.getString(R.string.lap_time)); .append(context.getString(R.string.lap_time));
@@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
@@ -109,25 +110,23 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
return; return;
} }
TrackPoint trackPoint = new TrackPoint(location, trackPointCreator.createNow());
boolean isAccurate = trackPoint.fulfillsAccuracy(thresholdHorizontalAccuracy);
boolean isValid = LocationUtils.isValidLocation(location);
if (gpsStatus != null) { 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); gpsStatus.onLocationChanged(trackPoint);
} }
if (!isValid) { if (!LocationUtils.isValidLocation(location)) {
Log.w(TAG, "Ignore newTrackPoint. location is invalid."); Log.w(TAG, "Ignore newTrackPoint. location is invalid.");
return; return;
} }
if (!isAccurate) { if (!LocationUtils.fulfillsAccuracy(location, thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy."); Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return; return;
} }
trackPointCreator.onNewTrackPoint(trackPoint); trackPointCreator.onChange(location);
} }
@Override @Override
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.services.handlers; package de.dennisguse.opentracks.services.handlers;
import android.content.Context; import android.content.Context;
import android.location.Location;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@@ -47,7 +48,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
this.gpsHandler = gpsHandler; this.gpsHandler = gpsHandler;
} }
public void start(@NonNull Context context) { public synchronized void start(@NonNull Context context) {
this.context = context; this.context = context;
gpsHandler.onStart(context); gpsHandler.onStart(context);
@@ -71,7 +72,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
gpsHandler.onStop(); gpsHandler.onStop();
} }
public void reset() { public synchronized void reset() {
if (remoteSensorManager == null || altitudeSumManager == null) { if (remoteSensorManager == null || altitudeSumManager == null) {
Log.d(TAG, "No recording running and no reset necessary."); Log.d(TAG, "No recording running and no reset necessary.");
return; return;
@@ -80,7 +81,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
altitudeSumManager.reset(); altitudeSumManager.reset();
} }
private SensorDataSet fill(TrackPoint trackPoint) { private SensorDataSet addSensorData(TrackPoint trackPoint) {
if (!isStarted()) { if (!isStarted()) {
Log.w(TAG, "Not started, should not be called."); Log.w(TAG, "Not started, should not be called.");
return null; return null;
@@ -98,7 +99,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
return sensorDataSet; return sensorDataSet;
} }
public void stop() { public synchronized void stop() {
gpsHandler.onStop(); gpsHandler.onStop();
if (remoteSensorManager != null) { if (remoteSensorManager != null) {
@@ -114,16 +115,21 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
this.context = null; 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. * Got a new TrackPoint from Bluetooth only; contains no GPS location.
*/ */
@Override @Override
public void onChange(SensorDataSet sensorDataSet) { public synchronized void onChange(@NonNull SensorDataSet unused) {
onNewTrackPoint(new TrackPoint(TrackPoint.Type.SENSORPOINT, createNow())); onNewTrackPoint(new TrackPoint(TrackPoint.Type.SENSORPOINT, createNow()));
} }
public synchronized void onNewTrackPoint(@NonNull TrackPoint trackPoint) { @VisibleForTesting
fill(trackPoint); public void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
addSensorData(trackPoint);
boolean stored = service.newTrackPoint(trackPoint, gpsHandler.getThresholdHorizontalAccuracy()); boolean stored = service.newTrackPoint(trackPoint, gpsHandler.getThresholdHorizontalAccuracy());
if (stored) { if (stored) {
@@ -131,13 +137,13 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
} }
} }
public TrackPoint createSegmentStartManual() { public synchronized TrackPoint createSegmentStartManual() {
return TrackPoint.createSegmentStartManualWithTime(createNow()); return TrackPoint.createSegmentStartManualWithTime(createNow());
} }
public TrackPoint createSegmentEnd() { public synchronized TrackPoint createSegmentEnd() {
TrackPoint segmentEnd = TrackPoint.createSegmentEndWithTime(createNow()); TrackPoint segmentEnd = TrackPoint.createSegmentEndWithTime(createNow());
fill(segmentEnd); addSensorData(segmentEnd);
reset(); reset();
return segmentEnd; return segmentEnd;
} }
@@ -158,11 +164,12 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
currentTrackPoint.setLatitude(lastStoredTrackPointWithLocation.getLatitude()); currentTrackPoint.setLatitude(lastStoredTrackPointWithLocation.getLatitude());
} }
SensorDataSet sensorDataSet = fill(currentTrackPoint); SensorDataSet sensorDataSet = addSensorData(currentTrackPoint);
return new Pair<>(currentTrackPoint, sensorDataSet); return new Pair<>(currentTrackPoint, sensorDataSet);
} }
@VisibleForTesting
Instant createNow() { Instant createNow() {
return Instant.now(clock); return Instant.now(clock);
} }
@@ -64,10 +64,23 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
} }
private void updateUnits() { private void updateUnits() {
boolean metricUnits = PreferencesUtils.isMetricUnits(); UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key)); 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); statsRatePreferences.setEntries(entries);
HackUtils.invalidatePreference(statsRatePreferences); HackUtils.invalidatePreference(statsRatePreferences);
@@ -9,7 +9,7 @@ import java.time.Duration;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance; 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 { public class GpsSettingsFragment extends PreferenceFragmentCompat {
@@ -17,30 +17,34 @@ public class GpsSettingsFragment extends PreferenceFragmentCompat {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_gps); 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)) findPreference(getString(R.string.recording_distance_interval_key))
.setSummaryProvider( .setSummaryProvider(
preference -> { preference -> {
boolean metricUnits = PreferencesUtils.isMetricUnits();
Distance distance = PreferencesUtils.getRecordingDistanceInterval(); 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)) findPreference(getString(R.string.max_recording_distance_key))
.setSummaryProvider( .setSummaryProvider(
preference -> { preference -> {
boolean metricUnits = PreferencesUtils.isMetricUnits();
Distance distance = PreferencesUtils.getMaxRecordingDistance(); 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)) findPreference(getString(R.string.recording_gps_accuracy_key))
.setSummaryProvider( .setSummaryProvider(
preference -> { preference -> {
boolean metricUnits = PreferencesUtils.isMetricUnits();
Distance distance = PreferencesUtils.getThresholdHorizontalAccuracy(); 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 android.os.Bundle;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
@@ -12,16 +13,20 @@ import java.util.Locale;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
public class ImportExportSettingsFragment extends PreferenceFragmentCompat { public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
private static final String TAG = ImportExportSettingsFragment.class.getSimpleName();
@Override @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_import_export); addPreferencesFromResource(R.xml.settings_import_export);
setExportTrackFileFormatOptions(); setExportTrackFileFormatOptions();
setExportDirectorySummary(); setExportDirectorySummary();
setFilenameTemplate();
} }
@Override @Override
@@ -45,7 +50,8 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA,
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA, TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
TrackFileFormat.GPX TrackFileFormat.GPX,
TrackFileFormat.CSV
}; };
String[] entries = new String[trackFileFormats.length]; String[] entries = new String[trackFileFormats.length];
String[] entryValues = 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)); 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.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance; 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.Speed;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.io.file.TrackFileFormat; 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.CsvLayoutUtils;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout; import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.util.TrackIconUtils; 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)); return getBoolean(R.string.publicapi_dashboard_enabled_key, resources.getBoolean(R.bool.publicapi_dashboard_enabled_default));
} }
public static boolean isMetricUnits() { public static UnitSystem getUnitSystem() {
final String STATS_UNIT = resources.getString(R.string.stats_units_default); final String STATS_UNIT_DEFAULT = resources.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(R.string.stats_units_key, STATS_UNIT));
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) { public static void setUnit(UnitSystem unitSystem) {
String unit; setString(R.string.stats_units_key, unitSystem.getPreferenceId());
if (metricUnits) { }
unit = resources.getString(R.string.stats_units_metric);
} else { //TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
unit = resources.getString(R.string.stats_units_imperial); 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) { 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() { public static Distance getVoiceAnnouncementDistance() {
final float DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_distance_default)); final float DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_distance_default));
float value = getFloat(R.string.voice_announcement_distance_key, 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() { static String[] getVoiceAnnouncementDistanceEntries() {
String[] values = resources.getStringArray(R.array.voice_announcement_distance_values); String[] values = resources.getStringArray(R.array.voice_announcement_distance_values);
String[] options = new String[values.length]; 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++) { for (int i = 0; i < values.length; i++) {
if (resources.getString(R.string.announcement_off).equals(values[i])) { if (resources.getString(R.string.announcement_off).equals(values[i])) {
options[i] = resources.getString(R.string.value_off); options[i] = resources.getString(R.string.value_off);
} else { } else {
int value = Integer.parseInt(values[i]); Distance distance = Distance.one(unitSystem).multipliedBy(Double.parseDouble(values[i]));
options[i] = resources.getString(metricUnits ? R.string.value_integer_kilometer : R.string.value_integer_mile, value); options[i] = formatter.formatDistance(distance);
} }
} }
return options; return options;
@@ -349,26 +368,34 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length]; String[] entries = new String[entryValues.length];
final int recordingDistanceIntervalDefault = (int) getRecordingDistanceIntervalDefault().toM(); 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++) { for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]); int value = Integer.parseInt(entryValues[i]);
String displayValue; Distance distance = Distance.of(1).multipliedBy(value);
if (metricUnits) {
displayValue = resources.getString(R.string.value_integer_meter, value); String displayValue = formatter.formatDistance(distance);
switch (unitSystem) {
case METRIC:
if (value == recordingDistanceIntervalDefault) { if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value); entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
} else { break;
int feet = (int) (value * UnitConversions.M_TO_FT); case IMPERIAL:
displayValue = resources.getString(R.string.value_integer_feet, feet);
if (value == recordingDistanceIntervalDefault) { if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet); entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break;
default:
throw new RuntimeException("Not implemented");
} }
} }
@@ -385,26 +412,34 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length]; String[] entries = new String[entryValues.length];
final int maxRecordingDistanceDefault = Integer.parseInt(resources.getString(R.string.max_recording_distance_default)); 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++) { for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]); int value = Integer.parseInt(entryValues[i]);
String displayValue; Distance distance = Distance.of(1).multipliedBy(value);
if (metricUnits) {
displayValue = resources.getString(R.string.value_integer_meter, value); String displayValue = formatter.formatDistance(distance);
switch (unitSystem) {
case METRIC:
if (value == maxRecordingDistanceDefault) { if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value); entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
} else { break;
int feet = (int) (value * UnitConversions.M_TO_FT); case IMPERIAL:
displayValue = resources.getString(R.string.value_integer_feet, feet);
if (value == maxRecordingDistanceDefault) { if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet); entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break;
default:
throw new RuntimeException("Not implemented");
} }
} }
@@ -449,13 +484,20 @@ public class PreferencesUtils {
final int recordingGPSAccuracyExcellent = Integer.parseInt(resources.getString(R.string.recording_gps_accuracy_excellent)); 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)); 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++) { for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]); int value = Integer.parseInt(entryValues[i]);
String displayValue; Distance distance = Distance.of(1).multipliedBy(value);
if (metricUnits) {
displayValue = resources.getString(R.string.value_integer_meter, value); String displayValue = formatter.formatDistance(distance);
switch (unitSystem) {
case METRIC:
if (value == recordingGPSAccuracyDefault) { if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value); entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else if (value == recordingGPSAccuracyExcellent) { } else if (value == recordingGPSAccuracyExcellent) {
@@ -465,17 +507,18 @@ public class PreferencesUtils {
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
} else { break;
int feet = (int) (value * UnitConversions.M_TO_FT); case IMPERIAL:
displayValue = resources.getString(R.string.value_integer_feet, feet);
if (value == recordingGPSAccuracyDefault) { if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet); entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else if (value == recordingGPSAccuracyExcellent) { } else if (value == recordingGPSAccuracyExcellent) {
entries[i] = resources.getString(R.string.value_integer_feet_excellent_gps, feet); entries[i] = resources.getString(R.string.value_integer_feet_excellent_gps, (int) distance.toFT());
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break;
default:
throw new RuntimeException("Not implemented");
} }
} }
@@ -495,17 +538,20 @@ public class PreferencesUtils {
final float idleSpeedDefault = Float.parseFloat(resources.getString(R.string.idle_speed_default)); 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++) { for (int i = 0; i < entryValues.length; i++) {
float value = Float.parseFloat(entryValues[i]); float value = Float.parseFloat(entryValues[i]);
if (metricUnits) {
switch (unitSystem) {
case METRIC:
if (value == idleSpeedDefault) { if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value); entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
} else { } else {
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value); entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
} }
} else { break;
case IMPERIAL:
double valueMPH = Speed.ofKMH(value).toMPH(); double valueMPH = Speed.ofKMH(value).toMPH();
if (value == idleSpeedDefault) { if (value == idleSpeedDefault) {
@@ -513,6 +559,9 @@ public class PreferencesUtils {
} else { } else {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH); 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(); 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() { public static TrackFileFormat getExportTrackFileFormat() {
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(R.string.export_trackfileformat_default, null); final String TRACKFILEFORMAT_NAME_DEFAULT = getString(R.string.export_trackfileformat_default, null);
String trackFileFormatName = getString(R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT); 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.R;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed; 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.Track;
import de.dennisguse.opentracks.data.models.UnitConversions; import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
@@ -209,8 +211,8 @@ public class DescriptionGenerator {
*/ */
@VisibleForTesting @VisibleForTesting
void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) { void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false); Pair<String, String> paceInMetrics = SpeedFormatter.Builder().setUnit(UnitSystem.METRIC).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false); 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 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); 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.ShareContentProvider;
import de.dennisguse.opentracks.data.models.Marker; import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class ShareUtils { public class ShareUtils {
@@ -54,8 +54,9 @@ public class ShareUtils {
continue; continue;
} }
String trackName = FileUtils.sanitizeFileName(track.getName()); TrackFileFormat format = PreferencesUtils.getExportTrackFileFormat();
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, PreferencesUtils.getExportTrackFileFormat()); String trackName = PreferencesUtils.getTrackFileformatGenerator().format(track, format);
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, format);
uris.add(uriAndMime.first); uris.add(uriAndMime.first);
mime = uriAndMime.second; mime = uriAndMime.second;
@@ -13,7 +13,10 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; 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.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -97,7 +100,7 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
private final TextView maxSpeedUnit; private final TextView maxSpeedUnit;
private final TextView maxSpeedLabel; private final TextView maxSpeedLabel;
private boolean metricsUnits; private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean reportSpeed; private boolean reportSpeed;
public ViewHolder(View view) { public ViewHolder(View view) {
@@ -118,15 +121,17 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) { public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
setCommonValues(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); avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second); avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_speed)); 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); maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second); maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(context.getString(R.string.stats_max_speed)); maxSpeedLabel.setText(context.getString(R.string.stats_max_speed));
@@ -134,16 +139,18 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
} }
public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) { public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
setCommonValues(aggregatedStatistic); 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); avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second); avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_pace)); 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); maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second); maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(R.string.stats_fastest_pace); maxSpeedLabel.setText(R.string.stats_fastest_pace);
@@ -155,13 +162,15 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
String category = aggregatedStatistic.getCategory(); String category = aggregatedStatistic.getCategory();
reportSpeed = PreferencesUtils.isReportSpeed(category); reportSpeed = PreferencesUtils.isReportSpeed(category);
metricsUnits = PreferencesUtils.isMetricUnits(); unitSystem = PreferencesUtils.getUnitSystem();
sportIcon.setImageResource(getIcon(aggregatedStatistic)); sportIcon.setImageResource(getIcon(aggregatedStatistic));
typeLabel.setText(category); typeLabel.setText(category);
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStatistic.getCountTracks()))); 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); distance.setText(parts.first);
distanceUnit.setText(parts.second); distanceUnit.setText(parts.second);
@@ -13,6 +13,9 @@ import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance; 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; import de.dennisguse.opentracks.util.StringUtils;
public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { 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 List<IntervalStatistics.Interval> intervalList;
private final Context context; private final Context context;
private final StackMode stackMode; private final StackMode stackMode;
private boolean metricUnits; private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean isReportSpeed; private boolean isReportSpeed;
public IntervalStatisticsAdapter(Context context, StackMode stackMode, boolean metricUnits, boolean isReportSpeed) { public IntervalStatisticsAdapter(Context context, StackMode stackMode, UnitSystem unitSystem, boolean isReportSpeed) {
this.metricUnits = metricUnits; this.unitSystem = unitSystem;
this.context = context; this.context = context;
this.stackMode = stackMode; this.stackMode = stackMode;
this.isReportSpeed = isReportSpeed; this.isReportSpeed = isReportSpeed;
@@ -54,12 +57,15 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
} else { } else {
sumDistance = interval.getDistance().multipliedBy(nextPosition); 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.gain.setText(StringUtils.formatAltitude(context, interval.getGain_m(), unitSystem));
viewHolder.loss.setText(StringUtils.formatAltitudeChange(context, interval.getLoss_m(), metricUnits)); viewHolder.loss.setText(StringUtils.formatAltitude(context, interval.getLoss_m(), unitSystem));
} }
@@ -71,8 +77,8 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
return intervalList.size(); return intervalList.size();
} }
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) { public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, UnitSystem unitSystem, boolean isReportSpeed) {
this.metricUnits = metricUnits; this.unitSystem = unitSystem;
this.isReportSpeed = isReportSpeed; this.isReportSpeed = isReportSpeed;
intervalList = data; 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.Track;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns; import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.settings.UnitSystem;
/** /**
* This model is used to load intervals for a track. * This model is used to load intervals for a track.
@@ -64,14 +65,14 @@ public class IntervalStatisticsModel extends AndroidViewModel {
handler = null; 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 (intervalsLiveData == null) {
if (interval == null) { if (interval == null) {
interval = IntervalOption.OPTION_1; interval = IntervalOption.OPTION_1;
} }
intervalsLiveData = new MutableLiveData<>(); intervalsLiveData = new MutableLiveData<>();
distanceInterval = interval.getDistance(metricUnits); distanceInterval = interval.getDistance(unitSystem);
intervalStatistics = new IntervalStatistics(distanceInterval); intervalStatistics = new IntervalStatistics(distanceInterval);
loadIntervalStatistics(trackId); 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) { if (interval == null) {
interval = IntervalOption.OPTION_1; interval = IntervalOption.DEFAULT;
} }
lastTrackPointId = null; lastTrackPointId = null;
distanceInterval = interval.getDistance(metricUnits); distanceInterval = interval.getDistance(unitSystem);
intervalStatistics = new IntervalStatistics(distanceInterval); intervalStatistics = new IntervalStatistics(distanceInterval);
loadIntervalStatistics(trackId); loadIntervalStatistics(trackId);
} }
@@ -119,6 +120,8 @@ public class IntervalStatisticsModel extends AndroidViewModel {
* Intervals length this view model support. * Intervals length this view model support.
*/ */
public enum IntervalOption { public enum IntervalOption {
OPTION_0_1(0.1f),
OPTION_0_5(0.5f),
OPTION_1(1), OPTION_1(1),
OPTION_2(2), OPTION_2(2),
OPTION_3(3), OPTION_3(3),
@@ -128,25 +131,26 @@ public class IntervalStatisticsModel extends AndroidViewModel {
OPTION_20(20), OPTION_20(20),
OPTION_50(50); 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; this.multiplier = multiplier;
} }
public Distance getDistance(boolean metricUnits) { public Distance getDistance(UnitSystem unitSystem) {
return Distance return Distance
.one(metricUnits) .one(unitSystem)
.multipliedBy(multiplier); .multipliedBy(multiplier);
} }
public double getMultiplier() {
return multiplier;
}
public boolean sameMultiplier(IntervalOption intervalOption) { public boolean sameMultiplier(IntervalOption intervalOption) {
return intervalOption != null && this.multiplier == intervalOption.multiplier; 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 androidx.recyclerview.widget.LinearLayoutManager;
import java.util.List; import java.util.List;
import java.util.Objects;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.IntervalListViewBinding; import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
/** /**
* A fragment to display the intervals from recorded track. * A fragment to display the intervals from recorded track.
@@ -39,7 +42,7 @@ public class IntervalsFragment extends Fragment {
private IntervalStatisticsModel.IntervalOption selectedInterval; private IntervalStatisticsModel.IntervalOption selectedInterval;
private Track.Id trackId; private Track.Id trackId;
private boolean metricUnits; private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private IntervalStatisticsAdapter adapter; private IntervalStatisticsAdapter adapter;
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> intervalsAdapter; private ArrayAdapter<IntervalStatisticsModel.IntervalOption> intervalsAdapter;
@@ -49,7 +52,7 @@ public class IntervalsFragment extends Fragment {
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.stats_units_key, key) || PreferencesUtils.isKey(R.string.stats_rate_key, 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) { if (intervalsAdapter != null) {
intervalsAdapter.notifyDataSetChanged(); intervalsAdapter.notifyDataSetChanged();
} }
@@ -62,7 +65,8 @@ public class IntervalsFragment extends Fragment {
* @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. * @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 bundle = new Bundle();
bundle.putBoolean(FROM_TOP_TO_BOTTOM_KEY, fromTopToBottom); bundle.putBoolean(FROM_TOP_TO_BOTTOM_KEY, fromTopToBottom);
bundle.putParcelable(TRACK_ID_KEY, trackId); bundle.putParcelable(TRACK_ID_KEY, trackId);
@@ -93,25 +97,30 @@ public class IntervalsFragment extends Fragment {
return viewBinding.getRoot(); return viewBinding.getRoot();
} }
@Deprecated //TODO This method must be re-implemented.
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, 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())); viewBinding.intervalList.setLayoutManager(new LinearLayoutManager(getContext()));
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView); // TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
viewBinding.intervalList.setAdapter(adapter); 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()) { intervalsAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
@NonNull @NonNull
@Override @Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
TextView v = (TextView) super.getView(position, convertView, 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()))); IntervalStatisticsModel.IntervalOption option = getItem(position);
} else { String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
v.setText(getContext().getString(R.string.value_integer_mile, Integer.parseInt(v.getText().toString()))); v.setText(stringValue);
}
return v; return v;
} }
@@ -122,12 +131,15 @@ public class IntervalsFragment extends Fragment {
}; };
viewBinding.intervalsDropdown.setAdapter(intervalsAdapter); 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( //TODO This duplicates the intervalAdapter code
getContext().getString(R.string.value_integer_kilometer, Integer.parseInt(selectedInterval != null ? selectedInterval.toString() : IntervalStatisticsModel.IntervalOption.values()[0].toString())), setIntervalsDropdownText();
false });
);
//TODO This duplicates the intervalAdapter code
setIntervalsDropdownText();
} }
@Override @Override
@@ -180,19 +192,32 @@ public class IntervalsFragment extends Fragment {
} }
viewBinding.intervalRate.setText(isReportSpeed ? getString(R.string.stats_speed) : getString(R.string.stats_pace)); 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<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(trackId, unitSystem, selectedInterval);
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, metricUnits, isReportSpeed)); liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, unitSystem, isReportSpeed));
setIntervalsDropdownText();
} }
private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) { private void setIntervalsDropdownText() {
boolean update = metricUnits != this.metricUnits 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 == null
|| !selectedInterval.sameMultiplier(this.selectedInterval); || !selectedInterval.sameMultiplier(this.selectedInterval);
this.metricUnits = metricUnits; this.unitSystem = unitSystem;
this.selectedInterval = selectedInterval; this.selectedInterval = selectedInterval;
if (update && viewModel != null) { 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) { 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); Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
if (exportDocumentFileUri == null) { if (exportDocumentFileUri == null) {
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName); final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
@@ -119,10 +119,6 @@ public class ExportUtils {
return exportDocumentFileUri; 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) { private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
final ContentResolver resolver = context.getContentResolver(); final ContentResolver resolver = context.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri)); final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
@@ -17,6 +17,8 @@ package de.dennisguse.opentracks.util;
import android.location.Location; import android.location.Location;
import de.dennisguse.opentracks.data.models.Distance;
/** /**
* Utility class for decimating tracks at a given level of precision. * 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.getLatitude()) <= 90
&& Math.abs(location.getLongitude()) <= 180; && 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.R;
import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance; 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.HeartRate;
import de.dennisguse.opentracks.data.models.Power; import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.settings.UnitSystem;
/** /**
* Various string manipulation methods. * Various string manipulation methods.
@@ -128,32 +129,6 @@ public class StringUtils {
return TextUtils.split(value, ":").length == 2 ? "0:" + value : value; 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). * 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)); 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) { public static Pair<String, String> getHeartRateParts(Context context, HeartRate heartrate) {
String value = context.getString(R.string.value_unknown); String value = context.getString(R.string.value_unknown);
if (heartrate != null) { if (heartrate != null) {
@@ -340,35 +254,18 @@ public class StringUtils {
/** /**
* @return the formatted altitude_m (or null) and it's unit as {@link Pair} * @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) { public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) {
String formattedValue = context.getString(R.string.value_unknown); DistanceFormatter formatter = DistanceFormatter.Builder()
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet); .setDecimalCount(1)
.setUnit(unitSystem)
.build(context);
if (altitude_m != null) { Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of(Double.NaN); //TODO Refactor Double.NaN
double value = Distance.of(altitude_m).toM_FT(metricUnits); return formatter.getDistanceParts(distance);
formattedValue = StringUtils.formatDecimal(value, 1);
} }
return new Pair<>(formattedValue, unit); public static String formatAltitude(Context context, Float altitude_m, UnitSystem unitSystem) {
} Pair<String, String> altitudeParts = getAltitudeParts(context, altitude_m, unitSystem);
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);
return context.getString(R.string.altitude_with_unit, altitudeParts.first, altitudeParts.second); 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.R;
import de.dennisguse.opentracks.data.models.Cadence; 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.Speed;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.RecordingData; import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField; import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
@@ -24,17 +27,17 @@ import de.dennisguse.opentracks.util.StringUtils;
public class StatisticDataBuilder { 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() List<StatisticData> statisticDataList = layout.getFields().stream()
.filter(DataField::isVisible) .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) .filter(Objects::nonNull)
.collect(Collectors.toList()); .collect(Collectors.toList());
statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, metricUnits)); statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, unitSystem));
return statisticDataList; 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 TrackPoint latestTrackPoint = recordingData.getLatestTrackPoint();
final SensorDataSet sensorDataSet = recordingData.getSensorDataSet(); final SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
final TrackStatistics trackStatistics = recordingData.getTrackStatistics(); final TrackStatistics trackStatistics = recordingData.getTrackStatistics();
@@ -46,6 +49,10 @@ public class StatisticDataBuilder {
final String sensorUnknown = context.getString(R.string.value_unknown); 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))) { if (fieldKey.equals(context.getString(R.string.stats_custom_layout_total_time_key))) {
valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()), null); valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()), null);
title = context.getString(R.string.stats_total_time); title = context.getString(R.string.stats_total_time);
@@ -53,48 +60,51 @@ public class StatisticDataBuilder {
valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(trackStatistics.getMovingTime()), null); valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(trackStatistics.getMovingTime()), null);
title = context.getString(R.string.stats_moving_time); title = context.getString(R.string.stats_moving_time);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_distance_key))) { } 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); 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))) { } 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"); boolean reportSpeed = fieldKey.equals("speed");
title = reportSpeed ? context.getString(R.string.stats_speed) : context.getString(R.string.stats_pace); 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) { 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; description = sensorDataSet.getSpeed().second;
} else { } else {
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null; 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); description = context.getString(R.string.description_speed_source_gps);
} }
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_moving_speed_key))) { } 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); title = context.getString(R.string.stats_average_moving_speed);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_speed_key))) { } 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); title = context.getString(R.string.stats_average_speed);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_max_speed_key))) { } 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); title = context.getString(R.string.stats_max_speed);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_moving_pace_key))) { } 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); title = context.getString(R.string.stats_average_moving_pace);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_pace_key))) { } 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); title = context.getString(R.string.stats_average_pace);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_fastest_pace_key))) { } 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); title = context.getString(R.string.stats_fastest_pace);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_altitude_key))) { } else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_altitude_key))) {
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null; Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
title = context.getString(R.string.stats_altitude); title = context.getString(R.string.stats_altitude);
description = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? context.getString(latestTrackPoint.getAltitude().getLabelId()) : null; 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))) { } 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); title = context.getString(R.string.stats_gain);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_loss_key))) { } 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); title = context.getString(R.string.stats_loss);
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_coordinates_key))) { } else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_coordinates_key))) {
title = context.getString(R.string.stats_coordinates); 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. * 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<>(); List<StatisticData> sensorDataList = new ArrayList<>();
SensorDataSet sensorDataSet = recordingData.getSensorDataSet(); SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
if (sensorDataSet == null) { if (sensorDataSet == null) {
return sensorDataList; return sensorDataList;
} }
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_heart_rate))) && sensorDataSet.getHeartRate() != null) { 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) { 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()) { 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; return sensorDataList;
@@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.services.RecordingData; import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout; import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
public class StatisticsDataModel extends AndroidViewModel { public class StatisticsDataModel extends AndroidViewModel {
@@ -26,9 +27,9 @@ public class StatisticsDataModel extends AndroidViewModel {
return statsData; return statsData;
} }
public void update(RecordingData recordingData, Layout layout, boolean metricUnit) { public void update(RecordingData recordingData, Layout layout, UnitSystem unitSystem) {
new Thread(() -> { new Thread(() -> {
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, metricUnit); List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, unitSystem);
statsData.postValue(statisticDataList); statsData.postValue(statisticDataList);
}).start(); }).start();
} }
+3 -1
View File
@@ -14,10 +14,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/TextAppearance.OpenTracks.PrimaryValue" style="@style/TextAppearance.OpenTracks.PrimaryValue"
android:value="@string/value_unknown" android:value="@string/value_unknown"
android:format12Hour="hh:mm:ss a"
android:format24Hour="HH:mm:ss"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
tools:text="00:00" /> tools:text="00:00:00" />
<TextView <TextView
android:id="@+id/stats_description_main" 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_kilometer">د/كم</string>
<string name="unit_minute_per_mile">دقيقة/ميل</string> <string name="unit_minute_per_mile">دقيقة/ميل</string>
<!-- Value --> <!-- Value -->
<string name="value_integer_feet">%1$d قدم</string>
<string name="value_integer_feet_excellent_gps">%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_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_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_excellent_gps">%1$d م (إشارة نظام تحديد المواقع ممتازة)</string>
<string name="value_integer_meter_poor_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_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_mile_hour">%1$d‏‏ ميل/ساعة</string>
<string name="value_integer_minute">%1$d دقيقة</string> <string name="value_integer_minute">%1$d دقيقة</string>
<string name="value_integer_second">%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_mile_per_hour">mi/h</string>
<string name="unit_minute_per_kilometer">min/km</string> <string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/mi</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_excellent_gps">%1$d ft (excellent GPS)</string>
<string name="value_integer_feet_recommended">%1$d ft (recommended)</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_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_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_poor_gps">%1$d m (poor GPS)</string>
<string name="value_integer_meter_recommended">%1$d m (recommended)</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_mile_hour">%1$d mph</string>
<string name="value_integer_minute">%1$d min</string> <string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d s</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_mile_per_hour">millas/hora</string>
<string name="unit_minute_per_kilometer">min/km</string> <string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/milla</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_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_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_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_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_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_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_mile_hour">%1$d mi/h</string>
<string name="value_integer_minute">%1$d mín</string> <string name="value_integer_minute">%1$d mín</string>
<string name="value_integer_second">%1$d s</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_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_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="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> </resources>
-4
View File
@@ -259,16 +259,12 @@ limitations under the License.
<string name="unit_minute_per_kilometer">мин/км</string> <string name="unit_minute_per_kilometer">мин/км</string>
<string name="unit_minute_per_mile">мин/мил</string> <string name="unit_minute_per_mile">мин/мил</string>
<!-- Value --> <!-- 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_excellent_gps">%1$d фт (отличен GPS)</string>
<string name="value_integer_feet_recommended">%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_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_excellent_gps">%1$d м (отличен GPS)</string>
<string name="value_integer_meter_poor_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_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_mile_hour">%1$d мил/ч</string>
<string name="value_integer_minute">%1$d мин</string> <string name="value_integer_minute">%1$d мин</string>
<string name="value_integer_second">%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_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/milles</string> <string name="unit_minute_per_mile">min/milles</string>
<!-- Value --> <!-- 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_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_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_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_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_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_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_mile_hour">%1$d mi/h</string>
<string name="value_integer_minute">%1$d min</string> <string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d s</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_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/míle</string> <string name="unit_minute_per_mile">min/míle</string>
<!-- Value --> <!-- 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_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_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_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_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_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_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_mile_hour">%1$d mil/h</string>
<string name="value_integer_minute">%1$d min</string> <string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d s</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="speed">Rychlost</string>
<string name="total_distance">Celková vzdálenost</string> <string name="total_distance">Celková vzdálenost</string>
<plurals name="voiceDistanceKilometers"> <plurals name="voiceDistanceKilometers">
<item quantity="one"></item> <item quantity="one"/>
<item quantity="few"></item> <item quantity="few"/>
<item quantity="other"></item> <item quantity="other"/>
</plurals> </plurals>
<string name="average_heart_rate">Průměrný srdeční tep</string> <string name="average_heart_rate">Průměrný srdeční tep</string>
<string name="current_heart_rate">Aktuální 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_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_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="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> </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_kilometer">min./km</string>
<string name="unit_minute_per_mile">min./mil</string> <string name="unit_minute_per_mile">min./mil</string>
<!-- Value --> <!-- 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_excellent_gps">%1$d fod (fremragende GPS)</string>
<string name="value_integer_feet_recommended">%1$d fod (anbefales)</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_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_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_poor_gps">%1$d m (dårlig GPS)</string>
<string name="value_integer_meter_recommended">%1$d m (anbefalet)</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_mile_hour">%1$d mil/t</string>
<string name="value_integer_minute">%1$d min</string> <string name="value_integer_minute">%1$d min</string>
<string name="value_integer_second">%1$d sek</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