mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-02 09:33:06 +02:00
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce613183fe | |||
| afb3914572 | |||
| c1b2aa33d4 | |||
| 42944f8627 | |||
| 16ddc990af | |||
| 1b6026b46e | |||
| 7506758568 | |||
| f84c6791cd | |||
| 7535697860 | |||
| faf31fde13 | |||
| 92a4713f21 | |||
| 7ebb629b44 | |||
| 1ae18836cb | |||
| f3e8d033cc | |||
| b1fafc4f2f | |||
| f3f9ad2154 | |||
| fa69320aee | |||
| dabb32c71f | |||
| dd1cfd53e3 | |||
| a854d8d99c | |||
| 8a1d9e626e | |||
| 06df78f8f8 | |||
| 1ad22becb8 | |||
| c803cca5f0 | |||
| 120ec9b01d | |||
| 1c3edcf6bc | |||
| 967289909e | |||
| 085eebe7f8 | |||
| eed9680699 | |||
| 4a6f652663 | |||
| 90d81c4d84 | |||
| e047100e34 | |||
| 80d7f8d92e | |||
| d038f5c33a | |||
| c6b4e738b9 | |||
| 5de2012452 | |||
| 0584f05ddc | |||
| 8c9e3882c7 | |||
| 310ac8c06e | |||
| 9b2126550e | |||
| 0e9f9b4db7 | |||
| 4dc524bf4f | |||
| 7455e591e1 | |||
| 9e4b1e0706 | |||
| 03dd0c2308 | |||
| 8eed8e9d8d | |||
| 94540606ef | |||
| 490141db2a | |||
| 892c56389b | |||
| a141bd77ad | |||
| e48b6054df | |||
| bb23bac7ec | |||
| fa362de67a | |||
| 76cba3f611 | |||
| 7ccb59beae | |||
| a70118b2dc | |||
| 738cbe150f | |||
| 7f40f67dc4 | |||
| f1b7adb6a0 | |||
| 1bd75754e4 | |||
| eaad33c23b | |||
| 9a0f927182 | |||
| 59aeec4fbc | |||
| df8306c2f8 | |||
| 5c6a4a71a4 | |||
| abbc61d419 | |||
| e2f598b282 | |||
| 41da2f5b53 | |||
| 8d7a8719bc | |||
| 8f5388edce | |||
| 81a78820cc | |||
| 2fe452c77c | |||
| 826cf0d468 | |||
| 6cd627e832 | |||
| 0ee4a4e72f | |||
| 150cbee95b | |||
| 47ceb297c9 | |||
| aed4af5736 | |||
| 333f7956b5 | |||
| d08552a07b | |||
| 330ca4a720 | |||
| 85171cdc46 | |||
| 5c1cb97c02 | |||
| 98a009e7c7 | |||
| 8f15382409 | |||
| 24212a226e | |||
| f80aaf00ec | |||
| edba2294f3 | |||
| 5743f29aed | |||
| 7b2ad1590c | |||
| 251a26d2f8 | |||
| fd362e17e1 | |||
| bf0dd9cda4 | |||
| 0fe176fa24 | |||
| 9b45a70e67 | |||
| bd1b68dffd | |||
| 3c5c69fdb0 | |||
| 87d7ebcb64 | |||
| a01edcc9e8 | |||
| 8b7063d18b | |||
| 25d4daa295 | |||
| 5dbd17f3bb | |||
| fd914630d0 | |||
| caf8b5526d | |||
| 8d5a7e7e8b | |||
| 9fb84cc47c | |||
| 5c7731bb1f | |||
| ce21fe3569 | |||
| d47fa27e02 | |||
| 58d79380cf | |||
| a0ab2e2896 | |||
| 0bc526df06 | |||
| f51edbf712 | |||
| 2384b947b3 | |||
| aca7f9063e | |||
| 78282b97ed | |||
| c32ab408e6 | |||
| 15f3ccbcbd | |||
| f2594371d3 | |||
| 4d54472c93 | |||
| 3a2a62a6d7 | |||
| 17487961ad | |||
| af268c608a | |||
| 605d4c02f5 | |||
| cae2de0982 | |||
| 257b051f13 | |||
| 0cf65e0b89 | |||
| 062d48e69f | |||
| a270b1f64f | |||
| f7bc25c17d | |||
| 90f82f7c5c | |||
| 062457f1aa | |||
| cd4d297608 | |||
| 06c87bc7d4 | |||
| df98fdd4de |
@@ -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'
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Archive
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: OpenTracks-APK.apk
|
||||
path: ./build/outputs/apk/nightly/*.apk
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -12,13 +12,13 @@ jobs:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '17'
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2.28.0
|
||||
uses: reactivecircus/android-emulator-runner@v2.29.0
|
||||
with:
|
||||
arch: x86_64
|
||||
api-level: 34
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Archive
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: code-coverage-report
|
||||
path: build/reports/androidTests/
|
||||
|
||||
@@ -44,6 +44,7 @@ For cycling (incl. cadence):
|
||||
* QUARQ Red DZero Powermeter
|
||||
* Tacx Satori Smart
|
||||
* Wahoo Kickr v4.0
|
||||
* Magene PES P505
|
||||
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is proprietary @ firmware v1.061)
|
||||
|
||||
For running:
|
||||
|
||||
+8
-7
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.0-rc01'
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +64,12 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 5506
|
||||
versionName "v4.9.4"
|
||||
versionCode 5641
|
||||
versionName "v4.11.0"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
minSdk 24
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
|
||||
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
|
||||
@@ -125,14 +125,15 @@ 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'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core:1.12.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.mediarouter:mediarouter:1.6.0'
|
||||
|
||||
@@ -141,7 +142,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.9.0'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||
}
|
||||
|
||||
@@ -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,7 @@
|
||||
v4.9.6: OpenTracks
|
||||
|
||||
Oprava chyby:
|
||||
- Export: existující soubory jsou nyní kompletně přepisovány
|
||||
|
||||
Vývojář:
|
||||
- Předěláno zpracování barometrických dat
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.7: OpenTracks
|
||||
|
||||
Oprava chyby:
|
||||
- zabránění pádu při ukončení nahrávání (vzniknuvší ve verzi v4.9.6)
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.8: OpenTracks
|
||||
|
||||
Oprava chyby:
|
||||
- Senzor kadence znovu funguje (chyba vznikla ve verzi v4.9.6)
|
||||
@@ -0,0 +1,6 @@
|
||||
v4.9.9: OpenTracks
|
||||
|
||||
Oprava chyby:
|
||||
- Pád při používání senzoru BLE na konci záznamu (závod)
|
||||
Důvod: subsystém senzorů byl spuštěn dvakrát a pouze druhý z nich byl vypnut.
|
||||
- Při nahrávání jsou znovu zobrazeny názvy senzorů BLE
|
||||
@@ -0,0 +1,15 @@
|
||||
v4.10.0: OpenTracks
|
||||
|
||||
Změny:
|
||||
- Podpora zisku/ztráty přes snímání prostředí BLE (barometr)
|
||||
- Interní barometr lze vypnout (např. při rozbití)
|
||||
- Material3 pro přepínače preferencí
|
||||
- Úvodní obrazovka podporuje denní/noční režim
|
||||
- Nastavení Bluetooth senzorů: zobrazení názvu vybraného snímače (pokud je uložen v mezipaměti)
|
||||
|
||||
Oprava chyb:
|
||||
- UI s intervaly bylo špatně zarovnané
|
||||
|
||||
Vývojáři:
|
||||
- Vyčištění TrackRecordingServiceConnection
|
||||
- TrackRecordingService se vždy spouští jako ForegroundService
|
||||
@@ -0,0 +1,11 @@
|
||||
v4.10.1: OpenTracks
|
||||
|
||||
Změny:
|
||||
- Android 14: pád při spuštění nahrávání při neudělených oprávněních
|
||||
- Názvy grafů jsou kompaktnější
|
||||
|
||||
Opravy chyb:
|
||||
- Po opětovném připojení senzoru BLE následoval pád (vzniklo ve verzi v4.9.6)
|
||||
- Výkon cyklistiky nebyl zpracován (vzniklo ve verzi v4.9.6)
|
||||
- Kadence cyklistiky a vzdálenost umožňovaly opakovaná data
|
||||
- Data z tlakoměru BLE nebyla správně zpracovávána
|
||||
@@ -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,5 @@
|
||||
v4.9.7: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- prevent crash when stopping a recording(introduced in v4.9.6)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.8: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Cycling cadence sensor works again (introduced in v4.9.6)
|
||||
@@ -0,0 +1,6 @@
|
||||
v4.9.9: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Crash when using a BLE sensor on recording end (race condition)
|
||||
Reason: sensor subsystem was started twice and only the 2nd one was shutdown.
|
||||
- While recording, BLE sensor names are shown again
|
||||
@@ -0,0 +1,15 @@
|
||||
v4.10.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Support gain/loss via BLE Environmental Sensing (barometer)
|
||||
- Internal barometer can be turned off (e.g., if broken)
|
||||
- Material3 for preference switches
|
||||
- SplashScreen supports day/night mode
|
||||
- Bluetooth sensor settings: show selected sensor name (if cached by Android)
|
||||
|
||||
Bugfix:
|
||||
- Interval UI was misaligned
|
||||
|
||||
Developer:
|
||||
- Cleanup TrackRecordingServiceConnection
|
||||
- TrackRecordingService always starts as ForegroundService
|
||||
@@ -0,0 +1,11 @@
|
||||
v4.10.1: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Android 14: crash on recording start if permissions are not granted
|
||||
- Chart titles are more compact
|
||||
|
||||
Bugfix:
|
||||
- BLE sensor re-connect resulted in crash (introduced in v4.9.6)
|
||||
- Cycling power was not processed (introduced in v4.9.6)
|
||||
- Cycling cadence as well as distance allow for repeated data
|
||||
- BLE barometer data was not processed correctly
|
||||
@@ -0,0 +1,7 @@
|
||||
v4.11.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Location for Markers is taken opening add dialog (was save)
|
||||
- Charts: use less sprace for axes
|
||||
- Charts: do not fill when showing more than two lines
|
||||
- Voice announcements improved (skip decimal if .0)
|
||||
@@ -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,22 @@
|
||||
Aplikacija za praćenje sportova koja poštuje tvoju privatnost.
|
||||
|
||||
Sport i vanjske aktivnosti donose sreću, vještinu i samopouzdanje.
|
||||
|
||||
Cijeni svoje zdravlje prateći svoj trening.
|
||||
Snima dok trčiš ili hodaš i pruža biciklističko računalo s većim ekranom za vožnju bicikla.
|
||||
Označi zanimljive lokacije na svom putu.
|
||||
Čuvaj vrlo detaljnu statistiku za analiziranje.
|
||||
Dijeli samo željene podatke s drugima.
|
||||
|
||||
* Glasovne najave.
|
||||
* Podržava Bluetooth LE senzore: broj otkucaja srca, brzinu i udaljenost (bicikl), ritam pedaliranja (bicikl) i mjerač snage (bicikl).
|
||||
* Povećanje i smanjenje visine: putem barometrijskog senzora.
|
||||
* Visina prikazana u EGM2008 modelu (nadmorska visina); izvozi se kao WGS84.
|
||||
* Izvezi podatke kao trake ili kao KMZ (uključujući fotografije), KML ili GPX.
|
||||
* Bez pristupa internetu ili dodatnih dozvola.
|
||||
* Tamna i svijetla tema, poštujući postavke sustava.
|
||||
* Bez reklama.
|
||||
|
||||
Libre softver / besplatni softver / softver otvorenog koda
|
||||
što znači da se izvorni kod može koristiti, proučavati, mijenjati i dijeliti.
|
||||
Apache 2.0 licenca
|
||||
@@ -0,0 +1 @@
|
||||
Aplikacija za praćenje sportova koja poštuje tvoju privatnost.
|
||||
@@ -0,0 +1 @@
|
||||
OpenTracks
|
||||
@@ -0,0 +1 @@
|
||||
ನಿಮ್ಮ ಗೌಪ್ಯತೆಯನ್ನು ಗೌರವಿಸುವ ಕ್ರೀಡಾ ಟ್ರ್ಯಾಕರ್ ಅಪ್ಲಿಕೇಶನ್.
|
||||
@@ -0,0 +1 @@
|
||||
OpenTracks
|
||||
@@ -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,7 @@
|
||||
v4.9.6: OpenTracks
|
||||
|
||||
Bugfix :
|
||||
- Exporteren: bestaande bestanden worden nu volledig overschreven
|
||||
|
||||
Ontwikkelaar:
|
||||
- Barometrische gegevensverwerking aangepast
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.7: OpenTracks
|
||||
|
||||
Bugfix :
|
||||
- voorkoming van crash bij het stoppen van een opname (geïntroduceerd in v4.9.6)
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.8: OpenTracks
|
||||
|
||||
Bugfix :
|
||||
- Fietscadanssensor werkt weer (geïntroduceerd in v4.9.6)
|
||||
@@ -0,0 +1,6 @@
|
||||
v4.9.9: OpenTracks
|
||||
|
||||
Bugfix :
|
||||
- Crash bij gebruik van een BLE-sensor aan het einde van opname(raceconditie)
|
||||
Reden: het subsysteem sensor is tweemaal gestart en alleen het tweede is uitgeschakeld.
|
||||
- Tijdens het opnemen worden namen van BLE-sensoren opnieuw weergegeven
|
||||
@@ -0,0 +1,15 @@
|
||||
v4.10.0: OpenTracks
|
||||
|
||||
Veranderingen:
|
||||
- Ondersteuning voor toename/afname via BLE Environmental Sensing (barometer)
|
||||
- Interne barometer kan worden uitgeschakeld (bijv., indien defect)
|
||||
- Materiaal 3 voor voorkeursschakelaars
|
||||
- Openingsscherm ondersteunt dag/nacht-modus
|
||||
- Bluetooth-sensorinstellingen: toon geselecteerde sensornaam (indien gecached door Android)
|
||||
|
||||
Bugfix :
|
||||
- Interval UI was verkeerd uitgelijnd
|
||||
|
||||
Ontwikkelaar:
|
||||
- Opruiming van trackRecordingServiceConnection
|
||||
- TrackRecordingService begint altijd als ForegroundService
|
||||
@@ -0,0 +1,11 @@
|
||||
v4.10.1: OpenTracks
|
||||
|
||||
Veranderingen:
|
||||
- Android 14: crash bij het starten van de opname als er geen rechten worden verleend
|
||||
- Kaarttitels zijn compacter
|
||||
|
||||
Bugfix :
|
||||
- BLE-sensor opnieuw aansluiten resulteerde in een crash (geïntroduceerd in v4.9.6)
|
||||
- Fietsvermogen is niet verwerkt (geïntroduceerd in v4.9.6)
|
||||
- Zowel de fietscadentie als de afstand maken herhaalde gegevens mogelijk
|
||||
- BLE-barometergegevens zijn niet correct verwerkt
|
||||
@@ -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,4 @@
|
||||
v4.9.8: OpenTracks
|
||||
|
||||
错误修正:
|
||||
- 骑行步频传感器再次正常工作(在 v4.9.6 中引入)
|
||||
@@ -0,0 +1,6 @@
|
||||
v4.9.9: OpenTracks
|
||||
|
||||
错误修正:
|
||||
- 在记录端使用 BLE 传感器时崩溃(竞赛条件)
|
||||
原因:传感器子系统启动了两次,只有第二个子系统被关闭。
|
||||
- 记录时,再次显示 BLE 传感器名称
|
||||
@@ -0,0 +1,15 @@
|
||||
v4.10.0: OpenTracks
|
||||
|
||||
更改:
|
||||
- 通过 BLE 环境传感(气压计)支持增益/损耗
|
||||
- 可关闭内部气压计(如坏掉时)
|
||||
- 用于偏好开关的 Material3
|
||||
- 闪屏支持日/夜模式
|
||||
- 蓝牙传感器设置:显示所选传感器名称(如果 Android 已缓存)
|
||||
|
||||
修正错误:
|
||||
- 间隔 UI 未对齐
|
||||
|
||||
开发相关:
|
||||
- 清理 TrackRecordingServiceConnection
|
||||
- TrackRecordingService 总是作为 ForegroundService 启动
|
||||
@@ -0,0 +1,11 @@
|
||||
v4.10.1: OpenTracks
|
||||
|
||||
更改:
|
||||
- Android 14:如果未授予权限,则在开始录制时崩溃
|
||||
- 图表标题更紧凑
|
||||
|
||||
错误修正:
|
||||
- BLE 传感器重新连接会导致崩溃(在 v4.9.6 中引入)
|
||||
- 未处理骑行功率(在 v4.9.6 中引入)
|
||||
- 骑行速度和距离允许重复数据
|
||||
- 未正确处理 BLE 气压计数据
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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" \
|
||||
|
||||
+52
-40
@@ -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("test", null));
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
+25
@@ -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.ofHPA(1001.65f), pressure);
|
||||
}
|
||||
}
|
||||
+51
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -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);
|
||||
}
|
||||
}
|
||||
+28
@@ -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());
|
||||
}
|
||||
}
|
||||
+28
@@ -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("test", null);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
+37
-94
@@ -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);
|
||||
}
|
||||
}
|
||||
+48
-39
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",,,,,,,,,,,,
|
||||
|
@@ -75,7 +75,7 @@ limitations under the License.
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/ThemeCustom">
|
||||
android:theme="@style/OpenTracksTheme">
|
||||
|
||||
<activity
|
||||
android:name=".publicapi.StartRecording"
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
@@ -30,11 +29,6 @@ public class Startup extends Application {
|
||||
|
||||
// Include version information into stack traces.
|
||||
Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE);
|
||||
// In debug builds: show thread and VM warnings.
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Enabling strict mode");
|
||||
StrictMode.enableDefaults();
|
||||
}
|
||||
|
||||
PreferencesUtils.initPreferences(this, getResources());
|
||||
// Set default values of preferences on first start.
|
||||
|
||||
@@ -28,7 +28,6 @@ import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.databinding.TrackEditBinding;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.TrackUtils;
|
||||
|
||||
/**
|
||||
@@ -44,7 +43,6 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
|
||||
private static final String ICON_VALUE_KEY = "icon_value_key";
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
private Track track;
|
||||
private ActivityType activityType;
|
||||
@@ -55,7 +53,6 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection();
|
||||
Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
if (trackId == null) {
|
||||
Log.e(TAG, "invalid trackId");
|
||||
@@ -114,18 +111,6 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
trackRecordingServiceConnection.startConnection(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
trackRecordingServiceConnection.unbind(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -73,7 +74,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||
|
||||
// The following are set in onCreate
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackRecordingServiceConnection recordingStatusConnection;
|
||||
private TrackListAdapter adapter;
|
||||
|
||||
private TrackListBinding viewBinding;
|
||||
@@ -138,13 +139,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
.observe(TrackListActivity.this, this::onGpsStatusChanged);
|
||||
|
||||
updateGpsMenuItem(true, recordingStatus.isRecording());
|
||||
|
||||
if (service.getGpsStatusObservable().getValue().isGpsStarted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO Not cool to do this in a callback that might be called more than once!
|
||||
service.tryStartSensors();
|
||||
};
|
||||
|
||||
@Override
|
||||
@@ -154,7 +148,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
|
||||
requestRequiredPermissions();
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
recordingStatusConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
|
||||
viewBinding.aggregatedStatsButton.setOnClickListener((view) -> startActivity(IntentUtils.newIntent(this, AggregatedStatisticsActivity.class)));
|
||||
viewBinding.sensorStartButton.setOnClickListener((view) -> {
|
||||
@@ -163,9 +157,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
} else {
|
||||
if (gpsStatusValue.isGpsStarted()) {
|
||||
trackRecordingServiceConnection.unbindAndStop(this);
|
||||
recordingStatusConnection.stopService(this);
|
||||
} else {
|
||||
trackRecordingServiceConnection.startAndBindWithCallback(this);
|
||||
TrackRecordingServiceConnection.execute(this, (service, connection) -> service.tryStartSensors());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -182,16 +176,15 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
}
|
||||
|
||||
// Not Recording -> Recording
|
||||
Log.i(TAG, "Starting recording");
|
||||
updateGpsMenuItem(false, true);
|
||||
new TrackRecordingServiceConnection((service, connection) -> {
|
||||
TrackRecordingServiceConnection.execute(this, (service, connection) -> {
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
|
||||
newIntent.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
|
||||
connection.unbind(this);
|
||||
}).startAndBind(this, true);
|
||||
});
|
||||
});
|
||||
viewBinding.trackListFabAction.setOnLongClickListener((view) -> {
|
||||
if (!recordingStatus.isRecording()) {
|
||||
@@ -201,7 +194,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
// Recording -> Stop
|
||||
ActivityUtils.vibrate(this, 1000);
|
||||
updateGpsMenuItem(false, false);
|
||||
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
|
||||
recordingStatusConnection.stopRecording(TrackListActivity.this);
|
||||
viewBinding.trackListFabAction.setImageResource(R.drawable.ic_baseline_record_24);
|
||||
viewBinding.trackListFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.red_dark));
|
||||
return true;
|
||||
@@ -220,7 +213,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
super.onStart();
|
||||
|
||||
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
trackRecordingServiceConnection.startConnection(this);
|
||||
recordingStatusConnection.bind(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -240,14 +233,14 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
super.onStop();
|
||||
|
||||
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
trackRecordingServiceConnection.unbind(this);
|
||||
recordingStatusConnection.unbind(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
viewBinding = null;
|
||||
trackRecordingServiceConnection = null;
|
||||
recordingStatusConnection = null;
|
||||
adapter = null;
|
||||
}
|
||||
|
||||
@@ -379,8 +372,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_detail_resume_track) {
|
||||
new TrackRecordingServiceConnection((service, connection) -> {
|
||||
TrackRecordingServiceConnection.execute(this, (service, connection) -> {
|
||||
service.resumeTrack(trackId);
|
||||
|
||||
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
|
||||
@@ -209,9 +209,8 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
startActivity(newIntent);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
|
||||
connection.unbind(this);
|
||||
finish();
|
||||
}).startAndBind(this, true);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
|
||||
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
|
||||
trackRecordingServiceConnection.startConnection(this);
|
||||
trackRecordingServiceConnection.bind(this);
|
||||
trackDataHub.start();
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
}
|
||||
|
||||
trackRecordingServiceConnection.startAndBindWithCallback(this);
|
||||
trackRecordingServiceConnection.bind(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -136,7 +136,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
|
||||
}
|
||||
|
||||
private void resumeTrackAndFinish() {
|
||||
new TrackRecordingServiceConnection((service, connection) -> {
|
||||
TrackRecordingServiceConnection.execute(this, (service, connection) -> {
|
||||
service.resumeTrack(trackId);
|
||||
|
||||
Intent newIntent = IntentUtils.newIntent(TrackStoppedActivity.this, TrackRecordingActivity.class)
|
||||
@@ -144,9 +144,8 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
|
||||
startActivity(newIntent);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
|
||||
connection.unbind(this);
|
||||
finish();
|
||||
}).startAndBind(this, true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -80,7 +80,7 @@ abstract class ChartValueSeries {
|
||||
// Make copies of the stroke paint with the default thickness
|
||||
titlePaint = new Paint(strokePaint);
|
||||
titlePaint.setTextSize(fontSizeMedium);
|
||||
titlePaint.setTextAlign(Align.CENTER);
|
||||
titlePaint.setTextAlign(Align.LEFT);
|
||||
titlePaint.setStyle(Style.FILL_AND_STROKE);
|
||||
|
||||
markerPaint = new Paint(strokePaint);
|
||||
@@ -136,8 +136,10 @@ abstract class ChartValueSeries {
|
||||
return path;
|
||||
}
|
||||
|
||||
void drawPath(Canvas canvas) {
|
||||
canvas.drawPath(path, fillPaint);
|
||||
void drawPath(Canvas canvas, boolean shouldFillPathArea) {
|
||||
if (shouldFillPathArea) {
|
||||
canvas.drawPath(path, fillPaint);
|
||||
}
|
||||
canvas.drawPath(path, strokePaint);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Iterator;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
@@ -115,6 +116,9 @@ public class ChartView extends View {
|
||||
private int height = 0;
|
||||
private int effectiveWidth = 0;
|
||||
private int effectiveHeight = 0;
|
||||
private TitleDimensions titleDimensions;
|
||||
private boolean twoLineYaxisNumbers = false;
|
||||
private int maxYaxisNumberHeight = 0;
|
||||
|
||||
private boolean chartByDistance = false;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
@@ -621,7 +625,7 @@ public class ChartView extends View {
|
||||
private void drawDataSeries(Canvas canvas) {
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
|
||||
chartValueSeries.drawPath(canvas);
|
||||
chartValueSeries.drawPath(canvas, titleDimensions.titlePositions.size() < 3 );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -630,14 +634,20 @@ public class ChartView extends View {
|
||||
synchronized (markers) {
|
||||
for (Marker marker : markers) {
|
||||
double xValue = getMarkerXValue(marker);
|
||||
if (xValue > maxX) {
|
||||
continue;
|
||||
double markerIconSizeInXaxisUnits = maxX*markerWidth/effectiveWidth / zoomLevel;
|
||||
if (xValue > maxX + markerIconSizeInXaxisUnits * (1-MARKER_X_ANCHOR)) {
|
||||
continue; // there is no chance that this marker will be visible
|
||||
}
|
||||
canvas.save();
|
||||
float x = getX(getMarkerXValue(marker));
|
||||
canvas.drawLine(x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
|
||||
canvas.translate(x - (markerWidth * MARKER_X_ANCHOR), topBorder + spacer);
|
||||
|
||||
// if marker is not near the end of the track then draw it normally
|
||||
if (xValue < maxX - markerIconSizeInXaxisUnits*(1-MARKER_X_ANCHOR)) {
|
||||
canvas.translate(x - (markerWidth * MARKER_X_ANCHOR), topBorder + spacer);
|
||||
} else { // marker at the end needs to be drawn mirrored so that it is more visible
|
||||
canvas.translate(x + (markerWidth * MARKER_X_ANCHOR), topBorder + spacer);
|
||||
canvas.scale(-1, 1);
|
||||
}
|
||||
markerPin.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
@@ -666,24 +676,30 @@ public class ChartView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
private record TitlePosition(
|
||||
int line, // line number (starts at 1, top to bottom numbering)
|
||||
int xPos // x position in points (starts at 0, left to right indexing)
|
||||
) {};
|
||||
private record TitleDimensions(
|
||||
int lineCount, // number of lines the titles will take
|
||||
int lineHeight, // height of a line (all lines have the same height)
|
||||
List<TitlePosition> titlePositions // positions of visible titles (the order corresponds to seriesList)
|
||||
) {};
|
||||
|
||||
/**
|
||||
* Draws series titles.
|
||||
*
|
||||
* @param canvas the canvas
|
||||
*/
|
||||
private void drawSeriesTitles(Canvas canvas) {
|
||||
int[] titleDimensions = getTitleDimensions();
|
||||
int lines = titleDimensions[0];
|
||||
int lineHeight = titleDimensions[1];
|
||||
int count = 0;
|
||||
Iterator<TitlePosition> tpI = titleDimensions.titlePositions.iterator();
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
|
||||
count++;
|
||||
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
|
||||
Paint paint = chartValueSeries.getTitlePaint();
|
||||
int x = (int) (0.5 * width) + getScrollX();
|
||||
int y = topBorder - spacer - (lines - count) * (lineHeight + spacer);
|
||||
canvas.drawText(title, x, y, paint);
|
||||
TitlePosition tp = tpI.next();
|
||||
int y = topBorder - spacer - (titleDimensions.lineCount - tp.line) * (titleDimensions.lineHeight + spacer);
|
||||
canvas.drawText(title, tp.xPos + getScrollX(), y, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -692,20 +708,27 @@ public class ChartView extends View {
|
||||
* Gets the title dimensions.
|
||||
* Returns an array of 2 integers, first element is the number of lines and the second element is the line height.
|
||||
*/
|
||||
private int[] getTitleDimensions() {
|
||||
int lines = 0;
|
||||
private TitleDimensions getTitleDimensions() {
|
||||
int lineCnt = 1;
|
||||
int lineHeight = 0;
|
||||
List<TitlePosition> tps = new ArrayList<TitlePosition>();
|
||||
int xPosInLine = spacer;
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
|
||||
lines++;
|
||||
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
|
||||
Rect rect = getRect(chartValueSeries.getTitlePaint(), title);
|
||||
if (rect.height() > lineHeight) {
|
||||
lineHeight = rect.height();
|
||||
if (rect.height() > lineHeight) lineHeight = rect.height();
|
||||
int xNextPosInLine = xPosInLine + rect.width() + 2*spacer;
|
||||
// if second or later title does not fully fit on this line then print it on the next line
|
||||
if (xPosInLine > spacer && xNextPosInLine-spacer > width) {
|
||||
lineCnt++;
|
||||
xPosInLine = spacer;
|
||||
}
|
||||
tps.add(new TitlePosition(lineCnt, xPosInLine));
|
||||
xPosInLine += rect.width() + 2*spacer;
|
||||
}
|
||||
}
|
||||
return new int[]{lines, lineHeight};
|
||||
return new TitleDimensions(lineCnt, lineHeight, tps);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -720,13 +743,13 @@ public class ChartView extends View {
|
||||
String label = getXAxisLabel();
|
||||
Rect rect = getRect(axisPaint, label);
|
||||
int yOffset = rect.height() / 2;
|
||||
canvas.drawText(label, x + effectiveWidth + spacer, y + yOffset, axisPaint);
|
||||
canvas.drawText(label, x + effectiveWidth - rect.width(), y + 3 * yOffset, axisPaint);
|
||||
|
||||
double interval = getXAxisInterval();
|
||||
NumberFormat numberFormat = interval < 1 ? X_FRACTION_FORMAT : X_NUMBER_FORMAT;
|
||||
|
||||
for (double markerPosition : getXAxisMarkerPositions(interval)) {
|
||||
drawXAxisMarker(canvas, markerPosition, numberFormat, spacer + yOffset);
|
||||
drawXAxisMarker(canvas, markerPosition, numberFormat, spacer + rect.width(), spacer + yOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,12 +772,16 @@ public class ChartView extends View {
|
||||
* @param canvas canvas
|
||||
* @param value value
|
||||
* @param numberFormat the number format
|
||||
* @param spacing the spacing between x axis and marker
|
||||
* @param xRightSpace the space taken up by the x axis label
|
||||
* @param yBottomSpace the space between x axis and marker
|
||||
*/
|
||||
private void drawXAxisMarker(Canvas canvas, double value, NumberFormat numberFormat, int spacing) {
|
||||
private void drawXAxisMarker(Canvas canvas, double value, NumberFormat numberFormat, int xRightSpace, int yBottomSpace) {
|
||||
String marker = chartByDistance ? numberFormat.format(value) : StringUtils.formatElapsedTime((Duration.ofMillis((long) value)));
|
||||
Rect rect = getRect(xAxisMarkerPaint, marker);
|
||||
canvas.drawText(marker, getX(value), topBorder + effectiveHeight + spacing + rect.height(), xAxisMarkerPaint);
|
||||
int markerXPos = getX(value);
|
||||
int markerEndXPos = markerXPos + rect.width()/2;
|
||||
if (markerEndXPos > getScrollX() + leftBorder + effectiveWidth - xRightSpace) return;
|
||||
canvas.drawText(marker, markerXPos, topBorder + effectiveHeight + yBottomSpace + rect.height(), xAxisMarkerPaint);
|
||||
}
|
||||
|
||||
private double getXAxisInterval() {
|
||||
@@ -790,17 +817,22 @@ public class ChartView extends View {
|
||||
* @param canvas the canvas
|
||||
*/
|
||||
private void drawYAxis(Canvas canvas) {
|
||||
int x = getScrollX() + leftBorder;
|
||||
int y = topBorder;
|
||||
final int x = getScrollX() + leftBorder;
|
||||
final int y = topBorder;
|
||||
canvas.drawLine(x, y, x, y + effectiveHeight, axisPaint);
|
||||
|
||||
//TODO
|
||||
int markerXPosition = x - spacer;
|
||||
for (int i = 0; i < seriesList.size(); i++) {
|
||||
int index = seriesList.size() - 1 - i;
|
||||
ChartValueSeries chartValueSeries = seriesList.get(index);
|
||||
int index = titleDimensions.titlePositions.size() - 1; // index only onver the visible chart series
|
||||
final int lastDrawn2ndLineMarkerIndex = getYmarkerCountOn1stLine();
|
||||
for (int i = seriesList.size()-1; i>=0 ;--i) { // draw markers from the last series to achieve right alignment
|
||||
ChartValueSeries chartValueSeries = seriesList.get(i);
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
|
||||
markerXPosition -= drawYAxisMarkers(chartValueSeries, canvas, markerXPosition) + spacer;
|
||||
int yOffset = !twoLineYaxisNumbers ? 0 : (spacer+maxYaxisNumberHeight)/2 * (index >= lastDrawn2ndLineMarkerIndex ? 1 : -1);
|
||||
markerXPosition -= drawYAxisMarkers(chartValueSeries, canvas, markerXPosition, yOffset) + spacer;
|
||||
if (twoLineYaxisNumbers && index == lastDrawn2ndLineMarkerIndex)
|
||||
markerXPosition = x - spacer;
|
||||
--index;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -811,14 +843,15 @@ public class ChartView extends View {
|
||||
* @param chartValueSeries the chart value series
|
||||
* @param canvas the canvas
|
||||
* @param xPosition the right most x position
|
||||
* @param yOffset offset to apply to y position
|
||||
* @return the maximum marker width.
|
||||
*/
|
||||
private float drawYAxisMarkers(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition) {
|
||||
private float drawYAxisMarkers(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yOffset) {
|
||||
int interval = chartValueSeries.getInterval();
|
||||
float maxMarkerWidth = 0;
|
||||
for (int i = 0; i <= Y_AXIS_INTERVALS; i++) {
|
||||
maxMarkerWidth = Math.max(maxMarkerWidth, drawYAxisMarker(chartValueSeries, canvas, xPosition,
|
||||
i * interval + chartValueSeries.getMinMarkerValue()));
|
||||
yOffset, i * interval + chartValueSeries.getMinMarkerValue()));
|
||||
}
|
||||
return maxMarkerWidth;
|
||||
}
|
||||
@@ -829,14 +862,15 @@ public class ChartView extends View {
|
||||
* @param chartValueSeries the chart value series
|
||||
* @param canvas the canvas
|
||||
* @param xPosition the right most x position
|
||||
* @parm yOffset offset to apply to y position
|
||||
* @param yValue the y value
|
||||
* @return the marker width.
|
||||
*/
|
||||
private float drawYAxisMarker(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
|
||||
private float drawYAxisMarker(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yOffset, int yValue) {
|
||||
String marker = chartValueSeries.formatMarker(yValue);
|
||||
Paint paint = chartValueSeries.getMarkerPaint();
|
||||
Rect rect = getRect(paint, marker);
|
||||
int yPosition = getY(chartValueSeries, yValue) + (rect.height() / 2);
|
||||
int yPosition = getY(chartValueSeries, yValue) + (rect.height() / 2 + yOffset);
|
||||
canvas.drawText(marker, xPosition, yPosition, paint);
|
||||
return paint.measureText(marker);
|
||||
}
|
||||
@@ -911,6 +945,10 @@ public class ChartView extends View {
|
||||
path.close();
|
||||
}
|
||||
|
||||
/// expected number of Y-axis markers on the first line if the Y-axis markers are split to two lines
|
||||
private int getYmarkerCountOn1stLine() {
|
||||
return (int)Math.ceil(titleDimensions.titlePositions.size()/2.0);
|
||||
}
|
||||
/**
|
||||
* Updates the chart dimensions.
|
||||
*/
|
||||
@@ -923,21 +961,35 @@ public class ChartView extends View {
|
||||
spacer = (int) (density * SPACER);
|
||||
yAxisOffset = (int) (density * Y_AXIS_OFFSET);
|
||||
|
||||
int markerLength = 0;
|
||||
titleDimensions = getTitleDimensions();
|
||||
topBorder = (int) (density * BORDER + titleDimensions.lineCount * (titleDimensions.lineHeight + spacer));
|
||||
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
|
||||
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer + (xAxisLabelRect.height() / 2));
|
||||
|
||||
final int markerCountOn1stLine = getYmarkerCountOn1stLine();
|
||||
int allMarkerLength = 0;
|
||||
int firstLineMarkerLength = 0;
|
||||
int secondLineMarkerLength = 0;
|
||||
int currentSeriesIndex = 0;
|
||||
maxYaxisNumberHeight = 0;
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
|
||||
Rect rect = getRect(chartValueSeries.getMarkerPaint(), chartValueSeries.getLargestMarker());
|
||||
markerLength += rect.width() + spacer;
|
||||
maxYaxisNumberHeight = Math.max(maxYaxisNumberHeight, rect.height() );
|
||||
int lengthInc = rect.width() + spacer;
|
||||
allMarkerLength += lengthInc;
|
||||
if (currentSeriesIndex < markerCountOn1stLine) firstLineMarkerLength += lengthInc;
|
||||
else secondLineMarkerLength += lengthInc;
|
||||
++currentSeriesIndex;
|
||||
}
|
||||
}
|
||||
if ( currentSeriesIndex > 1 && height-topBorder-bottomBorder-spacer > (Y_AXIS_INTERVALS+1)*(spacer+maxYaxisNumberHeight*3) ) {
|
||||
allMarkerLength = Math.max(firstLineMarkerLength, secondLineMarkerLength);
|
||||
twoLineYaxisNumbers = true;
|
||||
} else twoLineYaxisNumbers = false;
|
||||
|
||||
leftBorder = (int) (density * BORDER + markerLength);
|
||||
int[] titleDimensions = getTitleDimensions();
|
||||
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
|
||||
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
|
||||
// border + x axis marker + spacer + .5 x axis label
|
||||
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer + (xAxisLabelRect.height() / 2));
|
||||
rightBorder = (int) (density * BORDER + xAxisLabelRect.width() + spacer);
|
||||
leftBorder = (int) (density * BORDER + allMarkerLength);
|
||||
rightBorder = (int) (density * BORDER + spacer);
|
||||
updateEffectiveDimensions();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
@@ -3,7 +3,7 @@ package de.dennisguse.opentracks.data.models;
|
||||
public record AtmosphericPressure(float value) {
|
||||
|
||||
public static AtmosphericPressure ofPA(float value_Pa) {
|
||||
return new AtmosphericPressure(value_Pa * 100);
|
||||
return new AtmosphericPressure(value_Pa / 100);
|
||||
}
|
||||
|
||||
public static AtmosphericPressure ofHPA(float value_hPa) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
|
||||
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
trackRecordingServiceConnection.bind(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
|
||||
|
||||
if (PreferencesUtils.isPublicAPIenabled()) {
|
||||
Log.i(TAG, "Received and trying to execute requested action.");
|
||||
new TrackRecordingServiceConnection(serviceConnectedCallback)
|
||||
.startAndBind(this, isStartServiceForeground());
|
||||
TrackRecordingServiceConnection.execute(this, serviceConnectedCallback);
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.settings_public_api_disabled_toast), Toast.LENGTH_LONG).show();
|
||||
Log.w(TAG, "Public API is disabled; ignoring request.");
|
||||
@@ -46,10 +45,6 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isStartServiceForeground() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract void execute(TrackRecordingService service);
|
||||
|
||||
protected abstract boolean isPostExecuteStopService();
|
||||
|
||||
@@ -62,9 +62,4 @@ public class StartRecording extends AbstractAPIActivity {
|
||||
protected boolean isPostExecuteStopService() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isStartServiceForeground() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
|
||||
private static final String TAG = AbstractBluetoothConnectionManager.class.getSimpleName();
|
||||
|
||||
private final SensorDataObserver observer;
|
||||
|
||||
private final List<ServiceMeasurementUUID> serviceMeasurementUUIDs;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
switch (newState) {
|
||||
case BluetoothProfile.STATE_CONNECTING ->
|
||||
Log.i(TAG, "Connecting to sensor: " + gatt.getDevice());
|
||||
case BluetoothProfile.STATE_CONNECTED -> {
|
||||
Log.i(TAG, "Connected to sensor: " + gatt.getDevice() + "; discovering services.");
|
||||
gatt.discoverServices();
|
||||
}
|
||||
case BluetoothProfile.STATE_DISCONNECTING ->
|
||||
Log.i(TAG, "Disconnecting from sensor: " + gatt.getDevice());
|
||||
case BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
//This is also triggered, if no connection was established (ca. 30s)
|
||||
Log.i(TAG, "Disconnected from sensor: " + gatt.getDevice() + "; trying to reconnect");
|
||||
if (gatt.connect()) {
|
||||
Log.e(TAG, "Could not trigger reconnect for sensor: " + gatt.getDevice());
|
||||
}
|
||||
clearData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
|
||||
BluetoothGattService gattService = null;
|
||||
ServiceMeasurementUUID serviceMeasurement = null;
|
||||
for (ServiceMeasurementUUID s : serviceMeasurementUUIDs) {
|
||||
gattService = gatt.getService(s.serviceUUID());
|
||||
if (gattService != null) {
|
||||
serviceMeasurement = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (gattService == null) {
|
||||
Log.e(TAG, "Could not get gattService for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceMeasurement);
|
||||
return;
|
||||
}
|
||||
|
||||
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(serviceMeasurement.measurementUUID());
|
||||
if (characteristic == null) {
|
||||
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
|
||||
return;
|
||||
}
|
||||
gatt.setCharacteristicNotification(characteristic, true);
|
||||
|
||||
// Register for updates.
|
||||
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
|
||||
if (descriptor == null) {
|
||||
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID characteristic not available; cannot request notifications for changed data.");
|
||||
return;
|
||||
}
|
||||
|
||||
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||
gatt.writeDescriptor(descriptor);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AbstractBluetoothConnectionManager(ServiceMeasurementUUID serviceUUUID, SensorDataObserver observer) {
|
||||
this.serviceMeasurementUUIDs = List.of(serviceUUUID);
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
AbstractBluetoothConnectionManager(List<ServiceMeasurementUUID> serviceUUUID, SensorDataObserver observer) {
|
||||
this.serviceMeasurementUUIDs = serviceUUUID;
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
synchronized void connect(Context context, @NonNull BluetoothDevice device) {
|
||||
if (bluetoothGatt != null) {
|
||||
Log.w(TAG, "Already connected; ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private synchronized void clearData() {
|
||||
observer.onDisconnecting(createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
|
||||
}
|
||||
|
||||
|
||||
synchronized void disconnect() {
|
||||
if (bluetoothGatt == null) {
|
||||
Log.w(TAG, "Cannot disconnect if not connected.");
|
||||
return;
|
||||
}
|
||||
bluetoothGatt.close();
|
||||
clearData();
|
||||
bluetoothGatt = null;
|
||||
}
|
||||
|
||||
synchronized boolean isSameBluetoothDevice(String address) {
|
||||
if (bluetoothGatt == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
/**
|
||||
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
public class BluetoothConnectionManager implements Driver {
|
||||
|
||||
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
|
||||
|
||||
private final SensorManager.SensorDataChangedObserver observer;
|
||||
|
||||
private final SensorHandlerInterface sensorHandler;
|
||||
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
switch (newState) {
|
||||
case BluetoothProfile.STATE_CONNECTING ->
|
||||
Log.i(TAG, gatt.getDevice() + ": connecting to sensor");
|
||||
case BluetoothProfile.STATE_CONNECTED -> {
|
||||
Log.i(TAG, gatt.getDevice() + ": connected to sensor; discovering services");
|
||||
gatt.discoverServices();
|
||||
}
|
||||
case BluetoothProfile.STATE_DISCONNECTING ->
|
||||
Log.i(TAG, gatt.getDevice() + ": disconnecting from sensor: ");
|
||||
case BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
//This is also triggered, if no connection was established (ca. 30s)
|
||||
Log.i(TAG, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
|
||||
if (gatt.connect()) {
|
||||
Log.e(TAG, gatt.getDevice() + ": could not trigger reconnect for sensor");
|
||||
}
|
||||
clearData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
|
||||
BluetoothGattService gattService = null;
|
||||
ServiceMeasurementUUID serviceMeasurement = null;
|
||||
for (ServiceMeasurementUUID s : sensorHandler.getServices()) {
|
||||
gattService = gatt.getService(s.serviceUUID());
|
||||
if (gattService != null) {
|
||||
serviceMeasurement = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (gattService == null) {
|
||||
Log.e(TAG, gatt.getDevice() + ": could not get gattService for serviceUUID=" + serviceMeasurement);
|
||||
return;
|
||||
}
|
||||
|
||||
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(serviceMeasurement.measurementUUID());
|
||||
if (characteristic == null) {
|
||||
Log.e(TAG, gatt.getDevice() + ": could not get BluetoothCharacteristic for serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
|
||||
return;
|
||||
}
|
||||
gatt.setCharacteristicNotification(characteristic, true);
|
||||
|
||||
// Register for updates.
|
||||
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
|
||||
if (descriptor == null) {
|
||||
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID characteristic not available; cannot request notifications for changed data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
|
||||
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID could not be set to ENABLE_NOTIFICATION_VALUE");
|
||||
}
|
||||
if (!gatt.writeDescriptor(descriptor)) {
|
||||
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
UUID serviceUUID = characteristic.getService().getUuid();
|
||||
BluetoothDevice device = gatt.getDevice();
|
||||
Log.d(TAG, device + ": Received data with service " + serviceUUID + " and characteristics " + characteristic.getUuid());
|
||||
Optional<ServiceMeasurementUUID> serviceMeasurementUUID = sensorHandler.getServices()
|
||||
.stream()
|
||||
.filter(s -> s.serviceUUID().equals(characteristic.getService().getUuid()))
|
||||
.findFirst();
|
||||
if (serviceMeasurementUUID.isEmpty()) {
|
||||
Log.e(TAG, device + ": Unknown service UUID; not supported?");
|
||||
return;
|
||||
}
|
||||
|
||||
sensorHandler.handlePayload(observer, serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
|
||||
}
|
||||
};
|
||||
|
||||
BluetoothConnectionManager(BluetoothAdapter bluetoothAdapter, SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
|
||||
this.bluetoothAdapter = bluetoothAdapter;
|
||||
this.observer = observer;
|
||||
this.sensorHandler = sensorHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void connect(Context context, Handler handler, @NonNull String address) {
|
||||
if (!isBluetoothEnabled()) {
|
||||
Log.w(TAG, "Bluetooth not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SensorType.NONE.getPreferenceValue().equals(address)) {
|
||||
Log.w(TAG, "NONE: going to disconnect");
|
||||
if (isConnected()) {
|
||||
disconnect();
|
||||
observer.onRemove(sensorHandler.createEmptySensorData(null, null));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isConnected()) {
|
||||
Log.w(TAG, "Already connected; ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSameBluetoothDevice(address)) {
|
||||
return;
|
||||
} else {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
BluetoothDevice device;
|
||||
try {
|
||||
device = bluetoothAdapter.getRemoteDevice(address);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, address + ": Unable to get remote device for", e);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, device + ": trying to connect");
|
||||
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
|
||||
|
||||
observer.onConnect(sensorHandler.createEmptySensorData(device.getAddress(), device.getName()));
|
||||
}
|
||||
|
||||
private synchronized void clearData() {
|
||||
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress(), bluetoothGatt.getDevice().getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void disconnect() {
|
||||
if (!isConnected()) {
|
||||
Log.w(TAG, "Not connected; no need to re-connect.");
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, bluetoothGatt.getDevice() + ": start disconnect");
|
||||
bluetoothGatt.disconnect();
|
||||
bluetoothGatt.close();
|
||||
clearData();
|
||||
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
|
||||
bluetoothGatt = null;
|
||||
}
|
||||
|
||||
private synchronized boolean isSameBluetoothDevice(String address) {
|
||||
if (bluetoothGatt == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return address.equals(bluetoothGatt.getDevice().getAddress());
|
||||
}
|
||||
|
||||
|
||||
private boolean isBluetoothEnabled() {
|
||||
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return bluetoothGatt != null;
|
||||
}
|
||||
}
|
||||
-47
@@ -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;
|
||||
}
|
||||
}
|
||||
-32
@@ -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;
|
||||
}
|
||||
}
|
||||
-27
@@ -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;
|
||||
}
|
||||
}
|
||||
-27
@@ -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;
|
||||
}
|
||||
}
|
||||
-24
@@ -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);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
|
||||
private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
|
||||
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
|
||||
ENVIRONMENTAL_SENSING_SERVICE,
|
||||
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return List.of(BAROMETRIC_PRESSURE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Aggregator<?, ?> createEmptySensorData(String address, String name) {
|
||||
return new AggregatorBarometer(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
AtmosphericPressure value = parseEnvironmentalSensing(characteristic);
|
||||
if (value == null) return;
|
||||
|
||||
observer.onChange(new Raw<>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoding:
|
||||
* org.bluetooth.service.environmental_sensing.xml
|
||||
* org.bluetooth.characteristic.pressure.xml
|
||||
*/
|
||||
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] raw = characteristic.getValue();
|
||||
|
||||
if (raw.length < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
|
||||
return AtmosphericPressure.ofPA(pressure / 10f);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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, String name) {
|
||||
return new AggregatorCyclingCadence(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
if (serviceMeasurementUUID.equals(BluetoothHandlerManagerCyclingPower.CYCLING_POWER)) {
|
||||
BluetoothHandlerManagerCyclingPower.Data data = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
|
||||
if (data != null && data.crank() != null) {
|
||||
observer.onChange(new Raw<>(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
|
||||
) {}
|
||||
}
|
||||
+82
@@ -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, String name) {
|
||||
return new AggregatorCyclingDistanceSpeed(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> data = parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (data.first != null) {
|
||||
observer.onChange(new Raw<>(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
|
||||
) {}
|
||||
}
|
||||
+87
@@ -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, String name) {
|
||||
return new AggregatorCyclingPower(address, name);
|
||||
}
|
||||
|
||||
@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, String name) {
|
||||
return new AggregatorHeartRate(address, name);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
+88
@@ -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, String name) {
|
||||
return new AggregatorRunning(address, name);
|
||||
}
|
||||
|
||||
@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) {}
|
||||
}
|
||||
@@ -17,11 +17,9 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -29,47 +27,41 @@ 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;
|
||||
|
||||
/**
|
||||
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
|
||||
* <p>
|
||||
* Note: should only be instantiated once.
|
||||
* <p>
|
||||
* TODO: listen for Bluetooth enabled/disabled events.
|
||||
* <p>
|
||||
* TODO: In case, a cycling (Cadence and Speed) sensor reports both values, testing is required.
|
||||
* We establish two GATT separate GATT connections (as if two different sensors were used).
|
||||
* However, it is not clear if this is allowed.
|
||||
* Even if this works, it is not clear what happens if a user (while recording) changes one of the sensors in the settings as this will trigger a disconnect of one GATT.
|
||||
*
|
||||
* @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);
|
||||
|
||||
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);
|
||||
|
||||
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(context);
|
||||
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerHeartRate());
|
||||
this.cyclingCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingCadence());
|
||||
this.cyclingSpeed = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingDistanceSpeed());
|
||||
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerCyclingPower());
|
||||
this.runningSpeedAndCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerRunningSpeedAndCadence());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,55 +83,8 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
|
||||
started = false;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
|
||||
}
|
||||
|
||||
private synchronized void connect(AbstractBluetoothConnectionManager<?> connectionManager, String address) {
|
||||
if (!isEnabled()) {
|
||||
Log.w(TAG, "Bluetooth not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isBluetoothSensorAddressNone(address)) {
|
||||
Log.w(TAG, "No Bluetooth address.");
|
||||
connectionManager.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there is an ongoing connection; if yes, check if the address changed.
|
||||
if (connectionManager.isSameBluetoothDevice(address)) {
|
||||
return;
|
||||
} else {
|
||||
connectionManager.disconnect();
|
||||
}
|
||||
if (!PermissionRequester.BLUETOOTH.hasPermission(context)) {
|
||||
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
|
||||
}
|
||||
|
||||
Log.i(TAG, "Connecting to bluetooth address: " + address);
|
||||
try {
|
||||
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
|
||||
connectionManager.connect(context, 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;
|
||||
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
|
||||
connectionManager.connect(context, handler, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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,79 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
|
||||
*/
|
||||
public class GainManager implements SensorConnector {
|
||||
|
||||
private static final String TAG = GainManager.class.getSimpleName();
|
||||
|
||||
|
||||
private final SensorManager.SensorDataChangedObserver listener;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
connect();
|
||||
};
|
||||
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
private Driver driver;
|
||||
|
||||
public GainManager(SensorManager.SensorDataChangedObserver listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void start(Context context, Handler handler) {
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
public void stop(Context context) {
|
||||
Log.d(TAG, "Stop");
|
||||
this.context = null;
|
||||
this.handler = null;
|
||||
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
|
||||
onDisconnect();
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
onDisconnect();
|
||||
|
||||
String address = PreferencesUtils.getBarometerSensorAddress();
|
||||
switch (PreferencesUtils.getSensorType(address)) {
|
||||
case NONE -> {
|
||||
driver = null;
|
||||
listener.onRemove(new AggregatorBarometer(null, null));
|
||||
return;
|
||||
}
|
||||
case INTERNAL -> driver = new BarometerInternal(listener);
|
||||
case REMOTE -> driver =
|
||||
new BluetoothConnectionManager(
|
||||
BluetoothUtils.getAdapter(context),
|
||||
listener,
|
||||
new BluetoothHandlerBarometricPressure()
|
||||
);
|
||||
default -> throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
driver.connect(context, handler, address);
|
||||
}
|
||||
|
||||
private void onDisconnect() {
|
||||
if (driver == null) return;
|
||||
|
||||
driver.disconnect();
|
||||
listener.onDisconnect(new AggregatorBarometer("GainManager", null));
|
||||
}
|
||||
}
|
||||
@@ -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,21 +28,33 @@ 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<?, ?> aggregator) {
|
||||
sensorDataSet.add(aggregator);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(SensorData<?> sensorData) {
|
||||
sensorDataSet.remove(sensorData);
|
||||
public void onChange(Raw<?> data) {
|
||||
sensorDataSet.update(data);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Aggregator<?, ?> aggregator) {
|
||||
sensorDataSet.add(aggregator);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemove(Aggregator<?, ?> aggregator) {
|
||||
sensorDataSet.remove(aggregator);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
}
|
||||
};
|
||||
|
||||
private BluetoothRemoteSensorManager bluetoothSensorManager;
|
||||
|
||||
private AltitudeSumManager altitudeSumManager;
|
||||
private GainManager altitudeSumManager;
|
||||
|
||||
private GPSManager gpsManager;
|
||||
|
||||
@@ -50,8 +63,12 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
}
|
||||
|
||||
public void start(Context context, Handler handler) {
|
||||
if (gpsManager != null) {
|
||||
throw new RuntimeException("SensorManager cannot be started twice; stop first.");
|
||||
}
|
||||
|
||||
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 +92,6 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
}
|
||||
|
||||
public SensorDataSet fill(TrackPoint trackPoint) {
|
||||
altitudeSumManager.fill(trackPoint);
|
||||
sensorDataSet.fillTrackPoint(trackPoint);
|
||||
return new SensorDataSet(sensorDataSet);
|
||||
}
|
||||
@@ -86,13 +102,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 +116,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 +135,12 @@ 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);
|
||||
|
||||
void onRemove(Aggregator<?, ?> sensorData);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user