Compare commits

..

63 Commits

Author SHA1 Message Date
Dennis Guse edba2294f3 Release: v4.9.6 2023-12-04 20:16:24 +01:00
Stephan Paternotte 5743f29aed Translated using Weblate (Dutch)
Currently translated at 100.0% (125 of 125 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-12-04 07:27:05 +01:00
Max Xie 7b2ad1590c Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (516 of 516 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/zh_Hans/
2023-12-04 07:27:05 +01:00
gallegonovato 251a26d2f8 Translated using Weblate (Spanish)
Currently translated at 8.0% (10 of 125 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/es/
2023-12-04 07:27:05 +01:00
Fjuro fd362e17e1 Translated using Weblate (Czech)
Currently translated at 100.0% (125 of 125 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2023-12-04 07:27:05 +01:00
Luis Rosario bf0dd9cda4 Translated using Weblate (Portuguese (Portugal))
Currently translated at 92.2% (476 of 516 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/pt_PT/
2023-12-04 07:27:05 +01:00
dependabot[bot] 0fe176fa24 Bump org.mockito:mockito-android from 5.7.0 to 5.8.0
Bumps [org.mockito:mockito-android](https://github.com/mockito/mockito) from 5.7.0 to 5.8.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.7.0...v5.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-04 07:27:01 +01:00
Dennis Guse 9b45a70e67 Upgrade to Gradlew 8.5.0 2023-11-30 22:06:21 +01:00
Dennis Guse bd1b68dffd Bump com.android.tools.build:gradle from 8.2.0-rc01 to 8.2.0 2023-11-30 21:51:36 +01:00
dependabot[bot] 3c5c69fdb0 Bump actions/setup-java from 3 to 4
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-30 07:15:54 +01:00
Peter Storch 87d7ebcb64 fix #1769 export overwrite existing files 2023-11-28 21:35:17 +01:00
Dennis Guse a01edcc9e8 Bugfix: InputStream.transferTo() is only available from SDK33+.
Introcuced in 8b7063d18b.
2023-11-27 23:04:43 +01:00
Dennis Guse 8b7063d18b Cleanup: use InputStream.transferTo() 2023-11-24 07:11:22 +01:00
Dennis Guse 25d4daa295 Cleanup. 2023-11-24 07:11:07 +01:00
Peter Storch 5dbd17f3bb fix NPE at null barometer after unregister listener 2023-11-22 21:24:36 +01:00
Dennis Guse fd914630d0 Merge pull request #1755 from OpenTracksApp/barometer#1424
Refactoring sensor handling as preparation for BLE barometers.
2023-11-22 20:05:40 +01:00
Dennis Guse caf8b5526d Bugfix: BarometerInternal shouldn't send data after disconnect().
Part of #1424.
2023-11-21 07:18:10 +01:00
Dennis Guse 8d5a7e7e8b Barometer: use sensor aggregator.
Part of #1424.
2023-11-20 06:39:28 +01:00
Dennis Guse 9fb84cc47c Release: v4.9.5 2023-11-19 16:12:46 +01:00
Dennis Guse 5c7731bb1f Bugfix: removing sensor didn't remove the aggregator.
Introduced in 58d79380cf.
2023-11-16 06:53:48 +01:00
Dennis Guse ce21fe3569 Cleanup: renamed SensorData* to Aggregator*. 2023-11-15 18:41:13 +01:00
Dennis Guse d47fa27e02 Cleanup: SensorDataCyclingDistanceSpeed.Data is a record. 2023-11-15 07:26:04 +01:00
Dennis Guse 58d79380cf Sensors: separate data generation and aggregation.
Part of #1424.
2023-11-15 07:20:42 +01:00
Stephan Paternotte a0ab2e2896 Translated using Weblate (Dutch)
Currently translated at 100.0% (516 of 516 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/
2023-11-13 22:06:26 +01:00
plum7x 0bc526df06 Translated using Weblate (Chinese (Traditional))
Currently translated at 99.2% (512 of 516 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/zh_Hant/
2023-11-10 18:36:53 +00:00
Pavel Smirnov f51edbf712 Translated using Weblate (Russian)
Currently translated at 12.0% (15 of 124 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/ru/
2023-11-08 22:36:40 +00:00
Pavel Smirnov 2384b947b3 Translated using Weblate (Russian)
Currently translated at 98.2% (507 of 516 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/ru/
2023-11-08 22:36:37 +00:00
Stephan Paternotte aca7f9063e Translated using Weblate (Dutch)
Currently translated at 100.0% (516 of 516 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/
2023-11-07 15:06:10 +01:00
Hosted Weblate 78282b97ed Update translation files
Updated by "Remove blank strings" hook in Weblate.

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/
2023-11-07 07:02:03 +01:00
mm4c c32ab408e6 Translated using Weblate (Dutch)
Currently translated at 99.4% (513 of 516 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/
2023-11-07 07:02:02 +01:00
Dennis Guse 15f3ccbcbd Bluetooth: rename to BluetoothHandlers. 2023-11-06 18:09:19 +01:00
Dennis Guse f2594371d3 Bluetooth: inline BluetoothUtils into Handler classes. 2023-11-06 18:09:19 +01:00
Dennis Guse 4d54472c93 Bluetooth: use composition instead of inheritance.
Part of #1424.
2023-11-06 18:09:19 +01:00
Dennis Guse 3a2a62a6d7 Bluetooth: send sensor data directly to SensorManager.
Part of #1424.
2023-11-06 18:09:19 +01:00
Dennis Guse 17487961ad Bluetooth: upgrade minSDK=26 for simplification (race condition prevention).
Part of #1424.
2023-11-06 18:09:19 +01:00
mm4c af268c608a Translated using Weblate (Dutch)
Currently translated at 100.0% (516 of 516 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/
2023-11-06 07:46:22 +01:00
ngocanhtve 605d4c02f5 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (516 of 516 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/vi/
2023-11-06 07:46:22 +01:00
gallegonovato cae2de0982 Translated using Weblate (Spanish)
Currently translated at 100.0% (516 of 516 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/es/
2023-11-06 07:46:22 +01:00
Stephan Paternotte 257b051f13 Translated using Weblate (Dutch)
Currently translated at 100.0% (124 of 124 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-11-06 07:46:22 +01:00
Fjuro 0cf65e0b89 Translated using Weblate (Czech)
Currently translated at 100.0% (124 of 124 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2023-11-06 07:46:22 +01:00
Stephan Paternotte 062d48e69f Translated using Weblate (Dutch)
Currently translated at 100.0% (516 of 516 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/
2023-11-06 07:46:22 +01:00
Milo Ivir a270b1f64f Translated using Weblate (Croatian)
Currently translated at 100.0% (516 of 516 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/hr/
2023-11-06 07:46:22 +01:00
josé m f7bc25c17d Translated using Weblate (Galician)
Currently translated at 100.0% (516 of 516 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/gl/
2023-11-06 07:46:22 +01:00
Fjuro 90f82f7c5c Translated using Weblate (Czech)
Currently translated at 100.0% (516 of 516 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/
2023-11-06 07:46:22 +01:00
Dennis Guse 062457f1aa Cleanup: use Java 17's instanceof.
Fixes #1757.
2023-11-06 07:46:18 +01:00
dependabot[bot] cd4d297608 Bump com.android.tools:desugar_jdk_libs from 2.0.3 to 2.0.4
Bumps [com.android.tools:desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 2.0.3 to 2.0.4.
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 07:03:36 +01:00
dependabot[bot] 06c87bc7d4 Bump org.mockito:mockito-android from 5.6.0 to 5.7.0
Bumps [org.mockito:mockito-android](https://github.com/mockito/mockito) from 5.6.0 to 5.7.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.6.0...v5.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 07:03:12 +01:00
bneuburg df98fdd4de Fix idle announcement settings 2023-11-01 07:04:33 +01:00
Dennis Guse e9e9bb3874 Release: v4.9.4 2023-10-29 21:18:35 +01:00
Dennis Guse 1b39774e7a Merge branch 'weblate' 2023-10-29 21:08:54 +01:00
Dennis Guse 2c771ab75b Fix plurals. 2023-10-29 21:08:47 +01:00
ngocanhtve 6424b6ee41 Translated using Weblate (Vietnamese)
Currently translated at 7.3% (9 of 123 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/vi/
2023-10-29 20:01:56 +00:00
Stephan Paternotte 0ad94d562e Translated using Weblate (Dutch)
Currently translated at 100.0% (123 of 123 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-10-29 20:01:55 +00:00
ngocanhtve 2f7f466930 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (515 of 515 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/vi/
2023-10-29 20:01:44 +00:00
Milo Ivir 74a727426f Translated using Weblate (Croatian)
Currently translated at 100.0% (515 of 515 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/hr/
2023-10-29 20:01:43 +00:00
Dennis Guse cb48d7f706 Bugfix (Android14): for recording Location/GPS and Nearby Devices/Bluetooth are mandatory due to Android.
Fixes #1749.
2023-10-29 21:01:37 +01:00
Milo Ivir 6f364ab481 Translated using Weblate (Croatian)
Currently translated at 96.1% (495 of 515 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/hr/
2023-10-28 14:56:42 +02:00
Myoth c510bb66b2 Translated using Weblate (French)
Currently translated at 97.4% (502 of 515 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/fr/
2023-10-24 18:04:21 +02:00
Hosted Weblate d353a9985c Update translation files
Updated by "Remove blank strings" hook in Weblate.

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/
2023-10-21 04:17:43 +00:00
Limfjorden 0beb673db5 Translated using Weblate (Danish)
Currently translated at 93.2% (480 of 515 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/da/
2023-10-21 04:17:41 +00:00
Fjuro e1ee69dc38 Translated using Weblate (Czech)
Currently translated at 100.0% (123 of 123 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2023-10-19 06:12:37 +02:00
Reza Almanda 28ccae6cf2 Translated using Weblate (Indonesian)
Currently translated at 58.6% (302 of 515 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/id/
2023-10-19 06:12:29 +02:00
Peter Storch 4bf89f380a add template parameter to report_issue_link 2023-10-17 21:36:37 +02:00
113 changed files with 2514 additions and 2060 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
- name: checkout
uses: actions/checkout@v4
- uses: actions/setup-java@v3
- uses: actions/setup-java@v4
with:
distribution: 'adopt'
java-version: '17'
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-java@v3
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: checkout
uses: actions/checkout@v4
- uses: actions/setup-java@v3
- uses: actions/setup-java@v4
with:
distribution: 'adopt'
java-version: '17'
+6 -6
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0-beta06'
classpath 'com.android.tools.build:gradle:8.2.0'
}
}
@@ -64,12 +64,12 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 5491
versionName "v4.9.3"
versionCode 5554
versionName "v4.9.6"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdk 24
minSdk 26
targetSdk 34
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
@@ -125,7 +125,7 @@ android {
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.preference:preference:1.2.1'
@@ -141,7 +141,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'org.mockito:mockito-android:5.6.0'
androidTestImplementation 'org.mockito:mockito-android:5.8.0'
androidTestUtil 'androidx.test:orchestrator:1.4.2'
}
@@ -0,0 +1,4 @@
v4.9.3: OpenTracks
Bugfix:
- Zabránění pádu, protože toList() není vždy dostupný
@@ -0,0 +1,4 @@
v4.9.4: OpenTracks
Bugfix:
- Pád na systému Android 14 při neudělení oprávnění
@@ -0,0 +1,4 @@
v4.9.5: OpenTracks
Oprava chyby:
- nastavení oznámení o nečinnosti opět funguje
@@ -0,0 +1,5 @@
v4.9.4: OpenTracks
Bugfix:
- Crash on Android 14 if permissions were not granted
@@ -0,0 +1,4 @@
v4.9.5: OpenTracks
Bugfix:
- idle announcement setting is working again
@@ -0,0 +1,7 @@
v4.9.6: OpenTracks
Bugfix:
- Export: existing files is now completely overwriting
Developer:
- Refactored barometric data processing
@@ -0,0 +1,4 @@
v4.9.5: OpenTracks
Corrección de errores:
- La configuración del anuncio inactivo vuelve a funcionar
@@ -0,0 +1,10 @@
(versionName): OpenTracks
Cambios:
-
Corrección de errores:
-
Desarrollador:
-
@@ -0,0 +1,4 @@
v4.9.3: OpenTracks
Bugfix :
- Voorkom crash omdat toList() niet altijd beschikbaar is
@@ -0,0 +1,4 @@
v4.9.4: OpenTracks
Bugfix:
- Crash op Android 14 als er geen toestemming is verleend
@@ -0,0 +1,4 @@
v4.9.5: OpenTracks
Bugfix :
- inactieve aankondiging werkt weer
@@ -0,0 +1,15 @@
v3.0.0: OpenTracks
Первая версия OpenTracks (т.е. полное отделение от MyTracks)
Изменения:
- ребрендинг MyTracks в OpenTracks
- добавлена поддержка пульсометров Bluetooth LE
- удалены все возможности, требующие доступа в Интернет
- удалена интеграция с Google Maps
- удалено использование сервисов Google Play
- удалена поддержка датчиков ANT+
- удалена поддержка датчиков Bluetooth classic
- удален Protobuf
- удалены расчет калорий и определение типа активности
- удален интерфейс MyTracks Service API (т.е. больше нет интеграции с другими приложениями)
@@ -0,0 +1,17 @@
v3.0.1: OpenTracks
Изменения:
- можно делиться маркерами
- изменена структура настроек
- удалена кнопка "Назад" типа actionbar
- экспорт: убрана настройка размера фото
- экспорт: теперь можно выбрать формат KML
Исправление ошибок:
- папка экспорта теперь - /OpenTracks/
- ландшафтный режим больше не приводит к сбою
- исправлен экспорт KML/KMZ
Для разработчиков:
- тесты можно выполнить (но многие с ошибками)
- включена постоянная интеграция с TravisCI (без тестов)
@@ -0,0 +1,7 @@
v4.8.4: OpenTracks
Изменения:
- показывается макс. мощность для записанных треков
Для разработчиков:
- TrackList теперь использует RecyclerView
@@ -0,0 +1,23 @@
v4.9.0: OpenTracks
ВНИМАНИЕ: СУЩЕСТВЕННОЕ ИЗМЕНЕНИЕ
В этой версии меняется порядок определения OpenTracks состояния движения.
До настоящего времени скорость (от GPS или датчика) использовалась с применением порогового значения (например, менее 0,3 м/с).
Однако, это может приводить к получению очень "шумных" ("грязных") данных.
Теперь же используется длительность таймаута (т.е. период времени без существенных изменений местоположения длительностью X секунд).
Эта длительность задается пользователем.
События перехода в режим без движения сохраняются в базе данных, включая метки времени, поэтому статистика может быть пересчитана.
Возврат к предыдущей версии от этой не предусмотрен.
Пожалуйста, выполните резервную копию данных перед обновлением.
Изменения:
- определение того, что пользователь не двигается, на основе периода "без движения" вместо порога минимальной скорости
- добавлен вид активности "самокат"
Исправление ошибок:
- во время записи в списке треков индикация записи отображалась на всех треках
Для разработчиков:
- снова включены тесты выполнения действий Github
@@ -0,0 +1,4 @@
v4.9.1: OpenTracks
Исправление ошибок:
- сбой при выборе нескольких элементов в списке треков
@@ -0,0 +1,8 @@
v4.9.2: OpenTracks
Изменения:
- возможность использовать динамические цвета Android 12 (Material Design)
- поддержка предикативного действия "назад" Android 14 для некоторых ситуаций
Исправление ошибок:
- настройки для голосового уведомления об общем расстоянии ошибочно удалялись
@@ -0,0 +1,4 @@
v4.9.3: OpenTracks
Исправление ошибок:
- предотвращение сбоя из-за того, что toList() не всегда доступен
@@ -0,0 +1,4 @@
v4.9.4: OpenTracks
Исправление ошибок:
- сбой на Android 14, если не предоставлены разрешения
@@ -0,0 +1,10 @@
(версия): OpenTracks
Изменения:
-
Исправление ошибок:
-
Для разработчиков:
-
@@ -1,22 +1,22 @@
Спортивный трекер уважающий вашу приватность.
Спортивный трекер, уважающий вашу приватность.
Спорт и иные активности дарят радость, мастерство и уверенность в себе.
Цените свое здоровье, следя за тренировками.
Записывает, как вы бегаете или идете, и предоставляет вам функционал велокомпьютера с большим экраном для езды на велосипеде.
Цените свое здоровье, отслеживая тренировки.
Записывайте, как вы бегаете или прогуливаетесь, и получите функционал велокомпьютера с большим экраном.
Отмечайте интересные места на своем пути с помощью фотографий.
Храните записанные данные в мельчайших подробностях для анализа.
Делитесь только теми данными, которые хотите, чтобы другие имели доступ к ним.
Делитесь с другими только теми данными, которые выберете сами.
* Голосовые уведомления.
* Поддержка датчиков Bluetooth LE: пульс, скорость и дистанция (для велосипеда), частота вращения педалей (для велосипеда) и измеритель мощности (для велосипеда).
* Поддержка датчиков Bluetooth LE: пульс, скорость и расстояние (для велосипеда), частота вращения педалей (для велосипеда) и мощность (для велосипеда).
* Набор и потеря высоты: с помощью барометрического датчика.
* Высота указана в EGM2008 (над средним уровнем моря); экспортируется как WGS84.
* Экспорт данных в виде треков формата KMZ (включая фотографии), KML или GPX.
* Не требует соединения с Интернетом и лишних разрешений.
* Экспорт данных в форматах KMZ (включая фотографии), KML или GPX.
* Не требуются соединение с Интернетом и лишние разрешения.
* Темная и светлая темы с учетом системных настроек.
* Без рекламы.
Libre software / Свободное программное обеспечение / Открытый исходный код
означает, что вы можете использовать, изучать, изменять и делиться исходным кодом.
означает, что вы можете использовать, изучать, изменять исходный код и делиться им.
Лицензия Apache 2.0
@@ -0,0 +1,15 @@
v3.0.0: OpenTracks
Phát hành lần đầu của OpenTracks (tức là rẻ nhánh từ MyTracks)
Thay đổi:
- Đổi nhãn hiệu từ MyTracks thành OpenTracks
- Thêm cảm biến nhịp tim Bluetooth LE
- Loại bỏ mọi tính năng cần truy cập Internet
- Xóa tích hợp Google Maps
- Xóa việc sử dụng các dịch vụ của Google Play
- Đã loại bỏ cảm biến ANT +
- Loại bỏ cảm biến Bluetooth cổ điển
- Loại bỏ Protobuf
- Loại bỏ ước tính calo và ước tính hoạt động
- Xóa API dịch vụ MyTracks (tức là không tích hợp với các ứng dụng khác)
@@ -0,0 +1,8 @@
v4.9.2: OpenTracks
Thay đổi:
- Tùy chọn sử dụng Màu động (Material Design) của Android 12
- Quay lại dự đoán của Android 14 cho một số hoạt động
Sửa lỗi:
- Cài đặt cho thông báo bằng giọng nói tổng khoảng cách đã vô tình bị xóa
@@ -0,0 +1,4 @@
v4.9.3: OpenTracks
Sửa lỗi:
- Ngăn chặn sự cố vì toList() không phải lúc nào cũng có sẵn
@@ -0,0 +1,10 @@
(Tên phiên bản): OpenTracks
Thay đổi:
-
Sửa lỗi:
-
Nhà phát triển:
-
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+7 -7
View File
@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -202,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -48,6 +48,7 @@ import de.dennisguse.opentracks.TimezoneRule;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
@@ -58,11 +59,12 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@@ -71,14 +73,12 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p>
* Note: those tests are affected by {@link de.dennisguse.opentracks.sensors.sensorData.SensorData}.isRecent().
* Note: those tests are affected by {@link Aggregator}.isRecent().
* If the test device is too slow (like in a CI) these are likely to fail as the sensor data will be omitted from actual.
*/
@RunWith(AndroidJUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@@ -139,23 +139,22 @@ public class ExportImportTest {
Distance sensorDistance = Distance.of(10); // recording distance interval
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1);
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1f);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
// A sensor-only TrackPoint
trackPointCreator.setClock("2020-02-02T02:02:04Z");
mockAltitudeChange(trackPointCreator, 1);
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f);
trackPointCreator.setClock("2020-02-02T02:02:14Z");
mockBLESensorData(trackPointCreator, 15f, null, 67f, 3f, 50f);
mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null);
trackPointCreator.setClock("2020-02-02T02:02:15Z");
mockBLESensorData(trackPointCreator, null, null, 68f, 3f, 50f);
mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null);
trackPointCreator.setClock("2020-02-02T02:02:16Z");
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f); // Distance will be added to next TrackPoint
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null); // Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0f);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:18Z");
@@ -165,15 +164,16 @@ public class ExportImportTest {
trackPointCreator.setClock("2020-02-02T02:03:20Z");
service.resumeTrack(trackId);
sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0f);
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0f);
trackPointCreator.setClock("2020-02-02T02:03:30Z");
service.getTrackRecordingManager().onIdle();
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3, 16.001, 10, 27, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3, 16.001, 10, 27, 15, 10, 0f);
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
@@ -271,7 +271,9 @@ public class ExportImportTest {
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z")),
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z"))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:50Z"))
.setLatitude(3)
.setLongitude(16.001)
@@ -281,7 +283,6 @@ public class ExportImportTest {
.setAltitudeGain(0f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
}
@@ -545,40 +546,51 @@ public class ExportImportTest {
}
}
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) {
private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain) {
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate)));
AggregatorCyclingPower cyclingPower = Mockito.mock(AggregatorCyclingPower.class);
Mockito.when(cyclingPower.hasValue()).thenReturn(true);
Mockito.when(cyclingPower.getValue()).thenReturn(Power.of(power));
sensorDataSet.add(cyclingPower);
SensorDataCyclingCadence cyclingCadence = Mockito.mock(SensorDataCyclingCadence.class);
AggregatorHeartRate agheartRate = Mockito.mock(AggregatorHeartRate.class);
Mockito.when(agheartRate.getValue()).thenReturn(HeartRate.of(heartRate));
sensorDataSet.add(agheartRate);
AggregatorCyclingCadence cyclingCadence = Mockito.mock(AggregatorCyclingCadence.class);
Mockito.when(cyclingCadence.hasValue()).thenReturn(true);
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
sensorDataSet.set(cyclingCadence);
sensorDataSet.add(cyclingCadence);
if (distance != null && speed != null) {
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class);
AggregatorCyclingDistanceSpeed distanceSpeed = Mockito.mock(AggregatorCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed);
Mockito.when(distanceSpeed.getValue()).thenReturn(new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed)));
sensorDataSet.add(distanceSpeed);
} else {
sensorDataSet.add(new AggregatorCyclingDistanceSpeed("", ""));
}
trackPointCreator.getSensorManager().sensorDataSet = sensorDataSet;
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.onChange(sensorDataSet);
//TODO Might require: getSensorManager().sensorDataSet = new SensorDataSet()
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
AltitudeSumManager altitudeSumManager = trackPointCreator.getSensorManager().getAltitudeSumManager();
altitudeSumManager.setAltitudeGain_m(altitudeGain);
altitudeSumManager.setAltitudeLoss_m(altitudeGain);
private void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
if (altitudeGain != null) {
AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class);
Mockito.when(barometer.hasValue()).thenReturn(true);
Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
sensorDataSet.add(barometer);
} else {
sensorDataSet.add(new AggregatorBarometer(""));
}
}
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, float altitude, float altitudeGain) {
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, float altitude, Float altitudeGain) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
@@ -1,58 +0,0 @@
package de.dennisguse.opentracks.sensors;
import static org.mockito.Mockito.when;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
/**
* NOTE: Test data is completely artificial.
*/
@RunWith(MockitoJUnitRunner.class)
public class AltitudeSumManagerTest {
private static void addSensorValue(AltitudeSumManager altitudeSumManager, float[] values) {
for (float f : values) {
altitudeSumManager.onSensorValueChanged(AtmosphericPressure.ofHPA(f));
}
}
@Mock
BarometerInternal driver;
@Test
public void getAltitudeGainLoss_downhill() {
// given
AltitudeSumManager subject = new AltitudeSumManager(driver);
when(driver.isConnected()).thenReturn(true);
// then
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
// then
Assert.assertEquals(0f, subject.getAltitudeGain_m(), 0.01);
Assert.assertEquals(15f, subject.getAltitudeLoss_m(), 0.01);
}
@Test
public void sensorUnavailable() {
// given
AltitudeSumManager subject = new AltitudeSumManager(driver);
when(driver.isConnected()).thenReturn(true);
// then
subject.onSensorValueChanged(AtmosphericPressure.ofHPA(999f));
// then
Assert.assertNull(subject.getAltitudeGain_m());
Assert.assertNull(subject.getAltitudeLoss_m());
}
}
@@ -0,0 +1,25 @@
package de.dennisguse.opentracks.sensors;
import static org.junit.Assert.assertEquals;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
public class BluetoothHandlerBarometricPressureTest {
@Test
public void parseEnvironmentalSensing_Pa() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
// when
AtmosphericPressure pressure = BluetoothHandlerBarometricPressure.parseEnvironmentalSensing(characteristic);
// then
assertEquals(AtmosphericPressure.ofPA(100165), pressure);
}
}
@@ -0,0 +1,51 @@
package de.dennisguse.opentracks.sensors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Pair;
import org.junit.Test;
public class BluetoothHandlerCyclingDistanceSpeedTest {
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertNull(sensor.first);
assertEquals(200, sensor.second.crankRevolutionsCount());
}
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(65535 + 16777216, sensor.first.wheelRevolutionsCount());
assertNull(sensor.second);
}
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(200 + 16777216, sensor.first.wheelRevolutionsCount());
assertEquals(225, sensor.second.crankRevolutionsCount());
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import static org.junit.Assert.assertEquals;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
public class BluetoothHandlerCyclingPowerTest {
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
// then
assertEquals(40, powerCadence.power().getW(), 0.01);
}
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
// then
assertEquals(0, powerCadence.power().getW(), 0.01);
assertEquals(12, powerCadence.crank().crankRevolutionsCount());
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
}
}
@@ -0,0 +1,38 @@
package de.dennisguse.opentracks.sensors;
import static org.junit.Assert.assertEquals;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
import de.dennisguse.opentracks.data.models.HeartRate;
public class BluetoothHandlerManagerHeartRateTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(60), heartRate);
}
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(257), heartRate);
}
}
@@ -0,0 +1,28 @@
package de.dennisguse.opentracks.sensors;
import static org.junit.Assert.assertEquals;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
public class BluetoothHandlerRunningSpeedAndCadenceTest {
@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
// when
BluetoothHandlerRunningSpeedAndCadence.Data sensor = BluetoothHandlerRunningSpeedAndCadence.parseRunningSpeedAndCadence("sensorName", characteristic);
// then
assertEquals(Speed.of(5), sensor.speed());
assertEquals(Cadence.of(80), sensor.cadence());
assertEquals(Distance.of(6553.5 + 1677721.6), sensor.totalDistance());
}
}
@@ -1,139 +0,0 @@
package de.dennisguse.opentracks.sensors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
public class BluetoothUtilsTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(60), heartRate);
}
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(257), heartRate);
}
@Test
public void parseEnvironmentalSensing_Pa() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
// when
AtmosphericPressure pressure = BluetoothUtils.parseEnvironmentalSensing(characteristic);
// then
assertEquals(AtmosphericPressure.ofPA(100165), pressure);
}
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertNull(sensor.getDistanceSpeed());
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
}
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
// when
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(65535 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
assertNull(sensor.getCadence());
}
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(200 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
assertEquals(225, sensor.getCadence().getCrankRevolutionsCount());
}
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
// then
assertEquals(40, powerCadence.power().getValue().getW(), 0.01);
}
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
// then
assertEquals(0, powerCadence.power().getValue().getW(), 0.01);
assertEquals(12, powerCadence.cadence().getCrankRevolutionsCount());
assertEquals(17125, powerCadence.cadence().getCrankRevolutionsTime());
}
@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
// when
SensorDataRunning sensor = BluetoothUtils.parseRunningSpeedAndCadence("address", "sensorName", characteristic);
// then
assertEquals(Speed.of(5), sensor.getSpeed());
assertEquals(Cadence.of(80), sensor.getCadence());
assertEquals(Distance.of(6553.5 + 1677721.6), sensor.getTotalDistance());
}
}
@@ -0,0 +1,28 @@
package de.dennisguse.opentracks.sensors.sensorData;
import org.junit.Assert;
import org.junit.Test;
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)));
}
}
@Test
public void getAltitudeGainLoss_downhill() {
// given
AggregatorBarometer subject = new AggregatorBarometer("");
// then
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
// then
Assert.assertEquals(0f, subject.value.gain_m(), 0.01);
Assert.assertEquals(15f, subject.value.loss_m(), 0.01);
}
}
@@ -2,17 +2,17 @@ package de.dennisguse.opentracks.sensors.sensorData;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.UintUtils;
@RunWith(AndroidJUnit4.class)
@@ -20,12 +20,11 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_1() {
// given
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.compute(previous);
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 2048)));
// then
assertEquals(60, current.getValue().getRPM(), 0.01);
@@ -33,12 +32,11 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_2() {
// given
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 6184);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 8016);
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.compute(previous);
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 6184)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 8016)));
// then
assertEquals(33.53, current.getValue().getRPM(), 0.01);
@@ -46,12 +44,11 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_sameCount() {
// given
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 2048);
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.compute(previous);
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 2048)));
// then
assertEquals(Cadence.of(0), current.getValue());
@@ -60,126 +57,72 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_sameTime() {
// given
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 1024);
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.compute(previous);
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 1024)));
// then
assertFalse(current.hasValue());
assertFalse(current.hasValue()); //TODO Cadence should be 0?
}
@Test
public void compute_cadence_rollOverTime() {
// given
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 0);
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.compute(previous);
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 0)));
// then
assertEquals(60, current.getValue().getRPM(), 0.01);
}
@Ignore("Disabled from #953")
@Test
@Deprecated
public void compute_cadence_rollOverCount() {
// given
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.compute(previous);
// then
assertEquals(60, current.getValue().getRPM(), 0.01);
}
@Test
public void compute_cadence_overflow() {
// given
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous);
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(0, 2048)));
// then
// TODO See #953
// assertEquals(60, current.getValue().getRPM(), 0.01);
assertNull(current.getValue());
}
@Test
public void compute_speed() {
// given
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 1, 6184);
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 2, 8016);
AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", "");
current.setWheelCircumference(Distance.ofMM(2150));
// when
current.compute(previous, Distance.ofMM(2150));
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184)));
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016)));
// then
assertEquals(2.15, current.getValue().getDistance().toM(), 0.01);
assertEquals(1.20, current.getValue().getSpeed().toMPS(), 0.01);
assertEquals(2.15, current.getValue().distance().toM(), 0.01);
assertEquals(1.20, current.getValue().speed().toMPS(), 0.01);
}
@Ignore("Disabled from #953")
@Test
@Deprecated
public void compute_speed_rollOverCount() {
// given
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", "");
current.setWheelCircumference(Distance.ofMM(2000));
// when
current.compute(previous, Distance.ofMM(2000));
// then
assertEquals(2, current.getValue().getDistance().toM(), 0.01);
assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
}
@Test
public void compute_speed_overflow() {
// given
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous, Distance.ofMM(2000));
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(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());
}
@Test
public void equals_speed_with_no_data() {
// given
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress");
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
// then
assertNotEquals(previous, current);
assertNotEquals(previous, previous);
}
@Test
public void equals_cadence_with_no_data() {
// given
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress");
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
// then
assertNotEquals(previous, current);
assertNotEquals(previous, previous);
}
}
@@ -7,7 +7,6 @@ import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
@@ -19,6 +18,7 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.time.Instant;
import java.util.List;
@@ -28,24 +28,25 @@ import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.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.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests insert location.
* Note: on API23, the TrackRecordingService may not be stopped properly before the next test.
* So, if something fails, subsequent tests may be affected.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceRecordingTest {
@@ -58,14 +59,6 @@ public class TrackRecordingServiceRecordingTest {
private TrackRecordingService service;
private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(0f);
trackPoint.setAltitudeLoss(0f);
}
};
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
@@ -105,7 +98,8 @@ public class TrackRecordingServiceRecordingTest {
String startTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
// then
assertEquals(new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null)
@@ -142,7 +136,8 @@ public class TrackRecordingServiceRecordingTest {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
// when
String pauseTime = "2020-02-02T02:02:03Z";
@@ -188,7 +183,8 @@ public class TrackRecordingServiceRecordingTest {
String starTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(starTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
String pauseTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(pauseTime);
@@ -220,7 +216,8 @@ public class TrackRecordingServiceRecordingTest {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
String stopTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(stopTime);
@@ -230,7 +227,8 @@ public class TrackRecordingServiceRecordingTest {
String resumeTime = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(resumeTime);
service.resumeTrack(trackId);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
// then
new TrackPointAssert().assertEquals(List.of(
@@ -251,18 +249,19 @@ public class TrackRecordingServiceRecordingTest {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager();
mockAltitudeChange(trackPointCreator, 0);
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataSet.add(new AggregatorHeartRate("", ""));
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored
sensorManager.onChanged(new Raw<>(HeartRate.of(5))); //Should be ignored
String sensor3 = "2020-02-02T02:02:13Z";
trackPointCreator.setClock(sensor3);
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(7)));
sensorManager.onChanged(new Raw<>(HeartRate.of(7)));
String stopTime = "2020-02-02T02:02:15Z";
trackPointCreator.setClock(stopTime);
@@ -282,7 +281,6 @@ public class TrackRecordingServiceRecordingTest {
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_above() {
@@ -291,7 +289,7 @@ public class TrackRecordingServiceRecordingTest {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
// when
@@ -365,7 +363,8 @@ public class TrackRecordingServiceRecordingTest {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -429,7 +428,8 @@ public class TrackRecordingServiceRecordingTest {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -479,7 +479,8 @@ public class TrackRecordingServiceRecordingTest {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -525,7 +526,7 @@ public class TrackRecordingServiceRecordingTest {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
mockAltitudeChange(trackPointCreator, 0);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -581,25 +582,25 @@ public class TrackRecordingServiceRecordingTest {
@Test
public void testRecording_gpsAndSensor_gpsIdleMoving_sensorMoving() {
// TODO Check TrackStatistics
AltitudeSumManager altitudeSumManager = new AltitudeSumManager();
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager();
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataSet.add(new AggregatorRunning("", ""));
sensorManager.sensorDataSet.barometer = null;
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(0))); //Should be ignored
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(0)))); //Should be ignored
// when
String sensor2 = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(sensor2);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(2)));
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(2))));
// when
String gps1 = "2020-02-02T02:02:05Z";
@@ -608,12 +609,12 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor3 = "2020-02-02T02:02:06Z";
trackPointCreator.setClock(sensor3);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(12)));
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(12))));
// when
String sensor4 = "2020-02-02T02:02:07Z";
trackPointCreator.setClock(sensor4);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(14))); //Should be ignored
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(14)))); //Should be ignored
// when
String gps2 = "2020-02-02T02:02:08Z";
@@ -622,7 +623,7 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor5 = "2020-02-02T02:02:10Z";
trackPointCreator.setClock(sensor5);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(16))); //Should be ignored
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(16)))); //Should be ignored
// when
String gps3 = "2020-02-02T02:02:12Z";
@@ -671,4 +672,12 @@ public class TrackRecordingServiceRecordingTest {
.setSensorDistance(Distance.of(0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
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));
trackPointCreator.getSensorManager().sensorDataSet.barometer = barometer;
}
}
+1 -1
View File
@@ -8,6 +8,6 @@
"2020-02-02T03:03:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
"2020-02-02T03:03:21+01:00","TRACKPOINT",3,14.002,10,10,,54,0,0,,,,
"2020-02-02T03:03:22+01:00","SEGMENT_START_AUTOMATIC",3,16,10,10,,54,0,0,,,,
"2020-02-02T03:03:30+01:00","IDLE",,,,,,,,,,,,
"2020-02-02T03:03:30+01:00","IDLE",,,,,,,0,0,,,,
"2020-02-02T03:03:50+01:00","TRACKPOINT",3,16.001,10,10,,54,0,0,,,,
"2020-02-02T03:04:00+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
1 #time trackpoint_type latitude longitude altitude accuracy_horizontal accuracy_vertical speed altitude_gain altitude_loss sensor_distance heartrate cadence power
8 2020-02-02T03:03:20+01:00 SEGMENT_START_MANUAL
9 2020-02-02T03:03:21+01:00 TRACKPOINT 3 14.002 10 10 54 0 0
10 2020-02-02T03:03:22+01:00 SEGMENT_START_AUTOMATIC 3 16 10 10 54 0 0
11 2020-02-02T03:03:30+01:00 IDLE 0 0
12 2020-02-02T03:03:50+01:00 TRACKPOINT 3 16.001 10 10 54 0 0
13 2020-02-02T03:04:00+01:00 SEGMENT_END_MANUAL
@@ -212,7 +212,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
private void requestRequiredPermissions() {
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_LONG).show());
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_recording_failed, Toast.LENGTH_LONG).show());
}
@Override
@@ -379,8 +379,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
startGpsMenuItem.setVisibility(!isRecording ? View.VISIBLE : View.INVISIBLE);
if (!isRecording) {
startGpsMenuItem.setIcon(AppCompatResources.getDrawable(this, isGpsStarted ? gpsStatusValue.icon : R.drawable.ic_gps_off_24dp));
if (startGpsMenuItem.getIcon() instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) startGpsMenuItem.getIcon()).start();
if (startGpsMenuItem.getIcon() instanceof AnimatedVectorDrawable animatedVectorDrawable) {
animatedVectorDrawable.start();
}
}
}
@@ -416,8 +416,7 @@ public class TrackDataHub {
*
* @param track the track
*/
//TODO Could be @NonNull
void onTrackUpdated(Track track);
void onTrackUpdated(@NonNull Track track);
/**
* Called to clear previously-sent track points.
@@ -0,0 +1,3 @@
package de.dennisguse.opentracks.data.models;
public record AltitudeGainLoss(float gain_m, float loss_m) {}
@@ -38,15 +38,6 @@ public record Speed(double speed_mps) {
return speed2;
}
public static Speed absDiff(Speed speed1, Speed speed2) {
//TODO Why Math.abs? Seems to be a leftover.
return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps));
}
public Speed mul(double factor) {
return new Speed(factor * speed_mps);
}
public boolean isZero() {
return speed_mps == 0;
}
@@ -49,7 +49,6 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
private void loadData(List<DocumentFile> documentFiles) {
List<ArrayList<DocumentFile>> nestedFileList = documentFiles.stream()
.map(FileUtils::getFiles)
// TODO flatMap(Collection::stream) fails with ClassCastException; try in the future again
.collect(Collectors.toList());
List<DocumentFile> fileList = new ArrayList<>();
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -228,7 +229,6 @@ public class KmzTrackImporter {
* @param trackId the track's id which image belongs to.
* @param fileName the file name
*/
@Deprecated //TODO Use JDK9's inputStream.transferTo() instead of manual buffer
private void readAndSaveImageFile(ZipInputStream zipInputStream, Track.Id trackId, String fileName) throws IOException {
if (trackId == null || "".equals(fileName)) {
return;
@@ -238,10 +238,14 @@ public class KmzTrackImporter {
File file = new File(dir, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
byte[] buffer = new byte[4096];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
zipInputStream.transferTo(fileOutputStream);
} else {
byte[] buffer = new byte[4096];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
}
}
}
@@ -1,108 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
/**
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/
public class AltitudeSumManager implements SensorConnector {
private static final String TAG = AltitudeSumManager.class.getSimpleName();
private final BarometerInternal driver;
private AtmosphericPressure lastAcceptedSensorValue;
private AtmosphericPressure lastSeenSensorValue;
private Float altitudeGain_m;
private Float altitudeLoss_m;
public AltitudeSumManager() {
driver = new BarometerInternal();
}
@VisibleForTesting
public AltitudeSumManager(BarometerInternal mock) {
this.driver = mock;
}
public void start(Context context, Handler handler) {
driver.connect(context, handler, this);
lastAcceptedSensorValue = null;
reset();
}
public void stop(Context context) {
Log.d(TAG, "Stop");
driver.disconnect(context);
reset();
}
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(altitudeGain_m);
trackPoint.setAltitudeLoss(altitudeLoss_m);
}
@Nullable
public Float getAltitudeGain_m() {
return driver.isConnected() ? altitudeGain_m : null;
}
@VisibleForTesting
public void setAltitudeGain_m(float altitudeGain_m) {
this.altitudeGain_m = altitudeGain_m;
}
@Nullable
public Float getAltitudeLoss_m() {
return driver.isConnected() ? altitudeLoss_m : null;
}
@VisibleForTesting
public void setAltitudeLoss_m(float altitudeLoss_m) {
this.altitudeLoss_m = altitudeLoss_m;
}
public void reset() {
Log.d(TAG, "Reset");
altitudeGain_m = null;
altitudeLoss_m = null;
}
public void onSensorValueChanged(AtmosphericPressure currentSensorValue) {
if (lastAcceptedSensorValue == null) {
lastAcceptedSensorValue = currentSensorValue;
lastSeenSensorValue = currentSensorValue;
return;
}
altitudeGain_m = altitudeGain_m != null ? altitudeGain_m : 0;
altitudeLoss_m = altitudeLoss_m != null ? altitudeLoss_m : 0;
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, lastSeenSensorValue, currentSensorValue);
if (altitudeChange != null) {
altitudeGain_m += altitudeChange.getAltitudeGain_m();
altitudeLoss_m += altitudeChange.getAltitudeLoss_m();
lastAcceptedSensorValue = altitudeChange.currentSensorValue();
}
lastSeenSensorValue = currentSensorValue;
Log.v(TAG, "altitude gain: " + altitudeGain_m + ", altitude loss: " + altitudeLoss_m);
}
}
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.sensors;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
@@ -24,29 +25,27 @@ import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
/**
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
* Also parses the transferred data into {@link SensorDataObserver}.
*/
public abstract class AbstractBluetoothConnectionManager<DataType> {
@SuppressLint("MissingPermission")
public class BluetoothConnectionManager {
private static final String TAG = AbstractBluetoothConnectionManager.class.getSimpleName();
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final SensorDataObserver observer;
private final SensorManager.SensorDataChangedObserver observer;
private final List<ServiceMeasurementUUID> serviceMeasurementUUIDs;
private final SensorHandlerInterface sensorHandler;
private BluetoothGatt bluetoothGatt;
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@@ -76,7 +75,7 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothGattService gattService = null;
ServiceMeasurementUUID serviceMeasurement = null;
for (ServiceMeasurementUUID s : serviceMeasurementUUIDs) {
for (ServiceMeasurementUUID s : sensorHandler.getServices()) {
gattService = gatt.getService(s.serviceUUID());
if (gattService != null) {
serviceMeasurement = s;
@@ -112,37 +111,25 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
UUID serviceUUID = characteristic.getService().getUuid();
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress() + " with service " + serviceUUID + " and characteristics " + characteristic.getUuid());
Optional<ServiceMeasurementUUID> serviceMeasurementUUID = serviceMeasurementUUIDs.stream()
.filter(s -> s.serviceUUID().equals(characteristic.getService().getUuid())).findFirst();
Optional<ServiceMeasurementUUID> serviceMeasurementUUID = sensorHandler.getServices()
.stream()
.filter(s -> s.serviceUUID().equals(characteristic.getService().getUuid()))
.findFirst();
if (serviceMeasurementUUID.isEmpty()) {
Log.e(TAG, "Unknown service UUID; not supported?");
return;
}
SensorData<DataType> sensorData = parsePayload(serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
observer.onChanged(sensorData);
} else {
//TODO This might lead to NPEs in case of race conditions due to shutdown.
observer.getHandler().post(() -> observer.onChanged(sensorData));
}
}
sensorHandler.handlePayload(observer, serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
}
};
AbstractBluetoothConnectionManager(ServiceMeasurementUUID serviceUUUID, SensorDataObserver observer) {
this.serviceMeasurementUUIDs = List.of(serviceUUUID);
BluetoothConnectionManager(SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
this.observer = observer;
this.sensorHandler = sensorHandler;
}
AbstractBluetoothConnectionManager(List<ServiceMeasurementUUID> serviceUUUID, SensorDataObserver observer) {
this.serviceMeasurementUUIDs = serviceUUUID;
this.observer = observer;
}
synchronized void connect(Context context, @NonNull BluetoothDevice device) {
synchronized void connect(Context context, Handler handler, @NonNull BluetoothDevice device) {
if (bluetoothGatt != null) {
Log.w(TAG, "Already connected; ignoring.");
return;
@@ -150,20 +137,15 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
Log.d(TAG, "Connecting to: " + device);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, this.observer.getHandler());
} else {
bluetoothGatt = device.connectGatt(context, false, connectCallback);
}
SensorData<?> sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
observer.onChanged(sensorData);
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
observer.onConnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
}
private synchronized void clearData() {
observer.onDisconnecting(createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
}
synchronized void disconnect() {
if (bluetoothGatt == null) {
Log.w(TAG, "Cannot disconnect if not connected.");
@@ -181,22 +163,4 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
return address.equals(bluetoothGatt.getDevice().getAddress());
}
protected abstract SensorData<DataType> createEmptySensorData(String address);
/**
* @return null if data could not be parsed.
*/
protected abstract SensorData<DataType> parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, @NonNull BluetoothGattCharacteristic characteristic);
@Deprecated //TODO REMOVE
interface SensorDataObserver {
void onChanged(SensorData<?> sensorData);
void onDisconnecting(SensorData<?> sensorData);
@NonNull
Handler getHandler();
}
}
@@ -1,47 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
public class BluetoothConnectionManagerCyclingCadence extends AbstractBluetoothConnectionManager<Cadence> {
private static final String TAG = BluetoothConnectionManagerCyclingCadence.class.getSimpleName();
BluetoothConnectionManagerCyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_CADENCE, observer);
}
@Override
protected SensorDataCyclingCadence createEmptySensorData(String address) {
return new SensorDataCyclingCadence(address);
}
@Override
protected SensorDataCyclingCadence parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
//TODO Implement to ServiceMeasurement.parse()?
if (serviceMeasurementUUID.equals(BluetoothUtils.CYCLING_POWER)) {
SensorDataCyclingPower.Data data = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic);
if (data!= null) {
return data.cadence();
}
} else if (serviceMeasurementUUID.equals(BluetoothUtils.CYCLING_SPEED_CADENCE)) {
SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getCadence() != null) {
return cadenceAndSpeed.getCadence();
}
}
Log.e(TAG, "Don't know how to decode this payload.");
return null;
}
}
@@ -1,32 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
public class BluetoothConnectionManagerCyclingDistanceSpeed extends AbstractBluetoothConnectionManager<SensorDataCyclingDistanceSpeed.Data> {
BluetoothConnectionManagerCyclingDistanceSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE, observer);
}
@Override
protected SensorDataCyclingDistanceSpeed createEmptySensorData(String address) {
return new SensorDataCyclingDistanceSpeed(address);
}
@Override
protected SensorDataCyclingDistanceSpeed parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getDistanceSpeed() != null) {
return cadenceAndSpeed.getDistanceSpeed();
}
return null;
}
}
@@ -1,27 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
public class BluetoothConnectionManagerCyclingPower extends AbstractBluetoothConnectionManager<Power> {
BluetoothConnectionManagerCyclingPower(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_POWER, observer);
}
@Override
protected SensorDataCyclingPower createEmptySensorData(String address) {
return new SensorDataCyclingPower(address);
}
@Override
protected SensorDataCyclingPower parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCyclingPower.Data cyclingPower = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic);
return cyclingPower != null ? cyclingPower.power() : null;
}
}
@@ -1,27 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
public class BluetoothConnectionManagerHeartRate extends AbstractBluetoothConnectionManager<HeartRate> {
BluetoothConnectionManagerHeartRate(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.HEARTRATE, observer);
}
@Override
protected SensorDataHeartRate createEmptySensorData(String address) {
return new SensorDataHeartRate(address);
}
@Override
protected SensorDataHeartRate parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
return heartRate != null ? new SensorDataHeartRate(address, sensorName, heartRate) : null;
}
}
@@ -1,24 +0,0 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
public class BluetoothConnectionRunningSpeedAndCadence extends AbstractBluetoothConnectionManager<SensorDataRunning.Data> {
BluetoothConnectionRunningSpeedAndCadence(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.RUNNING_SPEED_CADENCE, observer);
}
@Override
protected SensorDataRunning createEmptySensorData(String address) {
return new SensorDataRunning(address);
}
@Override
protected SensorDataRunning parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
return BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic);
}
}
@@ -0,0 +1,45 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(BAROMETRIC_PRESSURE);
}
@Override
public Aggregator<?, ?> createEmptySensorData(String address) {
return null; //TODO
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
//TODO
}
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
if (raw.length < 4) {
return null;
}
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
return AtmosphericPressure.ofPA(pressure / 10f);
}
}
@@ -0,0 +1,57 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import android.util.Pair;
import java.util.List;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
private static final String TAG = BluetoothHandlerCyclingCadence.class.getSimpleName();
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
BluetoothHandlerManagerCyclingPower.CYCLING_POWER,
BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return CYCLING_CADENCE;
}
@Override
public AggregatorCyclingCadence createEmptySensorData(String address) {
return new AggregatorCyclingCadence(address);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
if (serviceMeasurementUUID.equals(BluetoothHandlerManagerCyclingPower.CYCLING_POWER)) {
BluetoothHandlerManagerCyclingPower.Data data = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
if (data != null && data.crank() != null) {
observer.onChange(new Raw<>(data.crank()));
}
return;
}
if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE)) {
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, CrankData> data = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (data != null && data.second != null) {
observer.onChange(new Raw<>(data.second));
}
return;
}
Log.e(TAG, "Don't know how to decode this payload.");
}
public record CrankData(
long crankRevolutionsCount, // UINT32
int crankRevolutionsTime // UINT16; 1/1024s
) {}
}
@@ -0,0 +1,82 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterface {
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
new UUID(0x181600001000L, 0x800000805f9b34fbL),
new UUID(0x2A5B00001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(CYCLING_SPEED_CADENCE);
}
@Override
public AggregatorCyclingDistanceSpeed createEmptySensorData(String address) {
return new AggregatorCyclingDistanceSpeed(address);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> data = parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (data.first != null) {
observer.onChange(new Raw<>(data.first));
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
return null;
}
int flags = characteristic.getValue()[0];
boolean hasWheel = (flags & 0x01) > 0;
boolean hasCrank = (flags & 0x02) > 0;
int index = 1;
WheelData wheelData = null;
if (hasWheel && valueLength - index >= 6) {
long wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
index += 4;
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
wheelData = new WheelData(wheelTotalRevolutionCount, wheelTime);
index += 2;
}
BluetoothHandlerCyclingCadence.CrankData crankData = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
}
return new Pair<>(wheelData, crankData);
}
public record WheelData(
long wheelRevolutionsCount, // UINT32
int wheelRevolutionsTime // UINT16; 1/1024s
) {}
}
@@ -0,0 +1,87 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerManagerCyclingPower implements SensorHandlerInterface {
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
new UUID(0x181800001000L, 0x800000805f9b34fbL),
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(CYCLING_POWER);
}
@Override
public AggregatorCyclingPower createEmptySensorData(String address) {
return new AggregatorCyclingPower(address);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Data cyclingPower = parseCyclingPower(characteristic);
if (cyclingPower != null) {
observer.onChange(new Raw<>(cyclingPower));
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static Data parseCyclingPower(BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
return null;
}
int index = 0;
int flags1 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
int flags2 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
boolean hasPedalPowerBalance = (flags1 & 0x01) > 0;
boolean hasAccumulatedTorque = (flags1 & 0x04) > 0;
boolean hasWheel = (flags1 & 16) > 0;
boolean hasCrank = (flags1 & 32) > 0;
Integer instantaneousPower = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, index);
index += 2;
if (hasPedalPowerBalance) {
index += 1;
}
if (hasAccumulatedTorque) {
index += 2;
}
if (hasWheel) {
index += 2 + 2;
}
BluetoothHandlerCyclingCadence.CrankData crankData = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
}
return new Data(Power.of(instantaneousPower), crankData);
}
public record Data(Power power, BluetoothHandlerCyclingCadence.CrankData crank) {}
}
@@ -0,0 +1,70 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerManagerHeartRate implements SensorHandlerInterface {
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
new UUID(0x2A3700001000L, 0x800000805f9b34fbL)
);
// Used for device discovery in preferences
public static final List<ServiceMeasurementUUID> HEART_RATE_SUPPORTING_DEVICES = List.of(
HEARTRATE,
//Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery).
new ServiceMeasurementUUID(
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"), //Miband3
HEARTRATE.measurementUUID()
)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return HEART_RATE_SUPPORTING_DEVICES;
}
@Override
public AggregatorHeartRate createEmptySensorData(String address) {
return new AggregatorHeartRate(address);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
HeartRate heartRate = parseHeartRate(characteristic);
if (heartRate != null) {
observer.onChange(new Raw<>(heartRate));
}
}
@VisibleForTesting
public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
byte[] raw = characteristic.getValue();
if (raw.length == 0) {
return null;
}
boolean formatUINT16 = ((raw[0] & 0x1) == 1);
if (formatUINT16 && raw.length >= 3) {
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1));
}
if (!formatUINT16 && raw.length >= 2) {
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1));
}
return null;
}
}
@@ -0,0 +1,88 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInterface {
public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID(
new UUID(0x181400001000L, 0x800000805f9b34fbL),
new UUID(0x2A5300001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(RUNNING_SPEED_CADENCE);
}
@Override
public AggregatorRunning createEmptySensorData(String address) {
return new AggregatorRunning(address);
}
@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));
}
@VisibleForTesting
public static Data parseRunningSpeedAndCadence(String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.rsc_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
return null;
}
int flags = characteristic.getValue()[0];
boolean hasStrideLength = (flags & 0x01) > 0;
boolean hasTotalDistance = (flags & 0x02) > 0;
boolean hasStatus = (flags & 0x03) > 0; // walking vs running
Speed speed = null;
Cadence cadence = null;
Distance totalDistance = null;
int index = 1;
if (valueLength - index >= 2) {
speed = Speed.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index) / 256f);
}
index = 3;
if (valueLength - index >= 1) {
cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index));
// Hacky workaround as the Wahoo Tickr X provides cadence in SPM (steps per minute) in violation to the standard.
if (sensorName != null && sensorName.startsWith("TICKR X")) {
cadence = Cadence.of(cadence.getRPM() / 2);
}
}
index = 4;
if (hasStrideLength && valueLength - index >= 2) {
Distance strideDistance = Distance.ofCM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index));
index += 2;
}
if (hasTotalDistance && valueLength - index >= 4) {
totalDistance = Distance.ofDM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index));
}
return new Data(speed, cadence, totalDistance);
}
public record Data(Speed speed, Cadence cadence, Distance totalDistance) {}
}
@@ -29,7 +29,6 @@ import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@@ -47,29 +46,34 @@ import de.dennisguse.opentracks.util.PermissionRequester;
*
* @author Sandor Dornbush
*/
public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBluetoothConnectionManager.SensorDataObserver, SharedPreferences.OnSharedPreferenceChangeListener {
public class BluetoothRemoteSensorManager implements SensorConnector, SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(50); //TODO
private final BluetoothAdapter bluetoothAdapter;
private final Context context;
private final Handler handler;
private final SensorManager.SensorDataChangedObserver observer;
private boolean started = false;
private final BluetoothConnectionManagerHeartRate heartRate = new BluetoothConnectionManagerHeartRate(this);
private final BluetoothConnectionManagerCyclingCadence cyclingCadence = new BluetoothConnectionManagerCyclingCadence(this);
private final BluetoothConnectionManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManagerCyclingDistanceSpeed(this);
private final BluetoothConnectionManagerCyclingPower cyclingPower = new BluetoothConnectionManagerCyclingPower(this);
private final BluetoothConnectionRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionRunningSpeedAndCadence(this);
private final BluetoothConnectionManager heartRate;
private final BluetoothConnectionManager cyclingCadence;
private final BluetoothConnectionManager cyclingSpeed;
private final BluetoothConnectionManager cyclingPower;
private final BluetoothConnectionManager runningSpeedAndCadence;
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) {
this.context = context;
this.handler = handler;
this.observer = observer;
bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerHeartRate());
this.cyclingCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingSpeed = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerCyclingPower());
this.runningSpeedAndCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerRunningSpeedAndCadence());
}
@Override
@@ -95,7 +99,7 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
private synchronized void connect(AbstractBluetoothConnectionManager<?> connectionManager, String address) {
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled.");
return;
@@ -120,28 +124,12 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
Log.i(TAG, "Connecting to bluetooth address: " + address);
try {
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
connectionManager.connect(context, device);
connectionManager.connect(context, handler, device);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to get remote device for: " + address, e);
}
}
@Override
public synchronized void onChanged(SensorData<?> sensorData) {
observer.onChange(sensorData);
}
@Override
public void onDisconnecting(SensorData<?> sensorData) {
observer.onDisconnect(sensorData);
}
@NonNull
@Override
public Handler getHandler() {
return handler;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences unused, @Nullable String key) {
if (!started) return;
@@ -21,23 +21,9 @@ import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.BatteryLevel;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
/**
* Utilities for dealing with bluetooth devices.
@@ -53,52 +39,9 @@ public class BluetoothUtils {
new UUID(0x2A1900001000L, 0x800000805f9b34fbL)
);
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
new UUID(0x2A3700001000L, 0x800000805f9b34fbL)
);
// Used for device discovery in preferences
public static final List<ServiceMeasurementUUID> HEART_RATE_SUPPORTING_DEVICES = List.of(
HEARTRATE,
//Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery).
new ServiceMeasurementUUID(
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"), //Miband3
HEARTRATE.measurementUUID()
)
);
private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL)
);
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
new UUID(0x181800001000L, 0x800000805f9b34fbL),
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
);
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
new UUID(0x181600001000L, 0x800000805f9b34fbL),
new UUID(0x2A5B00001000L, 0x800000805f9b34fbL)
);
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
CYCLING_POWER,
CYCLING_SPEED_CADENCE
);
public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID(
new UUID(0x181400001000L, 0x800000805f9b34fbL),
new UUID(0x2A5300001000L, 0x800000805f9b34fbL)
);
private static final String TAG = BluetoothUtils.class.getSimpleName();
private BluetoothUtils() {
}
public static BluetoothAdapter getAdapter(Context context) {
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager == null) {
@@ -123,152 +66,4 @@ public class BluetoothUtils {
final int batteryLevel = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
return BatteryLevel.of(batteryLevel);
}
public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
byte[] raw = characteristic.getValue();
if (raw.length == 0) {
return null;
}
boolean formatUINT16 = ((raw[0] & 0x1) == 1);
if (formatUINT16 && raw.length >= 3) {
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1));
}
if (!formatUINT16 && raw.length >= 2) {
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1));
}
return null;
}
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
if (raw.length < 4) {
return null;
}
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
return AtmosphericPressure.ofPA(pressure / 10f);
}
public static SensorDataCyclingPower.Data parseCyclingPower(String address, String sensorName, BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
return null;
}
int index = 0;
int flags1 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
int flags2 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
boolean hasPedalPowerBalance = (flags1 & 0x01) > 0;
boolean hasAccumulatedTorque = (flags1 & 0x04) > 0;
boolean hasWheel = (flags1 & 16) > 0;
boolean hasCrank = (flags1 & 32) > 0;
Integer instantaneousPower = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, index);
index += 2;
if (hasPedalPowerBalance) {
index += 1;
}
if (hasAccumulatedTorque) {
index += 2;
}
if (hasWheel) {
index += 2 + 2;
}
SensorDataCyclingCadence cadence = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime);
}
return new SensorDataCyclingPower.Data(new SensorDataCyclingPower(sensorName, address, Power.of(instantaneousPower)), cadence);
}
public static SensorDataCyclingCadenceAndDistanceSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
return null;
}
int flags = characteristic.getValue()[0];
boolean hasWheel = (flags & 0x01) > 0;
boolean hasCrank = (flags & 0x02) > 0;
int index = 1;
SensorDataCyclingDistanceSpeed speed = null;
if (hasWheel && valueLength - index >= 6) {
int wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
index += 4;
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
speed = new SensorDataCyclingDistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime);
index += 2;
}
SensorDataCyclingCadence cadence = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime);
}
return new SensorDataCyclingCadenceAndDistanceSpeed(address, sensorName, cadence, speed);
}
public static SensorDataRunning parseRunningSpeedAndCadence(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.rsc_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
return null;
}
int flags = characteristic.getValue()[0];
boolean hasStrideLength = (flags & 0x01) > 0;
boolean hasTotalDistance = (flags & 0x02) > 0;
boolean hasStatus = (flags & 0x03) > 0; // walking vs running
Speed speed = null;
Cadence cadence = null;
Distance totalDistance = null;
int index = 1;
if (valueLength - index >= 2) {
speed = Speed.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index) / 256f);
}
index = 3;
if (valueLength - index >= 1) {
cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index));
// Hacky workaround as the Wahoo Tickr X provides cadence in SPM (steps per minute) in violation to the standard.
if (sensorName != null && sensorName.startsWith("TICKR X")) {
cadence = Cadence.of(cadence.getRPM() / 2);
}
}
index = 4;
if (hasStrideLength && valueLength - index >= 2) {
Distance strideDistance = Distance.ofCM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index));
index += 2;
}
if (hasTotalDistance && valueLength - index >= 4) {
totalDistance = Distance.ofDM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index));
}
return new SensorDataRunning(address, sensorName, speed, cadence, totalDistance);
}
}
@@ -0,0 +1,46 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
/**
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/
public class GainManager implements SensorConnector {
private static final String TAG = GainManager.class.getSimpleName();
private final BarometerInternal driver;
private final SensorManager.SensorDataChangedObserver listener;
public GainManager(SensorManager.SensorDataChangedObserver listener) {
this.listener = listener;
driver = new BarometerInternal();
}
public void start(Context context, Handler handler) {
driver.connect(context, handler, this);
if (driver.isConnected()) {
listener.onConnect(new AggregatorBarometer("internal"));
}
}
public void stop(Context context) {
Log.d(TAG, "Stop");
driver.disconnect(context);
listener.onDisconnect(new AggregatorBarometer("internal"));
}
public void onSensorValueChanged(AtmosphericPressure currentSensorValue) {
listener.onChange(new Raw<>(currentSensorValue));
}
}
@@ -9,7 +9,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.GPSManager;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@@ -27,13 +28,19 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
private final SensorDataChangedObserver listener = new SensorDataChangedObserver() {
@Override
public void onChange(SensorData<?> sensorData) {
sensorDataSet.set(sensorData);
public void onConnect(Aggregator<?, ?> sensorData) {
sensorDataSet.add(sensorData);
observer.onChange(new SensorDataSet(sensorDataSet));
}
@Override
public void onDisconnect(SensorData<?> sensorData) {
public void onChange(Raw<?> sensorData) {
sensorDataSet.update(sensorData);
observer.onChange(new SensorDataSet(sensorDataSet));
}
@Override
public void onDisconnect(Aggregator<?, ?> sensorData) {
sensorDataSet.remove(sensorData);
observer.onChange(new SensorDataSet(sensorDataSet));
}
@@ -41,7 +48,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
private BluetoothRemoteSensorManager bluetoothSensorManager;
private AltitudeSumManager altitudeSumManager;
private GainManager altitudeSumManager;
private GPSManager gpsManager;
@@ -51,7 +58,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
public void start(Context context, Handler handler) {
gpsManager = new GPSManager(observer); //TODO Pass listener
altitudeSumManager = new AltitudeSumManager();
altitudeSumManager = new GainManager(listener);
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
onSharedPreferenceChanged(null, null);
@@ -75,7 +82,6 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
}
public SensorDataSet fill(TrackPoint trackPoint) {
altitudeSumManager.fill(trackPoint);
sensorDataSet.fillTrackPoint(trackPoint);
return new SensorDataSet(sensorDataSet);
}
@@ -86,13 +92,12 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
return;
}
sensorDataSet.reset();
altitudeSumManager.reset();
}
@Deprecated
@VisibleForTesting
public BluetoothRemoteSensorManager getBluetoothSensorManager() {
return bluetoothSensorManager;
public void onChanged(Raw<?> data) {
listener.onChange(data);
}
public GPSManager getGpsManager() {
@@ -101,13 +106,13 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
@Deprecated
@VisibleForTesting
public AltitudeSumManager getAltitudeSumManager() {
public GainManager getAltitudeSumManager() {
return altitudeSumManager;
}
@Deprecated
@VisibleForTesting
public void setAltitudeSumManager(AltitudeSumManager altitudeSumManager) {
public void setAltitudeSumManager(GainManager altitudeSumManager) {
this.altitudeSumManager = altitudeSumManager;
}
@@ -120,8 +125,10 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
}
public interface SensorDataChangedObserver {
void onChange(SensorData<?> sensorData);
void onDisconnect(SensorData<?> sensorData);
void onConnect(Aggregator<?, ?> sensorData);
void onChange(Raw<?> sensorData);
void onDisconnect(Aggregator<?, ?> sensorData);
}
}
@@ -11,44 +11,40 @@ import android.util.Log;
import java.util.concurrent.TimeUnit;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.sensors.GainManager;
public class BarometerInternal {
public class BarometerInternal implements SensorEventListener {
private static final String TAG = BarometerInternal.class.getSimpleName();
private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5);
private AltitudeSumManager observer;
private GainManager observer;
private final SensorEventListener listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (!isConnected()) {
Log.w(TAG, "Not connected to sensor, cannot process data.");
return;
}
observer.onSensorValueChanged(AtmosphericPressure.ofHPA(event.values[0]));
@Override
public void onSensorChanged(SensorEvent event) {
if (!isConnected()) {
Log.w(TAG, "Not connected to sensor, cannot process data.");
return;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
Log.w(TAG, "Sensor accuracy changes are (currently) ignored.");
}
};
observer.onSensorValueChanged(AtmosphericPressure.ofHPA(event.values[0]));
}
public void connect(Context context, Handler handler, AltitudeSumManager observer) {
this.observer = observer;
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
Log.w(TAG, "Sensor accuracy changes are (currently) ignored.");
}
public void connect(Context context, Handler handler, GainManager observer) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
if (pressureSensor == null) {
Log.w(TAG, "No pressure sensor available.");
this.observer = null;
return;
}
if (sensorManager.registerListener(listener, pressureSensor, SAMPLING_PERIOD, handler)) {
if (sensorManager.registerListener(this, pressureSensor, SAMPLING_PERIOD, handler)) {
this.observer = observer;
return;
}
@@ -58,7 +54,8 @@ public class BarometerInternal {
public void disconnect(Context context) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(listener);
sensorManager.unregisterListener(this);
observer = null;
}
public boolean isConnected() {
@@ -1,48 +1,48 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
public abstract class SensorData<T> {
public abstract class Aggregator<Input extends Record, Output> {
protected T value;
protected Raw<Input> previous;
protected Output value;
private final String sensorAddress;
private final String sensorName;
private final Instant time;
SensorData(String sensorAddress) {
Aggregator(String sensorAddress) {
this(sensorAddress, null);
}
SensorData(String sensorAddress, String sensorName) {
this(sensorAddress, sensorName, Instant.now());
}
@VisibleForTesting
SensorData(String sensorAddress, String sensorName, Instant time) {
Aggregator(String sensorAddress, String sensorName) {
this.sensorAddress = sensorAddress;
this.sensorName = sensorName;
this.time = time;
}
public String getSensorNameOrAddress() {
return sensorName != null ? sensorName : sensorAddress;
}
public final void add(Raw<Input> current) {
computeValue(current);
previous = current;
}
protected abstract void computeValue(Raw<Input> current);
public boolean hasValue() {
return value != null;
}
@NonNull
protected abstract T getNoneValue();
protected abstract Output getNoneValue();
public T getValue() {
public Output getValue() {
if (!hasValue()) {
return null;
}
@@ -55,20 +55,23 @@ public abstract class SensorData<T> {
/**
* Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance.
*/
public void reset() {
}
public void reset() {};
/**
* Is the data recent considering the current time.
*/
private boolean isRecent() {
if (previous == null) {
return false;
}
return Instant.now()
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE));
.isBefore(previous.time().plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE));
}
@NonNull
@Override
public String toString() {
return "sensorAddress='" + sensorAddress;
return "sensorAddress=" + sensorAddress + " data=" + value;
}
}
@@ -0,0 +1,46 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.PressureSensorUtils;
public class AggregatorBarometer extends Aggregator<AtmosphericPressure, AltitudeGainLoss> {
private AtmosphericPressure lastAcceptedSensorValue;
public AggregatorBarometer(String sensorAddress) {
super(sensorAddress);
}
@Override
protected void computeValue(Raw<AtmosphericPressure> current) {
if (previous == null) {
lastAcceptedSensorValue = current.value();
value = getNoneValue();
return;
}
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, previous.value(), current.value());
if (altitudeChange != null) {
value = new AltitudeGainLoss(value.gain_m() + altitudeChange.getAltitudeGain_m(), value.loss_m() + altitudeChange.getAltitudeLoss_m());
lastAcceptedSensorValue = altitudeChange.currentSensorValue();
}
}
@NonNull
@Override
protected AltitudeGainLoss getNoneValue() {
return new AltitudeGainLoss(0f, 0f);
}
@Override
public void reset() {
value = getNoneValue();
}
public record Data(Altitude gain, Altitude loss) {}
}
@@ -0,0 +1,52 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.UintUtils;
public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCyclingCadence.CrankData, Cadence> {
private final String TAG = AggregatorCyclingCadence.class.getSimpleName();
public AggregatorCyclingCadence(String sensorAddress) {
super(sensorAddress);
}
public AggregatorCyclingCadence(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@Override
protected void computeValue(Raw<BluetoothHandlerCyclingCadence.CrankData> current) {
if (previous != null) {
float timeDiff_ms = UintUtils.diff(current.value().crankRevolutionsTime(), previous.value().crankRevolutionsTime(), UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null;
return;
}
// TODO We have to treat with overflow according to the documentation: read https://github.com/OpenTracksApp/OpenTracks/pull/953#discussion_r711625268
if (current.value().crankRevolutionsCount() < previous.value().crankRevolutionsCount()) {
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
return;
}
long crankDiff = UintUtils.diff(current.value().crankRevolutionsCount(), previous.value().crankRevolutionsCount(), UintUtils.UINT32_MAX);
value = Cadence.of(crankDiff, timeDiff);
}
}
@NonNull
@Override
protected Cadence getNoneValue() {
return Cadence.of(0);
}
}
@@ -0,0 +1,77 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.UintUtils;
public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerCyclingDistanceSpeed.WheelData, AggregatorCyclingDistanceSpeed.Data> {
private final String TAG = AggregatorCyclingDistanceSpeed.class.getSimpleName();
private Distance wheelCircumference;
public AggregatorCyclingDistanceSpeed(String sensorAddress) {
super(sensorAddress);
}
public AggregatorCyclingDistanceSpeed(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@Override
protected void computeValue(Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData> current) {
if (previous != null) {
float timeDiff_ms = UintUtils.diff(current.value().wheelRevolutionsTime(), previous.value().wheelRevolutionsTime(), UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute speed.");
value = null;
return;
}
if (current.value().wheelRevolutionsCount() < previous.value().wheelRevolutionsCount()) {
Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed.");
return;
}
long wheelDiff = UintUtils.diff(current.value().wheelRevolutionsCount(), previous.value().wheelRevolutionsCount(), UintUtils.UINT32_MAX);
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
Distance distanceOverall = distance;
if (value != null) {
distanceOverall = distance.plus(value.distanceOverall);
}
Speed speed_mps = Speed.of(distance, timeDiff);
value = new Data(distance, distanceOverall, speed_mps);
}
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.distance, Distance.of(0), value.speed);
}
}
@NonNull
@Override
protected Data getNoneValue() {
if (value != null) {
return new Data(value.distance, value.distanceOverall, Speed.zero());
} else {
return new Data(Distance.of(0), Distance.of(0), Speed.zero());
}
}
public void setWheelCircumference(Distance wheelCircumference) {
this.wheelCircumference = wheelCircumference;
}
public record Data(Distance distance, Distance distanceOverall, Speed speed) {}
}
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Power;
public class AggregatorCyclingPower extends Aggregator<Power, Power> {
public AggregatorCyclingPower(String address) {
super(address);
}
public AggregatorCyclingPower(String name, String address) {
super(name, address);
}
@Override
public void computeValue(Raw<Power> current) {
this.value = current.value();
}
@NonNull
@Override
protected Power getNoneValue() {
return Power.of(0f);
}
}
@@ -4,21 +4,19 @@ import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.HeartRate;
public class SensorDataHeartRate extends SensorData<HeartRate> {
public class AggregatorHeartRate extends Aggregator<HeartRate, HeartRate> {
public SensorDataHeartRate(String address) {
public AggregatorHeartRate(String address) {
super(address);
}
public SensorDataHeartRate(String name, String address, @NonNull HeartRate heartRate) {
public AggregatorHeartRate(String name, String address) {
super(name, address);
this.value = heartRate;
}
@NonNull
@Override
public String toString() {
return super.toString() + " heart=" + value;
protected void computeValue(Raw<HeartRate> current) {
this.value = current.value();
}
@NonNull
@@ -0,0 +1,61 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
/**
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors.
*/
public final class AggregatorRunning extends Aggregator<BluetoothHandlerRunningSpeedAndCadence.Data, AggregatorRunning.Data> {
private static final String TAG = AggregatorRunning.class.getSimpleName();
public AggregatorRunning(String sensorAddress) {
super(sensorAddress);
}
public AggregatorRunning(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@Override
public void computeValue(Raw<BluetoothHandlerRunningSpeedAndCadence.Data> current) {
if (previous != null) {
Distance distance = null;
if (previous.value().totalDistance() != null && current.value().totalDistance() != null) {
distance = current.value().totalDistance().minus(previous.value().totalDistance());
if (value != null) {
distance = distance.plus(value.distance);
}
}
value = new Data(current.value().speed(), current.value().cadence(), distance);
}
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.speed, value.cadence, Distance.of(0));
}
}
@NonNull
@Override
protected Data getNoneValue() {
if (value != null) {
return new Data(Speed.zero(), Cadence.of(0f), value.distance);
} else {
return new Data(Speed.zero(), Cadence.of(0f), Distance.of(0));
}
}
public record Data(Speed speed, Cadence cadence, @NonNull Distance distance) {
}
}
@@ -0,0 +1,16 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import java.time.Instant;
public record Raw<T extends Record>(
@NonNull T value,
@NonNull Instant time
) {
public Raw(@NonNull T value) {
this(value, Instant.now()); //TODO We should be using the MonotonicClock
}
}
@@ -1,87 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.UintUtils;
public class SensorDataCyclingCadence extends SensorData<Cadence> {
private final String TAG = SensorDataCyclingCadence.class.getSimpleName();
private final Long crankRevolutionsCount; // UINT32
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
public SensorDataCyclingCadence(String sensorAddress) {
super(sensorAddress);
this.crankRevolutionsCount = null;
this.crankRevolutionsTime = null;
}
public SensorDataCyclingCadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
super(sensorAddress, sensorName);
this.crankRevolutionsCount = crankRevolutionsCount;
this.crankRevolutionsTime = crankRevolutionsTime;
}
public boolean hasData() {
return crankRevolutionsCount != null && crankRevolutionsTime != null;
}
public long getCrankRevolutionsCount() {
return crankRevolutionsCount;
}
public int getCrankRevolutionsTime() {
return crankRevolutionsTime;
}
@NonNull
@Override
protected Cadence getNoneValue() {
return Cadence.of(0);
}
public void compute(SensorDataCyclingCadence previous) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null;
return;
}
// TODO We have to treat with overflow according to the documentation: read https://github.com/OpenTracksApp/OpenTracks/pull/953#discussion_r711625268
if (crankRevolutionsCount < previous.crankRevolutionsCount) {
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
return;
}
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
value = Cadence.of(crankDiff, timeDiff);
}
}
@NonNull
@Override
public String toString() {
return super.toString() + " cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SensorDataCyclingCadence comp)) return false;
if (hasData() && comp.hasData() == hasData()) {
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
} else {
return false;
}
}
}
@@ -1,27 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
public class SensorDataCyclingCadenceAndDistanceSpeed extends SensorData<Pair<SensorDataCyclingCadence, SensorDataCyclingDistanceSpeed>> {
public SensorDataCyclingCadenceAndDistanceSpeed(String sensorAddress, String sensorName, @Nullable SensorDataCyclingCadence cadence, @Nullable SensorDataCyclingDistanceSpeed distanceSpeed) {
super(sensorAddress, sensorName);
this.value = new Pair<>(cadence, distanceSpeed);
}
public SensorDataCyclingCadence getCadence() {
return this.value != null ? this.value.first : null;
}
public SensorDataCyclingDistanceSpeed getDistanceSpeed() {
return this.value != null ? this.value.second : null;
}
@NonNull
@Override
protected Pair<SensorDataCyclingCadence, SensorDataCyclingDistanceSpeed> getNoneValue() {
return new Pair<>(null, null);
}
}
@@ -1,138 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.UintUtils;
public class SensorDataCyclingDistanceSpeed extends SensorData<SensorDataCyclingDistanceSpeed.Data> {
private final String TAG = SensorDataCyclingDistanceSpeed.class.getSimpleName();
private final Long wheelRevolutionsCount; // UINT32
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
public SensorDataCyclingDistanceSpeed(String sensorAddress) {
super(sensorAddress);
this.wheelRevolutionsCount = null;
this.wheelRevolutionsTime = null;
}
public SensorDataCyclingDistanceSpeed(String sensorAddress, String sensorName, long wheelRevolutionsCount, int wheelRevolutionsTime) {
super(sensorAddress, sensorName);
this.wheelRevolutionsCount = wheelRevolutionsCount;
this.wheelRevolutionsTime = wheelRevolutionsTime;
}
public boolean hasData() {
return wheelRevolutionsCount != null && wheelRevolutionsTime != null;
}
public long getWheelRevolutionsCount() {
return wheelRevolutionsCount;
}
public int getWheelRevolutionsTime() {
return wheelRevolutionsTime;
}
@NonNull
@Override
protected Data getNoneValue() {
if (value != null) {
return new Data(value.distance, value.distanceOverall, Speed.zero());
} else {
return new Data(Distance.of(0), Distance.of(0), Speed.zero());
}
}
public void compute(SensorDataCyclingDistanceSpeed previous, Distance wheelCircumference) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute speed.");
value = null;
return;
}
if (wheelRevolutionsCount < previous.wheelRevolutionsCount) {
Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed.");
return;
}
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX);
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
Distance distanceOverall = distance;
if (previous.hasValue()) {
distanceOverall = distance.plus(previous.getValue().distanceOverall);
}
Speed speed_mps = Speed.of(distance, timeDiff);
value = new Data(distance, distanceOverall, speed_mps);
}
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.distance, Distance.of(0), value.speed);
}
}
@NonNull
@Override
public String toString() {
return super.toString() + " data=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SensorDataCyclingDistanceSpeed comp)) return false;
if (!(hasData() && comp.hasData())) {
return false;
}
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
}
public static class Data {
private final Distance distance;
private final Distance distanceOverall;
private final Speed speed;
private Data(Distance distance, Distance distanceOverall, Speed speed) {
this.distance = distance;
this.distanceOverall = distanceOverall;
this.speed = speed;
}
public Distance getDistance() {
return distance;
}
public Distance getDistanceOverall() {
return distanceOverall;
}
public Speed getSpeed() {
return speed;
}
@NonNull
@Override
public String toString() {
return "Data{" +
"distance=" + getDistance() +
", distance_overall=" + getDistanceOverall() +
", speed=" + getSpeed() +
'}';
}
}
}
@@ -1,46 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.data.models.Power;
public class SensorDataCyclingPower extends SensorData<Power> {
public SensorDataCyclingPower(String address) {
super(address);
}
public SensorDataCyclingPower(String name, String address, Power power) {
super(name, address);
this.value = power;
}
@NonNull
@Override
protected Power getNoneValue() {
return Power.of(0f);
}
@NonNull
@Override
public String toString() {
return super.toString() + " data=" + value;
}
public record Data(SensorDataCyclingPower power, SensorDataCyclingCadence cadence) {
public Data(SensorDataCyclingPower power, @Nullable SensorDataCyclingCadence cadence) {
this.power = power;
this.cadence = cadence;
}
@NonNull
@Override
public String toString() {
return "Data{" +
"power=" + power +
", cadence=" + cadence +
'}';
}
}
}
@@ -1,89 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
/**
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors.
*/
public final class SensorDataRunning extends SensorData<SensorDataRunning.Data> {
private static final String TAG = SensorDataRunning.class.getSimpleName();
private final Speed speed;
private final Cadence cadence;
private final Distance totalDistance;
public SensorDataRunning(String sensorAddress) {
super(sensorAddress);
this.speed = null;
this.cadence = null;
this.totalDistance = null;
}
public SensorDataRunning(String sensorAddress, String sensorName, Speed speed, Cadence cadence, Distance totalDistance) {
super(sensorAddress, sensorName);
this.speed = speed;
this.cadence = cadence;
this.totalDistance = totalDistance;
}
private boolean hasTotalDistance() {
return totalDistance != null;
}
public Cadence getCadence() {
return cadence;
}
public Speed getSpeed() {
return speed;
}
@VisibleForTesting
public Distance getTotalDistance() {
return totalDistance;
}
@NonNull
@Override
protected Data getNoneValue() {
if (value != null) {
return new Data(Speed.zero(), Cadence.of(0f), value.distance);
} else {
return new Data(Speed.zero(), Cadence.of(0f), Distance.of(0));
}
}
public void compute(SensorDataRunning previous) {
if (speed != null && hasTotalDistance()) {
Distance overallDistance = null;
if (previous != null && previous.hasTotalDistance()) {
overallDistance = this.totalDistance.minus(previous.totalDistance);
if (previous.hasValue() && previous.getValue().distance() != null) {
overallDistance = overallDistance.plus(previous.getValue().distance());
}
}
value = new Data(speed, cadence, overallDistance);
}
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.speed, value.cadence, Distance.of(0));
}
}
public record Data(Speed speed, Cadence cadence, @Nullable Distance distance) {}
}
@@ -5,27 +5,41 @@ import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
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.settings.PreferencesUtils;
public final class SensorDataSet {
private static final String TAG = SensorDataSet.class.getSimpleName();
private SensorDataHeartRate heartRate;
@VisibleForTesting
public AggregatorHeartRate heartRate;
private SensorDataCyclingCadence cyclingCadence;
@VisibleForTesting
public AggregatorCyclingCadence cyclingCadence;
private SensorDataCyclingDistanceSpeed cyclingDistanceSpeed;
@VisibleForTesting
public AggregatorCyclingDistanceSpeed cyclingDistanceSpeed;
private SensorDataCyclingPower cyclingPower;
@VisibleForTesting
public AggregatorCyclingPower cyclingPower;
private SensorDataRunning runningDistanceSpeedCadence;
@VisibleForTesting
public AggregatorRunning runningDistanceSpeedCadence;
@VisibleForTesting
public AggregatorBarometer barometer;
public SensorDataSet() {
}
@@ -36,6 +50,7 @@ public final class SensorDataSet {
this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed;
this.cyclingPower = toCopy.cyclingPower;
this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence;
this.barometer = toCopy.barometer;
}
public Pair<HeartRate, String> getHeartRate() {
@@ -51,55 +66,79 @@ public final class SensorDataSet {
return new Pair<>(cyclingCadence.getValue(), cyclingCadence.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null) {
return new Pair<>(runningDistanceSpeedCadence.getCadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.value.cadence() != null) {
return new Pair<>(runningDistanceSpeedCadence.value.cadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
public Pair<Speed, String> getSpeed() {
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue().getSpeed() != null) {
return new Pair<>(cyclingDistanceSpeed.getValue().getSpeed(), cyclingDistanceSpeed.getSensorNameOrAddress());
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue().speed() != null) {
return new Pair<>(cyclingDistanceSpeed.getValue().speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.getValue().speed() != null) {
return new Pair<>(runningDistanceSpeedCadence.getSpeed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
return new Pair<>(runningDistanceSpeedCadence.value.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
public SensorDataCyclingCadence getCyclingCadence() {
return cyclingCadence;
}
public SensorDataCyclingDistanceSpeed getCyclingDistanceSpeed() {
return cyclingDistanceSpeed;
}
public SensorDataCyclingPower getCyclingPower() {
public AggregatorCyclingPower getCyclingPower() {
return cyclingPower;
}
public SensorDataRunning getRunningDistanceSpeedCadence() {
return runningDistanceSpeedCadence;
}
public void set(SensorData<?> data) {
public void add(@NonNull Aggregator<?, ?> data) {
set(data, data);
}
public void remove(SensorData<?> type) {
public void update(@NonNull Raw<?> data) {
Record value = data.value();
if (value instanceof HeartRate) {
this.heartRate.add((Raw<HeartRate>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingCadence.CrankData) {
this.cyclingCadence.add((Raw<BluetoothHandlerCyclingCadence.CrankData>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingDistanceSpeed.WheelData ) {
this.cyclingDistanceSpeed.setWheelCircumference(PreferencesUtils.getWheelCircumference()); //TODO Fetch once and then listen for changes.
this.cyclingDistanceSpeed.add((Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData>) data);
return;
}
if (value instanceof BluetoothHandlerRunningSpeedAndCadence.Data) {
this.runningDistanceSpeedCadence.add((Raw<BluetoothHandlerRunningSpeedAndCadence.Data>) data);
return;
}
if (value instanceof BluetoothHandlerManagerCyclingPower.Data) {
this.cyclingPower.add((Raw<Power>) data);
return;
}
if (value instanceof AtmosphericPressure) {
this.barometer.add((Raw<AtmosphericPressure>) data);
return;
}
throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName());
}
public void remove(@NonNull Aggregator<?, ?> type) {
set(type, null);
}
public void clear() {
Log.i(TAG, "Removing all aggregators");
this.heartRate = null;
this.cyclingCadence = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
this.runningDistanceSpeedCadence = null;
this.barometer = null;
}
public void fillTrackPoint(TrackPoint trackPoint) {
@@ -116,7 +155,7 @@ public final class SensorDataSet {
}
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().getDistanceOverall());
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distanceOverall());
}
if (cyclingPower != null && cyclingPower.hasValue()) {
@@ -126,81 +165,49 @@ public final class SensorDataSet {
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) {
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().distance());
}
if (barometer != null && barometer.hasValue()) {
trackPoint.setAltitudeGain(barometer.getValue().gain_m());
trackPoint.setAltitudeLoss(barometer.getValue().loss_m());
}
}
public void reset() {
Log.i(TAG, "Resetting data");
if (heartRate != null) heartRate.reset();
if (cyclingCadence != null) cyclingCadence.reset();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset();
if (cyclingPower != null) cyclingPower.reset();
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset();
if (barometer != null) barometer.reset();
}
@NonNull
@Override
public String toString() {
return (heartRate != null ? "" + heartRate : "")
+ (cyclingCadence != null ? " " + cyclingCadence : "")
+ (cyclingDistanceSpeed != null ? " " + cyclingDistanceSpeed : "")
+ (cyclingPower != null ? " " + cyclingPower : "")
+ (runningDistanceSpeedCadence != null ? " " + runningDistanceSpeedCadence : "");
}
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName());
private void set(@NonNull SensorData<?> type, @Nullable SensorData<?> sensorData) {
if (type instanceof SensorDataHeartRate) {
this.heartRate = (SensorDataHeartRate) sensorData;
if (type instanceof AggregatorHeartRate) {
heartRate = (AggregatorHeartRate) sensorData;
return;
}
if (type instanceof SensorDataCyclingCadence) {
SensorDataCyclingCadence previous = getCyclingCadence();
Log.d(TAG, "Previous: " + previous + "; current: " + sensorData);
if (sensorData != null && sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cadence data repeated.");
return;
}
this.cyclingCadence = (SensorDataCyclingCadence) sensorData;
if (this.cyclingCadence != null) {
this.cyclingCadence.compute(previous);
}
if (type instanceof AggregatorCyclingCadence) {
cyclingCadence = (AggregatorCyclingCadence) sensorData;
return;
}
if (type instanceof SensorDataCyclingDistanceSpeed) {
SensorDataCyclingDistanceSpeed previous = getCyclingDistanceSpeed();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (sensorData != null && sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cycling speed data repeated.");
return;
}
Distance preferenceWheelCircumference = PreferencesUtils.getWheelCircumference(); //TODO Fetch once and then listen for changes.
this.cyclingDistanceSpeed = (SensorDataCyclingDistanceSpeed) sensorData;
if (this.cyclingDistanceSpeed != null) {
this.cyclingDistanceSpeed.compute(previous, preferenceWheelCircumference);
}
if (type instanceof AggregatorCyclingDistanceSpeed) {
cyclingDistanceSpeed = (AggregatorCyclingDistanceSpeed) sensorData;
return;
}
if (type instanceof SensorDataCyclingPower) {
this.cyclingPower = (SensorDataCyclingPower) sensorData;
if (type instanceof AggregatorCyclingPower) {
cyclingPower = (AggregatorCyclingPower) sensorData;
return;
}
if (type instanceof SensorDataRunning) {
SensorDataRunning previous = getRunningDistanceSpeedCadence();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (sensorData != null && sensorData.equals(previous)) {
Log.d(TAG, "onChanged: running speed data repeated.");
return;
}
this.runningDistanceSpeedCadence = (SensorDataRunning) sensorData;
if (this.runningDistanceSpeedCadence != null) {
this.runningDistanceSpeedCadence.compute(previous);
}
if (type instanceof AggregatorRunning) {
runningDistanceSpeedCadence = (AggregatorRunning) sensorData;
return;
}
if (type instanceof AggregatorBarometer) {
barometer = (AggregatorBarometer) sensorData;
return;
}
@@ -0,0 +1,17 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public interface SensorHandlerInterface {
List<ServiceMeasurementUUID> getServices();
Aggregator<?, ?> createEmptySensorData(String address);
void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic);
}
@@ -26,6 +26,7 @@ import android.os.Looper;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.util.Pair;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -36,6 +37,7 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
@@ -45,6 +47,7 @@ import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
import de.dennisguse.opentracks.util.SystemUtils;
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver {
@@ -199,6 +202,14 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
trackPointCreator.start(this, handler);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (!PermissionRequester.RECORDING.hasPermission(this)) {
Toast.makeText(this, R.string.permission_recording_failed, Toast.LENGTH_LONG).show();
return;
}
}
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
} else {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
@@ -34,7 +34,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
@NonNull
private Clock clock = new MonotonicClock();
private SensorManager sensorManager;
private final SensorManager sensorManager;
public TrackPointCreator(Callback service) {
this.service = service;
@@ -64,7 +64,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
return sensorManager.fill(trackPoint);
}
public synchronized void stop() {
public void stop() {
sensorManager.stop(context);
this.context = null;
}
@@ -139,11 +139,6 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
return sensorManager;
}
@VisibleForTesting
public void setSensorManager(SensorManager sensorManager) {
this.sensorManager = sensorManager;
}
@VisibleForTesting
public void setClock(@NonNull String time) {
this.clock = Clock.fixed(Instant.parse(time), ZoneId.of("CET"));
@@ -49,8 +49,8 @@ public class ResetDialogPreference extends DialogPreference {
Toast.makeText(activity, R.string.settings_layout_reset_done, Toast.LENGTH_SHORT).show();
}
if (activity instanceof ResetCallback) {
((ResetCallback) activity).onReset();
if (activity instanceof ResetCallback resetCallback) {
resetCallback.onReset();
}
}
}
@@ -52,9 +52,9 @@ public class SensorsSettingsFragment extends PreferenceFragmentCompat {
private void setWheelCircumferenceInputFilter() {
EditTextPreference wheelPreference = findPreference(getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key));
wheelPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue instanceof String) {
if (newValue instanceof String newValueString) {
try {
int newValueInt = Integer.parseInt((String) newValue);
int newValueInt = Integer.parseInt(newValueString);
return newValueInt >= 100 && newValueInt < 4000;
} catch (NumberFormatException e) {
Log.w(TAG, "Entered string is no number.");
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPreference {
@@ -27,6 +27,6 @@ public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPrefer
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_CADENCE);
return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingCadence.CYCLING_CADENCE);
}
}
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
@@ -27,6 +27,6 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_POWER);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerCyclingPower.CYCLING_POWER);
}
}
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreference {
@@ -27,6 +27,6 @@ public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreferen
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE);
}
}
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerHeartRate;
public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference {
@@ -27,6 +27,6 @@ public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.HEART_RATE_SUPPORTING_DEVICES);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerHeartRate.HEART_RATE_SUPPORTING_DEVICES);
}
}
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSensorPreference {
@@ -27,6 +27,6 @@ public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSens
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.RUNNING_SPEED_CADENCE);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE);
}
}
@@ -61,12 +61,12 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
Object bundleMarkerId = getIntent().getExtras().get(EXTRA_MARKER_ID);
Marker.Id markerId = null;
if (bundleMarkerId instanceof Marker.Id) {
markerId = (Marker.Id) bundleMarkerId;
if (bundleMarkerId instanceof Marker.Id cast) {
markerId = cast;
}
if (bundleMarkerId instanceof Long) {
if (bundleMarkerId instanceof Long cast) {
//Incoming Intent via Dashboard API.
markerId = new Marker.Id((Long) bundleMarkerId);
markerId = new Marker.Id(cast);
}
if (markerId == null) {
Log.d(TAG, "invalid marker id");
@@ -75,7 +75,6 @@ public class ListItemUtils {
// Match list_item_time_distance in list_item.xml
String time = StringUtils.formatElapsedTime(totalTime);
// TODO unit system
String distance = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context)
@@ -20,10 +20,10 @@ public class ViewUtils {
for (int i = 0; i < view.getChildCount(); i++) {
final View child = view.getChildAt(i);
if (child instanceof ViewGroup) {
makeClickableLinks((ViewGroup) child);
} else if (child instanceof TextView) {
((TextView) child).setMovementMethod(LinkMovementMethod.getInstance());
if (child instanceof ViewGroup childVg) {
makeClickableLinks(childVg);
} else if (child instanceof TextView childTv) {
childTv.setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
@@ -68,7 +68,7 @@ public class ExportUtils {
}
TrackExporter trackExporter = exportTask.getTrackFileFormat().createTrackExporter(context, contentProviderUtils);
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) {
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri, "wt")) {
if (!trackExporter.writeTrack(tracks, outputStream)) {
if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
throw new RuntimeException("Unable to delete exportDocumentFile");
@@ -89,13 +89,17 @@ public class PermissionRequester {
}
private static final List<String> ALL_PERMISSIONS;
private static final List<String> RECORDING_PERMISSIONS;
static {
ArrayList<String> all = new ArrayList<>(GPS_PERMISSION);
all.addAll(BLUETOOTH_PERMISSIONS);
all.addAll(NOTIFICATION_PERMISSIONS);
ArrayList<String> recording = new ArrayList<>(GPS_PERMISSION);
recording.addAll(BLUETOOTH_PERMISSIONS);
ALL_PERMISSIONS = Collections.unmodifiableList(all);
RECORDING_PERMISSIONS = Collections.unmodifiableList(recording);
recording.addAll(NOTIFICATION_PERMISSIONS);
ALL_PERMISSIONS = Collections.unmodifiableList(recording);
}
public final static PermissionRequester GPS = new PermissionRequester(GPS_PERMISSION);
@@ -103,6 +107,7 @@ public class PermissionRequester {
public final static PermissionRequester NOTIFICATION = new PermissionRequester(NOTIFICATION_PERMISSIONS);
public final static PermissionRequester ALL = new PermissionRequester(ALL_PERMISSIONS);
public final static PermissionRequester RECORDING = new PermissionRequester(RECORDING_PERMISSIONS);
public interface RejectedCallback {
void rejected(PermissionRequester permissionRequester);
@@ -240,9 +240,9 @@ public class StringUtils {
public static OffsetDateTime parseTime(String xmlDateTime) {
try {
TemporalAccessor t = DateTimeFormatter.ISO_DATE_TIME.parseBest(xmlDateTime, ZonedDateTime::from, LocalDateTime::from);
if (t instanceof LocalDateTime) {
if (t instanceof LocalDateTime localDateTime) {
Log.w(TAG, "Date does not contain timezone information: using UTC.");
t = ((LocalDateTime) t).atZone(ZoneOffset.UTC);
t = localDateTime.atZone(ZoneOffset.UTC);
}
return OffsetDateTime.from(t);
} catch (Exception e) {
+1
View File
@@ -593,4 +593,5 @@ limitations under the License.
<string name="settings_ui_dynamic_colors_title">Dynamické barvy</string>
<string name="settings_recording_title">Záznam</string>
<string name="settings_ui_dynamic_colors_summary">Používat dynamické barvy definované uživatelem (pouze Android 12+; vyžaduje ruční restart)</string>
<string name="permission_recording_failed">Aplikace OpenTrack vyžaduje oprávnění k přístupu k vaší poloze a zařízením poblíž.</string>
</resources>
+151 -105
View File
@@ -15,75 +15,72 @@ See the License for the specific language governing permissions and
limitations under the License.
--><resources>
<string name="about_preference_title">Om OpenTracks</string>
<string name="about_description"><i> OpenTracks </i> er en sportssporingsapplikation, der fuldt ud respekterer dit privatliv.</string>
<string name="about_url">Projektwebside: <a href="%1$s">%1$s</a></string>
<string name="about_description"><i> OpenTracks </i> er en sportstracker, der fuldt ud respekterer dit privatliv.</string>
<string name="about_url">Projekthjemmeside: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Versionsnavn: %1$s</string>
<string name="about_version_code">Versionskode: %1$d</string>
<string name="about_history">OpenTracks er en gaffel fra <a href="https://code.google.com/archive/p/mytracks/"> <i> Googles MyTracks </i> </a> (afviklet i 2016).</string>
<string name="about_history">OpenTracks er en forgrening fra <a href="https://code.google.com/archive/p/mytracks/"> <i> Googles MyTracks </i> </a> (afviklet i 2016).</string>
<string name="about_libraries">Biblioteker</string>
<!-- Help -->
<string name="help_feature_title">OpenTracks har følgende funktioner:</string>
<string name="help_feature_content">
* Opret GPS-spor \n
* Brug en pulssensor (kun Bluetooth LE) \n
* stemmemeddelelser (efter tid eller afstand) \n
* Opret markører (inkl. billeder) \n
* fungerer helt offline \n
* vis spor på et kort (se nedenfor)</string>
<string name="help_feature_content">* opret GPS-spor
\n* brug en pulssensor (kun Bluetooth LE)
\n* stemmemeddelelser (efter tid eller afstand)
\n* opret markører (inkl. billeder)
\n* fungerer helt offline
\n* vis spor på et kort (se nedenfor)</string>
<string name="help_track_map_title">Hvordan viser man et spor på et kort?</string>
<string name="help_track_map_content"> Brug det lille kortikon.
\n a) (liveopdateringer) Installer <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Det giver online eller offline kort via <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\n Faktisk er OSMDashboard en separat app, der kun kan få adgang (midlertidigt) til dataene for de(t) spor, der skal vises.
\n b) Via et normalt kortprogram (f.eks. <a href="http://osmand.net/">OsmAnd</a> og <a href="https://maps.me/">MAPS.ME</a>). Disse programmer skal understøtte KMZ-filformatet (kun lokaliteter og tidsstempler). Faktisk skal en app understøtte <i>ACTION_VIEW</i> med MIME <i>application/vnd.google-earth.kmz</i>. </string>
\na) (liveopdateringer) Installer <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Den tilbyder online eller offline kort via <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\nFaktisk er OSMDashboard en separat app, der kun (midlertidigt) kan få adgang til dataene for de(t) spor, der vises.
\nb) Via et normalt kortprogram (f.eks. <a href="http://osmand.net/">OsmAnd</a> og <a href="https://maps.me/">MAPS.ME</a>). Disse programmer skal understøtte KMZ-filformatet (kun lokaliteter og tidsstempler). Faktisk skal en app understøtte <i>ACTION_VIEW</i> med MIME <i>application/vnd.google-earth.kmz</i>. </string>
<string name="help_storage_pictures_title">Hvordan gemmer OpenTracks billeder, der blev taget som markører?</string>
<string name="help_storage_pictures_content">Disse gemmes i appens interne lager (dvs. kun tilgængelig inden for OpenTracks).</string>
<string name="help_track_export_title">Hvordan kan jeg eksportere / dele indspillede numre?</string>
<string name="help_track_export_content"> Spor kan eksporteres fra indstillingerne som KMZ, KML eller GPX. KMZ / KML alle data; GPX kun placeringer. Spor, der deles via <i>delingsikonet</i> deles som KMZ. </string>
<string name="help_data_google_play_services_title">Kræver/bruger OpenTracks Google Play Service?</string>
<string name="help_storage_pictures_content">Disse gemmes i appens interne lager (dvs. kun tilgængelige i OpenTracks).</string>
<string name="help_track_export_title">Hvordan kan jeg eksportere/dele registrerede spor\?</string>
<string name="help_track_export_content"> Spor kan eksporteres fra indstillinger som KMZ, KML eller GPX. KMZ / KML – alle data; GPX – kun placeringer. Spor der deles via <i>delingsikonet</i> deles som KMZ. </string>
<string name="help_data_google_play_services_title">Gør OpenTracks brug af Google Play Services\?</string>
<string name="help_recording_title">Hvordan registrerer OpenTracks data?</string>
<string name="help_recording_content">
OpenTracks bruger enhedens GPS-sensor til at registrere placeringen kontinuerligt.
Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, højde), kan OpenTracks ikke gøre noget ved det.</string>
<string name="help_recording_content">OpenTracks bruger enhedens GPS-sensor til at registrere placeringen kontinuerligt. Hvis GPS-enheden rapporterer unøjagtige data (f.eks. placering, hastighed, højde), kan OpenTracks ikke gøre noget ved det.</string>
<string name="help_data_storage_title">Hvordan gemmer OpenTracks sensordata?</string>
<string name="help_data_storage_content"> Sensordata gemmes pr. Optaget placering. Med hensyn til en pulssensor betyder det, at der kun er <i> en </i> måling af hjertefrekvensen lagret pr. Placering. Hvis du står stille eller er indendørs, registreres ingen sensordata (men de vil blive vist). </string>
<string name="help_gps_troubleshooting_title">OpenTracks optager ikke placeringer?</string>
<string name="help_data_storage_content"> Sensordata gemmes per registrerede placering. For en pulssensor betyder det, at der kun er <i> en </i> måling af hjertefrekvensen lagret per Placering. Hvis du står stille eller er indendørs, registreres ingen sensordata (men de vil blive vist). </string>
<string name="help_gps_troubleshooting_title">Registrerer OpenTracks ikke placering\?</string>
<string name="help_gps_troubleshooting_content"> Hvad du kan gøre:
\n a) Kontroller, om GPS er aktiveret,
\n b) forbedre GPS-modtagelse (f.eks. gå udendørs),
\n c) Kontroller, om du har appen <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/"> GPSTest </a> og
\n d) deaktiver strømbesparelse.
\na) kontroller om GPS er aktiveret,
\nb) gør GPS-modtagelse bedre (gå f.eks. udendørs),
\nc) kontroller, om du har appen <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/"> GPSTest </a> og
\nd) deaktiver strømbesparelse
\n
\n Hvis alt ikke hjælper, skal du rapportere en fejl. </string>
\nHvis intet af ovenstående hjælper, skal du rapportere det som en fejl. </string>
<string name="help_support_title">Hvordan kan jeg støtte OpenTracks?</string>
<string name="help_support_content"> Du kan:
\n a) sprede ordet om OpenTracks,
\n b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> foreslå funktioner </a>,
\n c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> rapportfejl </a>,
\n d) hjælp til softwareudvikling,
\n e) køb i <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play Store </a>,
\n f) donere, og
\n g) gør hvad du synes hjælper OpenTracks. </string>
\na) fortælle andre om OpenTracks,
\nb) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> foreslå funktioner </a>,
\nc) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> rapportere om fejl </a>,
\nd) hjælpe med udvikling af appen,
\ne) købe appen i <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play Store </a>,
\nf) donere, og
\ng) gøre alt andent, som du synes hjælper OpenTracks. </string>
<!-- Activity Type -->
<string name="activity_type_airplane">fly</string>
<string name="activity_type_atv">Terrængående køretøj</string>
<string name="activity_type_biking">cykling</string>
<string name="activity_type_blimp">luftskib</string>
<string name="activity_type_boat">skib</string>
<string name="activity_type_climbing">stigning</string>
<string name="activity_type_boat">båd</string>
<string name="activity_type_climbing">klatring</string>
<string name="activity_type_commercial_airplane">rutefly</string>
<string name="activity_type_cross_country_skiing">langrend</string>
<string name="activity_type_cycling">cykling</string>
<string name="activity_type_dirt_bike">motocross</string>
<string name="activity_type_donkey_back_riding">æselriding</string>
<string name="activity_type_donkey_back_riding">æselridning</string>
<string name="activity_type_driving">kørsel</string>
<string name="activity_type_driving_bus">buskørsel</string>
<string name="activity_type_driving_car">bilkørsel</string>
<string name="activity_type_ferry">færge</string>
<string name="activity_type_frisbee">friske</string>
<string name="activity_type_frisbee">disckastning</string>
<string name="activity_type_gliding">svæveflyvning</string>
<string name="activity_type_hang_gliding">drageflyvning</string>
<string name="activity_type_helicopter">helikopter</string>
<string name="activity_type_hiking">vandretur</string>
<string name="activity_type_hiking">vandring</string>
<string name="activity_type_horse_back_riding">hesteridning</string>
<string name="activity_type_hot_air_balloon">luftballon</string>
<string name="activity_type_ice_sailing">issejlads</string>
@@ -94,8 +91,8 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="activity_type_mixed_type">blandet type</string>
<string name="activity_type_motor_bike">motorcykel</string>
<string name="activity_type_motor_boating">motorbåd</string>
<string name="activity_type_mountain_biking">mountainbike</string>
<string name="activity_type_off_trail_hiking">vandretur i terræn</string>
<string name="activity_type_mountain_biking">mountainbiking</string>
<string name="activity_type_off_trail_hiking">vandring i terræn</string>
<string name="activity_type_other">andet</string>
<string name="activity_type_paddling">padling</string>
<string name="activity_type_para_gliding">paragliding</string>
@@ -110,7 +107,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="activity_type_sailing">sejlads</string>
<string name="activity_type_seaplane">vandfly</string>
<string name="activity_type_skate_boarding">skateboarding</string>
<string name="activity_type_skating">rulleskøjteløb</string>
<string name="activity_type_skating">skøjtning</string>
<string name="activity_type_skiing">skiløb</string>
<string name="activity_type_sky_jumping">faldskærmsudspring</string>
<string name="activity_type_sled">slædekørsel</string>
@@ -121,21 +118,21 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="activity_type_surfing">surfing</string>
<string name="activity_type_track_cycling">banecykling</string>
<string name="activity_type_track_running">baneløb</string>
<string name="activity_type_trail_hiking">vandretur</string>
<string name="activity_type_trail_hiking">vandring på sti</string>
<string name="activity_type_trail_running">løb på sti</string>
<string name="activity_type_train">tog</string>
<string name="activity_type_ultimate_frisbee">ultimativ frisbee</string>
<string name="activity_type_train">togkørsel</string>
<string name="activity_type_ultimate_frisbee">ultimate (disc)</string>
<string name="activity_type_unknown">ukendt</string>
<string name="activity_type_wakeboarding">vågneboarding</string>
<string name="activity_type_walking">til fods</string>
<string name="activity_type_wakeboarding">wakeboarding</string>
<string name="activity_type_walking">gang</string>
<string name="activity_type_water_skiing">vandski</string>
<string name="activity_type_wind_surfing">windsurfing</string>
<!-- Description -->
<string name="description_activity_type">Aktivitetstype: %1$s</string>
<string name="description_average_moving_pace_in_minute">Gennemsnitsfart: %1$s min./km (%2$s min./mil)</string>
<string name="description_average_moving_speed">Gennemsnitlig bevægelseshastighed: %1$.2f km/t (%2$.1f mil/t)</string>
<string name="description_average_moving_pace_in_minute">Gennemsnitstempo i bevægelse: %1$s min./km (%2$s min./mil)</string>
<string name="description_average_moving_speed">Gennemsnitshastighed i bevægelse: %1$.2f km/t (%2$.1f mil/t)</string>
<string name="description_average_pace_in_minute">Gennemsnitstempo: %1$s min./km (%2$s min./mil)</string>
<string name="description_average_speed">Gennemsnitlig hastighed: %1$.2f km/t (%2$.1f mil/t)</string>
<string name="description_average_speed">Gennemsnitshastighed: %1$.2f km/t (%2$.1f mil/t)</string>
<string name="description_altitude_gain">Stigning: %1$d m (%2$d fod)</string>
<string name="description_altitude_imperial">Højde (ft)</string>
<string name="description_altitude_metric">Højde (m)</string>
@@ -143,7 +140,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="description_max_altitude">Maks. højde: %1$d m (%2$d fod)</string>
<string name="description_max_speed">Maks. hastighed: %1$.2f km/t (%2$.1f mil/t)</string>
<string name="description_min_altitude">Min. højde: %1$d m (%2$d fod)</string>
<string name="description_moving_time">Bevægelseshastighed: %1$s</string>
<string name="description_moving_time">Tid i bevægelse: %1$s</string>
<string name="description_pace_imperial">Tempo (min./mil)</string>
<string name="description_pace_metric">Tempo (min./km)</string>
<string name="description_recorded_time">Registreret: %1$s</string>
@@ -164,14 +161,14 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<!-- External Storage -->
<!-- Generic -->
<string name="generic_add">Tilføj</string>
<string name="generic_delete_selected_confirm_title">Skal de valgte slettes?</string>
<string name="generic_delete_selected_confirm_title">Slet valgte\?</string>
<string name="generic_description">Beskrivelse</string>
<string name="generic_description_line">Beskrivelse: %1$s</string>
<string name="generic_name">Navn</string>
<string name="generic_name_line">Navn: %1$s</string>
<string name="generic_recording">Registrerer…</string>
<string name="generic_save">Gem</string>
<string name="generic_tracks">Ruter</string>
<string name="generic_tracks">Spor</string>
<!-- Gps -->
<string name="gps_starting">Starter GPS</string>
<string name="gps_wait_for_better_signal">Venter på et bedre GPS-signal</string>
@@ -182,16 +179,16 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="image_photo_marker">Billedmarkør</string>
<string name="image_record">Registrer</string>
<string name="image_stop">Stop</string>
<string name="image_track">Rute</string>
<string name="image_track">Spor</string>
<!-- Import -->
<string name="import_progress_message">Importerer fra %1$s…</string>
<!-- Marker -->
<string name="marker_add_error">En markør kan ikke indsættes. Intet GPS-signal. Prøv igen.</string>
<string name="marker_add_success">Der blev indsat en markør</string>
<string name="marker_add_canceled">Annulleret, ingen markør blev tilføjet</string>
<string name="marker_add_photo_canceled">Annulleret, ingen fotomarkering tilføjet</string>
<string name="marker_add_error">Markør kan ikke indsættes. Intet GPS-signal. Prøv igen.</string>
<string name="marker_add_success">Markør placeret</string>
<string name="marker_add_canceled">Annulleret, ingen markør placeret</string>
<string name="marker_add_photo_canceled">Annulleret, ingen fotomarkør placeret</string>
<string name="marker_delete_one_confirm_message">Markøren slettes permanent fra enheden</string>
<string name="marker_delete_one_confirm_title">Vil du slette markøren?</string>
<string name="marker_delete_one_confirm_title">Slet markøren\?</string>
<string name="marker_delete_multiple_confirm_message">De valgte markører slettes permanent fra enheden</string>
<string name="marker_name_format">Markør %1$d</string>
<string name="marker_title">%1$d af %2$d</string>
@@ -204,7 +201,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="menu_aggregated_statistics">Samlet statistik</string>
<string name="menu_delete">Slet</string>
<string name="menu_edit">Rediger</string>
<string name="menu_export_all">Eksportér alle</string>
<string name="menu_export_all">Eksportér alle (en fil per spor)</string>
<string name="menu_help">Hjælp</string>
<string name="menu_import_all">Importér alle</string>
<string name="menu_insert_marker">Indsæt markør</string>
@@ -225,9 +222,9 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<!-- Sensor State -->
<string name="sensor_state_heart_rate">Puls</string>
<string name="sensor_state_heart_rate_value">%1$d slag per minut</string>
<string name="sensor_state_power">Strøm</string>
<string name="sensor_state_power">Effekt</string>
<string name="sensor_could_not_scan">Kunne ikke scanne efter Bluetooth-enheder (fejl %1$i).</string>
<string name="bluetooth_disabled">Aktivér Bluetooth.</string>
<string name="bluetooth_disabled">Aktivér venligst Bluetooth.</string>
<!-- Settings Advanced -->
<string name="settings_advanced">Avanceret</string>
<!-- Settings Chart -->
@@ -255,15 +252,15 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="settings_reset_done">Alle indstillinger er blevet gendannet til standardværdierne</string>
<!-- Settings Sensor -->
<!-- Settings Stats -->
<string name="settings_stats_rate_title">Foretrukkenfartenhed</string>
<string name="settings_stats_rate_title">Foretrukken fartenhed</string>
<string name="settings_stats_units_imperial">Britisk (mil, ft)</string>
<string name="settings_stats_units_metric">Metrisk (km, m)</string>
<string name="settings_stats_units_title">Foretrukne enheder</string>
<!-- Share Track -->
<string name="share_track_share_file_body">Jeg tror, du måske er interesseret i dette nummer.
<string name="share_track_share_file_body">Jeg tror, du måske er interesseret i dette spor.
\n
\n%1$s</string>
<string name="share_track_subject">Jeg vil gerne dele en rute med dig</string>
<string name="share_track_subject">Jeg vil gerne dele et spor med dig</string>
<!-- Stats -->
<string name="stats_average_moving_pace">Gns. bevægelsestempo</string>
<string name="stats_average_moving_speed">Gns. bevægelseshastighed</string>
@@ -281,12 +278,12 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="stats_distance">Afstand</string>
<string name="stats_total_time">Samlet tid</string>
<!-- Track -->
<string name="track_name_format">Rute %1$d</string>
<string name="track_recording_notification_accuracy">Placeringsnøjagtighed: %1$s</string>
<string name="track_name_format">Spor %1$d</string>
<string name="track_recording_notification_accuracy">Lokationsnøjagtighed: %1$s</string>
<string name="track_markers">Markører på %1$s</string>
<!-- Track Delete -->
<string name="track_delete_one_confirm_message">Ruten og de tilhørende markører slettes permanent fra enheden.</string>
<string name="track_delete_one_confirm_title">Vil du slette ruten?</string>
<string name="track_delete_one_confirm_title">Slet ruten\?</string>
<string name="track_delete_multiple_confirm_message">De valgte ruter og tilhørende markører slettes permanent fra enheden.</string>
<string name="track_delete_progress_message">Sletter…</string>
<!-- Track Detail -->
@@ -376,50 +373,50 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="share_image_subject">Jeg vil gerne dele et billede med dig</string>
<string name="share_image_body">Jeg tror, du måske er interesseret i dette billede.</string>
<string name="open_track_as_trackfileformat">Åbn som %1$s</string>
<string name="activity_type_escooter">elektrisk scooter</string>
<string name="generic_click_twice_cancel">Klik tilbage igen for at annullere</string>
<string name="activity_type_escooter">elløbehjulskørsel</string>
<string name="generic_click_twice_cancel">Stryg tilbage igen for at annullere</string>
<string name="import_unable_to_import_file">Kunne ikke importere filen:%1$s</string>
<string name="import_prevent_reimport">Du har indstillet forhindre genimport af spor</string>
<string name="import_prevent_reimport">Du har slået genimport af spor fra</string>
<string name="settings_default_export_uri_title">Spor eksportmappe</string>
<string name="instant_export_enabled_summary">Eksporter spor til lagring, når optagelsen er færdig</string>
<string name="export_progress_summary_new_msg">Nye spor eksporteret</string>
<string name="export_progress_summary_new_msg">Nye filer eksporteret</string>
<string name="instant_export_enabled_title">Øjeblikkelig eksport efter træning</string>
<string name="generic_separator_done_total">/</string>
<string name="generic_skip">Springe</string>
<string name="description_altitude_loss">Højdetab: %1$d m (%2$d ft)</string>
<string name="export_error_post_workout">Eksport efter træning mislykkedes. Kontroller eksportmappen.</string>
<string name="export_track_already_exists_msg">Sporet %1$s findes allerede i destinationsmappen.</string>
<string name="export_all_do_it_for_all">gør det for alle konflikter</string>
<string name="export_track_errors">Spor ikke eksporteret</string>
<string name="generic_skip">Spring over</string>
<string name="description_altitude_loss">Fald: %1$d m (%2$d ft)</string>
<string name="export_error_post_workout">Eksport efter træning mislykkedes. Kontroller venligst om eksportmappen er opsat rigtigt.</string>
<string name="export_track_already_exists_msg">Filen %1$s findes allerede i destinationsmappen.</string>
<string name="export_all_do_it_for_all">gør for alle konflikter</string>
<string name="export_track_errors">Filer ikke eksporteret</string>
<string name="gps_disabled_msg">GPS deaktiveret</string>
<string name="gps_fixed_and_ready">GPS fast og klar</string>
<string name="gps_fixed_and_ready">GPS fastsat og klar</string>
<string name="import_no_kml_file_found">Ingen doc.kml-fil fundet i KMZ</string>
<string name="sensor_state_cadence_avg">Gennemsnitlig kadence</string>
<string name="sensor_state_cadence_max">Max kadence</string>
<string name="sensor_state_cadence_avg">Gns. kadence</string>
<string name="sensor_state_cadence_max">Maks. kadence</string>
<string name="sensor_state_heart_rate_avg">Gns. Puls</string>
<string name="sensor_state_heart_rate_max">Maks. Puls</string>
<string name="sensor_state_power_avg">Gennemsnitlig strøm</string>
<string name="sensor_state_power_avg">Gennemsnitlig effekt</string>
<string name="settings_default_trackfileformat">Eksport/del filformat</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Fuld skærm</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Under optagelse, vises på fuld skærm.</string>
<string name="settings_night_mode_title">UI Tema</string>
<string name="settings_night_mode_option_system">Ordning</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Vis på fuld skærm under optagelse.</string>
<string name="settings_night_mode_title">Brugergrænsefladens tema</string>
<string name="settings_night_mode_option_system">System</string>
<string name="settings_night_mode_option_day">Dag</string>
<string name="settings_night_mode_option_night">Nat</string>
<string name="settings_sensor_wheel_circumference">Hjulomkreds (mm)</string>
<string name="stats_split_interval">Split, divide</string>
<string name="stats_split_interval">Del interval</string>
<string name="interval_list_empty_message">Der er ikke noget interval endnu</string>
<string name="aggregated_stats_empty_message">Optag dit første spor for at se aggregerede statistikker</string>
<string name="track_detail_intervals_tab">Intervaller</string>
<string name="hold_to_stop">Hold for at stoppe</string>
<string name="settings_prevent_reimport_tracks_title">Forhindre genimport af spor</string>
<string name="settings_prevent_reimport_tracks_title">Forhindr genimport af spor</string>
<plurals name="generic_completed_with_errors">
<item quantity="one">Afsluttet med en fejl</item>
<item quantity="other">Afsluttet med %1$d fejl</item>
</plurals>
<string name="stats_loss">Tab</string>
<string name="export_progress_summary_overwrite_msg">Spor overskrevet</string>
<string name="export_progress_summary_skip_msg">Spor sprunget over</string>
<string name="stats_loss">Fald</string>
<string name="export_progress_summary_overwrite_msg">Filer overskrevet</string>
<string name="export_progress_summary_skip_msg">Filer sprunget over</string>
<string name="export_progress_summary_errors_msg">Fejl</string>
<string name="generic_open_track">Åbn spor</string>
<string name="generic_completed">Afsluttet</string>
@@ -430,7 +427,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="import_progress_summary_ok_msg">Importerede filer</string>
<string name="import_progress_summary_exists_msg">Eksisterer allerede</string>
<string name="import_progress_summary_errors_msg">Fejl</string>
<string name="import_file_imported">Filen%1$s er importeret</string>
<string name="import_file_imported">Filen %1$s er importeret</string>
<string name="import_error_info">%1$s: %2$s</string>
<string name="generic_show_errors">Vis fejl</string>
<string name="generic_overwrite">Overskriv</string>
@@ -439,7 +436,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="track_speed_notification">Hastighed: %1$s</string>
<string name="about_third_party_data">Data fra tredjeparter</string>
<string name="menu_voice_rate">Stemmehastighed</string>
<string name="track_delete_not_recording">Dette spor kan ikke slettes, da det er optaget i øjeblikket.</string>
<string name="track_delete_not_recording">Dette spor kan ikke slettes, da det er registreret i øjeblikket.</string>
<string name="settings_sensor_cycling_distance_speed">Afstand og hastighed</string>
<string name="settings_sensor_cycling_cadence">Kadence</string>
<string name="settings_sensor_running_speed_and_cadence">Afstand, hastighed og kadence</string>
@@ -448,23 +445,23 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="menu_filter">Filter</string>
<string name="settings_open_tracks_summary">Alt om OpenTracks-projektet</string>
<string name="settings_defaults_title">Standardenheder og aktivitet</string>
<string name="settings_defaults_summary">Foretrukne enheder, standardaktivitet, spor standardnavn</string>
<string name="settings_gps_summary">Tidsinterval, Distanceinterval, Maksimal afstand, Nøjagtighed</string>
<string name="settings_sensors_summary">Cykelsensorer, Hjulomkreds, Løbesensorer</string>
<string name="settings_defaults_summary">Foretrukne enheder, standardaktivitet, standardspornavn</string>
<string name="settings_gps_summary">Tidsinterval, distanceinterval, maksimal afstand, nøjagtighed</string>
<string name="settings_sensors_summary">Cykelsensorer, hjulomkreds, løbesensorer</string>
<string name="settings_announcements_statistics_title">Stemmemeddelelser</string>
<string name="settings_announcements_summary">Tid, afstand, stemmehastighed</string>
<string name="settings_import_export_title">Import/eksport</string>
<string name="settings_import_export_summary">Import, eksport, automatisk eksport</string>
<string name="settings_reset_summary">Nulstil indstillingerne til standardværdierne</string>
<string name="settings_recording_customize_layout_title">Tilpas dit optagelseslayout</string>
<string name="settings_recording_customize_layout_title">Tilpas dit registreringslayout</string>
<string name="settings_recording_customize_layout_select_columns">Datafelter pr. række</string>
<string name="settings_announcements_totaltime_frequency">Tidsinterval</string>
<string name="settings_announcements_distance_frequency">Afstandsinterval</string>
<string name="custom_layout_list_title">Opsætning</string>
<string name="custom_layout_list_edit_already_exists">Layoutets navn findes allerede</string>
<string name="custom_layout_list_layout_removed">Layout blev fjernet</string>
<string name="custom_layout_list_edit_already_exists">Opsætningens navn findes allerede</string>
<string name="custom_layout_list_layout_removed">Opsæting blev fjernet</string>
<string name="settings_layout_reset">Nulstil brugerdefineret layout</string>
<string name="settings_layout_reset_confirm_title">Nulstille dine layouts\?</string>
<string name="settings_layout_reset_confirm_title">Nulstil dine opsætninger\?</string>
<string name="stats_sensors_power">Strøm</string>
<string name="stats_sensors_cadence">Kadence</string>
<string name="stats_coordinates">Breddegrad/længdegrad</string>
@@ -479,7 +476,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="settings_running_sensor">Kører</string>
<string name="settings_import_section">Importere</string>
<string name="settings_export_section">Eksport</string>
<string name="stats_clock">Klokken</string>
<string name="stats_clock">Ur</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (anbefalet)</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="generic_to">Til</string>
@@ -488,7 +485,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="settings_gps_title">GPS</string>
<string name="settings_activity_defaults_category">Aktivitetens standardværdier</string>
<string name="settings_units_category">Enheder</string>
<string name="settings_layout_reset_done">Alle layoutindstillinger er blevet tilbageført til standardværdierne</string>
<string name="settings_layout_reset_done">Alle opsætningsindstillinger er blevet tilbageført til standardværdierne</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/t (anbefalet)</string>
<string name="field_set_primary">Primært</string>
<string name="generic_choose_an_option">Vælg en mulighed</string>
@@ -496,7 +493,7 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="settings_generic_sensor">Generisk</string>
<string name="permission_bluetooth_failed">OpenTracks kræver tilladelse til at bruge Bluetooth.</string>
<string name="permission_bluetooth_failed_rejected">Du skal manuelt give OpenTracks adgang til Nearby Devices i systemindstillingerne.</string>
<string name="stats_sensors_heart_rate">Hjertefrekvens</string>
<string name="stats_sensors_heart_rate">Puls</string>
<string name="settings_sensor_bluetooth_service_filter_on">Vis kun Bluetooth-enheder, der annoncerer de krævede tjenester</string>
<string name="aggregated_stats_filter_no_results">Der er ingen aktiviteter for det angivne filter</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
@@ -504,11 +501,60 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="location_latitude_longitude">%1$s, %2$s</string>
<string name="generic_undo">Fortryd</string>
<string name="menu_select_layout">Vælg layout</string>
<string name="settings_ui_summary">Udseende, Optagelse af layout, Tema</string>
<string name="settings_ui_summary">Udseende, registreringslayout, tema</string>
<string name="description_speed_source_gps">GPS</string>
<string name="generic_filter">Filter</string>
<string name="custom_layout_list_edit_name">Skriv navnet på det layout, der skal tilføjes</string>
<string name="settings_layout_reset_confirm_message">Alle layoutindstillinger vil blive tilbageført til standardværdierne. Dette vil ikke slette spor på enheden.</string>
<string name="custom_layout_list_edit_name">Skriv navnet på den opsætning, der skal tilføjes</string>
<string name="settings_layout_reset_confirm_message">Alle opsætningsindstillinger vil blive tilbageført til standardværdierne. Dette vil ikke slette registrede spor på enheden.</string>
<string name="settings_all_reset">Nulstil</string>
<string name="no_compatible_file_manager_installed">Ingen kompatibel filhåndtering installeret</string>
<string name="unit_minute_per_nautical_mile">min/NM</string>
<string name="activity_type_workout">kropstræning</string>
<string name="value_integer_knots">%1$d knob</string>
<string name="help_data_google_play_services_content">Nej</string>
<string name="menu_export_all_one_file">Eksporter alle (en fil for alle spor)</string>
<string name="track_delete_number_of_tracks">%1$d spor</string>
<string name="settings_public_api_package_title">Pakkenavn for den offentlige API</string>
<string name="settings_announcements_idle_category_title">Meddelelser om stilstand</string>
<string name="settings_stats_units_imperial_meter">Imperial (mi, m)</string>
<string name="settings_public_api_disabled_toast">OpenTracks offentlig API er slået fra; den kan slåes til i indstillingerne.</string>
<string name="image_discard">Kassér</string>
<string name="settings_public_api_enabled_summary_on">Andre installerede apps kan starte og stoppe registreringen.</string>
<string name="settings_ui_dynamic_colors_title">Dynamiske farver</string>
<string name="description_speed_nautical">Hastighed (knob)</string>
<string name="settings_sensor_heart_rate_max">Zoner: Maksimum puls (bpm)</string>
<string name="settings_announcements_average_heart_rate">Gennemsnitlig puls</string>
<string name="settings_recording_title">Registrering</string>
<string name="settings_ui_dynamic_colors_summary">Brug brugerdefinerede dynamiske farver (kun Android 12+; kræver manuel genstart)</string>
<string name="description_pace_nautical">Tempo (min./NN)</string>
<string name="settings_public_api_dashboard_enabled_summary_off">Registreret data vil ikke blive delt automatisk med andre apps.</string>
<string name="settings_stats_units_nautical">Maritimt (NM, ft)</string>
<string name="activity_type_swimming_open">svømning i åbent vand</string>
<string name="sensor_state_power_max">Maks. effekt</string>
<string name="settings_announcements_lap_heart_rate">Rundens puls</string>
<string name="activity_type_kickscooter">knallertkørsel</string>
<string name="settings_public_api_dashboard_enabled_title">Automatisk dataoverførsel</string>
<string name="settings_api_summary">Fjernkontrol og -statisik API til andre apps</string>
<string name="settings_sensor_bluetooth_service_filter_off">Vis alle synlige Bluetooth-enheder</string>
<string name="settings_api_title">Offentlig API</string>
<string name="unit_nautical_mile">sømil</string>
<string name="settings_announcements_lap_speed_pace">Rundens hastighed/tempo</string>
<string name="track_discarding">Kasserer Spor</string>
<string name="settings_public_api_enabled_title">Offentlig API</string>
<string name="settings_public_api_enabled_summary_off">Kun OpenTracks kan starte og stoppe registreringen.</string>
<string name="voiceIdle">Går til stilstand.</string>
<string name="generic_dismiss">Afvis</string>
<string name="settings_export_filename_title">Format på filnavnet</string>
<string name="value_float_knots">%1$.1f knob</string>
<string name="settings_public_api_dashboard_enabled_summary_on">En app der startede en registrering kan også tilgå de registrede data.</string>
<string name="settings_announcements_idle_title">Stillestående</string>
<string name="unit_knots">knob</string>
<string name="settings_recording_idle_timeout_title">Tid før stilstand aktiveres</string>
<string name="settings_announcements_average_speed_pace">Gennemsnitlig hastighed/tempo</string>
<string name="value_float_knots_recommended">%1$.1f knob (anbefalet)</string>
<string name="activity_type_swimming">svømning</string>
<string name="settings_recording_min_required_accuracy_summary">Placering ignoreres hvis nøjagtigheden er større end %1$s</string>
<string name="gps_recording_without_signal">OpenTracks optager uden GPS-data</string>
<string name="value_int_seconds">%1$d s (anbefalet)</string>
<string name="settings_sensor_bluetooth_service_filter_title">Filtrer Bluetooth-enheder</string>
</resources>
+4
View File
@@ -645,4 +645,8 @@ limitations under the License.
<string name="settings_announcements_idle_title">Inactivo</string>
<string name="settings_recording_idle_timeout_title">Umbral de inactividad</string>
<string name="value_int_seconds">%1$d s (recomendado)</string>
<string name="settings_ui_dynamic_colors_title">Colores dinámicos</string>
<string name="settings_recording_title">Grabando</string>
<string name="settings_ui_dynamic_colors_summary">Utilizar colores dinámicos definidos por el usuario (sólo Android 12+; requiere un reinicio manual)</string>
<string name="permission_recording_failed">OpenTracks requiere permiso para acceder a su ubicación y a los dispositivos cercanos.</string>
</resources>

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