Compare commits

...

44 Commits

Author SHA1 Message Date
Dennis Guse 4bbdd55254 Release: v4.17.6 2025-01-20 07:55:59 +01:00
Dennis Guse 7df625ff0c Revert "Bugfix: on idle detection reset movement-based sensor data."
This reverts commit fd9646bb1f.
2025-01-20 07:53:45 +01:00
dependabot[bot] 7d01048a98 Bump com.android.tools.build:gradle from 8.7.3 to 8.8.0
Bumps com.android.tools.build:gradle from 8.7.3 to 8.8.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-10 07:36:55 +01:00
Dennis Guse 70fdb0e877 Link to reproducible build
Part of #418.
2025-01-03 22:50:50 +01:00
dependabot[bot] ec61383ffd Bump org.mockito:mockito-android from 5.14.2 to 5.15.2
Bumps [org.mockito:mockito-android](https://github.com/mockito/mockito) from 5.14.2 to 5.15.2.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.14.2...v5.15.2)

---
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>
2025-01-03 07:15:03 +01:00
Patrick Santa Maria 28e24615b8 Translated using Weblate (Corsican)
Currently translated at 99.6% (531 of 533 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/co/
2024-12-29 20:45:32 +00:00
Stephan Paternotte 4c6abcfecd Translated using Weblate (Dutch)
Currently translated at 100.0% (155 of 155 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2024-12-29 20:45:32 +00:00
Fjuro 8fb294ea9e Translated using Weblate (Czech)
Currently translated at 100.0% (155 of 155 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2024-12-29 20:45:31 +00:00
Stephan Paternotte 52d52ddcab Translated using Weblate (Dutch)
Currently translated at 99.8% (532 of 533 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nl/
2024-12-29 20:45:30 +00:00
NiaJ3oE2LM 1769bc26a2 Translated using Weblate (Italian)
Currently translated at 98.6% (526 of 533 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/it/
2024-12-29 20:45:30 +00:00
Ghost of Sparta e3fab57c43 Translated using Weblate (Hungarian)
Currently translated at 99.8% (532 of 533 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/hu/
2024-12-29 20:45:29 +00:00
Aindriú Mac Giolla Eoin f7030cd3eb Translated using Weblate (Irish)
Currently translated at 99.8% (532 of 533 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/ga/
2024-12-29 20:45:29 +00:00
Fjuro b5ba8a9f67 Translated using Weblate (Czech)
Currently translated at 100.0% (533 of 533 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/cs/
2024-12-29 20:45:28 +00:00
Stephan Paternotte 6ae7cb49f4 Translated using Weblate (Dutch)
Currently translated at 100.0% (155 of 155 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2024-12-29 20:45:27 +00:00
Aindriú Mac Giolla Eoin 16899004b5 Translated using Weblate (Irish)
Currently translated at 100.0% (155 of 155 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/ga/
2024-12-29 20:45:27 +00:00
Fjuro 69fe5e8127 Translated using Weblate (Czech)
Currently translated at 100.0% (153 of 153 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2024-12-29 20:45:26 +00:00
josé m 65913a1ba2 Translated using Weblate (Galician)
Currently translated at 18.3% (28 of 153 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/gl/
2024-12-29 20:45:25 +00:00
Stephan Paternotte 0b1e72cf58 Translated using Weblate (Dutch)
Currently translated at 100.0% (153 of 153 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2024-12-29 20:45:25 +00:00
Stephan Paternotte f89a81cb22 Translated using Weblate (Dutch)
Currently translated at 100.0% (152 of 152 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2024-12-29 20:45:24 +00:00
Stephan Paternotte ece9f3f0b2 Translated using Weblate (Dutch)
Currently translated at 100.0% (151 of 151 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2024-12-29 20:45:24 +00:00
Aindriú Mac Giolla Eoin 3011198b2c Translated using Weblate (Irish)
Currently translated at 100.0% (151 of 151 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/ga/
2024-12-29 20:45:23 +00:00
Fjuro 77d56a864f Translated using Weblate (Czech)
Currently translated at 100.0% (151 of 151 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2024-12-29 20:45:23 +00:00
Patrick Santa Maria 0bb468e31d Translated using Weblate (Corsican)
Currently translated at 99.8% (531 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/co/
2024-12-29 20:45:22 +00:00
Simon Artus 627d7c9288 Translated using Weblate (French)
Currently translated at 19.3% (29 of 150 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/fr/
2024-12-29 20:45:22 +00:00
Stephan Paternotte f428096164 Translated using Weblate (Dutch)
Currently translated at 100.0% (150 of 150 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2024-12-29 20:45:21 +00:00
Fjuro dfaaf4b022 Translated using Weblate (Czech)
Currently translated at 100.0% (150 of 150 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2024-12-29 20:45:20 +00:00
Peter Storch b576ccc4e2 fix nightly filename 2024-12-29 21:44:43 +01:00
Dennis Guse 0e0958c5e8 Announcements: add current heartrate.
Fixes #1635.
2024-12-29 12:38:15 +01:00
Dennis Guse 43e1cbcdc6 Announcements: added current time.
DateTimeFormatter behaves differently starting from SDK34 (produces NBSP).

Part of #1683.
2024-12-29 12:18:56 +01:00
Dennis Guse 073b3d2052 Announcements: added current time.
Fixes #1683.
2024-12-28 21:07:45 +01:00
Dennis Guse 01f4359cdf Sensor data: use monotonic clock (for isRecent).
Part of #1966.
2024-12-28 17:33:53 +01:00
Dennis Guse 333bc6f31b Sensor data: use monotonic clock.
Part of #1966.
2024-12-28 17:33:53 +01:00
Dennis Guse a2a9132658 Test: use applicationId (was hardcoded).
Part of #418.
2024-12-28 13:57:40 +01:00
Dennis Guse 5857c0032e Release: v4.17.5 2024-12-27 17:45:14 +01:00
Dennis Guse d086aa3c06 Reproducible build: we should use the alternative (PlayStore) icon.
Part of #418.
2024-12-27 17:20:35 +01:00
Dennis Guse fba09bcc3a Reproducible build: we also need a build script.
Part of #418.
2024-12-27 17:20:15 +01:00
Dennis Guse 3ce0e2cede Reproducible build: update release documentation.
Part of #418.
2024-12-27 16:31:58 +01:00
Dennis Guse df0d3ce600 Release: v4.17.4 2024-12-27 09:58:58 +01:00
Dennis Guse c05cbc2fa6 Reproducible build: remove dependenciesInfo.
Part of #418.
2024-12-27 09:58:13 +01:00
Dennis Guse 2f07208d9d Release: v4.17.3 2024-12-25 23:31:22 +01:00
Dennis Guse ded98eddb5 Reproducible build: use a proper file name for the apk (again).
Part of #418.
2024-12-25 23:30:06 +01:00
Dennis Guse 45732735d4 Reproducible build: use a proper file name for the apk.
Part of #418.
2024-12-25 19:56:54 +01:00
Dennis Guse b43dfd05ef Release: v4.17.2 2024-12-25 17:56:33 +01:00
Dennis Guse c011fd4e60 Reproducible build: F-Droid needs build flavors (not build types).
Part of #418.
2024-12-25 17:49:51 +01:00
75 changed files with 861 additions and 305 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
shell: bash
run: |
echo ${NIGHTLY_STORE_FILE} | base64 -d > KEY_NIGHTLY.jks
./gradlew assembleNightly -Dnightly_store_file=KEY_NIGHTLY.jks -Dnightly_store_password="${NIGHTLY_STORE_PASSWORD}" -Dnightly_key_alias="${NIGHTLY_KEY_ALIAS}" -Dnightly_key_password="${NIGHTLY_STORE_PASSWORD}"
./gradlew assembleNightlyRelease -Dnightly_store_file=KEY_NIGHTLY.jks -Dnightly_store_password="${NIGHTLY_STORE_PASSWORD}" -Dnightly_key_alias="${NIGHTLY_KEY_ALIAS}" -Dnightly_key_password="${NIGHTLY_STORE_PASSWORD}"
env:
NIGHTLY_STORE_FILE: ${{ secrets.NIGHTLY_STORE_FILE }}
NIGHTLY_STORE_PASSWORD: ${{ secrets.NIGHTLY_STORE_PASSWORD }}
+2 -2
View File
@@ -12,7 +12,7 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</tr>
<tr>
<td align="center">
<a href="https://f-droid.org/packages/de.dennisguse.opentracks">
<a href="https://f-droid.org/packages/de.dennisguse.opentracks.playstore">
<img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="60" align="middle">
</a>
</td>
@@ -29,7 +29,7 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</tr>
<tr>
<td align="center">
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.svg" align="middle" >
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.playstore.svg" align="middle" >
</td>
<td></td>
<td align="center">
+19 -27
View File
@@ -2,39 +2,31 @@
## Craft new releases
1. Get next _version code_ (main): `git rev-list HEAD --count main`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `build.gradle`
4. Create changelog (`_version code_.txt`)
5. Create commit with all changes
6. Tag newly create commit with _version name_ (e.g., v3.2.1)
7. Push commits and tags to public repository
8. Add changelog to tag (Github releases)
1. Execute use `RELEASE.sh`.
2. Push to Github
3. Create release on Github (incl. changelog)
This actually creates a tag that is later used by F-Droid
4. Pull from Github
5. Build reproducible release and attach to release
6. ... wait until reproducibility is verified by F-Droid
## F-Droid
Application id: de.dennisguse.opentracks
Two releases will be created:
* Application id: `de.dennisguse.opentracks` (irreproducible)
* Application id: `de.dennisguse.opentracks.playstore` (reproducible)
It if it properly tagged F-Droid takes care of building the app and publishing.
Usually, it takes two days before the update is published (build, signed, and published).
For F-Droid the build status can be found [here](https://monitor.f-droid.org/builds/build).
The recent build activity can be found [here](https://monitor.f-droid.org/builds/build)
## Reproducible builds
## Google Play Store
Application id: `de.dennisguse.opentracks.playstore`
A lot of detailed information can be found [here](https://f-droid.org/docs/Reproducible_Builds/)
Requirement: `gem install fastlane`
1. Build signed apk
(requires keystore)
2. Upload signed apk to [Google Play Console](https://play.google.com/apps/publish)
3. Update store meta data (e.g., description and screenshots)
`fastlane metadata`
(requires API key configured in `Appfile`)
Supported languages: https://support.google.com/googleplay/android-developer/table/4419860
Technical requirements:
* Must be build using OpenJDK17
## Translations (localization)
Translations of the OpenTracks are handled in
Weblate: [https://hosted.weblate.org/projects/opentracks/](https://hosted.weblate.org/projects/opentracks)
.
Weblate: [https://hosted.weblate.org/projects/opentracks/](https://hosted.weblate.org/projects/opentracks).
## Google Play Store
Supported languages: https://support.google.com/googleplay/android-developer/table/4419860
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
set -e
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64/
JAVA_VERSION=`java --version`
if [[ $JAVA_VERSION != "openjdk 17."* ]]; then
echo "OpenJDK version should be 17.X"
exit -1;
fi
RELEASE_STORE_PASSWORD=
read -s -p "Enter store password: " RELEASE_STORE_PASSWORD
echo ""
read -s -p "Enter key alias: " RELEASE_KEY_ALIAS
echo ""
read -s -p "Enter key password: " RELEASE_KEY_PASSWORD
echo ""
./gradlew --no-configuration-cache \
clean \
assembleReproducibleRelease \
-Drelease_store_file=`ls ../*.jks` \
-Drelease_store_file="../keystore_opentracks.jks" \
-Drelease_store_password="$RELEASE_STORE_PASSWORD" \
-Drelease_key_alias="$RELEASE_KEY_ALIAS" \
-Drelease_key_password="$RELEASE_KEY_PASSWORD"
+55 -33
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.3'
classpath 'com.android.tools.build:gradle:8.8.0'
}
}
@@ -63,25 +63,33 @@ android {
}
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 5978
versionName "v4.17.1"
applicationId 'de.dennisguse.opentracks'
versionCode 6022
versionName "v4.17.6"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdk 26
targetSdk 35
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
testInstrumentationRunner 'de.dennisguse.opentracks.TestRunner'
testInstrumentationRunnerArguments clearPackageData: 'true'
}
signingConfigs {
nightly {
if (System.getProperty("nightly_store_file") != null) {
storeFile file(System.getProperty("nightly_store_file"))
storePassword System.getProperty("nightly_store_password")
keyAlias System.getProperty("nightly_key_alias")
keyPassword System.getProperty("nightly_key_password")
if (System.getProperty('nightly_store_file') != null) {
storeFile file(System.getProperty('nightly_store_file'))
storePassword System.getProperty('nightly_store_password')
keyAlias System.getProperty('nightly_key_alias')
keyPassword System.getProperty('nightly_key_password')
}
}
release {
if (System.getProperty("release_store_file") != null) {
storeFile file(System.getProperty("release_store_file"))
storePassword System.getProperty("release_store_password")
keyAlias System.getProperty("release_key_alias")
keyPassword System.getProperty("release_key_password")
}
}
}
@@ -90,49 +98,63 @@ android {
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
applicationIdSuffix '.debug'
versionNameSuffix '-debug'
}
nightly {
applicationIdSuffix ".nightly"
signingConfig signingConfigs.nightly
}
release {
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
crunchPngs false
minifyEnabled false
versionNameSuffix "irreproducible"
}
}
flavorDimensions 'version'
productFlavors {
nightly {
dimension 'version'
applicationId 'de.dennisguse.opentracks.nightly'
signingConfig signingConfigs.nightly
}
irreproducible {
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
dimension 'version'
applicationId 'de.dennisguse.opentracks'
versionNameSuffix 'irreproducible'
}
reproducible {
// Developer Binaries: https://github.com/OpenTracksApp/OSMDashboard/releases/download/v%v/de.dennisguse.opentracks.playstore_%v.apk
// FDroid: https://f-droid.org/de/packages/de.dennisguse.opentracks.playstore
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
applicationIdSuffix ".playstore"
crunchPngs false
minifyEnabled false
dimension 'version'
applicationId 'de.dennisguse.opentracks.playstore'
signingConfig signingConfigs.release
}
}
applicationVariants.configureEach { variant ->
variant.resValue "string", "applicationId", variant.applicationId
variant.resValue 'string', 'applicationId', variant.applicationId
variant.outputs.configureEach {
if (variant.buildType.name == 'reproducible') {
outputFileName = "${applicationId}_${variant.buildType.name}_${variant.versionName}.apk"
} else {
outputFileName = "${applicationId}_${variant.buildType.name}_${variant.versionCode}.apk"
}
if (variant.buildType.name == 'nightly') {
if (variant.flavorName == 'nightly') {
setVersionCodeOverride(getVersionCode())
setVersionNameOverride(getVersionName())
}
if (variant.flavorName == 'reproducible') {
outputFileName = "${applicationId}_${variant.versionName}.apk"
} else {
outputFileName = "${applicationId}_${variant.versionCode}.apk"
}
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
}
dependencies {
@@ -154,7 +176,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'org.mockito:mockito-android:5.14.2'
androidTestImplementation 'org.mockito:mockito-android:5.15.2'
androidTestUtil 'androidx.test:orchestrator:1.5.1'
}
@@ -0,0 +1,7 @@
v4.17.0: OpenTracks
Změny:
- Veřejné rozhraní API pro vytváření značek (během záznamu)
Oprava chyby:
- Senzory: při nečinnosti nastavena data pohybových senzorů na 0
@@ -0,0 +1,5 @@
v4.17.1: OpenTracks
Změny:
- Reprodukovatelné sestavení: uvidíme, jestli to bude fungovat.
Varianta pro Obchod Play bude odeslána F-Droidu, který ji ověří a zveřejní.
@@ -0,0 +1,8 @@
v4.17.2: OpenTracks
Změny:
- Reprodukovatelné sestavení: uvidíme, zda to bude fungovat
Varianta pro Obchod Play bude odeslána do F-Droidu, kde bude ověřena a zveřejněna.
Vývojář:
- F-Droid vyžaduje použití Product Flavors (namísto Build Types)
@@ -0,0 +1,7 @@
v4.17.2: OpenTracks
Změny:
- Reprodukovatelné sestavení: uvidíme, zda to bude fungovat.
Vývojář:
- Pokus o opravu názvu souboru APK...
@@ -0,0 +1,8 @@
v4.17.4: OpenTracks
Změny:
- Reprodukovatelné sestavení: uvidíme, jestli to bude fungovat.
Vývojář:
- Odstranění DependencyInfo.
@@ -0,0 +1,10 @@
v4.17.5: OpenTracks
Změny:
- Reprodukovatelné sestavení: uvidíme, jestli to bude fungovat.
Oprava chyby:
- Reprodukovatelné sestavení používá ikonu z Obchodu Play.
Vývojář:
- Přidán soubor RELEASE_BUILD.sh
@@ -0,0 +1,9 @@
v4.17.2: OpenTracks
Changes:
- Reproducible build: let's see if it works.
The PlayStore variant will be submitted to F-Droid, verified and published them.
Developer:
- F-Droid requires to use Product Flavors (instead of Build Types)
@@ -0,0 +1,9 @@
v4.17.3: OpenTracks
Changes:
- Reproducible build: let's see if it works.
Developer:
- Just trying to get the APK filename, right...
@@ -0,0 +1,8 @@
v4.17.4: OpenTracks
Changes:
- Reproducible build: let's see if it works.
Developer:
- Removing DependencyInfo.
@@ -0,0 +1,10 @@
v4.17.5: OpenTracks
Changes:
- Reproducible build: let's see if it works.
Bugfix:
- Reproducible build uses PlayStore icon.
Developer:
- Added RELEASE_BUILD.sh
@@ -0,0 +1,5 @@
v4.17.6: OpenTracks
Bugfix:
- UI: GPS data may be not shown regularly (does not affect recorded data)
Introduced in v4.17.0
@@ -0,0 +1,7 @@
v4.17.0 : OpenTracks
Changements :
− IPA (API) publique pour créer des marqueurs (lors de l’enregistrement)
Correction de bogue :
− Capteurs : quand immobile, les données des capteurs de mouvements sont mises à 0
@@ -0,0 +1,7 @@
v4.17.0: OpenTracks
Athruithe:
- API poiblí chun marcóirí a chruthú (agus tú ag taifeadadh)
Ceartúchán Bug:
- Braiteoirí: i gcás díomhaoin, socraigh sonraí braiteora bunaithe ar ghluaiseacht go 0
@@ -0,0 +1,5 @@
v4.17.1: OpenTracks
Athruithe:
- Tógáil atáirgthe: féachaimis an n-oibríonn sé.
Cuirfear an leagan PlayStore faoi bhráid F-Droid, fíorófar iad agus foilseofar iad.
@@ -0,0 +1,8 @@
v4.17.2: OpenTracks
Athruithe:
- Tógáil atáirgthe: féachaimis an n-oibríonn sé.
Cuirfear an leagan PlayStore faoi bhráid F-Droid, fíorófar iad agus foilseofar iad.
Forbróir:
- Éilíonn F-Droid Blasanna Táirge a úsáid (in ionad Cineálacha Tógála)
@@ -0,0 +1,7 @@
v4.17.3: OpenTracks
Athruithe:
- Tógáil atáirgthe: féachaimis an n-oibríonn sé.
Forbróir:
- Díreach ag iarraidh ainm an chomhaid APK a fháil, ceart ...
@@ -0,0 +1,8 @@
v4.17.4: OpenTracks
Athruithe:
- Tógáil atáirgthe: féachaimis an n-oibríonn sé.
Forbróir:
- DependencyInfo a bhaint.
@@ -0,0 +1,10 @@
v4.17.5: OpenTracks
Athruithe:
- Tógáil atáirgthe: féachaimis an n-oibríonn sé.
Ceartúchán Bug:
- Úsáideann tógáil atáirgthe íocón PlayStore.
Forbróir:
- Curtha leis RELEASE_BUILD.sh
@@ -0,0 +1,8 @@
v4.17.2: OpenTracks
Cambios:
- Compilación reproducible: comprobemos se funciona ben.
Enviaremos a F-Droid a variante da Play Store, para que a verifiquen e publiquen.
Desenvolvemento:
- F-Droid require que usemos Product Flavors (no lugar de Build Types)
@@ -0,0 +1,7 @@
v4.17.3: OpenTracks
Cambios:
- Compilación reproducible: agardemos que funcione ben.
Desenvolvemento:
- Intento de que o nome do ficheiro APK sai a ben, vexamos...
@@ -0,0 +1,9 @@
v4.17.0: OpenTracks
Aanpassing:
- Openbare API om markeringen te maken (tijdens opname)
Opgelost:
- Sensoren: bij rust de op beweging gebaseerde sensorgegevens op 0 zetten
@@ -0,0 +1,5 @@
v4.17.1: OpenTracks
Veranderingen:
- Reproduceerbare build: laten we eens kijken of het werkt.
De PlayStore-variant wordt ingediend bij F-Droid, geverifieerd en gepubliceerd.
@@ -0,0 +1,8 @@
v4.17.2: OpenTracks
Veranderingen:
- Reproduceerbare build: laten we eens kijken of het werkt.
De PlayStore-variant wordt ingediend bij F-Droid, geverifieerd en gepubliceerd.
Ontwikkelaar:
- F-Droid vereist het gebruik van productsmaken (in plaats van buildtypes)
@@ -0,0 +1,7 @@
v4.17.3: OpenTracks
Veranderingen:
- Reproduceerbare build: laten we eens kijken of het werkt.
Ontwikkelaar:
- Ik probeer gewoon de APK-bestandsnaam te krijgen, toch...
@@ -0,0 +1,8 @@
v4.17.4: OpenTracks
Veranderingen:
- Reproduceerbare build: laten we eens kijken of het werkt.
Ontwikkelaar:
- DependencyInfo verwijderen.
@@ -0,0 +1,10 @@
v4.17.5: OpenTracks
Veranderingen:
- Reproduceerbare build: laten we eens kijken of het werkt.
Opgelost probleem:
- Reproduceerbare build maakt gebruik van het PlayStore-pictogram.
Ontwikkelaar:
- Toegevoegd RELEASE_BUILD.sh
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -9,6 +9,7 @@ import org.junit.Test;
import java.util.Set;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -34,6 +35,7 @@ public class ShareContentProviderTest {
public void testCreateURIescapeFilename() {
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(Set.of(new Track.Id(1)), "../../&1=1", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/KML_WITH_TRACKDETAIL_AND_SENSORDATA/1/..%2F..%2F%261%3D1"), shareURIandMIME.first);
assertEquals(Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".content/tracks/KML_WITH_TRACKDETAIL_AND_SENSORDATA/1/..%2F..%2F%261%3D1"), shareURIandMIME.first);
}
}
@@ -157,7 +157,7 @@ public class ExportImportTest {
contentProviderUtils.insertMarker(new Marker("Marker 2", "Marker 2 desc", "Marker 2 category", null, trackId, service.getLastStoredTrackPointWithLocation(), ""));
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet(trackPointCreator);
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -172,7 +172,7 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3, 16.001, 10, 27, 15, 10, 0f);
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet(trackPointCreator);
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
@@ -550,22 +550,22 @@ public class ExportImportTest {
AggregatorCyclingPower cyclingPower = Mockito.mock(AggregatorCyclingPower.class);
Mockito.when(cyclingPower.hasValue()).thenReturn(true);
Mockito.when(cyclingPower.getValue()).thenReturn(Power.of(power));
Mockito.when(cyclingPower.getValue(Mockito.any())).thenReturn(Power.of(power));
sensorDataSet.add(cyclingPower);
AggregatorHeartRate agheartRate = Mockito.mock(AggregatorHeartRate.class);
Mockito.when(agheartRate.getValue()).thenReturn(HeartRate.of(heartRate));
sensorDataSet.add(agheartRate);
AggregatorHeartRate avgHeartRate = Mockito.mock(AggregatorHeartRate.class);
Mockito.when(avgHeartRate.getValue(Mockito.any())).thenReturn(HeartRate.of(heartRate));
sensorDataSet.add(avgHeartRate);
AggregatorCyclingCadence cyclingCadence = Mockito.mock(AggregatorCyclingCadence.class);
Mockito.when(cyclingCadence.hasValue()).thenReturn(true);
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
Mockito.when(cyclingCadence.getValue(Mockito.any())).thenReturn(Cadence.of(cadence));
sensorDataSet.add(cyclingCadence);
if (distance != null && speed != null) {
AggregatorCyclingDistanceSpeed distanceSpeed = Mockito.mock(AggregatorCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed)));
Mockito.when(distanceSpeed.getValue(Mockito.any())).thenReturn(new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed)));
sensorDataSet.add(distanceSpeed);
} else {
sensorDataSet.add(new AggregatorCyclingDistanceSpeed("", ""));
@@ -582,7 +582,7 @@ public class ExportImportTest {
if (altitudeGain != null) {
AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class);
Mockito.when(barometer.hasValue()).thenReturn(true);
Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
Mockito.when(barometer.getValue(Mockito.any())).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
sensorDataSet.add(barometer);
} else {
sensorDataSet.add(new AggregatorBarometer("test", null));
@@ -3,13 +3,15 @@ package de.dennisguse.opentracks.sensors.sensorData;
import org.junit.Assert;
import org.junit.Test;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
public class AggregatorBarometerTest {
private static void addSensorValue(AggregatorBarometer aggregatorBarometer, float[] values) {
for (float f : values) {
aggregatorBarometer.add(new Raw<>(AtmosphericPressure.ofHPA(f)));
aggregatorBarometer.add(new Raw<>(Instant.MIN, AtmosphericPressure.ofHPA(f)));
}
}
@@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
@@ -23,11 +25,11 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 2048)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 2048)));
// then
assertEquals(60, current.getValue().getRPM(), 0.01);
assertEquals(60, current.getValue(Instant.MIN).getRPM(), 0.01);
}
@Test
@@ -35,11 +37,11 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 6184)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 8016)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 6184)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 8016)));
// then
assertEquals(33.53, current.getValue().getRPM(), 0.01);
assertEquals(33.53, current.getValue(Instant.MIN).getRPM(), 0.01);
}
@Test
@@ -47,11 +49,11 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 2048)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 2048)));
// then
assertEquals(Cadence.of(0), current.getValue());
assertEquals(Cadence.of(0), current.getValue(Instant.MIN));
}
@@ -60,8 +62,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 1024)));
// then
assertFalse(current.hasValue()); //TODO Cadence should be 0?
@@ -72,11 +74,11 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 0)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 0)));
// then
assertEquals(60, current.getValue().getRPM(), 0.01);
assertEquals(60, current.getValue(Instant.MIN).getRPM(), 0.01);
}
@Test
@@ -85,13 +87,13 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(0, 2048)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(0, 2048)));
// then
// TODO See #953
// assertEquals(60, current.getValue().getRPM(), 0.01);
assertNull(current.getValue());
assertNull(current.getValue(Instant.MIN));
}
@Test
@@ -100,12 +102,12 @@ public class SensorDataCyclingTest {
current.setWheelCircumference(Distance.ofMM(2150));
// when
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184)));
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016)));
// then
assertEquals(2.15, current.getValue().distance().toM(), 0.01);
assertEquals(1.20, current.getValue().speed().toMPS(), 0.01);
assertEquals(2.15, current.getValue(Instant.MIN).distance().toM(), 0.01);
assertEquals(1.20, current.getValue(Instant.MIN).speed().toMPS(), 0.01);
}
@Test
@@ -115,14 +117,14 @@ public class SensorDataCyclingTest {
current.setWheelCircumference(Distance.ofMM(2000));
// when
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(0, 2048)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(0, 2048)));
// then
// TODO See #953
// assertEquals(2, current.getValue().getDistance().toM(), 0.01);
// assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
assertNull(current.getValue());
assertNull(current.getValue(Instant.MIN));
}
}
@@ -304,11 +304,11 @@ public class TrackRecordingServiceRecordingTest {
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(HeartRate.of(5))); //Should be ignored
sensorManager.onChanged(new Raw<>(sensor1, HeartRate.of(5))); //Should be ignored
String sensor3 = "2020-02-02T02:02:13Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(HeartRate.of(7)));
sensorManager.onChanged(new Raw<>(sensor3, HeartRate.of(7)));
String stopTime = "2020-02-02T02:02:15Z";
trackPointCreator.setClock(stopTime);
@@ -646,12 +646,12 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(0)))); //Should be ignored
sensorManager.onChanged(new Raw<>(sensor1, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(0)))); //Should be ignored
// when
String sensor2 = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(sensor2);
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(2))));
sensorManager.onChanged(new Raw<>(sensor2, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(2))));
// when
String gps1 = "2020-02-02T02:02:05Z";
@@ -660,12 +660,12 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor3 = "2020-02-02T02:02:06Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(12))));
sensorManager.onChanged(new Raw<>(sensor3, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(12))));
// when
String sensor4 = "2020-02-02T02:02:07Z";
trackPointCreator.setClock(sensor4);
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(14)))); //Should be ignored
sensorManager.onChanged(new Raw<>(sensor4, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(14)))); //Should be ignored
// when
String gps2 = "2020-02-02T02:02:08Z";
@@ -674,7 +674,7 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor5 = "2020-02-02T02:02:10Z";
trackPointCreator.setClock(sensor5);
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(16)))); //Should be ignored
sensorManager.onChanged(new Raw<>(sensor5, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(16)))); //Should be ignored
// when
String gps3 = "2020-02-02T02:02:12Z";
@@ -730,7 +730,7 @@ public class TrackRecordingServiceRecordingTest {
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class);
Mockito.when(barometer.hasValue()).thenReturn(true);
Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
Mockito.when(barometer.getValue(Mockito.any())).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
trackPointCreator.getSensorManager().sensorDataSet.barometer = barometer;
}
@@ -1,9 +1,11 @@
package de.dennisguse.opentracks.services.announcement;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import android.content.Context;
import android.icu.text.MessageFormat;
import android.os.Build;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
@@ -13,8 +15,10 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.time.Duration;
import java.time.Instant;
import java.util.Locale;
import java.util.Map;
@@ -26,6 +30,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
@@ -45,6 +50,7 @@ public class VoiceAnnouncementUtilsTest {
public void setUp() {
contentProviderUtils = new ContentProviderUtils(context);
PreferencesUtils.setVoiceAnnounceHeartRateCurrent(false);
PreferencesUtils.setVoiceAnnounceLapHeartRate(false);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
PreferencesUtils.setVoiceAnnounceTotalDistance(true);
@@ -53,68 +59,101 @@ public class VoiceAnnouncementUtilsTest {
PreferencesUtils.setVoiceAnnounceLapSpeedPace(true);
}
@Test
public void sdkRequirement() {
assertFalse("Due to DateTimeFormatter using NBSP, these tests only work in SDK34+", Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
}
@Test
public void getAnnouncement_metric_speed() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Average moving speed 18.4 kilometers per hour.", announcement);
assertEquals("12:00 AM. Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Average moving speed 18.4 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_speed_rounding_check() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusSeconds(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.0 kilometers. 1 hour 1 second. Average moving speed 20.0 kilometers per hour.", announcement);
assertEquals("12:00 AM. Total distance 20.0 kilometers. 1 hour 1 second. Average moving speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_rounding_check() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(19999));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.0 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement);
assertEquals("12:00 AM. Total distance 20.0 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_rounding_check_two() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(19900));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 19.9 kilometers. 1 hour. Average moving speed 19.9 kilometers per hour.", announcement);
assertEquals("12:00 AM. Total distance 19.9 kilometers. 1 hour. Average moving speed 19.9 kilometers per hour.", announcement);
}
@Test
@@ -132,27 +171,40 @@ public class VoiceAnnouncementUtilsTest {
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
Track track = new Track();
track.setTrackStatistics(stats);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, null).toString();
// then
assertEquals("Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_pace() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, false, null, null).toString();
// then
assertEquals("Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement);
assertEquals("12:00 AM. Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement);
}
@Test
@@ -170,69 +222,103 @@ public class VoiceAnnouncementUtilsTest {
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, false, lastInterval, null).toString();
// then
assertEquals("Total distance 14.2 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
}
@Test
public void getAnnouncement_imperial_speed() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
// then
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
assertEquals("12:00 AM. Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
}
@Test
public void getAnnouncement_imperial_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.ofMile(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
// then
assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
assertEquals("12:00 AM. Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
}
@Test
public void getAnnouncement_imperial_meter_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.ofMile(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_METER, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_METER, true, null, null).toString();
// then
assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
assertEquals("12:00 AM. Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
}
@Test
public void getAnnouncement_metric_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.ofKilometer(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 1.1 kilometers. 1 hour. Average moving speed 1.1 kilometers per hour.", announcement);
assertEquals("12:00 AM. Total distance 1.1 kilometers. 1 hour. Average moving speed 1.1 kilometers per hour.", announcement);
}
@Test
@@ -250,27 +336,40 @@ public class VoiceAnnouncementUtilsTest {
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, lastInterval, null).toString();
// then
assertEquals("Total distance 8.8 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
assertEquals("12:16 AM. Total distance 8.8 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
}
@Test
public void getAnnouncement_imperial_pace() {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime(Instant.EPOCH);
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, false, null, null).toString();
// then
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
assertEquals("12:00 AM. Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
}
@Test
@@ -288,11 +387,17 @@ public class VoiceAnnouncementUtilsTest {
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, false, lastInterval, null).toString();
// then
assertEquals("Total distance 8.8 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
assertEquals("12:16 AM. Total distance 8.8 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
}
@Test
@@ -314,17 +419,25 @@ public class VoiceAnnouncementUtilsTest {
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
// then
assertEquals("Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving 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("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
}
@Test
public void getAnnouncement_only_lap_heart_rate() {
public void getAnnouncement_heart_rate() {
PreferencesUtils.setVoiceAnnounceHeartRateCurrent(true);
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(true);
PreferencesUtils.setVoiceAnnounceTotalDistance(false);
PreferencesUtils.setVoiceAnnounceMovingTime(false);
PreferencesUtils.setVoiceAnnounceAverageSpeedPace(false);
@@ -344,11 +457,17 @@ public class VoiceAnnouncementUtilsTest {
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
Track track = new Track();
track.setTrackStatistics(stats);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
// then
assertEquals(" Current heart rate 133 bpm.", announcement);
assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
}
@Test
@@ -118,7 +118,13 @@ public class Track {
}
public OffsetDateTime getStartTime() {
return trackStatistics.getStartTime().atOffset(zoneOffset);
return trackStatistics
.getStartTime().atOffset(zoneOffset);
}
public OffsetDateTime getStopTime() {
return trackStatistics
.getStopTime().atOffset(zoneOffset);
}
@NonNull
@@ -33,7 +33,7 @@ public class BluetoothHandlerBarometricPressure implements SensorHandlerInterfac
AtmosphericPressure value = parseEnvironmentalSensing(characteristic);
if (value == null) return;
observer.onChange(new Raw<>(value));
observer.onChange(new Raw<>(observer.getNow(), value));
}
/**
@@ -33,7 +33,7 @@ public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
if (serviceMeasurementUUID.equals(BluetoothHandlerManagerCyclingPower.CYCLING_POWER)) {
BluetoothHandlerManagerCyclingPower.Data data = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
if (data != null && data.crank() != null) {
observer.onChange(new Raw<>(data.crank()));
observer.onChange(new Raw<>(observer.getNow(), data.crank()));
}
return;
}
@@ -42,7 +42,7 @@ public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, CrankData> data = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (data != null && data.second != null) {
observer.onChange(new Raw<>(data.second));
observer.onChange(new Raw<>(observer.getNow(), data.second));
}
return;
}
@@ -34,7 +34,7 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> data = parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (data.first != null) {
observer.onChange(new Raw<>(data.first));
observer.onChange(new Raw<>(observer.getNow(), data.first));
}
}
@@ -35,7 +35,7 @@ public class BluetoothHandlerManagerCyclingPower implements SensorHandlerInterfa
Data cyclingPower = parseCyclingPower(characteristic);
if (cyclingPower != null) {
observer.onChange(new Raw<>(cyclingPower));
observer.onChange(new Raw<>(observer.getNow(), cyclingPower));
}
}
@@ -45,7 +45,7 @@ public class BluetoothHandlerManagerHeartRate implements SensorHandlerInterface
HeartRate heartRate = parseHeartRate(characteristic);
if (heartRate != null) {
observer.onChange(new Raw<>(heartRate));
observer.onChange(new Raw<>(observer.getNow(), heartRate));
}
}
@@ -36,7 +36,7 @@ public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInte
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Data data = parseRunningSpeedAndCadence(sensorName, characteristic);
observer.onChange(new Raw<>(data));
observer.onChange(new Raw<>(observer.getNow(), data));
}
@VisibleForTesting
@@ -8,6 +8,8 @@ import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
@@ -21,7 +23,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
//TODO Should be final and not be visible for testing
@VisibleForTesting
public SensorDataSet sensorDataSet = new SensorDataSet();
public SensorDataSet sensorDataSet;
private final TrackPointCreator observer;
@@ -47,6 +49,11 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
public void onRemove(Aggregator<?, ?> aggregator) {
sensorDataSet.remove(aggregator);
}
@Override
public Instant getNow() {
return observer.createNow();
}
};
private BluetoothRemoteSensorManager bluetoothSensorManager;
@@ -57,6 +64,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
public SensorManager(TrackPointCreator observer) {
this.observer = observer;
this.sensorDataSet = new SensorDataSet(observer);
}
public void start(Context context, Handler handler) {
@@ -138,5 +146,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
void onDisconnect(Aggregator<?, ?> sensorData);
void onRemove(Aggregator<?, ?> sensorData);
Instant getNow();
}
}
@@ -22,7 +22,7 @@ public class BarometerInternal implements Driver {
private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5);
private final SensorManager.SensorDataChangedObserver listener;
private final SensorManager.SensorDataChangedObserver observer;
private Context context;
@@ -34,7 +34,7 @@ public class BarometerInternal implements Driver {
return;
}
listener.onChange(new Raw<>(AtmosphericPressure.ofHPA(event.values[0])));
observer.onChange(new Raw<>(observer.getNow(), AtmosphericPressure.ofHPA(event.values[0])));
}
@Override
@@ -43,8 +43,8 @@ public class BarometerInternal implements Driver {
}
};
public BarometerInternal(@NonNull SensorManager.SensorDataChangedObserver listener) {
this.listener = listener;
public BarometerInternal(@NonNull SensorManager.SensorDataChangedObserver observer) {
this.observer = observer;
}
@@ -60,7 +60,7 @@ public class BarometerInternal implements Driver {
if (sensorManager.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
this.context = context;
listener.onConnect(new AggregatorBarometer("internal", null));
observer.onConnect(new AggregatorBarometer("internal", null));
return;
}
@@ -42,11 +42,11 @@ public abstract class Aggregator<Input, Output> {
@NonNull
protected abstract Output getNoneValue();
public Output getValue() {
public Output getValue(Instant now) {
if (!hasValue()) {
return null; //TODO Check if this is a good idea!
}
if (isRecent()) {
if (isRecent(now)) {
return value;
}
return getNoneValue();
@@ -55,19 +55,17 @@ public abstract class Aggregator<Input, Output> {
/**
* Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance.
*/
public void reset() {
value = getNoneValue();
}
public void reset() {}
/**
* Is the data recent considering the current time.
*/
private boolean isRecent() {
private boolean isRecent(Instant now) {
if (previous == null) {
return false;
}
return Instant.now()
return now
.isBefore(previous.time().plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE));
}
@@ -37,5 +37,10 @@ public class AggregatorBarometer extends Aggregator<AtmosphericPressure, Altitud
return new AltitudeGainLoss(0f, 0f);
}
@Override
public void reset() {
value = getNoneValue();
}
public record Data(Altitude gain, Altitude loss) {}
}
@@ -16,6 +16,11 @@ public class AggregatorGPS extends Aggregator<Position, Position> {
value = current.value();
}
@Override
public void reset() {
value = null;
}
@NonNull
@Override
protected Position getNoneValue() {
@@ -15,11 +15,6 @@ public class AggregatorHeartRate extends Aggregator<HeartRate, HeartRate> {
this.value = current.value();
}
@Override
public void reset() {
// We don't need to reset the heart rate as this value is valid for a certain amount of time: and it is not an aggregate.
}
@NonNull
@Override
protected HeartRate getNoneValue() {
@@ -2,15 +2,21 @@ package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Clock;
import java.time.Instant;
public record Raw<T>(
@NonNull T value,
@NonNull Instant time
@NonNull Instant time,
@NonNull T value
) {
public Raw(@NonNull T value) {
this(value, Instant.now()); //TODO We should be using the MonotonicClock
public Raw(@NonNull Clock clock, @NonNull T value) {
this(clock.instant(), value);
}
@VisibleForTesting
public Raw(@NonNull String time, @NonNull T value) {
this(Instant.parse(time), value);
}
}
@@ -17,9 +17,10 @@ import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public final class SensorDataSet {
public class SensorDataSet {
private static final String TAG = SensorDataSet.class.getSimpleName();
@@ -43,11 +44,15 @@ public final class SensorDataSet {
public AggregatorGPS gps;
public SensorDataSet() {
private TrackPointCreator trackPointCreator;
public SensorDataSet(TrackPointCreator trackPointCreator) {
this.trackPointCreator = trackPointCreator;
}
@Deprecated //TODO This is not a copy constructor anymore, but it should be - aggregators are no value objects; best guess: can be removed
public SensorDataSet(SensorDataSet toCopy) {
//TODO This is not a copy constructor anymore, but it should be - aggregators are no value objects
this.trackPointCreator = toCopy.trackPointCreator;
this.heartRate = toCopy.heartRate;
this.cyclingCadence = toCopy.cyclingCadence;
this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed;
@@ -59,7 +64,7 @@ public final class SensorDataSet {
public Pair<HeartRate, String> getHeartRate() {
if (heartRate != null) {
return new Pair<>(heartRate.getValue(), heartRate.getSensorNameOrAddress());
return new Pair<>(heartRate.getValue(trackPointCreator.createNow()), heartRate.getSensorNameOrAddress());
}
return null;
@@ -67,7 +72,7 @@ public final class SensorDataSet {
public Pair<Cadence, String> getCadence() {
if (cyclingCadence != null) {
return new Pair<>(cyclingCadence.getValue(), cyclingCadence.getSensorNameOrAddress());
return new Pair<>(cyclingCadence.getValue(trackPointCreator.createNow()), cyclingCadence.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.value.cadence() != null) {
@@ -78,11 +83,11 @@ public final class SensorDataSet {
}
public Pair<Speed, String> getSpeed() {
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue().speed() != null) {
return new Pair<>(cyclingDistanceSpeed.getValue().speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue(trackPointCreator.createNow()).speed() != null) {
return new Pair<>(cyclingDistanceSpeed.getValue(trackPointCreator.createNow()).speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.getValue().speed() != null) {
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.getValue(trackPointCreator.createNow()).speed() != null) {
return new Pair<>(runningDistanceSpeedCadence.value.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
@@ -152,7 +157,7 @@ public final class SensorDataSet {
public void fillTrackPoint(TrackPoint trackPoint) {
if (gps != null && gps.hasValue()) {
trackPoint.setPosition(gps.getValue());
trackPoint.setPosition(gps.getValue(trackPointCreator.createNow()));
}
if (getHeartRate() != null) {
@@ -168,20 +173,20 @@ public final class SensorDataSet {
}
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distanceOverall());
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue(trackPointCreator.createNow()).distanceOverall());
}
if (cyclingPower != null && cyclingPower.hasValue()) {
trackPoint.setPower(cyclingPower.getValue());
trackPoint.setPower(cyclingPower.getValue(trackPointCreator.createNow()));
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) {
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().distance());
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue(trackPointCreator.createNow()).distance());
}
if (barometer != null && barometer.hasValue()) {
trackPoint.setAltitudeGain(barometer.getValue().gain_m());
trackPoint.setAltitudeLoss(barometer.getValue().loss_m());
trackPoint.setAltitudeGain(barometer.getValue(trackPointCreator.createNow()).gain_m());
trackPoint.setAltitudeLoss(barometer.getValue(trackPointCreator.createNow()).loss_m());
}
}
@@ -317,7 +317,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
// Compute temporary track statistics using sensorData and update time.
Pair<Track, Pair<TrackPoint, SensorDataSet>> data = trackRecordingManager.getDataForUI();
voiceAnnouncementManager.announceStatisticsIfNeeded(data.first);
voiceAnnouncementManager.announceStatisticsIfNeeded(data.first, data.second.second);
recordingDataObservable.postValue(new RecordingData(data.first, data.second.first, data.second.second));
}
@@ -22,7 +22,6 @@ import android.media.AudioManager;
import android.media.MediaPlayer;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.text.Spannable;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -118,7 +117,7 @@ public class TTSManager {
}
}
public void announce(@NonNull Spannable announcement) {
public void speak(@NonNull CharSequence announcement) {
synchronized (this) {
if (!ttsReady) {
ttsReady = ttsInitStatus == TextToSpeech.SUCCESS;
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -115,10 +116,10 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
return;
}
voiceAnnouncement.announce(VoiceAnnouncementUtils.createIdle(context));
voiceAnnouncement.speak(VoiceAnnouncementUtils.createIdle(context));
}
public void announceStatisticsIfNeeded(@NonNull Track track) {
public void announceStatisticsIfNeeded(@NonNull Track track, @NonNull SensorDataSet sensorDataSet) {
if (shouldNotAnnounce()) {
return;
}
@@ -135,11 +136,11 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
}
if (announce) {
voiceAnnouncement.announce(createAnnouncement(track));
voiceAnnouncement.speak(createAnnouncement(track, sensorDataSet));
}
}
private Spannable createAnnouncement(Track track) {
private Spannable createAnnouncement(Track track, SensorDataSet sensorDataSet) {
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
if (currentIntervalDistance != intervalDistance) {
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
@@ -155,7 +156,7 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
return VoiceAnnouncementUtils.createStatistics(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics);
return VoiceAnnouncementUtils.createStatistics(context, track, sensorDataSet, PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics);
}
public void stop() {
@@ -3,29 +3,38 @@ package de.dennisguse.opentracks.services.announcement;
import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageHeartRate;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceHeartRateCurrent;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapPower;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceMovingTime;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTime;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceUnit;
import android.content.Context;
import android.icu.text.MessageFormat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.TtsSpan;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Map;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -42,8 +51,15 @@ class VoiceAnnouncementUtils {
.append(context.getString(R.string.voiceIdle));
}
static Spannable createStatistics(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
TrackStatistics trackStatistics = track.getTrackStatistics();
SpannableStringBuilder builder = new SpannableStringBuilder();
if (shouldVoiceAnnounceTime()) {
appendTime(builder, track.getStopTime());
}
Distance totalDistance = trackStatistics.getTotalDistance();
Speed averageMovingSpeed = trackStatistics.getAverageMovingSpeed();
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
@@ -92,88 +108,99 @@ class VoiceAnnouncementUtils {
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
}
if (totalDistance.isZero()) {
return builder;
}
boolean hasTravelledDistance = !totalDistance.isZero();
// Announce time
Duration movingTime = trackStatistics.getMovingTime();
if (shouldVoiceAnnounceMovingTime() && !movingTime.isZero()) {
if (shouldVoiceAnnounceMovingTime() && hasTravelledDistance && !movingTime.isZero()) {
appendDuration(context, builder, movingTime);
builder.append(".");
}
if (isReportSpeed) {
if (shouldVoiceAnnounceAverageSpeedPace()) {
double speedInUnit = averageMovingSpeed.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.speed));
String template = context.getResources().getString(speedId);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", speedInUnit)), speedInUnit, 1, unitSpeedTTS);
builder.append(".");
}
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) {
if (hasTravelledDistance) {
if (isReportSpeed) {
if (shouldVoiceAnnounceAverageSpeedPace()) {
double speedInUnit = averageMovingSpeed.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.lap_speed));
.append(context.getString(R.string.speed));
String template = context.getResources().getString(speedId);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", currentDistancePerTimeInUnit)), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", speedInUnit)), speedInUnit, 1, unitSpeedTTS);
builder.append(".");
}
}
} else {
if (shouldVoiceAnnounceAverageSpeedPace()) {
Duration time = averageMovingSpeed.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.pace));
appendDuration(context, builder, time);
builder.append(" ")
.append(context.getString(perUnitStringId))
.append(".");
}
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) {
builder.append(" ")
.append(context.getString(R.string.lap_speed));
String template = context.getResources().getString(speedId);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", currentDistancePerTimeInUnit)), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
builder.append(".");
}
}
} else {
if (shouldVoiceAnnounceAverageSpeedPace()) {
Duration time = averageMovingSpeed.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.pace));
appendDuration(context, builder, time);
builder.append(" ")
.append(context.getString(perUnitStringId))
.append(".");
}
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
Duration currentTime = currentDistancePerTime.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.lap_time));
appendDuration(context, builder, currentTime);
builder.append(" ")
.append(context.getString(perUnitStringId))
.append(".");
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
Duration currentTime = currentDistancePerTime.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.lap_time));
appendDuration(context, builder, currentTime);
builder.append(" ")
.append(context.getString(perUnitStringId))
.append(".");
}
}
}
if (shouldVoiceAnnounceAverageHeartRate() && sensorStatistics != null && sensorStatistics.hasHeartRate()) {
int averageHeartRate = Math.round(sensorStatistics.avgHeartRate().getBPM());
builder.append(" ")
.append(context.getString(R.string.average_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate);
builder.append(".");
}
if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) {
int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM());
Pair<HeartRate, String> heartrate = sensorDataSet.getHeartRate();
if (shouldVoiceAnnounceHeartRateCurrent() && heartrate != null && heartrate.first != null) { //TODO Check has an announcable value?
int averageHeartRate = Math.round(heartrate.first.getBPM());
builder.append(" ")
.append(context.getString(R.string.current_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate);
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate);
builder.append(".");
}
if (hasTravelledDistance) {
if (shouldVoiceAnnounceAverageHeartRate() && sensorStatistics != null && sensorStatistics.hasHeartRate()) {
int averageHeartRate = Math.round(sensorStatistics.avgHeartRate().getBPM());
if (shouldVoiceAnnounceLapPower() && currentInterval != null && currentInterval.hasAveragePower()) {
int currentPower = Math.round(currentInterval.getAveragePower().getW());
if(shouldVoiceAnnounceUnit()) {
String template = context.getResources().getString(R.string.power_x_watt);
builder.append(" ")
.append(MessageFormat.format(template, Map.of("x", currentPower)));
} else {
builder.append(" ").append(context.getString(R.string.power)).append(" ");
builder.append(String.valueOf(currentPower));
.append(context.getString(R.string.average_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate);
builder.append(".");
}
builder.append(".");
}
if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) {
int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM());
builder.append(" ")
.append(context.getString(R.string.lap_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate);
builder.append(".");
}
if (shouldVoiceAnnounceLapPower() && currentInterval != null && currentInterval.hasAveragePower()) {
int currentPower = Math.round(currentInterval.getAveragePower().getW());
if (shouldVoiceAnnounceUnit()) {
String template = context.getResources().getString(R.string.power_x_watt);
builder.append(" ")
.append(MessageFormat.format(template, Map.of("x", currentPower)));
} else {
builder.append(" ").append(context.getString(R.string.power)).append(" ");
builder.append(String.valueOf(currentPower));
}
builder.append(".");
}
}
return builder;
}
@@ -196,6 +223,20 @@ class VoiceAnnouncementUtils {
}
}
private static void appendTime(@NonNull SpannableStringBuilder builder, OffsetDateTime now) {
TtsSpan.TimeBuilder ttsSpan = new TtsSpan.TimeBuilder();
ttsSpan.setHours(now.getHour());
ttsSpan.setMinutes(now.getMinute());
String formattedTime = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
.format(now);
Spannable spannable = new SpannableString(formattedTime + ".");
spannable.setSpan(ttsSpan.build(), 0, spannable.length(), SPAN_INCLUSIVE_EXCLUSIVE);
builder.append(spannable);
builder.append(" ");
}
/**
* Speaks as: 98.14 [UNIT] - ninety eight point one four [UNIT with correct plural form]
*
@@ -37,7 +37,7 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
private TrackPointCreator trackPointCreator;
private SensorManager.SensorDataChangedObserver listener;
private SensorManager.SensorDataChangedObserver observer;
private Context context;
private Handler handler;
@@ -46,9 +46,9 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
private Duration gpsInterval;
private Distance thresholdHorizontalAccuracy;
public GPSManager(TrackPointCreator trackPointCreator, SensorManager.SensorDataChangedObserver listener) {
public GPSManager(TrackPointCreator trackPointCreator, SensorManager.SensorDataChangedObserver observer) {
this.trackPointCreator = trackPointCreator;
this.listener = listener;
this.observer = observer;
}
public void start(@NonNull Context context, @NonNull Handler handler) {
@@ -60,7 +60,7 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
gpsStatusManager = new GpsStatusManager(context, this, handler);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
listener.onConnect(new AggregatorGPS("internal"));
observer.onConnect(new AggregatorGPS("internal"));
registerLocationListener();
gpsStatusManager.start();
@@ -82,8 +82,8 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
gpsStatusManager.stop();
gpsStatusManager = null;
listener.onDisconnect(new AggregatorGPS("internal"));
listener = null;
observer.onDisconnect(new AggregatorGPS("internal"));
observer = null;
trackPointCreator = null;
}
@@ -139,7 +139,7 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
return;
}
listener.onChange(new Raw<>(Position.of(location)));
observer.onChange(new Raw<>(observer.getNow(), Position.of(location)));
}
@Override
@@ -124,8 +124,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
return new Pair<>(currentTrackPoint, sensorDataSet);
}
@VisibleForTesting
Instant createNow() {
public Instant createNow() {
return Instant.now(clock);
}
@@ -332,6 +332,10 @@ public class PreferencesUtils {
return getBoolean(R.string.voice_announce_idle_key, true);
}
public static boolean shouldVoiceAnnounceTime() {
return getBoolean(R.string.voice_announce_unit_key, true);
}
public static boolean shouldVoiceAnnounceUnit() {
return getBoolean(R.string.voice_announce_unit_key, true);
}
@@ -429,14 +433,23 @@ public class PreferencesUtils {
setBoolean(R.string.voice_announce_lap_speed_pace_key, value);
}
public static boolean shouldVoiceAnnounceLapHeartRate() {
return getBoolean(R.string.voice_announce_lap_heart_rate_key, false);
}
public static boolean shouldVoiceAnnounceLapPower() {
return getBoolean(R.string.voice_announce_lap_power_key, false);
}
public static boolean shouldVoiceAnnounceHeartRateCurrent() {
return getBoolean(R.string.voice_announce_heart_rate_current_key, false);
}
@VisibleForTesting
public static void setVoiceAnnounceHeartRateCurrent(boolean value) {
setBoolean(R.string.voice_announce_heart_rate_current_key, value);
}
public static boolean shouldVoiceAnnounceLapHeartRate() {
return getBoolean(R.string.voice_announce_lap_heart_rate_key, false);
}
@VisibleForTesting
public static void setVoiceAnnounceLapHeartRate(boolean value) {
setBoolean(R.string.voice_announce_lap_heart_rate_key, value);
@@ -46,9 +46,9 @@ public class TrackStatistics {
private final ExtremityMonitor altitudeExtremities = new ExtremityMonitor();
// The track start time.
private Instant startTime;
private Instant startTime; //TODO Should never be null!
// The track stop time.
private Instant stopTime;
private Instant stopTime; //TODO Should never be null!
private Distance totalDistance;
// Updated when new points are received, may be stale.
@@ -3,6 +3,8 @@ package de.dennisguse.opentracks.viewmodels;
import android.util.Pair;
import android.view.LayoutInflater;
import java.time.Instant;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.HeartRateZones;
import de.dennisguse.opentracks.databinding.StatsSensorItemBinding;
@@ -92,7 +94,7 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
Pair<String, String> valueAndUnit;
if (sensorDataSet != null && sensorDataSet.getCyclingPower() != null) {
valueAndUnit = StringUtils.getPowerParts(getContext(), sensorDataSet.getCyclingPower().getValue());
valueAndUnit = StringUtils.getPowerParts(getContext(), sensorDataSet.getCyclingPower().getValue(Instant.now())); //TODO Use MonotonicClock
sensorName = sensorDataSet.getCyclingPower().getSensorNameOrAddress();
} else {
valueAndUnit = StringUtils.getCadenceParts(getContext(), null);
+7 -22
View File
@@ -85,7 +85,7 @@
<string name="description_pace_nautical">Andatura (min/NM)</string>
<string name="description_recorded_time">Data d’arregistramentu : %1$s</string>
<string name="description_speed_ms">Vitezza (m/s)</string>
<string name="description_sensor_cadence">Cadenza (rpm)</string>
<string name="description_sensor_cadence">Cadenza (g/min)</string>
<string name="description_sensor_power">Putenza (W)</string>
<string name="description_default_speed_or_pace">Da tipu d’attività</string>
<string name="description_speed_imperial">Vitezza (mi/ora)</string>
@@ -165,29 +165,13 @@
<string name="about_history">OpenTracks hè una scansa di <a href="https://code.google.com/archive/p/mytracks/"><i>MyTracks da Google</i></a> (abbandunatu in u 2016).</string>
<string name="help_track_export_title">Cumu possu espurtà o scumparte i tracciati arregistrati ?</string>
<string name="help_track_export_content"> I tracciati ponu esse espurtati da i Parametri à u furmatu KMZ, KML, o GPX. KMZ o KML per tutti i dati ; GPX solu per e pusizioni. I tracciati scumparti via l’<i>icona di spartera</i> sò à u furmatu KMZ. </string>
<string name="help_track_map_content"> \u0020Impiegate a chjuca icona in forma di carta.
\n \u0020a) \u0020(messe à livellu in vivu) Installate <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. \u0020Furnisce carte in linea o fora di linea via <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\n \u0020In fatti, OSMDashboard hè un’altra appiecazione chì pò solu accede (timpurariamente) à i dati di i tracciati chì anu da esse affissati). \u0020
\n \u0020b) \u0020Via un’appiecazione classica di cartugrafia (p.i. <a href="http://osmand.net/">OsmAnd</a> è <a href="https://maps.me/">MAPS.ME</a>). \u0020St’appiecazioni devenu piglià in carica u furmatu di schedariu KMZ (solu e pusizioni è e stampiglie). \u0020In fatti, l’appiecazione deve incaricassi di <i>ACTION_VIEW</i> cù u tipu MIME <i>application/vnd.google-earth.kmz</i>. \u0020</string>
<string name="help_track_map_content">Impiegate a chjuca icona in forma di carta.\n \u0020a) (messe à livellu in vivu) Installate <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Furnisce carte in linea o fora di linea via <a href="https://www.openstreetmap.org">OpenStreetMap</a>.\n \u0020In fatti, OSMDashboard hè un’altra appiecazione chì pò solu accede (timpurariamente) à i dati di i tracciati chì anu da esse affissati).\n \u0020b) Via un’appiecazione classica di cartugrafia (p.i. <a href="http://osmand.net/">OsmAnd</a> è <a href="https://maps.me/">MAPS.ME</a>). \u0020St’appiecazioni devenu piglià in carica u furmatu di schedariu KMZ (solu e pusizioni è e stampiglie). \u0020In fatti, l’appiecazione deve incaricassi di <i>ACTION_VIEW</i> cù u tipu MIME <i>application/vnd.google-earth.kmz</i>.</string>
<string name="help_recording_content">OpenTracks impiegheghja u cattore GPS di l’apparechju per arregistrà di cuntinuu a pusizione. S’è l’apparechju GPS signaleghja dati falsi (per indettu, pusizione, vitezza o altezza), OpenTracks ùn pò fà nunda.</string>
<string name="help_data_storage_title">Cumu OpenTracks arregistreghja i dati di i cattori ?</string>
<string name="activity_type_driving_car">vittura</string>
<string name="help_data_storage_content"> I dati di i cattori sò piazzati à a pusizione arregistrata. In casu d’un cattore di frequenza cardiaca, vole si dì chì solu <i>una</i> misura di frequenza cardiaca hè arregistrata à a pusizione. S’è vo state immobile o s’è vo site dentru, nisunu datu di cattore ùn serà arregistratu (ma serà affissatu). </string>
<string name="help_gps_troubleshooting_content"> \u0020Ciò ch’ella si pò fà :
\n \u0020a) verificate chì u GPS hè attivatu,
\n \u0020b) amendà a ricezzione (p.i. andate fora),
\n \u0020c) cuntrollate cù l’appiecazione <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a>, è
\n \u0020d) disattivate l’ecunumia d’energia.
\n
\n \u0020S’è tuttu què ùn aiuta micca, ci vole à signalà u prublema. \u0020</string>
<string name="help_support_content"> \u0020Pudete :
\n \u0020a) fà cunnosce OpenTracks,
\n \u0020b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">prupone funzioni</a>,
\n \u0020c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">signalà i prublemi</a>,
\n \u0020d) aiutà à u sviluppu di u prugramma,
\n \u0020e) cumprallu in u <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,
\n \u0020f) fà una dunazione, è
\n \u0020g) fà tuttu ciò ch’ella vi pare ghjuvevule per OpenTracks. \u0020</string>
<string name="help_gps_troubleshooting_content">Ciò ch’ella si pò fà :\n a) verificate chì u GPS hè attivatu,\n b) amendà a ricezzione (p.i. andate fora),\n c) cuntrollate cù l’appiecazione <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a>, è\n d) disattivate l’ecunumia d’energia.\n\n S’è tuttu què ùn aiuta micca, ci vole à signalà u prublema.</string>
<string name="help_support_content">Pudete :\n a) fà cunnosce OpenTracks,\n b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">prupone funzioni</a>,\n c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">signalà i prublemi</a>,\n d) aiutà à u sviluppu di u prugramma,\n e) cumprallu in u <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,\n f) fà una dunazione, è\n g) fà tuttu ciò ch’ella vi pare ghjuvevule per OpenTracks.</string>
<string name="activity_type_boat">battellu</string>
<string name="activity_type_cross_country_skiing">schì di fondu</string>
<string name="help_feature_title">OpenTracks pussede quelle funzioni :</string>
@@ -423,7 +407,7 @@
<string name="power_x_watt">Power {x, plural, =1 {1 vattu} other {# vatti} }</string>
<string name="permission_recording_failed">OpenTracks richiede u permessu d’accede à a vostra pusizione ma dinù à l’apparechji vicini.</string>
<string name="permission_bluetooth_failed_rejected">Duvete cunsente manualmente à OpenTracks d’accede à l’apparechji vicini in i parametri di u sistema.</string>
<string name="sensor_state_cadence_unit">rpm</string>
<string name="sensor_state_cadence_unit">g/min</string>
<string name="sensor_state_heart_rate_value">%1$d palpiti à u minutu</string>
<string name="sensor_state_power_unit">W</string>
<string name="settings_defaults_summary">Unità preferite, attività predefinita, nome predefinitu di u tracciatu</string>
@@ -554,7 +538,8 @@
<string name="settings_announcements_idle_title">Inattività</string>
<string name="voiceIdle">Diventà inattivu.</string>
<string name="settings_locale_title">Lingua</string>
<string name="settings_locale_system_default">Parametri predefiniti di u sistema</string>
<string name="settings_locale_system_default">Predefinizione di u sistema</string>
<string name="stats_gain_loss">Guadagnu è perdita</string>
<string name="voice_announce_lap_power_key">AnnunziamentuVucalePutenzaÀUGiru</string>
<string name="lap_heart_rate">Frequenza cardiaca à u giru</string>
</resources>
+1
View File
@@ -594,4 +594,5 @@ limitations under the License.
<string name="settings_locale_title">Jazyk</string>
<string name="settings_locale_system_default">Podle systému</string>
<string name="stats_gain_loss">Stoupání a klesání</string>
<string name="lap_heart_rate">Tepová frekvence za kolo</string>
</resources>
+1
View File
@@ -545,4 +545,5 @@
<string name="settings_locale_system_default">Réamhshocrú córais</string>
<string name="settings_locale_title">Teanga</string>
<string name="stats_gain_loss">Gnóthachan &amp; Caillteanas</string>
<string name="lap_heart_rate">Ráta croí glúine</string>
</resources>
+2 -1
View File
@@ -501,7 +501,7 @@ limitations under the License.
<string name="show_on_dashboard_not_installed">Nincs OpenTracks Dashboard API-t támogató alkalmazás telepítve.</string>
<string name="value_float_mile_hour">%1$.1f mérföld/óra</string>
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
<string name="settings_announcements_lap_heart_rate">Kör pulzusszám</string>
<string name="settings_announcements_lap_heart_rate">Körönkénti pulzusszám</string>
<string name="settings_sensor_bluetooth_service_filter_title">Bluetooth-eszközök szűrése</string>
<string name="activity_type_kickscooter">roller</string>
<string name="gps_recording_without_signal">Az OpenTracks GPS-adatok nélkül rögzít.</string>
@@ -588,4 +588,5 @@ limitations under the License.
<string name="settings_locale_title">Nyelv</string>
<string name="settings_locale_system_default">Rendszer alapértelmezett</string>
<string name="stats_gain_loss">Növekedés és veszteség</string>
<string name="lap_heart_rate">Körönkénti pulzusszám</string>
</resources>
+111 -11
View File
@@ -14,7 +14,7 @@ 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.
--><resources>
<string name="about_preference_title">A proposito di OpenTracks</string>
<string name="about_preference_title">Informazioni su OpenTracks</string>
<string name="about_description"><i>OpenTracks</i>è un\'applicazione di tracciamento sportivo che rispetta completamente la tua privacy.</string>
<string name="about_url">Pagina web del progetto: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Nome versione: %1$s</string>
@@ -189,7 +189,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="menu_aggregated_statistics">Statistiche aggregate</string>
<string name="menu_delete">Elimina</string>
<string name="menu_edit">Modifica</string>
<string name="menu_export_all">Esporta tutto</string>
<string name="menu_export_all">Esporta tutto (un file per traccia)</string>
<string name="menu_help">Aiuto</string>
<string name="menu_import_all">Importa tutto</string>
<string name="menu_insert_marker">Inserisci indicatore</string>
@@ -213,7 +213,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="settings_chart_by_distance">Per distanza</string>
<string name="settings_chart_by_time">Per durata</string>
<string name="settings_recording_default_activity_title">Tipo di attività predefinita</string>
<string name="settings_recording_location_frequency_summary">%1$s tra posizioni registrate</string>
<string name="settings_recording_location_frequency_summary">%1$s tra posizioni marcate</string>
<string name="settings_recording_max_recording_distance_summary">Viene creato un nuovo segmento se la distanza tra due punti è maggiore di %1$s</string>
<string name="settings_recording_max_recording_distance_title">Distanza massima</string>
<string name="settings_recording_min_recording_distance_title">Intervallo distanza tra registrazioni</string>
@@ -371,7 +371,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="activity_type_escooter">Scooter elettrico</string>
<string name="export_with_photos">con foto</string>
<string name="export_without_photos">senza foto</string>
<string name="export_track_already_exists_msg">Il percorso %1$s esiste già nella cartella di salvataggio.</string>
<string name="export_track_already_exists_msg">Il file %1$s esiste già nella cartella di salvataggio.</string>
<plurals name="generic_completed_with_errors">
<item quantity="one">Completato con un errore</item>
<item quantity="other">Completato con %1$d errori</item>
@@ -384,7 +384,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Durante la registrazione del percorso, mostra le statistiche senza sbloccare il dispositivo.</string>
<string name="description_default_speed_or_pace">Per tipo di attività</string>
<string name="export_all_do_it_for_all">applica lo stesso per tutti i conflitti</string>
<string name="export_track_errors">Percorsi non esportati</string>
<string name="export_track_errors">Files non esportati</string>
<string name="generic_overwrite">Sovrascrivere</string>
<string name="generic_skip">Ignora</string>
<string name="generic_separator_done_total">/</string>
@@ -422,9 +422,9 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="export_error_post_workout">Esportazione post-allenamento non riuscita, controllare la cartella di esportazione.</string>
<string name="settings_prevent_reimport_tracks_title">Impedire la reimportazione delle tracce</string>
<string name="export_progress_summary_errors_msg">Errori</string>
<string name="export_progress_summary_skip_msg">Tracce saltate</string>
<string name="export_progress_summary_overwrite_msg">Tracce sovrascritte</string>
<string name="export_progress_summary_new_msg">Nuove tracce esportate</string>
<string name="export_progress_summary_skip_msg">Files saltati</string>
<string name="export_progress_summary_overwrite_msg">Files sovrascritti</string>
<string name="export_progress_summary_new_msg">Nuovi files esportati</string>
<string name="import_no_kml_file_found">Nessun file kml trovato in KMZ</string>
<string name="import_progress_summary_ok_msg">File importati</string>
<string name="import_progress_summary_exists_msg">Esiste già</string>
@@ -448,7 +448,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="settings_sensor_running_speed_and_cadence">Distanza, Velocità e Cadenza</string>
<string name="stats_clock">Orologio</string>
<string name="settings_announcements_totaltime_frequency">Intervallo di tempo</string>
<string name="settings_announcements_distance_frequency">Intervallo di distanza</string>
<string name="settings_announcements_distance_frequency">Lunghezza giro</string>
<string name="location_latitude_longitude">%1$s, %2$s</string>
<string name="location_coordinate">%1$s°</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (consigliato)</string>
@@ -482,7 +482,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="settings_sensors_title">Sensori</string>
<string name="settings_announcements_statistics_title">Annunci Vocali</string>
<string name="settings_announcements_summary">Tempo, Distanza, Velocità della Voce</string>
<string name="settings_import_export_title">Importa/Esporta</string>
<string name="settings_import_export_title">Importa ed Esporta</string>
<string name="settings_import_export_summary">Importa, Esporta, Auto Esporta</string>
<string name="settings_units_category">Unità</string>
<string name="settings_activity_defaults_category">Predefiniti dell\'Attività</string>
@@ -491,7 +491,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="settings_layout_reset_confirm_message">Tutte le impostazioni di layout saranno reimpostate ai valori predefiniti. Questo non eliminerà alcuna traccia dal dispositivo.</string>
<string name="settings_layout_reset_confirm_title">Rempostare i tuoi layout\?</string>
<string name="settings_all_reset">Reimposta</string>
<string name="stats_sensors_heart_rate">Frequenza Cardiaca</string>
<string name="stats_sensors_heart_rate">Frequenza cardiaca</string>
<string name="stats_sensors_power">Potenza</string>
<string name="custom_layout_list_title">Disegni</string>
<string name="custom_layout_list_edit_name">Digitare il nome del layout da aggiungere</string>
@@ -513,4 +513,104 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="export_cannot_write_to_dir">Impossibile scrivere nella directory di esportazione.</string>
<string name="export_dir_not_writable">" (non scrivibile!)"</string>
<string name="no_compatible_file_manager_installed">Nessun file manager compatibile installato</string>
<string name="value_integer_knots">%1$d nodi</string>
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
<string name="total_distance">Distanza totale</string>
<string name="average_heart_rate">Frequenza cardiaca media</string>
<string name="error_crash_title">%1$s si è interrotto</string>
<string name="description_pace_nautical">Ritmo (min/NM)</string>
<string name="description_speed_nautical">Velocità (nodi)</string>
<string name="gps_recording_without_signal">OpenTracks sta registrando senza segnale GPS.</string>
<string name="image_discard">Cancella</string>
<string name="menu_export_all_one_file">Esporta tutto (un file con tutte le tracce)</string>
<string name="permission_recording_failed">OpenTracks richiede il permesso di accedere alla tua posizione e ai dispositivi vicini.</string>
<string name="sensor_state_heart_rate_unit">bpm</string>
<string name="sensor_state_cadence_unit">rpm (rotazioni per minuto)</string>
<string name="sensor_state_pressure">Barometro</string>
<string name="sensor_state_power_max">Potenza Massima</string>
<string name="sensor_state_power_unit">W</string>
<string name="settings_night_mode_option_night_oled">Notte (schermi OLED)</string>
<string name="settings_recording_idle_timeout_title">Soglia di inattività</string>
<string name="settings_announcements_lap_speed_pace">Velocità/ritmo del giro</string>
<string name="settings_api_title">API pubblica</string>
<string name="settings_public_api_disabled_toast">L\'API pubblica di OpenTracks è disabilitata: può essere riabilitata nelle impostazioni.</string>
<string name="settings_public_api_dashboard_enabled_title">Trasferimento dati automatico</string>
<string name="voice_per_kilometer">per kilometro</string>
<string name="share_crash_report">Condividi il report d\'errore</string>
<string name="report_bug">Segnala un problema</string>
<string name="show_on_dashboard">OpenTracks Dashboard API</string>
<string name="introduction_osm_dashboard">OpenTracks non fornisce mappe. Installa OSMDashboard per visualizzare le tue registrazioni su una mappa.</string>
<string name="show_on_dashboard_not_installed">Nessuna delle applicazioni installate supporta l\'API di OpenTracks Dashboard.</string>
<string name="settings_locale_title">Lingua</string>
<string name="settings_locale_system_default">Default di sistema</string>
<string name="settings_announcements_lap_heart_rate">Frequenza cardiaca del giro</string>
<string name="settings_announcements_average_speed_pace">Media velocità/ritmo</string>
<string name="settings_announcements_unit">Unità di misura della distanza/velocità</string>
<string name="settings_sensor_bluetooth_service_filter_title">Filtra dispositivi Bluetooth</string>
<string name="settings_sensor_bluetooth_service_filter_off">Mostra tutti i dispositivi Bluetooth che possono essere associati</string>
<string name="settings_stats_units_imperial_meter">Imperiale (mi,m)</string>
<string name="settings_stats_units_nautical">Nautiche (NM, ft)</string>
<string name="settings_public_api_enabled_title">API pubblica</string>
<string name="settings_public_api_enabled_summary_on">Altre applicazioni installate possono far partire o fermare le registrazioni.</string>
<string name="settings_api_summary">API per controllo remoto e statistiche con altre applicazioni</string>
<string name="settings_public_api_dashboard_enabled_summary_off">I dati registrati non saranno condivisi in automatico con altre applicazioni.</string>
<string name="track_delete_number_of_tracks">%1$d tracce</string>
<string name="voiceDistanceMilesPlural">{n, plural, =1 {un miglio} other {{n,number,#.0} miglia} }</string>
<string name="no_compatible_gallery_installed">Nessuna delle applicazioni galleria (foto) installate è compatibile</string>
<string name="no_compatible_camera_installed">Nessuna delle applicazioni telecamera installate è compatibile</string>
<string name="voice_announce_lap_power_key">voiceAnnounceLapPower</string>
<string name="settings_announcements_idle_category_title">Annunci di inattività</string>
<string name="settings_announcements_idle_title">Inattività</string>
<string name="settings_recording_title">Registrazione</string>
<string name="settings_theme_switch_restart">Cambiamenti al tema potrebbero richiedere un riavvio manuale dell\'applicazione.</string>
<string name="settings_ui_dynamic_colors_title">Colori Dinamici</string>
<string name="settings_ui_dynamic_colors_summary">Utilizza Colori Dinamici definiti dall\'utente (si applica solo ad Android 12+; richiede un riavvio manuale dell\'applicazione)</string>
<string name="settings_recording_min_required_accuracy_summary">La posizione viene ignorata se l\'accuratezza del segnale è maggiore di %1$s</string>
<string name="voice_on_device_speaker_title">Utilizza l\'altoparlante dello smartphone</string>
<string name="voice_per_nautical_mile">per miglio nautico</string>
<string name="voiceDistanceNauticalMilesPlural">{n, plural, =1 {un miglio nautico} other {{n,number,#.0} miglia nautiche} }</string>
<string name="current_heart_rate">Frequenza cardiaca istantanea</string>
<string name="stats_gain_loss">Guadagno &amp; Perdita</string>
<string name="track_discarding">Traccia da cancellare</string>
<string name="unit_knots">kn</string>
<string name="value_float_knots">%1$.1f nodi</string>
<string name="value_float_knots_recommended">%1$.1f nodi (consigliato)</string>
<string name="value_internal_sensor">Sensore interno</string>
<string name="value_int_seconds">%1$d s (consigliato)</string>
<string name="voiceHoursPlural">{n, plural, =1 {un\'ora} other {{n,number} ore} }</string>
<string name="voiceSecondsPlural">{n, plural, =1 {un secondo} other {{n,number} secondi} }</string>
<string name="voiceSpeedKilometersPerHourPlural">{n, plural, =1 {un kilometro all\'ora} other {{n,number,#.0} kilometri all\'ora} }</string>
<string name="voiceSpeedMilesPerHourPlural">{n, plural, =1 {un miglio all\'ora} other {{n,number,#.0} miglia all\'ora} }</string>
<string name="voiceSpeedMKnotsPlural">{n, plural, =1 {1 nodo} other {{n,number,#.0} nodi} }</string>
<string name="lap_time">Tempo del giro</string>
<string name="speed">Velocità media in movimento</string>
<string name="voiceDistanceKilometersPlural">{n, plural, =1 {un kilometro} other {{n,number,#.0} kilometri} }</string>
<string name="settings_announcements_lap_power">Potenza del giro</string>
<string name="power">Potenza</string>
<string name="settings_recording_location_distance_summary">%1$s tra posizioni registrate</string>
<string name="power_x_watt">Potenza {x, plural, =1 {un watt} other {# watt} }</string>
<string name="voice_per_mile">per miglio</string>
<string name="unit_nautical_mile">MN</string>
<string name="help_data_google_play_services_content">No</string>
<string name="generic_dismiss">Ignora</string>
<string name="always">Sempre</string>
<string name="activity_type_swimming">nuoto</string>
<string name="gps_recording_status">%1$s: %2$s</string>
<string name="activity_type_swimming_open">nuoto in mare</string>
<string name="settings_public_api_enabled_summary_off">Solo OpenTracks può far partire o fermare le registrazioni.</string>
<string name="pace">Ritmo</string>
<string name="lap_speed">Velocità del giro</string>
<string name="unit_minute_per_nautical_mile">min/NM</string>
<string name="voiceMinutesPlural">{n, plural, =1 {un minuto} other {{n,number} minuti} }</string>
<string name="settings_announcements_average_heart_rate">Frequenza cardiaca media</string>
<string name="activity_type_kickscooter">monopattino</string>
<string name="settings_recording_min_sampling_interval_title">Intervallo temporale di campionamento</string>
<string name="create_marker_error">Impossibile marcare la posizione. Riprovare.</string>
<string name="settings_export_filename_title">Formato del nome del file</string>
<string name="settings_sensor_heart_rate_max">Zone: frequenza cardiaca massima (bpm)</string>
<string name="settings_public_api_package_title">Nome del pacchetto per API pubblica</string>
<string name="settings_public_api_dashboard_enabled_summary_on">Un\'applicazione che ha fatto partire una registrazione può anche accedere ai dati registrati.</string>
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
<string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string>
<string name="lap_heart_rate">Frequenza cardiaca del giro</string>
</resources>
+2 -1
View File
@@ -520,7 +520,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
<string name="report_bug">Bug rapporteren</string>
<string name="share_crash_report">Deel het crash rapport</string>
<string name="settings_announcements_average_heart_rate">Gemiddelde hartslag</string>
<string name="settings_announcements_lap_heart_rate">Hartslag per ronde</string>
<string name="settings_announcements_lap_heart_rate">Ronde-hartslag</string>
<string name="image_discard">Weggooien</string>
<string name="settings_announcements_average_speed_pace">Gemiddelde snelheid/tred</string>
<string name="track_discarding">Track weggooien</string>
@@ -594,4 +594,5 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
<string name="settings_locale_title">Taal</string>
<string name="settings_locale_system_default">Systeemstandaard</string>
<string name="stats_gain_loss">Winst &amp; verlies</string>
<string name="lap_heart_rate">Ronde-hartslag</string>
</resources>
+4
View File
@@ -250,9 +250,13 @@
<bool name="voice_announce_average_speed_pace_default" translatable="false">true</bool>
<string name="voice_announce_lap_speed_pace_key" translatable="false">voiceAnnounceLapSpeedPace</string>
<bool name="voice_announce_lap_speed_pace_default" translatable="false">true</bool>
<string name="voice_announce_time_key" translatable="false">voiceAnnounceTime</string>
<bool name="voice_announce_time_default" translatable="false">true</bool>
<string name="voice_announce_unit_key" translatable="false">voiceAnnounceUnit</string>
<bool name="voice_announce_unit_default" translatable="false">true</bool>
<string name="voice_announce_heart_rate_current_key" translatable="false">voiceAnnounceHeartRateCurrent</string>
<bool name="voice_announce_heart_rate_current_default" translatable="false">false</bool>
<string name="voice_announce_average_heart_rate_key" translatable="false">voiceAnnounceAverageHeartRate</string>
<bool name="voice_announce_average_heart_rate_default" translatable="false">false</bool>
<string name="voice_announce_lap_heart_rate_key" translatable="false">voiceAnnounceLapHeartRate</string>
+1
View File
@@ -609,6 +609,7 @@ limitations under the License.
</string>
<string name="average_heart_rate">Average heart rate</string>
<string name="current_heart_rate">Current heart rate</string>
<string name="lap_heart_rate">Lap heart rate</string>
<!-- Waypoint Type -->
<string name="waypoint_type_atm">ATM</string>
<string name="waypoint_type_bank">bank</string>
@@ -39,6 +39,11 @@
android:title="@string/settings_announcements_totaltime_frequency"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="@bool/voice_announce_time_default"
android:key="@string/voice_announce_time_key"
android:title="@string/description_time" />
<SwitchPreferenceCompat
android:defaultValue="@bool/voice_announce_total_distance_default"
android:key="@string/voice_announce_total_distance_key"
@@ -54,6 +59,11 @@
android:key="@string/voice_announce_average_speed_pace_key"
android:title="@string/settings_announcements_average_speed_pace" />
<SwitchPreferenceCompat
android:defaultValue="@bool/voice_announce_heart_rate_current_default"
android:key="@string/voice_announce_heart_rate_current_key"
android:title="@string/current_heart_rate" />
<SwitchPreferenceCompat
android:defaultValue="@bool/voice_announce_average_heart_rate_default"
android:key="@string/voice_announce_average_heart_rate_key"