mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-24 03:05:17 +02:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 117281469d | |||
| 3782a22684 | |||
| aed2025a5a | |||
| 780be5d1a9 | |||
| a2d9b10381 | |||
| 5950c73785 | |||
| 99a9248b27 | |||
| 6bcf1c0818 | |||
| 96dd057287 | |||
| 84aeebea4a | |||
| cfb3ff9985 | |||
| 389c4bed96 | |||
| d8acf7df22 | |||
| 53ea905713 | |||
| 8cceadd93e | |||
| d8eed2ccc9 | |||
| f5190aef06 | |||
| 9834c60f1c | |||
| 6eeb9a0eb2 | |||
| ea23e25a47 | |||
| 53e97a5e4c | |||
| f1f9153ec6 | |||
| e442558eed | |||
| 382acadc6a | |||
| 1ed807054b | |||
| c96635c46b | |||
| 9b2c93f74f | |||
| e7a86b6aad | |||
| b86f8c9a20 | |||
| e9130f3402 | |||
| a08ec97c29 | |||
| a6340d13fa | |||
| dbbf6abcd6 | |||
| 683bf2d280 | |||
| 6c2377a8e8 | |||
| b6661ccafd | |||
| 932c142691 | |||
| 57918249bc | |||
| 3405eca556 | |||
| a932adde3f | |||
| a03dfd81ac | |||
| 7550525462 | |||
| 5d70381b23 | |||
| 96a4cbf70d | |||
| 959aadbb62 | |||
| fa59752c04 | |||
| 5305ec3206 | |||
| 584339b7eb | |||
| 151c3a79de | |||
| a90c7deb32 | |||
| c3601e41cd | |||
| b3d98340dc | |||
| ebbd9c1535 | |||
| 14fc605c45 | |||
| c5ca6644f4 | |||
| e15c729c00 | |||
| 93c89690c9 | |||
| f3f05512c9 | |||
| d2fc2598d1 | |||
| fd2d2c5820 | |||
| 01f10bce24 | |||
| fd963b0fdb | |||
| 0947405563 | |||
| f595bc4d1e | |||
| 4b26583c27 | |||
| 734d4e08e9 | |||
| 6615034314 | |||
| 039495d107 | |||
| 1d926d72a7 | |||
| d1af0fa072 | |||
| ef802d1948 | |||
| 322e91bc22 | |||
| 055f2beb1e | |||
| 6cfc05af82 | |||
| 70a2040142 | |||
| 181e1a503c | |||
| b08302237f | |||
| 881f68c580 | |||
| a8a5aa4268 | |||
| 9c8b9ca05d | |||
| b70241c967 | |||
| a6f3c05e82 | |||
| f9640b0fb1 | |||
| 80a48563d3 | |||
| 9e496f97c1 | |||
| 455c2fcf75 | |||
| e590e4e07e | |||
| 36474e8541 | |||
| be7a43823a | |||
| 64326590e6 | |||
| 82aa23f0bb | |||
| 13eb5c30a2 | |||
| 7bbdd55a93 | |||
| 1617a7377b | |||
| af25d158e1 | |||
| 47525a260e | |||
| 2984a8328a | |||
| c7a9a1a7b5 | |||
| b56eaa740f | |||
| 214f4adb70 | |||
| bc1c3eabca | |||
| cd1ba6d1ae | |||
| 0c543e6083 | |||
| a28bf53245 | |||
| 6961f3ee50 | |||
| 52624b5cfe | |||
| dfdbc373ff |
@@ -18,7 +18,7 @@ jobs:
|
||||
java-version: '17'
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2.30.1
|
||||
uses: reactivecircus/android-emulator-runner@v2.32.0
|
||||
with:
|
||||
arch: x86_64
|
||||
api-level: 34
|
||||
|
||||
@@ -70,7 +70,7 @@ hosted.weblate.org</a>.
|
||||
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
|
||||
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
|
||||
* __Altitude:__
|
||||
* gain/loss via barometric sensor (if present)
|
||||
* gain/loss via barometric sensor (internal if present or via Bluetooth's Environmental Sensing Service)
|
||||
* shown in EGM2008 (above mean sea level); exported as WGS84
|
||||
* __Bluetooth LE sensors:__
|
||||
* heart rate
|
||||
@@ -133,6 +133,8 @@ The Public API is disabled by default to protect the user's privacy, but it can
|
||||
| [OsmAnd](https://github.com/osmandapp/OsmAnd) | ? | [no](https://github.com/osmandapp/OsmAnd/issues/15271) | [no](https://github.com/osmandapp/OsmAnd/issues/15271) |
|
||||
| [FitTrackee](https://github.com/SamR1/FitTrackee) | yes | n/a | n/a |
|
||||
| [SportsTracker](https://github.com/ssaring/sportstracker) | yes, [single tracks only](https://github.com/ssaring/sportstracker/issues/260) | no | no |
|
||||
| [ExifTool](https://exiftool.org) | [yes](https://exiftool.org/forum/index.php?topic=15972.0) | no | no |
|
||||
|
||||
|
||||
## Dashboard API (incl. map)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
|
||||
* Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again)
|
||||
* Garmin HRM-Dual (Reference: 010-12883-00)
|
||||
* CYCPLUS H1
|
||||
|
||||
## 0x1816: Cycling Cadence and Speed Service
|
||||
|
||||
@@ -60,7 +61,15 @@ We do not support type of movement (i.e., walking vs running).
|
||||
* Stryd (incl. power)
|
||||
* Wahoo Tickr X (Cadence appears to be reported in SPM rather than RPM)
|
||||
|
||||
## Tested Barometric Sensor Smartphones
|
||||
## Barometer (gain/loss)
|
||||
|
||||
### 0x2A6D: Environmental Sensing Service
|
||||
Sadly, Bluetooth sensors providing _air pressure_ via Environmental Sensing Service seem to be very rare.
|
||||
|
||||
* NoTrustIssues: https://github.com/OpenTracksApp/NoTrustIssues
|
||||
An Android app that exposes the internal barometer via Environmental Sensing Service.
|
||||
|
||||
### Internal: tested Barometric Sensor Smartphones
|
||||
|
||||
OpenTracks derives the altitude gain and altitude loss using the internal barometric sensor of a device (if present).
|
||||
However, barometric sensor seem to be prone to a lot of noise.
|
||||
|
||||
+13
-13
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.3.1'
|
||||
classpath 'com.android.tools.build:gradle:8.5.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 5730
|
||||
versionName "v4.12.1"
|
||||
versionCode 5837
|
||||
versionName "v4.13.0"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -127,22 +127,22 @@ android {
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
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.11.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core:1.12.0'
|
||||
implementation 'androidx.core:core:1.13.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.mediarouter:mediarouter:1.7.0'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
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.11.0'
|
||||
androidTestImplementation 'androidx.test:core:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
androidTestImplementation 'org.mockito:mockito-android:5.12.0'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.5.0'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.0: OpenTracks
|
||||
|
||||
Změny:
|
||||
- Tmavý motiv pro obrazovky OLED (vyžaduje ruční restart)
|
||||
- Změna uspořádání nastavení (GPS je senzor)
|
||||
|
||||
Vývojáři:
|
||||
- Přidány skripty pro nahrání Fastlane na Obchod Play (překlady)
|
||||
@@ -0,0 +1,5 @@
|
||||
v4.12.1: OpenTracks
|
||||
|
||||
Oprava chyb:
|
||||
- Android 14: nahlášení chybějících oprávnění (namísto nuceného pádu se stacktrace)
|
||||
- POST_NOTIFICATION je volitelné
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.12.2.: OpenTracks
|
||||
|
||||
Oprava chyvy:
|
||||
- Umožnění spuštění senzorů před začátkem záznamu
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.3: OpenTracks
|
||||
|
||||
Opraveny chyby:
|
||||
- Používání funkcionality IDLE a GPS ukládalo souřadnice 0.0/0.0 a tím pádem přeceňovalo všechny statistiky (vzdálenost >10 000 km). (Vzniklo ve v4.12.0)
|
||||
- Nečinnost: také spustit, pokud byla obdržena pouze jedna lokace.
|
||||
|
||||
Ohledně chyby přeceňování vzdálenosti: uložená data nebudou v OpenTracks opravena.
|
||||
Doporučení: exportujte záznamy do KML/KMZ a nahraďte neplatné souřadnice.
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.4: OpenTracks
|
||||
|
||||
Změny:
|
||||
- Oznámení: jednotky lze vynechat
|
||||
- Oznámení: přidán výkon pro jízdu na kole
|
||||
|
||||
Oprava chyby:
|
||||
- Pád při odstranění značky ze seznamu
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.5: OpenTracks
|
||||
|
||||
Oprava chyby:
|
||||
- Pohyb je nyní opět počítán správně.
|
||||
Byl rozbitý od verze 4.9.0 z důvodu představení funkce přestávky. Viz #1756.
|
||||
|
||||
Změny:
|
||||
- Podpora prediktivního zadávání
|
||||
@@ -0,0 +1,15 @@
|
||||
v3.0.0: OpenTracks
|
||||
|
||||
Første udgivelse af OpenTracks (dvs. hard fork fra MyTracks)
|
||||
|
||||
Ændringer:
|
||||
- Omdøbt fra MyTracks til OpenTracks
|
||||
- Tilføjet Bluetooth LE-pulssensor
|
||||
- Fjernet alle funktioner, der kræver internetadgang
|
||||
- Fjernet Google Maps-integration
|
||||
- Fjernet brug af Google Play-tjenester
|
||||
- ANT+ sensorer fjernet
|
||||
- Fjernet klassiske Bluetooth-sensorer
|
||||
- Fjernet Protobuf
|
||||
- Fjernet kalorieberegning og aktivitetsberegning
|
||||
- Fjernet MyTracks Service-API (dvs. ingen integration med andre apps)
|
||||
@@ -1,11 +1,22 @@
|
||||
OpenTracks er et program til sporing af sport, der fuldt ud respekterer dit privatliv.
|
||||
En sporingsmakker, der respekterer dit privatliv.
|
||||
|
||||
Funktioner:
|
||||
* Sport tracking: Spor dine sports- og udendørsaktiviteter
|
||||
* Billeder og markører: Markér interessante steder under sporing
|
||||
* Højdeforøgelse og -tab: via barometrisk sensor
|
||||
* Højde vist i EGM2008 (over middelhavets overflade); eksporteret som WGS84
|
||||
* Stemmemeddelelser
|
||||
* Understøtter Bluetooth LE-sensorer: puls, hastighed og distance (cykling), kadence (cykling) og effektmåler (cykling)
|
||||
* Eksport af data: Eksportér spor enten som KMZ (inkl. fotos), KML eller GPX
|
||||
* Ingen internetadgang: Internet bruges ikke
|
||||
Sports- og udendørsaktiviteter giver glæde, dygtighed og selvtillid.
|
||||
|
||||
Værdsæt dit helbred ved at holde styr på din træning.
|
||||
Registrerer, mens du løber eller går, og giver dig en cykelcomputer med en større skærm til cykling.
|
||||
Markér interessante steder undervejs med billeder.
|
||||
Opbevar registrerede statistikker i detaljer til analyse.
|
||||
Del kun de data, du ønsker, at andre skal have.
|
||||
|
||||
* Stemmemeddelelser.
|
||||
* Understøtter Bluetooth LE-sensorer: puls, hastighed og distance (cykling), kadence (cykling) og effektmåler (cykling).
|
||||
* Højdestigning og -fald: via barometrisk sensor.
|
||||
* Højde vist i EGM2008 (over havets overflade); eksporteret som WGS84.
|
||||
* Eksport af data i form af spor, enten som KMZ (inkl. fotos), KML eller GPX.
|
||||
* Ingen internetadgang eller ekstra tilladelser.
|
||||
* Mørkt og lyst tema, der respekterer systemindstillinger.
|
||||
* Ingen reklamer.
|
||||
|
||||
Libre Software / Fri Software / Open Source
|
||||
hvilket betyder, at du kan bruge, studere, ændre og dele kildekoden.
|
||||
Licenseret Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
En sportstracker, der respekterer dit privatliv fuldt ud.
|
||||
En sportstracker, der respekterer dit privatliv.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.12.2.: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Allow to start sensors before recording
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.3: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Using GPS and IDLE functionality stored coordinates 0.0/0.0 and thus overestimating all statistics (distance >10.000 km). (Introduced in v4.12.0)
|
||||
- Idle: also trigger if only one location was received.
|
||||
|
||||
Regarding the overestimating distance bug: the stored data will not be fixed within OpenTracks.
|
||||
Recommendation: export the as KML/KMZ and replace the invalid coordinates.
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.4: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Announcements: units can be omitted
|
||||
- Announcements: added lap cycling power
|
||||
|
||||
Bugfix:
|
||||
- Delete markers from list crashed
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.5: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Moving is now computed correctly again.
|
||||
Was broken since v4.9.0 due to the introduction of idle functionality. See #1756.
|
||||
|
||||
Changes:
|
||||
- Support for Predictive Back
|
||||
@@ -0,0 +1,15 @@
|
||||
v4.13.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Markers can be created via Public API
|
||||
This allows to create Markers at any location and after recording.
|
||||
Drawback: Markers are not shown in diagrams anymore.
|
||||
NOTE: OSMDashboard supports this API after version: v4.1.0
|
||||
|
||||
Bugfix:
|
||||
- Default activity setting should have been a dropdown
|
||||
- Back arrow not working in settings
|
||||
- Label for off trail hiking now shows "off trail hiking"
|
||||
|
||||
Developer:
|
||||
- Use Position instead of Android's Location
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.0: OpenTracks
|
||||
|
||||
Cambios:
|
||||
- decorado escuro amigable con OLED (hai que reiniciar)
|
||||
- nova orde nos axustes (GPS é un sensor)
|
||||
|
||||
Desenvolvemento:
|
||||
- Engadidos scripts Fastlane para subir á PlayStore (traducións)
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.0: OpenTracks
|
||||
|
||||
Veranderingen:
|
||||
- OLED-vriendelijk donker thema (handmatige herstart vereist)
|
||||
- Re-order instellingen (GPS is een sensor)
|
||||
|
||||
Ontwikkelaar:
|
||||
- Scripts toegevoegd voor Fastlane om PlayStore te uploaden (vertalingen)
|
||||
@@ -0,0 +1,5 @@
|
||||
v4.12.1: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Android 14: ontbrekende rechten melden (in plaats van geforceerde chrash met stacktrace)
|
||||
- POST_NOTIFICATION is optioneel
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.12.2 .: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Toestaan dat sensoren voor de opname worden gestart
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.3: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Gebruikmakend van GPS- en IDLE-functionaliteit opgeslagen coördinaten 0.0 / 0.0 en overschat zo alle statistieken (afstand >10.000 km). (Ingevoerd in v4.12.0)
|
||||
- Idle: ook trigger als er maar één locatie is ontvangen.
|
||||
|
||||
Met betrekking tot de bug rond de overschatting van afstand: de opgeslagen gegevens worden niet binnen OpenTracks gecorrigeerd.
|
||||
Aanbeveling: exporteer de als KML / KMZ en vervang de ongeldige coördinaten.
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.4: OpenTracks
|
||||
|
||||
Veranderingen:
|
||||
- Aankondigingen: eenheden kunnen worden weggelaten
|
||||
- Aankondigingen: extra wielrenkracht
|
||||
|
||||
Bugfix:
|
||||
- Markeringen verwijderen uit lijst gecrasht
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.5: OpenTracks
|
||||
|
||||
Foutcorrectie:
|
||||
- Verplaatsing wordt weer correct berekend.
|
||||
Was defect sinds v4.9.0 t.g.v. de introductie van inactieve functionaliteit. Zie # 1756.
|
||||
|
||||
Veranderingen:
|
||||
- Ondersteuning voor Predictive Back
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.11.1: OpenTracks
|
||||
|
||||
Hata Düzeltme:
|
||||
- PublicAPI durdurulurken çöküyor
|
||||
@@ -0,0 +1,5 @@
|
||||
v4.11.2: OpenTracks
|
||||
|
||||
Hata Düzeltme:
|
||||
- Ekran kapandığında kayıt durduruluyor (v4.11.1 versiyonundan beri)
|
||||
(geri dönülüyor da3dd29b8e44624a8222caea9067955f252ef5f8)
|
||||
@@ -0,0 +1,5 @@
|
||||
v4.12.1: OpenTracks
|
||||
|
||||
Hata Ayıklama:
|
||||
- Android 14: eksik izinleri bildir (hata yerine)
|
||||
- POST_NOTIFICATION opsiyonel
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.12.2.: OpenTracks
|
||||
|
||||
Hata Ayıklama:
|
||||
- Sensörleri kayıttan önce başlatmaya izin ver
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.4: OpenTracks
|
||||
|
||||
Değişiklikler:
|
||||
- Duyurular: birimler ihmal edilebilir
|
||||
- Duyurular: tur bisikleti gücü eklendi
|
||||
|
||||
Hata Ayıklama:
|
||||
- İşaretçileri çöken listesinden sil
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.5: OpenTracks
|
||||
|
||||
Hata Ayıklama:
|
||||
- Hareketler artık yeniden doğru bir şekilde hesaplanıyor.
|
||||
boşta durulan vakit özelliğinin getirilmesinden dolayı v4.9.0'den beri bozuktu. #1756'ya bakınız
|
||||
|
||||
Değişiklikler:
|
||||
- Tahmine Dayalı Geri Dönüş Desteği eklendi
|
||||
@@ -0,0 +1,10 @@
|
||||
(versiyonAdı): OpenTracks
|
||||
|
||||
Değişiklikler:
|
||||
-
|
||||
|
||||
Hata Ayıklama:
|
||||
-
|
||||
|
||||
Geliştirici:
|
||||
-
|
||||
@@ -0,0 +1 @@
|
||||
OpenTracks
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.12.0: OpenTracks
|
||||
|
||||
更改:
|
||||
- OLED 友好型暗色主题(需要手动重启)
|
||||
- 重新排序设置(GPS 是一个传感器)
|
||||
|
||||
开发人员:
|
||||
- 为 PlayStore 上传添加了 Fastlane 脚本(翻译)
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Vendored
+10
-10
@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -88,9 +88,9 @@ public class TestDataUtil {
|
||||
stats.setTotalDistance(Distance.of(0));
|
||||
stats.setTotalTime(Duration.ofMillis(0));
|
||||
List<Marker> markers = List.of(
|
||||
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, stats, trackPoints.get(1), null),
|
||||
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, stats, trackPoints.get(4), null),
|
||||
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, stats, trackPoints.get(5), null)
|
||||
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, trackPoints.get(1), null),
|
||||
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, trackPoints.get(4), null),
|
||||
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, trackPoints.get(5), null)
|
||||
);
|
||||
|
||||
return new TrackData(track, trackPoints, markers);
|
||||
@@ -152,7 +152,7 @@ public class TestDataUtil {
|
||||
stats.setTotalDistance(Distance.of(0));
|
||||
stats.setTotalTime(Duration.ofMillis(0));
|
||||
|
||||
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, stats, trackPoint, photoUrl);
|
||||
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, trackPoint, photoUrl);
|
||||
}
|
||||
|
||||
public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
|
||||
|
||||
+4
-7
@@ -9,7 +9,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
@@ -140,7 +139,7 @@ public class ExportImportTest {
|
||||
Distance sensorDistance = Distance.of(10); // recording distance interval
|
||||
|
||||
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);
|
||||
contentProviderUtils.insertMarker(new Marker("Marker 1", "Marker 1 desc", "Marker 1 category", null, trackId, service.getLastStoredTrackPointWithLocation(), ""));
|
||||
|
||||
// A sensor-only TrackPoint
|
||||
trackPointCreator.setClock("2020-02-02T02:02:04Z");
|
||||
@@ -155,7 +154,7 @@ public class ExportImportTest {
|
||||
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, 0f);
|
||||
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
||||
contentProviderUtils.insertMarker(new Marker("Marker 2", "Marker 2 desc", "Marker 2 category", null, trackId, service.getLastStoredTrackPointWithLocation(), ""));
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:18Z");
|
||||
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
|
||||
@@ -595,15 +594,13 @@ public class ExportImportTest {
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
location.setAccuracy(accuracy);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
location.setVerticalAccuracyMeters(verticalAccuracy);
|
||||
}
|
||||
location.setVerticalAccuracyMeters(verticalAccuracy);
|
||||
location.setSpeed(speed);
|
||||
location.setAltitude(altitude);
|
||||
|
||||
mockAltitudeChange(trackPointCreator, altitudeGain);
|
||||
|
||||
trackPointCreator.setClock(time);
|
||||
trackPointCreator.onChange(location);
|
||||
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -166,7 +166,7 @@ public class KMLTrackImporterTest {
|
||||
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkers(importTrackId).size());
|
||||
assertEquals(1, contentProviderUtils.getMarkers(importTrackId).size());
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TestUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
@@ -35,12 +41,23 @@ public class PublicApiTest {
|
||||
@Test
|
||||
public void StartStopTest() throws InterruptedException {
|
||||
PreferencesUtils.setBoolean(R.string.publicapi_enabled_key, true);
|
||||
|
||||
context.startActivity(IntentUtils.newIntent(context, StartRecording.class));
|
||||
Intent startIntent = IntentUtils.newIntent(context, StartRecording.class);
|
||||
startIntent.putExtra("TRACK_NAME", "trackName");
|
||||
startIntent.putExtra("TRACK_CATEGORY", "activityTypeLocalized");
|
||||
startIntent.putExtra("TRACK_ICON", "airplane");
|
||||
startIntent.putExtra("TRACK_DESCRIPTION", "description");
|
||||
context.startActivity(startIntent);
|
||||
|
||||
Thread.sleep(5000);
|
||||
|
||||
context.startActivity(IntentUtils.newIntent(context, StopRecording.class));
|
||||
|
||||
List<Track> tracks = new ContentProviderUtils(context).getTracks();
|
||||
Assert.assertEquals(1, tracks.size());
|
||||
Track track = tracks.get(0);
|
||||
Assert.assertEquals("trackName", track.getName());
|
||||
Assert.assertEquals("activityTypeLocalized", track.getActivityTypeLocalized());
|
||||
Assert.assertEquals("airplane", track.getActivityType().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright 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.services;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackRecordingServiceMarkerTest {
|
||||
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private TrackRecordingService service;
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void finalTearDown() {
|
||||
if (Looper.myLooper() != null) Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
private TrackRecordingService startService() throws TimeoutException {
|
||||
Intent startIntent = new Intent(context, TrackRecordingService.class);
|
||||
return ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent))
|
||||
.getService();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws TimeoutException {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
service = startService();
|
||||
tearDown();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
// Ensure that the database is empty after every test
|
||||
contentProviderUtils.deleteAllTracks(context);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void notRecording_testInsertMarker() {
|
||||
// given
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
// when
|
||||
Marker.Id markerId = service.insertMarker(null, null, null, null);
|
||||
|
||||
// then
|
||||
assertNull(markerId);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_noGPSfix_cannotCreateMarker() {
|
||||
// given
|
||||
service.startNewTrack();
|
||||
|
||||
// when
|
||||
Marker.Id markerId = service.insertMarker(null, null, null, null);
|
||||
|
||||
// then
|
||||
assertNull(markerId);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_GPSfix_createsMarker() {
|
||||
// given
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:02Z");
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
service.stopUpdateRecordingData();
|
||||
|
||||
assertTrue(service.isRecording());
|
||||
trackPointCreator.onNewTrackPoint(
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:03Z"))
|
||||
.setLatitude(10)
|
||||
.setLongitude(10)
|
||||
);
|
||||
|
||||
// when
|
||||
Marker.Id markerId = service.insertMarker(null, null, null, null);
|
||||
|
||||
// then
|
||||
assertNotEquals(new Marker.Id(-1L), markerId);
|
||||
Marker wpt = contentProviderUtils.getMarker(markerId);
|
||||
assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
|
||||
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
|
||||
assertEquals(trackId, wpt.getTrackId());
|
||||
assertEquals(0.0, wpt.getLength().toM(), 0.01);
|
||||
assertNotNull(wpt.getLocation());
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:04Z");
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
}
|
||||
+54
@@ -10,6 +10,7 @@ import android.os.Looper;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
@@ -20,12 +21,14 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TestUtil;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
@@ -54,6 +57,9 @@ public class TrackRecordingServiceRecordingTest {
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
@@ -127,6 +133,47 @@ public class TrackRecordingServiceRecordingTest {
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test that an IDLE event, doesn't store invalid GPS-provided data.
|
||||
*/
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_startIdle() throws InterruptedException {
|
||||
|
||||
// given
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
String startTime = "2020-02-02T02:02:02Z";
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
String gps1 = "2020-02-02T02:02:03Z";
|
||||
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
|
||||
String gps2 = "2020-02-02T02:02:04Z";
|
||||
|
||||
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 15);
|
||||
|
||||
// when
|
||||
String idleTime = "2020-02-02T02:02:17Z";
|
||||
trackPointCreator.setClock("2020-02-02T02:02:17Z");
|
||||
Thread.sleep(Duration.ofSeconds(15).toMillis());
|
||||
|
||||
// then
|
||||
new TrackPointAssert().assertEquals(List.of(
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
|
||||
.setLatitude(45)
|
||||
.setLongitude(35)
|
||||
.setHorizontalAccuracy(Distance.of(1))
|
||||
.setSpeed(Speed.of(15)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps2))
|
||||
.setLatitude(45)
|
||||
.setLongitude(35)
|
||||
.setHorizontalAccuracy(Distance.of(1))
|
||||
.setSpeed(Speed.of(15)),
|
||||
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse(idleTime))
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_startPauseResume() {
|
||||
@@ -415,6 +462,10 @@ public class TrackRecordingServiceRecordingTest {
|
||||
.setAltitudeGain(0f)
|
||||
.setAltitudeLoss(0f),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
|
||||
.setLatitude(45.00002)
|
||||
.setLongitude(35)
|
||||
.setHorizontalAccuracy(Distance.of(1))
|
||||
.setSpeed(Speed.of(15))
|
||||
.setAltitudeGain(0f)
|
||||
.setAltitudeLoss(0f)
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
@@ -667,6 +718,9 @@ public class TrackRecordingServiceRecordingTest {
|
||||
.setSpeed(Speed.of(5))
|
||||
.setSensorDistance(Distance.of(0)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
|
||||
.setLatitude(45.001)
|
||||
.setLongitude(35)
|
||||
.setHorizontalAccuracy(Distance.of(1))
|
||||
.setSensorDistance(Distance.of(11))
|
||||
.setSpeed(Speed.of(5))
|
||||
.setSensorDistance(Distance.of(0))
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
package de.dennisguse.opentracks.services.handlers;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class GPSManagerTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Mock
|
||||
private TrackPointCreator trackPointCreator;
|
||||
|
||||
@InjectMocks
|
||||
private GPSManager locationHandler;
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// Let's use default values.
|
||||
PreferencesUtils.clear();
|
||||
|
||||
Mockito.when(trackPointCreator.createNow())
|
||||
.thenReturn(Instant.now());
|
||||
|
||||
locationHandler.start(context, new Handler());
|
||||
}
|
||||
|
||||
/**
|
||||
* When a valid location changed in LocationHandler -> newTrackPoint service method is called.
|
||||
*/
|
||||
@Test
|
||||
public void testOnLocationChanged_okay() {
|
||||
// when
|
||||
locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis()));
|
||||
|
||||
// then
|
||||
verify(trackPointCreator, times(1)).onChange(any(Location.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* When location changed in LocationHandler with bad location -> newTrackPoint service method is not called.
|
||||
*/
|
||||
@Test
|
||||
public void testOnLocationChanged_badLocation() {
|
||||
// given
|
||||
float latitude = 91f;
|
||||
|
||||
// when
|
||||
// bad latitude
|
||||
locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis()));
|
||||
|
||||
// then
|
||||
verify(trackPointCreator, times(0)).onChange(any(Location.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* When location changed in LocationHandler with poor accuracy -> newTrackPoint service method is not called.
|
||||
*/
|
||||
@Test
|
||||
public void testOnLocationChanged_poorAccuracy() {
|
||||
// given
|
||||
Distance prefAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
|
||||
|
||||
// when
|
||||
locationHandler.onLocationChanged(createLocation(45f, 35f, (float) (prefAccuracy.toM() + 1), 5, System.currentTimeMillis()));
|
||||
|
||||
// then
|
||||
// no newTrackPoint called
|
||||
verify(trackPointCreator, times(0)).onChange(any(Location.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnLocationChanged_movingInaccurate() {
|
||||
// when
|
||||
locationHandler.onLocationChanged(createLocation(45.0, 35.0, 5, 15, System.currentTimeMillis()));
|
||||
locationHandler.onLocationChanged(createLocation(45.1, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
|
||||
locationHandler.onLocationChanged(createLocation(45.2, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
|
||||
locationHandler.onLocationChanged(createLocation(45.3, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
|
||||
locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
|
||||
|
||||
// then
|
||||
verify(trackPointCreator, times(1)).onChange(any(Location.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a location with parameters and returns the Location object.
|
||||
*/
|
||||
private static Location createLocation(double latitude, double longitude, float accuracy, long speed, long time) {
|
||||
Location location = new Location("gps");
|
||||
location.setLongitude(longitude);
|
||||
location.setLatitude(latitude);
|
||||
location.setAccuracy(accuracy);
|
||||
location.setSpeed(speed);
|
||||
location.setTime(time);
|
||||
location.setBearing(3.0f);
|
||||
return location;
|
||||
}
|
||||
}
|
||||
+61
-2
@@ -1,7 +1,9 @@
|
||||
package de.dennisguse.opentracks.stats;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
@@ -231,7 +233,7 @@ public class TrackStatisticsUpdaterTest {
|
||||
));
|
||||
|
||||
// then
|
||||
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(35), subject.getTrackStatistics().getMovingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -261,10 +263,67 @@ public class TrackStatisticsUpdaterTest {
|
||||
));
|
||||
|
||||
// then
|
||||
assertEquals(Duration.ofSeconds(45), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Distance.of(1040), subject.getTrackStatistics().getTotalDistance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTrackPoint_idle_remain_idle() {
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
|
||||
subject.addTrackPoint(
|
||||
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(10))
|
||||
.setSensorDistance(Distance.of(10)));
|
||||
|
||||
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)));
|
||||
// then
|
||||
assertTrue(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(10), subject.getTrackStatistics().getTotalDistance());
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
|
||||
.setSensorDistance(Distance.of(0)));
|
||||
// then
|
||||
assertTrue(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(10), subject.getTrackStatistics().getTotalDistance());
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
|
||||
.setSensorDistance(Distance.of(1)));
|
||||
// then
|
||||
assertTrue(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(45), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(11), subject.getTrackStatistics().getTotalDistance());
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(50))
|
||||
.setSensorDistance(Distance.of(10)));
|
||||
// then
|
||||
assertFalse(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(50), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(21), subject.getTrackStatistics().getTotalDistance());
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(60)));
|
||||
|
||||
// then
|
||||
assertFalse(subject.getTrackStatistics().isIdle());
|
||||
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(60), subject.getTrackStatistics().getTotalTime());
|
||||
assertEquals(Distance.of(21), subject.getTrackStatistics().getTotalDistance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copy_constructor() {
|
||||
// given
|
||||
|
||||
@@ -38,7 +38,6 @@ import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.TextStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
@@ -138,10 +137,10 @@ public class StringUtilsTest {
|
||||
ArrayList<String> shortMonths = Arrays.stream(Month.values()).map(m -> m.getDisplayName(TextStyle.SHORT, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
|
||||
|
||||
LocalDate today = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toLocalDate();
|
||||
LocalDate yesterday = today.minus(1, ChronoUnit.DAYS);
|
||||
LocalDate dayName = today.minus(2, ChronoUnit.DAYS);
|
||||
LocalDate thisYear = today.minus(15, ChronoUnit.DAYS);
|
||||
LocalDate aYearAgo = today.minus(400, ChronoUnit.DAYS);
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
LocalDate dayName = today.minusDays(2);
|
||||
LocalDate thisYear = today.minusDays(15);
|
||||
LocalDate aYearAgo = today.minusDays(400);
|
||||
|
||||
int offsetFromLocale = OffsetDateTime.now().getOffset().getTotalSeconds() - 28800; // -08:00
|
||||
|
||||
|
||||
@@ -314,7 +314,7 @@ limitations under the License.
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.markers.MarkerDetailActivity"
|
||||
android:name=".publicapi.ShowMarkerActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="de.dennisguse.opentracks.MarkerDetails" />
|
||||
@@ -322,43 +322,24 @@ limitations under the License.
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".publicapi.CreateMarkerActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="de.dennisguse.opentracks.CreateMarker" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ui.markers.MarkerDetailActivity" />
|
||||
<activity android:name=".ui.markers.MarkerEditActivity" />
|
||||
<activity android:name=".ui.markers.MarkerListActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.markers.MarkerListActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/menu_markers"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".TrackListActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".TrackListActivity" />
|
||||
<activity android:name=".TrackRecordedActivity" />
|
||||
<activity android:name=".TrackStoppedActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".TrackRecordingActivity"
|
||||
android:parentActivityName=".TrackListActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".TrackListActivity" />
|
||||
</activity>
|
||||
<activity android:name=".TrackRecordingActivity" />
|
||||
|
||||
<activity android:name=".TrackEditActivity" />
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
@@ -26,7 +25,6 @@ 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;
|
||||
import android.view.View;
|
||||
@@ -34,7 +32,6 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
@@ -129,7 +126,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
};
|
||||
|
||||
// Menu items
|
||||
private MenuItem searchMenuItem;
|
||||
|
||||
private String searchQuery;
|
||||
|
||||
@@ -249,6 +245,14 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackListBinding.inflate(getLayoutInflater());
|
||||
|
||||
viewBinding.trackListSearchView.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
searchQuery = viewBinding.trackListSearchView.getEditText().getText().toString();
|
||||
viewBinding.trackListSearchView.hide();
|
||||
loadData();
|
||||
return true;
|
||||
});
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@@ -256,9 +260,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.track_list, menu);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.track_list_search);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -280,13 +281,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_list_search) {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
searchView.setIconified(false);
|
||||
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_list_help) {
|
||||
startActivity(IntentUtils.newIntent(this, HelpActivity.class));
|
||||
return true;
|
||||
@@ -295,42 +289,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
|
||||
if (searchQuery != null) {
|
||||
searchQuery = null;
|
||||
loadData();
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
searchQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||
} else {
|
||||
searchQuery = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
viewBinding.trackListToolbar.setText(searchQuery);
|
||||
|
||||
viewBinding.trackListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.app_name)));
|
||||
|
||||
Cursor tracks = new ContentProviderUtils(this).searchTracks(searchQuery);
|
||||
|
||||
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.TrackDataHub;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
|
||||
@@ -269,9 +270,14 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_detail_insert_marker) {
|
||||
TrackPoint trackPoint = trackRecordingServiceConnection.getTrackRecordingService().getLastStoredTrackPointWithLocation();
|
||||
if (trackPoint == null) {
|
||||
return true;
|
||||
}
|
||||
Intent intent = IntentUtils
|
||||
.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId)
|
||||
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.getLocation());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -110,15 +110,6 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
|
||||
contentProviderUtils);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (isDiscarding) {
|
||||
return;
|
||||
}
|
||||
super.onBackPressed();
|
||||
resumeTrackAndFinish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackStoppedBinding.inflate(getLayoutInflater());
|
||||
|
||||
@@ -41,9 +41,9 @@ import androidx.core.view.GestureDetectorCompat;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Iterator;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
@@ -152,31 +152,6 @@ public class ChartView extends View {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent event) {
|
||||
// Check if the y event is within markerHeight of the marker center
|
||||
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) {
|
||||
int minDistance = Integer.MAX_VALUE;
|
||||
Marker nearestMarker = null;
|
||||
synchronized (markers) {
|
||||
for (Marker marker : markers) {
|
||||
int distance = Math.abs(getX(getMarkerXValue(marker)) - (int) event.getX() - getScrollX());
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestMarker = marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nearestMarker != null && minDistance < markerWidth) {
|
||||
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestMarker.getId());
|
||||
getContext().startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
private final ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
@@ -589,7 +564,6 @@ public class ChartView extends View {
|
||||
|
||||
clipToGraphArea(canvas);
|
||||
drawDataSeries(canvas);
|
||||
drawMarker(canvas);
|
||||
drawGrid(canvas);
|
||||
|
||||
canvas.restore();
|
||||
@@ -630,30 +604,6 @@ public class ChartView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawMarker(Canvas canvas) {
|
||||
synchronized (markers) {
|
||||
for (Marker marker : markers) {
|
||||
double xValue = getMarkerXValue(marker);
|
||||
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);
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the grid.
|
||||
*
|
||||
@@ -679,12 +629,13 @@ 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.
|
||||
@@ -711,7 +662,7 @@ public class ChartView extends View {
|
||||
private TitleDimensions getTitleDimensions() {
|
||||
int lineCnt = 1;
|
||||
int lineHeight = 0;
|
||||
List<TitlePosition> tps = new ArrayList<TitlePosition>();
|
||||
List<TitlePosition> tps = new ArrayList<>();
|
||||
int xPosInLine = spacer;
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
|
||||
@@ -1042,14 +993,6 @@ public class ChartView extends View {
|
||||
return topBorder + yAxisOffset + (int) ((1 - percentage) * rangeHeight);
|
||||
}
|
||||
|
||||
private double getMarkerXValue(Marker marker) {
|
||||
if (chartByDistance) {
|
||||
return marker.getLength().toKM_Miles(unitSystem);
|
||||
} else {
|
||||
return marker.getDuration().toMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a paint's Rect for a string.
|
||||
*
|
||||
|
||||
@@ -377,8 +377,6 @@ public class ContentProviderUtils {
|
||||
int categoryIndex = cursor.getColumnIndexOrThrow(MarkerColumns.CATEGORY);
|
||||
int iconIndex = cursor.getColumnIndexOrThrow(MarkerColumns.ICON);
|
||||
int trackIdIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TRACKID);
|
||||
int lengthIndex = cursor.getColumnIndexOrThrow(MarkerColumns.LENGTH);
|
||||
int durationIndex = cursor.getColumnIndexOrThrow(MarkerColumns.DURATION);
|
||||
int longitudeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.LONGITUDE);
|
||||
int latitudeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.LATITUDE);
|
||||
int timeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TIME);
|
||||
@@ -419,13 +417,6 @@ public class ContentProviderUtils {
|
||||
if (!cursor.isNull(iconIndex)) {
|
||||
marker.setIcon(cursor.getString(iconIndex));
|
||||
}
|
||||
if (!cursor.isNull(lengthIndex)) {
|
||||
marker.setLength(Distance.of(cursor.getFloat(lengthIndex)));
|
||||
}
|
||||
if (!cursor.isNull(durationIndex)) {
|
||||
marker.setDuration(Duration.ofMillis(cursor.getLong(durationIndex)));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(photoUrlIndex)) {
|
||||
marker.setPhotoUrl(cursor.getString(photoUrlIndex));
|
||||
}
|
||||
@@ -542,9 +533,6 @@ public class ContentProviderUtils {
|
||||
values.put(MarkerColumns.CATEGORY, marker.getCategory());
|
||||
values.put(MarkerColumns.ICON, marker.getIcon());
|
||||
values.put(MarkerColumns.TRACKID, marker.getTrackId().id());
|
||||
values.put(MarkerColumns.LENGTH, marker.getLength().toM());
|
||||
values.put(MarkerColumns.DURATION, marker.getDuration().toMillis());
|
||||
|
||||
values.put(MarkerColumns.LONGITUDE, (int) (marker.getLongitude() * 1E6));
|
||||
values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6));
|
||||
values.put(MarkerColumns.TIME, marker.getTime().toEpochMilli());
|
||||
|
||||
@@ -29,7 +29,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
||||
|
||||
private static final int DATABASE_VERSION = 37;
|
||||
private static final int DATABASE_VERSION = 38;
|
||||
|
||||
private final Context context;
|
||||
|
||||
@@ -81,6 +81,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 35 -> upgradeFrom34to35(db);
|
||||
case 36 -> upgradeFrom35to36(db);
|
||||
case 37 -> upgradeFrom36to37(db);
|
||||
case 38 -> upgradeFrom37to38(db);
|
||||
default -> throw new RuntimeException("Not implemented: upgrade to " + toVersion);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +106,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 34 -> downgradeFrom35to34(db);
|
||||
case 35 -> downgradeFrom36to35(db);
|
||||
case 36 -> downgradeFrom37to36(db);
|
||||
case 37 -> downgradeFrom38to37(db);
|
||||
default -> throw new RuntimeException("Not implemented: downgrade to " + toVersion);
|
||||
}
|
||||
}
|
||||
@@ -644,4 +646,28 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void upgradeFrom37to38(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE markers RENAME TO markers_old");
|
||||
db.execSQL("CREATE TABLE markers (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||
db.execSQL("INSERT INTO markers SELECT _id, name, description, category, icon, trackid, longitude, latitude, time, elevation, accuracy, bearing, photoUrl FROM markers_old");
|
||||
db.execSQL("DROP TABLE markers_old");
|
||||
|
||||
db.execSQL("CREATE INDEX markers_trackid_index ON markers(trackid)");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void downgradeFrom38to37(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE markers ADD COLUMN length FLOAT");
|
||||
db.execSQL("ALTER TABLE markers ADD COLUMN duration INTEGER");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public enum ActivityType {
|
||||
MOTOR_BIKE("motor bike", R.string.activity_type_motor_bike, R.drawable.ic_activity_mtb_24dp, true),
|
||||
MOTOR_BOATING("motor boating", R.string.activity_type_motor_boating, R.drawable.ic_activity_boat_24dp, true),
|
||||
MOUNTAIN_BIKING("mountain biking", R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true),
|
||||
OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false),
|
||||
OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_off_trail_hiking, R.drawable.ic_activity_walk_24dp, false),
|
||||
OTHER("other", R.string.activity_type_other, ActivityIcon.ICON_UNKNOWN, true),
|
||||
PADDLING("paddling", R.string.activity_type_paddling, ActivityIcon.ICON_UNKNOWN, true),
|
||||
PARA_GLIDING("para gliding", R.string.activity_type_para_gliding, ActivityIcon.ICON_UNKNOWN, true),
|
||||
|
||||
@@ -27,8 +27,6 @@ import androidx.annotation.Nullable;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
/**
|
||||
* NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true.
|
||||
*
|
||||
@@ -53,10 +51,6 @@ public final class Marker {
|
||||
private Altitude altitude;
|
||||
private Float bearing;
|
||||
|
||||
//TODO It is the distance from the track starting point; rename to something more meaningful
|
||||
private Distance length;
|
||||
private Duration duration;
|
||||
|
||||
@Deprecated //TODO Make an URI instead of String
|
||||
private String photoUrl = "";
|
||||
|
||||
@@ -65,7 +59,6 @@ public final class Marker {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@Deprecated //TODO Marker cannot be created without length AND duration!
|
||||
public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) {
|
||||
this.trackId = trackId;
|
||||
|
||||
@@ -75,20 +68,15 @@ public final class Marker {
|
||||
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
||||
|
||||
setTrackPoint(trackPoint);
|
||||
|
||||
this.length = Distance.of(0); //TODO Not cool!
|
||||
this.duration = Duration.ofMillis(0); //TODO Not cool!
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackPoint trackPoint, String photoUrl) {
|
||||
this(trackId, trackPoint);
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.category = category;
|
||||
this.icon = icon;
|
||||
this.length = statistics.getTotalDistance();
|
||||
this.duration = statistics.getTotalTime();
|
||||
this.photoUrl = photoUrl;
|
||||
}
|
||||
|
||||
@@ -234,22 +222,6 @@ public final class Marker {
|
||||
this.bearing = bearing;
|
||||
}
|
||||
|
||||
public Distance getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(Distance length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public Duration getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(@NonNull Duration duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public String getPhotoUrl() {
|
||||
return photoUrl;
|
||||
}
|
||||
@@ -263,7 +235,7 @@ public final class Marker {
|
||||
}
|
||||
|
||||
public boolean hasPhoto() {
|
||||
return photoUrl != null && !"".equals(photoUrl);
|
||||
return photoUrl != null && !photoUrl.isEmpty();
|
||||
}
|
||||
|
||||
public record Id(long id) implements Parcelable {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
//TODO Use everywhere instead of android.location.Location.
|
||||
public record Position(
|
||||
Instant time,
|
||||
Double latitude,
|
||||
Double longitude,
|
||||
Distance horizontalAccuracy,
|
||||
Altitude altitude,
|
||||
Distance verticalAccuracy,
|
||||
Float bearing,
|
||||
Speed speed
|
||||
) {
|
||||
public static Position empty() {
|
||||
return new Position(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static Position of(Location location) {
|
||||
return new Position(
|
||||
Instant.ofEpochMilli(location.getTime()),
|
||||
location.getLatitude(),
|
||||
location.getLongitude(),
|
||||
location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null,
|
||||
location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null,
|
||||
location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null,
|
||||
location.hasBearing() ? location.getBearing() : null,
|
||||
location.hasSpeed() ? Speed.of(location.getSpeed()) : null
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return latitude != null && longitude != null;
|
||||
}
|
||||
|
||||
public boolean hasHorizontalAccuracy() {
|
||||
return horizontalAccuracy != null;
|
||||
}
|
||||
|
||||
public boolean hasAltitude() {
|
||||
return altitude != null;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
return bearing != null;
|
||||
}
|
||||
|
||||
public boolean hasSpeed() {
|
||||
return speed != null;
|
||||
}
|
||||
|
||||
public Location toLocation() {
|
||||
if (!hasLocation()) {
|
||||
throw new RuntimeException("Cannot convert to Location.");
|
||||
}
|
||||
|
||||
Location location = new Location("");
|
||||
location.setTime(time.toEpochMilli());
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
if (hasBearing()) {
|
||||
location.setBearing(bearing);
|
||||
}
|
||||
if (hasHorizontalAccuracy()) {
|
||||
location.setAccuracy((float) horizontalAccuracy.toM());
|
||||
}
|
||||
if (hasAltitude()) {
|
||||
location.setAltitude(altitude.toM());
|
||||
}
|
||||
if (hasSpeed()) {
|
||||
location.setSpeed((float) speed.toMPS());
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,16 @@ public class Track {
|
||||
this.trackStatistics = trackStatistics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Track{" +
|
||||
"name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", activityTypeLocalized='" + activityTypeLocalized + '\'' +
|
||||
", activityType=" + activityType +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
@@ -26,6 +25,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Sensor and/or location information for a specific point in time.
|
||||
@@ -42,6 +42,7 @@ public class TrackPoint {
|
||||
@NonNull
|
||||
private final Instant time;
|
||||
|
||||
//TODO We may use Position for these items
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Distance horizontalAccuracy;
|
||||
@@ -135,6 +136,10 @@ public class TrackPoint {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isIdleTriggered() {
|
||||
return type == Type.IDLE;
|
||||
}
|
||||
|
||||
public boolean isSegmentManualStart() {
|
||||
return type == Type.SEGMENT_START_MANUAL;
|
||||
}
|
||||
@@ -181,42 +186,40 @@ public class TrackPoint {
|
||||
return this;
|
||||
}
|
||||
|
||||
//TODO Better return null, if no location is present aka latitude == null etc.
|
||||
@NonNull
|
||||
public Location getLocation() {
|
||||
Location location = new Location("");
|
||||
location.setTime(time.toEpochMilli());
|
||||
if (hasLocation()) {
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
}
|
||||
if (hasBearing()) {
|
||||
location.setBearing(bearing);
|
||||
}
|
||||
if (hasHorizontalAccuracy()) {
|
||||
location.setAccuracy((float) horizontalAccuracy.toM());
|
||||
}
|
||||
if (hasAltitude()) {
|
||||
location.setAltitude(altitude.toM());
|
||||
}
|
||||
if (hasSpeed()) {
|
||||
location.setSpeed((float) speed.toMPS());
|
||||
}
|
||||
|
||||
return location;
|
||||
public Position getPosition() {
|
||||
return new Position(
|
||||
time,
|
||||
latitude,
|
||||
longitude,
|
||||
horizontalAccuracy,
|
||||
altitude,
|
||||
verticalAccuracy,
|
||||
bearing,
|
||||
speed
|
||||
);
|
||||
}
|
||||
|
||||
public TrackPoint setLocation(@NonNull Location location) {
|
||||
this.latitude = location.getLatitude();
|
||||
this.longitude = location.getLongitude();
|
||||
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
|
||||
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
|
||||
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.verticalAccuracy = location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null;
|
||||
}
|
||||
public TrackPoint setPosition(Position location) {
|
||||
this.latitude = location.latitude();
|
||||
this.longitude = location.longitude();
|
||||
this.altitude = location.altitude();
|
||||
this.speed = location.speed();
|
||||
this.horizontalAccuracy = location.horizontalAccuracy();
|
||||
this.verticalAccuracy = location.verticalAccuracy();
|
||||
|
||||
//TODO Should we copy the bearing?
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@NonNull
|
||||
public Location getLocation() {
|
||||
return getPosition().toLocation();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public TrackPoint setLocation(@NonNull Location location) {
|
||||
setPosition(Position.of(location));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -288,6 +291,7 @@ public class TrackPoint {
|
||||
this.speed = speed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
return bearing != null;
|
||||
}
|
||||
@@ -348,13 +352,20 @@ public class TrackPoint {
|
||||
return hasHorizontalAccuracy() && horizontalAccuracy.lessThan(thresholdHorizontalAccuracy);
|
||||
}
|
||||
|
||||
public float bearingTo(@NonNull TrackPoint dest) {
|
||||
public Optional<Float> bearingTo(@NonNull TrackPoint dest) {
|
||||
if (!dest.hasLocation() || !hasLocation()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return bearingTo(dest.getLocation());
|
||||
}
|
||||
|
||||
//TODO Bearing requires a location; what do we do if we don't have any?
|
||||
public float bearingTo(@NonNull Location dest) {
|
||||
return getLocation().bearingTo(dest);
|
||||
@Deprecated
|
||||
public Optional<Float> bearingTo(@NonNull Location dest) {
|
||||
if (!hasLocation()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(getLocation().bearingTo(dest));
|
||||
}
|
||||
|
||||
// Sensor data
|
||||
|
||||
@@ -41,10 +41,6 @@ public interface MarkerColumns extends BaseColumns {
|
||||
String CATEGORY = "category"; // marker category
|
||||
String ICON = "icon"; // marker icon
|
||||
String TRACKID = "trackid"; // track id
|
||||
|
||||
String LENGTH = "length"; // length of the track (without smoothing)
|
||||
String DURATION = "duration"; // total duration of the track from the beginning until now
|
||||
|
||||
String LONGITUDE = "longitude"; // longitude
|
||||
String LATITUDE = "latitude"; // latitude
|
||||
String TIME = "time"; // time
|
||||
@@ -61,8 +57,6 @@ public interface MarkerColumns extends BaseColumns {
|
||||
+ CATEGORY + " TEXT, "
|
||||
+ ICON + " TEXT, "
|
||||
+ TRACKID + " INTEGER NOT NULL, "
|
||||
+ LENGTH + " FLOAT, "
|
||||
+ DURATION + " INTEGER, "
|
||||
+ LONGITUDE + " INTEGER, "
|
||||
+ LATITUDE + " INTEGER, "
|
||||
+ TIME + " INTEGER, "
|
||||
|
||||
@@ -310,7 +310,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
}
|
||||
|
||||
public Distance writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints, Distance trackDistance) {
|
||||
Distance cumulativeDistance = null;
|
||||
Distance cumulativeDistance;
|
||||
|
||||
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
@@ -274,7 +275,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
float heading = getHeading(marker.getTrackId(), marker.getLocation());
|
||||
writePhotoOverlay(marker, heading, zoneOffset);
|
||||
} else {
|
||||
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation(), zoneOffset);
|
||||
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation(), marker.getTime(), zoneOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +369,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
|
||||
@VisibleForTesting
|
||||
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
|
||||
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>");
|
||||
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getTime()) + "</when>");
|
||||
|
||||
trackpointTypeList.add(trackPoint.getType());
|
||||
|
||||
@@ -411,12 +412,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
printWriter.println("</SimpleArrayData>");
|
||||
}
|
||||
|
||||
private void writePlacemark(String name, String activityType, String description, Location location, ZoneOffset zoneOffset) {
|
||||
private void writePlacemark(String name, String activityType, String description, Location location, Instant time, ZoneOffset zoneOffset) {
|
||||
if (location != null) {
|
||||
printWriter.println("<Placemark>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
|
||||
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, location) + "</when></TimeStamp>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, time) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + KMLTrackExporter.MARKER_STYLE + "</styleUrl>");
|
||||
writeTypeLocalized(activityType);
|
||||
printWriter.println("<Point>");
|
||||
@@ -437,7 +438,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
printWriter.print("<heading>" + heading + "</heading>");
|
||||
printWriter.print("<tilt>90</tilt>");
|
||||
printWriter.println("</Camera>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getLocation()) + "</when></TimeStamp>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getTime()) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + MARKER_STYLE + "</styleUrl>");
|
||||
writeTypeLocalized(marker.getCategory());
|
||||
|
||||
@@ -460,14 +461,13 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
|
||||
/**
|
||||
* Returns the formatted time of the location; either absolute or relative depending exportTrackDetail.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private String getTime(ZoneOffset zoneOffset, Location location) {
|
||||
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime()), zoneOffset);
|
||||
private String getTime(ZoneOffset zoneOffset, Instant instant) {
|
||||
return StringUtils.formatDateTimeIso8601(instant, zoneOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: check if this is a useful feature (likely not).
|
||||
* Gets the heading to a location.
|
||||
*
|
||||
* @param trackId the track id containing the location
|
||||
@@ -480,7 +480,10 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
TrackPoint viewLocation = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
if (viewLocation != null) {
|
||||
return viewLocation.bearingTo(location);
|
||||
Optional<Float> bearing = viewLocation.bearingTo(location);
|
||||
if (bearing.isPresent()) {
|
||||
return bearing.get();
|
||||
}
|
||||
}
|
||||
|
||||
return location.getBearing();
|
||||
@@ -495,7 +498,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
|
||||
private void writeTypeLocalized(String localizedValue) {
|
||||
if (localizedValue == null || localizedValue.equals("")) {
|
||||
if (localizedValue == null || localizedValue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
printWriter.println("<ExtendedData>");
|
||||
|
||||
@@ -414,7 +414,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
Float value = null;
|
||||
if (content != null) {
|
||||
content = content.trim();
|
||||
if (!"".equals(content)) {
|
||||
if (!content.isEmpty()) {
|
||||
try {
|
||||
value = Float.parseFloat(content);
|
||||
} catch (NumberFormatException e) {
|
||||
|
||||
@@ -16,7 +16,6 @@ import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
@@ -26,7 +25,6 @@ import de.dennisguse.opentracks.data.models.Marker;
|
||||
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.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
@@ -158,7 +156,7 @@ public class TrackImporter {
|
||||
contentProviderUtils.bulkInsertTrackPoint(trackPoints, trackId);
|
||||
|
||||
// Store Markers
|
||||
matchMarkers2TrackPoints(trackId);
|
||||
updateMarkers(trackId);
|
||||
for (Marker marker : markers)
|
||||
marker.setTrackId(trackId); //TODO Should happen in bulkInsertMarkers
|
||||
|
||||
@@ -206,7 +204,8 @@ public class TrackImporter {
|
||||
}
|
||||
|
||||
if (!current.hasBearing()) {
|
||||
current.setBearing(previous.bearingTo(current));
|
||||
previous.bearingTo(current)
|
||||
.ifPresent(current::setBearing);
|
||||
}
|
||||
|
||||
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
|
||||
@@ -217,55 +216,16 @@ public class TrackImporter {
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Modifies content of markers (incl. removal).
|
||||
* NOTE: Modifies content of markers.
|
||||
*/
|
||||
private void matchMarkers2TrackPoints(Track.Id trackId) {
|
||||
List<TrackPoint> trackPointsWithLocation = trackPoints.stream()
|
||||
.filter(TrackPoint::hasLocation)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Marker> todoMarkers = new LinkedList<>(markers);
|
||||
List<Marker> doneMarkers = new LinkedList<>();
|
||||
|
||||
for (final TrackPoint trackPoint : trackPointsWithLocation) {
|
||||
if (todoMarkers.isEmpty()) {
|
||||
break;
|
||||
private void updateMarkers(Track.Id trackId) {
|
||||
markers.forEach(marker -> {
|
||||
if (marker.hasPhoto()) {
|
||||
marker.setPhotoUrl(getInternalPhotoUrl(trackId, marker.getPhotoUrl()));
|
||||
}
|
||||
|
||||
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
|
||||
updater.addTrackPoint(trackPoint);
|
||||
|
||||
List<Marker> matchedMarkers = todoMarkers.stream()
|
||||
.filter(it -> trackPoint.getLatitude() == it.getLatitude()
|
||||
&& trackPoint.getLongitude() == it.getLongitude()
|
||||
&& trackPoint.getTime().equals(it.getTime())
|
||||
)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
TrackStatistics statistics = updater.getTrackStatistics();
|
||||
for (Marker marker : matchedMarkers) {
|
||||
if (marker.hasPhoto()) {
|
||||
marker.setPhotoUrl(getInternalPhotoUrl(trackId, marker.getPhotoUrl()));
|
||||
}
|
||||
|
||||
marker.setIcon(context.getString(R.string.marker_icon_url)); //TODO Why?
|
||||
|
||||
marker.setLength(statistics.getTotalDistance());
|
||||
marker.setDuration(statistics.getTotalTime());
|
||||
|
||||
marker.setTrackPoint(trackPoint);
|
||||
}
|
||||
|
||||
todoMarkers.removeAll(matchedMarkers);
|
||||
doneMarkers.addAll(matchedMarkers);
|
||||
}
|
||||
|
||||
if (todoMarkers.isEmpty()) {
|
||||
Log.w(TAG, "Some markers could not be attached to TrackPoints; those are not imported.");
|
||||
}
|
||||
|
||||
markers.clear();
|
||||
markers.addAll(doneMarkers);
|
||||
marker.setIcon(context.getString(R.string.marker_icon_url)); //TODO Why?
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
/**
|
||||
* Public API to creates a Marker for a given track with a given location
|
||||
*/
|
||||
public class CreateMarkerActivity extends AppCompatActivity {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_LOCATION = "location";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Track.Id trackId = new Track.Id(getIntent().getLongExtra(EXTRA_TRACK_ID, 0L));
|
||||
Location location = getIntent().getParcelableExtra(EXTRA_LOCATION);
|
||||
|
||||
TrackRecordingServiceConnection.execute(this, (service, self) -> {
|
||||
Intent intent = IntentUtils
|
||||
.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId)
|
||||
.putExtra(MarkerEditActivity.EXTRA_LOCATION, location);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.AbstractActivity;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.databinding.MarkerDetailActivityBinding;
|
||||
import de.dennisguse.opentracks.ui.markers.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerDetailFragment;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
/**
|
||||
* Public api to show an existing marker
|
||||
*/
|
||||
public class ShowMarkerActivity extends AppCompatActivity {
|
||||
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
|
||||
private static final String TAG = ShowMarkerActivity.class.getSimpleName();
|
||||
|
||||
private MarkerDetailActivityBinding viewBinding;
|
||||
|
||||
private List<Marker.Id> markerIds;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
Marker.Id markerId = new Marker.Id(getIntent().getLongExtra(EXTRA_MARKER_ID, 0));
|
||||
if (markerId.id() == 0) {
|
||||
throw new IllegalStateException("No valid markerId provided");
|
||||
}
|
||||
Intent intent = IntentUtils.newIntent(this, MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, markerId);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,9 +20,7 @@ public class GainManager implements SensorConnector {
|
||||
|
||||
private final SensorManager.SensorDataChangedObserver listener;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
connect();
|
||||
};
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> connect();
|
||||
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
|
||||
@@ -30,7 +30,6 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
@Override
|
||||
public void onConnect(Aggregator<?, ?> aggregator) {
|
||||
sensorDataSet.add(aggregator);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -42,13 +41,11 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
@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));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,7 +64,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
throw new RuntimeException("SensorManager cannot be started twice; stop first.");
|
||||
}
|
||||
|
||||
gpsManager = new GPSManager(observer); //TODO Pass listener
|
||||
gpsManager = new GPSManager(observer, listener);
|
||||
altitudeSumManager = new GainManager(listener);
|
||||
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
|
||||
|
||||
@@ -104,7 +101,6 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
sensorDataSet.reset();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public void onChanged(Raw<?> data) {
|
||||
listener.onChange(data);
|
||||
|
||||
@@ -6,7 +6,7 @@ import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
|
||||
|
||||
public abstract class Aggregator<Input extends Record, Output> {
|
||||
public abstract class Aggregator<Input, Output> {
|
||||
|
||||
protected Raw<Input> previous;
|
||||
|
||||
@@ -44,7 +44,7 @@ public abstract class Aggregator<Input extends Record, Output> {
|
||||
|
||||
public Output getValue() {
|
||||
if (!hasValue()) {
|
||||
return null;
|
||||
return null; //TODO Check if this is a good idea!
|
||||
}
|
||||
if (isRecent()) {
|
||||
return value;
|
||||
@@ -55,7 +55,7 @@ public abstract class Aggregator<Input extends Record, Output> {
|
||||
/**
|
||||
* Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance.
|
||||
*/
|
||||
public void reset() {};
|
||||
public void reset() {}
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
|
||||
public class AggregatorGPS extends Aggregator<Position, Position> {
|
||||
|
||||
|
||||
public AggregatorGPS(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<Position> current) {
|
||||
value = current.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
value = null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Position getNoneValue() {
|
||||
return Position.empty();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record Raw<T extends Record>(
|
||||
public record Raw<T>(
|
||||
@NonNull T value,
|
||||
|
||||
@NonNull Instant time
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
@@ -40,16 +41,20 @@ public final class SensorDataSet {
|
||||
@VisibleForTesting
|
||||
public AggregatorBarometer barometer;
|
||||
|
||||
public AggregatorGPS gps;
|
||||
|
||||
public SensorDataSet() {
|
||||
}
|
||||
|
||||
public SensorDataSet(SensorDataSet toCopy) {
|
||||
//TODO This is not a copy constructor anymore, but it should be - aggregators are no value objects
|
||||
this.heartRate = toCopy.heartRate;
|
||||
this.cyclingCadence = toCopy.cyclingCadence;
|
||||
this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed;
|
||||
this.cyclingPower = toCopy.cyclingPower;
|
||||
this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence;
|
||||
this.barometer = toCopy.barometer;
|
||||
this.gps = toCopy.gps;
|
||||
}
|
||||
|
||||
public Pair<HeartRate, String> getHeartRate() {
|
||||
@@ -93,7 +98,7 @@ public final class SensorDataSet {
|
||||
}
|
||||
|
||||
public void update(@NonNull Raw<?> data) {
|
||||
Record value = data.value();
|
||||
Object value = data.value();
|
||||
|
||||
if (value instanceof HeartRate) {
|
||||
this.heartRate.add((Raw<HeartRate>) data);
|
||||
@@ -122,6 +127,10 @@ public final class SensorDataSet {
|
||||
this.barometer.add((Raw<AtmosphericPressure>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Position) {
|
||||
this.gps.add((Raw<Position>) data);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName());
|
||||
}
|
||||
@@ -138,9 +147,14 @@ public final class SensorDataSet {
|
||||
this.cyclingPower = null;
|
||||
this.runningDistanceSpeedCadence = null;
|
||||
this.barometer = null;
|
||||
this.gps = null;
|
||||
}
|
||||
|
||||
public void fillTrackPoint(TrackPoint trackPoint) {
|
||||
if (gps != null && gps.hasValue()) {
|
||||
trackPoint.setPosition(gps.getValue());
|
||||
}
|
||||
|
||||
if (getHeartRate() != null) {
|
||||
trackPoint.setHeartRate(getHeartRate().first);
|
||||
}
|
||||
@@ -180,6 +194,7 @@ public final class SensorDataSet {
|
||||
if (cyclingPower != null) cyclingPower.reset();
|
||||
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset();
|
||||
if (barometer != null) barometer.reset();
|
||||
if (gps != null) gps.reset();
|
||||
}
|
||||
|
||||
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
|
||||
@@ -209,6 +224,10 @@ public final class SensorDataSet {
|
||||
barometer = (AggregatorBarometer) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorGPS) {
|
||||
gps = (AggregatorGPS) sensorData;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
@@ -19,7 +17,6 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
@@ -138,31 +135,6 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
return new Pair<>(track, current);
|
||||
}
|
||||
|
||||
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
|
||||
if (name == null) {
|
||||
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(trackId);
|
||||
if (nextMarkerNumber == null) {
|
||||
nextMarkerNumber = 1;
|
||||
}
|
||||
name = context.getString(R.string.marker_name_format, nextMarkerNumber + 1);
|
||||
}
|
||||
|
||||
if (lastStoredTrackPointWithLocation == null) {
|
||||
Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
|
||||
return null;
|
||||
}
|
||||
|
||||
category = category != null ? category : "";
|
||||
description = description != null ? description : "";
|
||||
String icon = context.getString(R.string.marker_icon_url);
|
||||
photoUrl = photoUrl != null ? photoUrl : "";
|
||||
|
||||
// Insert marker
|
||||
Marker marker = new Marker(name, description, category, icon, trackId, getTrackStatistics(), lastStoredTrackPointWithLocation, photoUrl);
|
||||
Uri uri = contentProviderUtils.insertMarker(marker);
|
||||
return new Marker.Id(ContentUris.parseId(uri));
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public void onIdle() {
|
||||
Log.d(TAG, "Becoming idle");
|
||||
@@ -197,6 +169,9 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
if (trackPoint.hasLocation() && lastStoredTrackPointWithLocation == null) {
|
||||
insertTrackPoint(trackPoint, true);
|
||||
|
||||
handler.removeCallbacks(ON_IDLE);
|
||||
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -244,7 +219,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
|
||||
TrackStatistics getTrackStatistics() {
|
||||
return trackStatisticsUpdater.getTrackStatistics();
|
||||
return trackStatisticsUpdater == null ? null : trackStatisticsUpdater.getTrackStatistics();
|
||||
}
|
||||
|
||||
private void insertTrackPoint(@NonNull TrackPoint trackPoint, boolean storeLastTrackPointIfUseful) {
|
||||
@@ -305,6 +280,10 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
}
|
||||
|
||||
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||
return lastStoredTrackPointWithLocation;
|
||||
}
|
||||
|
||||
public interface IdleObserver {
|
||||
void onIdle();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import java.io.StringWriter;
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
@@ -57,6 +56,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
|
||||
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
|
||||
|
||||
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||
return trackRecordingManager.getLastStoredTrackPointWithLocation();
|
||||
}
|
||||
|
||||
public class Binder extends android.os.Binder {
|
||||
|
||||
private Binder() {
|
||||
@@ -205,6 +208,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
private synchronized void startSensors() {
|
||||
if (isSensorStarted()) {
|
||||
Log.i(TAG, "sensors already started; skipping");
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "startSensors");
|
||||
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
|
||||
@@ -268,14 +272,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
gpsStatusObservable.postValue(gpsStatusValue);
|
||||
}
|
||||
|
||||
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
|
||||
if (!isRecording()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trackRecordingManager.insertMarker(name, category, description, photoUrl);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public TrackPointCreator getTrackPointCreator() {
|
||||
|
||||
+47
-27
@@ -4,9 +4,11 @@ import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageHeartRate;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapPower;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceMovingTime;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceUnit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.icu.text.MessageFormat;
|
||||
@@ -22,11 +24,13 @@ import java.util.Map;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
class VoiceAnnouncementUtils {
|
||||
|
||||
@@ -44,34 +48,37 @@ class VoiceAnnouncementUtils {
|
||||
Speed averageMovingSpeed = trackStatistics.getAverageMovingSpeed();
|
||||
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
|
||||
|
||||
int perUnitStringId;
|
||||
int distanceId;
|
||||
int speedId;
|
||||
String unitDistanceTTS;
|
||||
String unitSpeedTTS;
|
||||
switch (unitSystem) {
|
||||
case METRIC -> {
|
||||
perUnitStringId = R.string.voice_per_kilometer;
|
||||
distanceId = R.string.voiceDistanceKilometersPlural;
|
||||
speedId = R.string.voiceSpeedKilometersPerHourPlural;
|
||||
unitDistanceTTS = "kilometer";
|
||||
unitSpeedTTS = "kilometer per hour";
|
||||
int perUnitStringId = R.string.empty;
|
||||
int distanceId = R.string.voiceDistance;
|
||||
int speedId = R.string.voiceSpeed;
|
||||
String unitDistanceTTS = StringUtils.EMPTY;
|
||||
String unitSpeedTTS = StringUtils.EMPTY;
|
||||
|
||||
if(shouldVoiceAnnounceUnit()) {
|
||||
switch (unitSystem) {
|
||||
case METRIC -> {
|
||||
perUnitStringId = R.string.voice_per_kilometer;
|
||||
distanceId = R.string.voiceDistanceKilometersPlural;
|
||||
speedId = R.string.voiceSpeedKilometersPerHourPlural;
|
||||
unitDistanceTTS = "kilometer";
|
||||
unitSpeedTTS = "kilometer per hour";
|
||||
}
|
||||
case IMPERIAL_FEET, IMPERIAL_METER -> {
|
||||
perUnitStringId = R.string.voice_per_mile;
|
||||
distanceId = R.string.voiceDistanceMilesPlural;
|
||||
speedId = R.string.voiceSpeedMilesPerHourPlural;
|
||||
unitDistanceTTS = "mile";
|
||||
unitSpeedTTS = "mile per hour";
|
||||
}
|
||||
case NAUTICAL_IMPERIAL -> {
|
||||
perUnitStringId = R.string.voice_per_nautical_mile;
|
||||
distanceId = R.string.voiceDistanceNauticalMilesPlural;
|
||||
speedId = R.string.voiceSpeedMKnotsPlural;
|
||||
unitDistanceTTS = "nautical mile";
|
||||
unitSpeedTTS = "knots";
|
||||
}
|
||||
default -> throw new RuntimeException("Not implemented");
|
||||
}
|
||||
case IMPERIAL_FEET, IMPERIAL_METER -> {
|
||||
perUnitStringId = R.string.voice_per_mile;
|
||||
distanceId = R.string.voiceDistanceMilesPlural;
|
||||
speedId = R.string.voiceSpeedMilesPerHourPlural;
|
||||
unitDistanceTTS = "mile";
|
||||
unitSpeedTTS = "mile per hour";
|
||||
}
|
||||
case NAUTICAL_IMPERIAL -> {
|
||||
perUnitStringId = R.string.voice_per_nautical_mile;
|
||||
distanceId = R.string.voiceDistanceNauticalMilesPlural;
|
||||
speedId = R.string.voiceSpeedMKnotsPlural;
|
||||
unitDistanceTTS = "nautical mile";
|
||||
unitSpeedTTS = "knots";
|
||||
}
|
||||
default -> throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
double distanceInUnit = totalDistance.toKM_Miles(unitSystem);
|
||||
@@ -154,6 +161,19 @@ class VoiceAnnouncementUtils {
|
||||
builder.append(".");
|
||||
}
|
||||
|
||||
if (shouldVoiceAnnounceLapPower() && currentInterval != null && currentInterval.hasAveragePower()) {
|
||||
int currentPower = Math.round(currentInterval.getAveragePower().getW());
|
||||
if(shouldVoiceAnnounceUnit()) {
|
||||
String template = context.getResources().getString(R.string.power_x_watt);
|
||||
builder.append(" ")
|
||||
.append(MessageFormat.format(template, Map.of("x", currentPower)));
|
||||
} else {
|
||||
builder.append(" ").append(context.getString(R.string.power)).append(" ");
|
||||
builder.append(String.valueOf(currentPower));
|
||||
}
|
||||
builder.append(".");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,12 @@ import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.SensorConnector;
|
||||
import de.dennisguse.opentracks.sensors.SensorManager;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
@@ -32,6 +36,8 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
|
||||
private static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
|
||||
|
||||
private TrackPointCreator trackPointCreator;
|
||||
|
||||
private SensorManager.SensorDataChangedObserver listener;
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
|
||||
@@ -40,8 +46,9 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
|
||||
private Duration gpsInterval;
|
||||
private Distance thresholdHorizontalAccuracy;
|
||||
|
||||
public GPSManager(TrackPointCreator trackPointCreator) {
|
||||
public GPSManager(TrackPointCreator trackPointCreator, SensorManager.SensorDataChangedObserver listener) {
|
||||
this.trackPointCreator = trackPointCreator;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void start(@NonNull Context context, @NonNull Handler handler) {
|
||||
@@ -52,6 +59,9 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
|
||||
|
||||
gpsStatusManager = new GpsStatusManager(context, this, handler);
|
||||
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
|
||||
listener.onConnect(new AggregatorGPS("internal"));
|
||||
|
||||
registerLocationListener();
|
||||
gpsStatusManager.start();
|
||||
}
|
||||
@@ -72,6 +82,9 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
|
||||
gpsStatusManager.stop();
|
||||
gpsStatusManager = null;
|
||||
|
||||
listener.onDisconnect(new AggregatorGPS("internal"));
|
||||
listener = null;
|
||||
|
||||
trackPointCreator = null;
|
||||
}
|
||||
|
||||
@@ -126,7 +139,7 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
|
||||
return;
|
||||
}
|
||||
|
||||
trackPointCreator.onChange(location);
|
||||
listener.onChange(new Raw<>(Position.of(location)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.services.handlers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
@@ -69,10 +68,6 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
public synchronized void onChange(@NonNull Location location) {
|
||||
onNewTrackPoint(new TrackPoint(location, createNow()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Got a new TrackPoint from Bluetooth only; contains no GPS location.
|
||||
*/
|
||||
|
||||
@@ -229,7 +229,7 @@ public class PreferencesUtils {
|
||||
|
||||
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||
public static void applyDefaultUnit() {
|
||||
if (getString(R.string.stats_units_key, "").equals("")) {
|
||||
if (getString(R.string.stats_units_key, "").isEmpty()) {
|
||||
if (!Locale.US.equals(Locale.getDefault())) {
|
||||
setUnit(UnitSystem.METRIC);
|
||||
} else {
|
||||
@@ -333,6 +333,10 @@ public class PreferencesUtils {
|
||||
return getBoolean(R.string.voice_announce_idle_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceUnit() {
|
||||
return getBoolean(R.string.voice_announce_unit_key, true);
|
||||
}
|
||||
|
||||
public static Duration getVoiceAnnouncementFrequency() {
|
||||
final int DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_frequency_default));
|
||||
int value = getInt(R.string.voice_announcement_frequency_key, DEFAULT);
|
||||
@@ -430,6 +434,10 @@ public class PreferencesUtils {
|
||||
return getBoolean(R.string.voice_announce_lap_heart_rate_key, false);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceLapPower() {
|
||||
return getBoolean(R.string.voice_announce_lap_power_key, false);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setVoiceAnnounceLapHeartRate(boolean value) {
|
||||
setBoolean(R.string.voice_announce_lap_heart_rate_key, value);
|
||||
|
||||
@@ -42,8 +42,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
fragment = new MainSettingsFragment();
|
||||
}
|
||||
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
setSupportActionBar(viewBinding.topAppBar);
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
|
||||
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, fragment).commit();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.time.Instant;
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
@@ -60,6 +61,7 @@ public class TrackStatistics {
|
||||
private Float totalAltitudeLoss_m = null;
|
||||
// The average heart rate seen on this track
|
||||
private HeartRate avgHeartRate = null;
|
||||
private Power avgPower = null;
|
||||
|
||||
private boolean isIdle;
|
||||
|
||||
@@ -83,6 +85,7 @@ public class TrackStatistics {
|
||||
totalAltitudeGain_m = other.totalAltitudeGain_m;
|
||||
totalAltitudeLoss_m = other.totalAltitudeLoss_m;
|
||||
avgHeartRate = other.avgHeartRate;
|
||||
avgPower = other.avgPower;
|
||||
isIdle = other.isIdle;
|
||||
}
|
||||
|
||||
@@ -104,6 +107,7 @@ public class TrackStatistics {
|
||||
*
|
||||
* @param other another statistics data object
|
||||
*/
|
||||
//TODO Should be refactored to append only [mainly due to isIdle] (NOTE: This requires to use a custom value object for AggregatedStatistics; this is anyhow recommended).
|
||||
public void merge(TrackStatistics other) {
|
||||
if (startTime == null) {
|
||||
startTime = other.startTime;
|
||||
@@ -116,6 +120,8 @@ public class TrackStatistics {
|
||||
stopTime = stopTime.isAfter(other.stopTime) ? stopTime : other.stopTime;
|
||||
}
|
||||
|
||||
isIdle = other.isIdle; //TODO This implicitly assumes append mode.
|
||||
|
||||
if (avgHeartRate == null) {
|
||||
avgHeartRate = other.avgHeartRate;
|
||||
} else {
|
||||
@@ -129,6 +135,19 @@ public class TrackStatistics {
|
||||
}
|
||||
}
|
||||
|
||||
if (avgPower == null) {
|
||||
avgPower = other.avgPower;
|
||||
} else {
|
||||
if (other.avgPower != null) {
|
||||
// Using total time as weights for the averaging.
|
||||
// Important to do this before total time is updated
|
||||
avgPower = Power.of(
|
||||
(totalTime.getSeconds() * avgPower.getW() + other.totalTime.getSeconds() * other.avgPower.getW())
|
||||
/ (totalTime.getSeconds() + other.totalTime.getSeconds())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
totalDistance = totalDistance.plus(other.totalDistance);
|
||||
totalTime = totalTime.plus(other.totalTime);
|
||||
movingTime = movingTime.plus(other.movingTime);
|
||||
@@ -270,6 +289,15 @@ public class TrackStatistics {
|
||||
return avgHeartRate;
|
||||
}
|
||||
|
||||
public boolean hasPower() {
|
||||
return avgPower != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Power getAveragePower() {
|
||||
return avgPower;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average speed.
|
||||
* This calculation only takes into account the displacement until the last point that was accounted for in statistics.
|
||||
@@ -333,6 +361,12 @@ public class TrackStatistics {
|
||||
}
|
||||
}
|
||||
|
||||
public void setAveragePower(Power power) {
|
||||
if (power != null) {
|
||||
avgPower = power;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasTotalAltitudeGain() {
|
||||
return totalAltitudeGain_m != null;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Updater for {@link TrackStatistics}.
|
||||
@@ -43,6 +45,8 @@ public class TrackStatisticsUpdater {
|
||||
|
||||
private float averageHeartRateBPM;
|
||||
private Duration totalHeartRateDuration = Duration.ZERO;
|
||||
private float averagePowerW;
|
||||
private Duration totalPowerDuration = Duration.ZERO;
|
||||
|
||||
// The current segment's statistics
|
||||
private final TrackStatistics currentSegment;
|
||||
@@ -53,11 +57,6 @@ public class TrackStatisticsUpdater {
|
||||
this(new TrackStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new{@link TrackStatisticsUpdater} with a {@link TrackStatisticsUpdater} already existed.
|
||||
*
|
||||
* @param trackStatistics a {@link TrackStatisticsUpdater}
|
||||
*/
|
||||
public TrackStatisticsUpdater(TrackStatistics trackStatistics) {
|
||||
this.trackStatistics = trackStatistics;
|
||||
this.currentSegment = new TrackStatistics();
|
||||
@@ -84,9 +83,6 @@ public class TrackStatisticsUpdater {
|
||||
trackPoints.stream().forEachOrdered(this::addTrackPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public void addTrackPoint(TrackPoint trackPoint) {
|
||||
if (trackPoint.isSegmentManualStart()) {
|
||||
reset(trackPoint);
|
||||
@@ -125,6 +121,17 @@ public class TrackStatisticsUpdater {
|
||||
currentSegment.setAverageHeartRate(HeartRate.of(averageHeartRateBPM));
|
||||
}
|
||||
|
||||
// Update power
|
||||
if (trackPoint.hasPower() && lastTrackPoint != null) {
|
||||
Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||
Duration newTotalDuration = totalPowerDuration.plus(trackPointDuration);
|
||||
|
||||
averagePowerW = (totalPowerDuration.toMillis() * averagePowerW + trackPointDuration.toMillis() * trackPoint.getPower().getW()) / newTotalDuration.toMillis();
|
||||
totalPowerDuration = newTotalDuration;
|
||||
|
||||
currentSegment.setAveragePower(Power.of(averagePowerW));
|
||||
}
|
||||
|
||||
{
|
||||
// Update total distance
|
||||
Distance movingDistance = null;
|
||||
@@ -137,18 +144,23 @@ public class TrackStatisticsUpdater {
|
||||
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
|
||||
}
|
||||
if (movingDistance != null) {
|
||||
currentSegment.setIdle(false);
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
}
|
||||
|
||||
if (!currentSegment.isIdle() && !trackPoint.isSegmentManualStart()) {
|
||||
if (lastTrackPoint != null) {
|
||||
if (!currentSegment.isIdle()) {
|
||||
if (!trackPoint.isSegmentManualStart() && lastTrackPoint != null) {
|
||||
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint.getType() == TrackPoint.Type.IDLE) {
|
||||
if (trackPoint.isIdleTriggered()) {
|
||||
currentSegment.setIdle(true);
|
||||
} else if (currentSegment.isIdle()) {
|
||||
// Shall we switch to non-idle?
|
||||
if (movingDistance != null
|
||||
&& movingDistance.greaterOrEqualThan(PreferencesUtils.getRecordingDistanceInterval())) {
|
||||
currentSegment.setIdle(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint.hasSpeed()) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.List;
|
||||
import de.dennisguse.opentracks.data.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
@@ -103,6 +104,7 @@ public class IntervalStatistics {
|
||||
private Float gain_m;
|
||||
private Float loss_m;
|
||||
private HeartRate avgHeartRate;
|
||||
private Power avgPower;
|
||||
|
||||
public Interval() {
|
||||
}
|
||||
@@ -118,6 +120,7 @@ public class IntervalStatistics {
|
||||
gain_m = i.gain_m;
|
||||
loss_m = i.loss_m;
|
||||
avgHeartRate = i.avgHeartRate;
|
||||
avgPower = i.avgPower;
|
||||
}
|
||||
|
||||
public Interval(Interval i) {
|
||||
@@ -126,6 +129,7 @@ public class IntervalStatistics {
|
||||
gain_m = i.gain_m;
|
||||
loss_m = i.loss_m;
|
||||
avgHeartRate = i.avgHeartRate;
|
||||
avgPower = i.avgPower;
|
||||
}
|
||||
|
||||
public Distance getDistance() {
|
||||
@@ -160,12 +164,21 @@ public class IntervalStatistics {
|
||||
return avgHeartRate;
|
||||
}
|
||||
|
||||
public boolean hasAveragePower() {
|
||||
return avgPower != null;
|
||||
}
|
||||
|
||||
public Power getAveragePower() {
|
||||
return avgPower;
|
||||
}
|
||||
|
||||
private void add(TrackStatistics trackStatistics, @Nullable TrackPoint lastTrackPoint) {
|
||||
distance = distance.plus(trackStatistics.getTotalDistance());
|
||||
time = time.plus(trackStatistics.getTotalTime());
|
||||
gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m;
|
||||
loss_m = trackStatistics.hasTotalAltitudeLoss() ? trackStatistics.getTotalAltitudeLoss() : loss_m;
|
||||
avgHeartRate = trackStatistics.getAverageHeartRate();
|
||||
avgPower = trackStatistics.getAveragePower();
|
||||
if (lastTrackPoint == null) {
|
||||
return;
|
||||
}
|
||||
@@ -183,6 +196,7 @@ public class IntervalStatistics {
|
||||
gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m;
|
||||
loss_m = trackStatistics.hasTotalAltitudeLoss() ? trackStatistics.getTotalAltitudeLoss() : loss_m;
|
||||
avgHeartRate = trackStatistics.getAverageHeartRate();
|
||||
avgPower = trackStatistics.getAveragePower();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +59,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
Object bundleMarkerId = getIntent().getExtras().get(EXTRA_MARKER_ID);
|
||||
Marker.Id markerId = null;
|
||||
if (bundleMarkerId instanceof Marker.Id cast) {
|
||||
markerId = cast;
|
||||
}
|
||||
if (bundleMarkerId instanceof Long cast) {
|
||||
//Incoming Intent via Dashboard API.
|
||||
markerId = new Marker.Id(cast);
|
||||
}
|
||||
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if (markerId == null) {
|
||||
Log.d(TAG, "invalid marker id");
|
||||
finish();
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
@@ -36,19 +37,19 @@ import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.PickVisualMediaRequest;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.AbstractActivity;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.MarkerEditBinding;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
|
||||
/**
|
||||
* An activity to add/edit a marker.
|
||||
@@ -59,13 +60,14 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
public static final String EXTRA_LOCATION = "location";
|
||||
|
||||
private static final String CAMERA_PHOTO_URI_KEY = "camera_photo_uri_key";
|
||||
|
||||
private static final String NEW_MARKER_ID = "new_marker_id";
|
||||
|
||||
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
||||
private Track.Id trackId;
|
||||
private Location location;
|
||||
private Marker.Id markerId;
|
||||
private Marker marker;
|
||||
|
||||
private MenuItem insertPhotoMenuItem;
|
||||
@@ -93,16 +95,16 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
@Nullable Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
final boolean isNewMarker = markerId == null;
|
||||
location = getIntent().getParcelableExtra(EXTRA_LOCATION);
|
||||
markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if ((trackId == null || location == null) && markerId == null) {
|
||||
throw new IllegalStateException("TrackId and Location must be provided or an existing markerId");
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
cameraPhotoUri = Uri.parse(savedInstanceState.getString(CAMERA_PHOTO_URI_KEY, ""));
|
||||
Marker.Id newMarkerId = savedInstanceState.getParcelable(NEW_MARKER_ID);
|
||||
if (newMarkerId != null) {
|
||||
markerId = newMarkerId;
|
||||
}
|
||||
}
|
||||
boolean isNewMarker = markerId == null;
|
||||
|
||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
|
||||
@@ -157,16 +159,8 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (markerId == null) {
|
||||
TrackRecordingServiceConnection.execute(this, (service, self) -> {
|
||||
Marker.Id newMarkerId = createNewMarker(service);
|
||||
if (newMarkerId == null) {
|
||||
finish();
|
||||
} else {
|
||||
loadMarkerData(newMarkerId);
|
||||
}
|
||||
});
|
||||
if (isNewMarker) {
|
||||
createNewMarker().observe(this, this::loadMarkerData);
|
||||
} else {
|
||||
loadMarkerData(markerId);
|
||||
}
|
||||
@@ -174,35 +168,27 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
}
|
||||
|
||||
private Marker.Id createNewMarker(TrackRecordingService trackRecordingService) {
|
||||
try {
|
||||
Marker.Id marker = trackRecordingService.insertMarker("", "", "", null);
|
||||
if (marker == null) {
|
||||
Toast.makeText(this, R.string.marker_add_error, Toast.LENGTH_LONG).show();
|
||||
return null;
|
||||
}
|
||||
|
||||
return marker;
|
||||
} catch (IllegalStateException e) {
|
||||
Log.e(TAG, "Unable to add marker.", e);
|
||||
return null;
|
||||
}
|
||||
private LiveData<Marker> createNewMarker() {
|
||||
TrackPoint trackPoint = new TrackPoint(location, Instant.now());
|
||||
return viewModel.createNewMarker(trackId, trackPoint);
|
||||
}
|
||||
|
||||
private void loadMarkerData(Marker.Id markerId) {
|
||||
viewModel.getMarkerData(markerId).observe(this, data -> {
|
||||
marker = data;
|
||||
viewBinding.markerEditName.setText(marker.getName());
|
||||
viewBinding.markerEditMarkerType.setText(marker.getCategory());
|
||||
viewBinding.markerEditDescription.setText(marker.getDescription());
|
||||
if (marker.hasPhoto()) {
|
||||
setMarkerImageView(marker.getPhotoURI());
|
||||
} else {
|
||||
viewBinding.markerEditPhoto.setImageDrawable(null);
|
||||
}
|
||||
viewModel.getMarkerData(markerId).observe(this, this::loadMarkerData);
|
||||
}
|
||||
|
||||
hideAndShowOptions();
|
||||
});
|
||||
private void loadMarkerData(Marker data) {
|
||||
marker = data;
|
||||
viewBinding.markerEditName.setText(marker.getName());
|
||||
viewBinding.markerEditMarkerType.setText(marker.getCategory());
|
||||
viewBinding.markerEditDescription.setText(marker.getDescription());
|
||||
if (marker.hasPhoto()) {
|
||||
setMarkerImageView(marker.getPhotoURI());
|
||||
} else {
|
||||
viewBinding.markerEditPhoto.setImageDrawable(null);
|
||||
}
|
||||
|
||||
hideAndShowOptions();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -210,6 +196,8 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
super.onDestroy();
|
||||
|
||||
trackId = null;
|
||||
location = null;
|
||||
markerId = null;
|
||||
viewBinding = null;
|
||||
viewModel = null;
|
||||
takePictureFromGallery = null;
|
||||
@@ -222,8 +210,6 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
if (cameraPhotoUri != null) {
|
||||
outState.putString(CAMERA_PHOTO_URI_KEY, cameraPhotoUri.toString());
|
||||
}
|
||||
|
||||
outState.putParcelable(NEW_MARKER_ID, marker.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.util.NoSuchElementException;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class MarkerEditViewModel extends AndroidViewModel {
|
||||
@@ -122,22 +124,40 @@ public class MarkerEditViewModel extends AndroidViewModel {
|
||||
marker.setName(name);
|
||||
marker.setCategory(category);
|
||||
marker.setDescription(description);
|
||||
new ContentProviderUtils(getApplication()).updateMarker(getApplication(), marker);
|
||||
if (marker.getId() == null) {
|
||||
new ContentProviderUtils(getApplication()).insertMarker(marker);
|
||||
} else {
|
||||
new ContentProviderUtils(getApplication()).updateMarker(getApplication(), marker);
|
||||
}
|
||||
|
||||
if (photoOriginalUri != null && (!marker.hasPhoto() || !photoOriginalUri.equals(marker.getPhotoURI()))) {
|
||||
deletePhoto(photoOriginalUri);
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<Marker> createNewMarker(Track.Id trackId, TrackPoint trackPoint) {
|
||||
Integer nextMarkerNumber = new ContentProviderUtils(getApplication()).getNextMarkerNumber(trackId);
|
||||
if (nextMarkerNumber == null) {
|
||||
nextMarkerNumber = 1;
|
||||
}
|
||||
String name = getApplication().getString(R.string.marker_name_format, nextMarkerNumber + 1);
|
||||
String icon = getApplication().getString(R.string.marker_icon_url);
|
||||
|
||||
Marker marker = new Marker(name, "", "", icon, trackId, trackPoint, "");
|
||||
|
||||
if (markerData == null) {
|
||||
markerData = new MutableLiveData<>();
|
||||
}
|
||||
markerData.postValue(marker);
|
||||
return markerData;
|
||||
}
|
||||
|
||||
public void onCancel(boolean isNewMarker) {
|
||||
Marker marker = getMarker();
|
||||
if (isNewMarker) {
|
||||
// it's new marker -> clean all photos.
|
||||
deletePhoto(marker);
|
||||
deletePhoto(photoOriginalUri);
|
||||
|
||||
new ContentProviderUtils(getApplication()).deleteMarker(getApplication(), marker.getId());
|
||||
|
||||
} else if (photoOriginalUri == null || (marker.hasPhoto() && !marker.getPhotoURI().equals(photoOriginalUri))) {
|
||||
// it's an edit marker -> delete photo if it was empty or it was changed (leaving the original in that case).
|
||||
deletePhoto(marker);
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package de.dennisguse.opentracks.ui.markers;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
@@ -24,7 +23,7 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import java.util.List;
|
||||
@@ -48,7 +47,7 @@ import de.dennisguse.opentracks.util.IntentUtils;
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MarkerListActivity extends AbstractActivity {
|
||||
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerDialogFragment.DeleteMarkerCaller {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
|
||||
@@ -94,7 +93,6 @@ public class MarkerListActivity extends AbstractActivity {
|
||||
}
|
||||
};
|
||||
private MenuItem insertMarkerMenuItem;
|
||||
private MenuItem searchMenuItem;
|
||||
|
||||
private String searchQuery;
|
||||
|
||||
@@ -116,9 +114,7 @@ public class MarkerListActivity extends AbstractActivity {
|
||||
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
setSupportActionBar(viewBinding.markerListToolbar);
|
||||
viewBinding.bottomAppBarLayout.bottomAppBar.setNavigationOnClickListener(item -> {
|
||||
finish();
|
||||
});
|
||||
viewBinding.bottomAppBarLayout.bottomAppBar.setNavigationOnClickListener(item -> finish());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -152,6 +148,14 @@ public class MarkerListActivity extends AbstractActivity {
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = MarkerListBinding.inflate(getLayoutInflater());
|
||||
|
||||
viewBinding.markerListSearchView.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
searchQuery = viewBinding.markerListSearchView.getEditText().getText().toString();
|
||||
viewBinding.markerListSearchView.hide();
|
||||
loadData();
|
||||
return true;
|
||||
});
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@@ -161,9 +165,6 @@ public class MarkerListActivity extends AbstractActivity {
|
||||
|
||||
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.marker_list_search);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -235,34 +236,10 @@ public class MarkerListActivity extends AbstractActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
|
||||
if (searchQuery != null) {
|
||||
searchQuery = null;
|
||||
loadData();
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
searchQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||
} else {
|
||||
searchQuery = null;
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void loadData() {
|
||||
viewBinding.markerListToolbar.setText(searchQuery);
|
||||
|
||||
viewBinding.markerListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.menu_markers)));
|
||||
|
||||
List<Marker> markers = contentProviderUtils.searchMarkers(trackId, searchQuery);
|
||||
@@ -281,4 +258,9 @@ public class MarkerListActivity extends AbstractActivity {
|
||||
private void onRecordingStatusChanged(RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMarkerDeleted() {
|
||||
runOnUiThread(this::loadData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,13 @@ package de.dennisguse.opentracks.ui.util;
|
||||
|
||||
import static android.content.Context.VIBRATOR_SERVICE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@@ -21,19 +16,6 @@ public class ActivityUtils {
|
||||
|
||||
private static final String TAG = ActivityUtils.class.getSimpleName();
|
||||
|
||||
public static SearchView configureSearchWidget(Activity activity, final MenuItem menuItem) {
|
||||
final SearchView searchView = (SearchView) menuItem.getActionView();
|
||||
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
//NOTE: Use searchManager.getSearchableInfo(new ComponentName(activity, SearchActivity.class)) if another activity should handle the search
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(activity.getComponentName()));
|
||||
} else {
|
||||
Log.w(TAG, "Could not retrieve SearchManager.");
|
||||
}
|
||||
searchView.setSubmitButtonEnabled(true);
|
||||
return searchView;
|
||||
}
|
||||
|
||||
public static void vibrate(@NonNull Context context, Duration duration) {
|
||||
final Vibrator vibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
||||
@@ -57,6 +57,7 @@ import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
public class StringUtils {
|
||||
|
||||
private static final String TAG = StringUtils.class.getSimpleName();
|
||||
public static final String EMPTY = "";
|
||||
|
||||
private StringUtils() {
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="5.8208"
|
||||
android:viewportHeight="5.8208">
|
||||
<path
|
||||
|
||||
@@ -22,14 +22,24 @@ limitations under the License.
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
<com.google.android.material.search.SearchBar
|
||||
android:id="@+id/marker_list_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_marker_show_24dp"
|
||||
app:title="@string/menu_markers" />
|
||||
app:layout_behavior="@string/searchbar_scrolling_view_behavior"
|
||||
app:menu="@menu/marker_list" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.search.SearchView
|
||||
android:id="@+id/marker_list_search_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:hint="@string/menu_search"
|
||||
app:layout_anchor="@id/marker_list_toolbar">
|
||||
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
|
||||
</com.google.android.material.search.SearchView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -30,7 +30,7 @@ limitations under the License.
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
style="?attr/textInputOutlinedStyle"
|
||||
style="?attr/textInputOutlinedExposedDropdownMenuStyle"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_marginEnd="8dp">
|
||||
|
||||
@@ -22,15 +22,24 @@ limitations under the License.
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
<com.google.android.material.search.SearchBar
|
||||
android:id="@+id/track_list_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_logo_color_24dp"
|
||||
app:title="@string/app_name"
|
||||
app:layout_behavior="@string/searchbar_scrolling_view_behavior"
|
||||
app:menu="@menu/track_list" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.search.SearchView
|
||||
android:id="@+id/track_list_search_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:hint="@string/menu_search"
|
||||
app:layout_anchor="@id/track_list_toolbar">
|
||||
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
|
||||
</com.google.android.material.search.SearchView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -17,12 +17,7 @@ limitations under the License.
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/marker_list_insert_marker"
|
||||
android:icon="@drawable/ic_marker_show_24dp"
|
||||
android:icon="@drawable/ic_marker_add_24dp"
|
||||
android:title="@string/menu_insert_marker"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/marker_list_search"
|
||||
android:title="@string/menu_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -15,12 +15,6 @@ limitations under the License.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<!-- TODO SearchAction is only working if it is shown as action. However, only two slots are available. See #757. -->
|
||||
<item
|
||||
android:id="@+id/track_list_search"
|
||||
android:title="@string/menu_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/track_list_markers"
|
||||
android:icon="@drawable/ic_marker_show_24dp"
|
||||
@@ -29,7 +23,8 @@ limitations under the License.
|
||||
<item
|
||||
android:id="@+id/track_list_settings"
|
||||
android:icon="@drawable/ic_settings_24dp"
|
||||
android:title="@string/menu_settings" />
|
||||
android:title="@string/menu_settings"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/track_list_help"
|
||||
android:icon="@drawable/ic_help_24dp"
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="activity_type_airplane">aeroplane</string>
|
||||
<string name="activity_type_atv">ATV</string>
|
||||
<string name="activity_type_biking">biking</string>
|
||||
<string name="activity_type_atv">ATV 4-Wheeler</string>
|
||||
<string name="activity_type_biking">A bike ride</string>
|
||||
<string name="activity_type_blimp">airship</string>
|
||||
<string name="activity_type_boat">boat</string>
|
||||
<string name="activity_type_boat">Boating</string>
|
||||
<string name="activity_type_climbing">climbing</string>
|
||||
<string name="activity_type_commercial_airplane">commercial aeroplane</string>
|
||||
<string name="activity_type_commercial_airplane">commercial flight</string>
|
||||
<string name="activity_type_cross_country_skiing">Cross-country skiing</string>
|
||||
<string name="activity_type_cycling">cycling</string>
|
||||
<string name="activity_type_dirt_bike">dirt bike</string>
|
||||
<string name="activity_type_donkey_back_riding">donkey riding</string>
|
||||
<string name="activity_type_donkey_back_riding">Donkey ride</string>
|
||||
<string name="activity_type_driving">driving</string>
|
||||
<string name="activity_type_driving_bus">driving bus</string>
|
||||
<string name="activity_type_driving_car">driving car</string>
|
||||
<string name="activity_type_driving_car">Car driving</string>
|
||||
<string name="activity_type_ferry">ferry</string>
|
||||
<string name="activity_type_frisbee">frisbee</string>
|
||||
<string name="activity_type_gliding">gliding</string>
|
||||
|
||||
@@ -37,12 +37,12 @@ limitations under the License.
|
||||
<string name="help_storage_pictures_title">Jak ukládá OpenTracks obrázky pořízené jako značky na cestě\?</string>
|
||||
<string name="help_storage_pictures_content">Jsou uloženy v interním úložišti aplikace (tj. přístupné pouze v rámci OpenTracks).</string>
|
||||
<string name="help_track_export_title">Jak mohu exportovat/sdílet zaznamenané stopy\?</string>
|
||||
<string name="help_track_export_content"> \u0020Trasy lze exportovat z Nastavení jako KMZ, KML nebo GPX. \u0020KMZ a KML obsahuje všechna data, GPX pouze polohu. \u0020Trasy sdílené pomocí <i>ikony sdílení</i> jsou sdíleny jako KMZ. \u0020</string>
|
||||
<string name="help_track_export_content"> Trasy lze exportovat z Nastavení jako KMZ, KML nebo GPX. KMZ a KML obsahuje všechna data, GPX pouze polohu. Trasy sdílené pomocí <i>ikony sdílení</i> jsou sdíleny jako KMZ. </string>
|
||||
<string name="help_data_google_play_services_title">Vyžaduje/používá OpenTracks službu Google Play\?</string>
|
||||
<string name="help_recording_title">Jak OpenTracks zaznamenává data?</string>
|
||||
<string name="help_recording_content">OpenTracks používá GPS senzor zařízení k nepřetržitému zaznamenávání polohy. Pokud zařízení GPS hlásí nepřesná data (např. polohu, rychlost, nadmořskou výšku), OpenTracks s tím nemůže nic udělat.</string>
|
||||
<string name="help_data_storage_title">Jak OpenTracks ukládá data ze senzorů\?</string>
|
||||
<string name="help_data_storage_content"> \u0020Data senzoru se ukládají na zaznamenané místo. \u0020Pokud jde o snímač srdeční frekvence, znamená to, že na každé místo je uloženo pouze <i> jedno </i> měření srdeční frekvence. \u0020Pokud stojíte v klidu nebo jste uvnitř, nebudou zaznamenána žádná data ze senzorů (ale budou zobrazena). \u0020</string>
|
||||
<string name="help_data_storage_content"> Data senzorů se ukládají na zaznamenané místo. Pokud jde o snímač srdeční frekvence, znamená to, že na každé místo je uloženo pouze <i>jedno</i> měření srdeční frekvence. Pokud stojíte v klidu nebo jste uvnitř, nebudou zaznamenána žádná data ze senzorů (ale budou zobrazena). </string>
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks nezaznamenává místa?</string>
|
||||
<string name="help_gps_troubleshooting_content"> \u0020Co můžete udělat:
|
||||
\n \u0020a) zkontrolujte zda je zapnuta GPS,
|
||||
@@ -419,14 +419,14 @@ limitations under the License.
|
||||
<item quantity="other">Ukončeno s %1$d chybami</item>
|
||||
</plurals>
|
||||
<string name="import_progress_summary_errors_msg">Chyba</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Zabraňte opětovnému importu aktivit</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Zabránit opětov. importu stop</string>
|
||||
<string name="export_progress_summary_errors_msg">Chyba</string>
|
||||
<string name="export_progress_summary_skip_msg">Soubory přeskočeny</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Soubory přepsány</string>
|
||||
<string name="export_progress_summary_new_msg">Nové soubory exportovány</string>
|
||||
<string name="import_progress_summary_exists_msg">Existovat</string>
|
||||
<string name="import_progress_summary_ok_msg">Importované soubory</string>
|
||||
<string name="generic_open_track">Otevřená trať</string>
|
||||
<string name="generic_open_track">Otevřít stopu</string>
|
||||
<string name="sensor_state_power_avg">Prům. výkon</string>
|
||||
<string name="sensor_state_heart_rate_max">Maximální srdeční frekvence</string>
|
||||
<string name="sensor_state_heart_rate_avg">Avg srdeční frekvence</string>
|
||||
@@ -601,4 +601,12 @@ limitations under the License.
|
||||
<string name="settings_recording_location_distance_summary">%1$s mezi zaznamenanými polohami</string>
|
||||
<string name="settings_theme_switch_restart">Změna motivu může vyžadovat ruční restart.</string>
|
||||
<string name="settings_night_mode_option_night_oled">Noční (OLED)</string>
|
||||
<string name="settings_announcements_unit">Jednotka vzdálenosti/rychlosti</string>
|
||||
<string name="voiceSpeed">{n, plural, =1 {1} few {{n,number}} other {{n,number,#.0}} }</string>
|
||||
<string name="voiceDistance">{n, plural, =1 {1 } few {{n,number}} other {{n,number,#.0}} }</string>
|
||||
<string name="voice_announce_lap_power_key">voiceAnnounceLapPower</string>
|
||||
<string name="power">Výkon</string>
|
||||
<string name="settings_announcements_lap_power">Výkon kola</string>
|
||||
<string name="power_x_watt">Výkon {x, plural, =1 {1 watt} few {# watty} other {# wattů} }</string>
|
||||
<string name="create_marker_error">Nepodařilo se vytvořit značku. Zkuste to znovu.</string>
|
||||
</resources>
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
--><resources>
|
||||
<string name="about_preference_title">Om OpenTracks</string>
|
||||
<string name="about_description"><i> OpenTracks </i> er en sportstracker, der fuldt ud respekterer dit privatliv.</string>
|
||||
<string name="about_url">Projekthjemmeside: <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_url">Projektets hjemmeside: <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_version_name">Versionsnavn: %1$s</string>
|
||||
<string name="about_version_code">Versionskode: %1$d</string>
|
||||
<string name="about_history">OpenTracks er en forgrening fra <a href="https://code.google.com/archive/p/mytracks/"> <i> Googles MyTracks </i> </a> (afviklet i 2016).</string>
|
||||
|
||||
@@ -23,13 +23,12 @@ limitations under the License.
|
||||
<string name="about_libraries">Bibliotheken</string>
|
||||
<!-- Help -->
|
||||
<string name="help_feature_title">OpenTracks hat die folgenden Funktionen:</string>
|
||||
<string name="help_feature_content">
|
||||
* GPS-Tracks erstellen\n
|
||||
* einen Herzfrequenzsensor verwenden (nur Bluetooth LE)\n
|
||||
* Sprachansagen (nach Zeit oder Entfernung)\n
|
||||
* Markierungen erstellen (inkl. Bilder)\n
|
||||
* arbeitet vollständig offline\n
|
||||
* Tracks auf einer Karte anzeigen (siehe unten)</string>
|
||||
<string name="help_feature_content">* GPS-Tracks erstellen
|
||||
\n* einen Herzfrequenzsensor verwenden (nur Bluetooth LE)
|
||||
\n* Sprachansagen (nach Zeit oder Entfernung)
|
||||
\n* Markierungen erstellen (inkl. Bilder)
|
||||
\n* arbeitet vollständig offline
|
||||
\n* Tracks auf einer Karte anzeigen (siehe unten)</string>
|
||||
<string name="help_track_map_title">Wie wird ein Track auf einer Karte angezeigt?</string>
|
||||
<string name="help_track_map_content"> Verwende das kleine Kartensymbol.
|
||||
\n a) (Live-Aktualisierungen) Installiere <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. OSMDashboard stellt Online- oder Offline-Karten über <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
||||
@@ -243,7 +242,7 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
||||
<string name="settings_recording_keep_screen_on_while_recording_title">Bildschirm eingeschaltet lassen</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Lasse während der Aufnahme den Bildschirm eingeschaltet.</string>
|
||||
<string name="settings_recording_default_activity_title">Standardaktivität</string>
|
||||
<string name="settings_recording_location_frequency_summary">Entfernungs-/Zeitintervall %1$s zwischen zwei Standortaufzeichnungen</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s zwischen Standort Überprüfungen</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Ein neues Segment wird erstellt, wenn die Entfernung zwischen zwei Standorten mehr als %1$s beträgt</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Max. Entfernung</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Aufzeichnungsintervall</string>
|
||||
@@ -490,7 +489,7 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
||||
<string name="menu_select_layout">Wähle Layout</string>
|
||||
<string name="generic_choose_an_option">Wähle eine Option</string>
|
||||
<string name="settings_gps_sensor">GPS</string>
|
||||
<string name="settings_import_export_title">Import/Export</string>
|
||||
<string name="settings_import_export_title">Import und Export</string>
|
||||
<string name="settings_sensors_title">Sensoren</string>
|
||||
<string name="settings_sensors_summary">Fahrradsensoren, Radumfang und Lauf-Sensoren</string>
|
||||
<string name="settings_announcements_statistics_title">Sprachansagen</string>
|
||||
@@ -532,7 +531,7 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
||||
<string name="export_dir_not_writable">" (nicht schreibbar)"</string>
|
||||
<string name="stats_clock">Uhr</string>
|
||||
<string name="image_discard">Verwerfen</string>
|
||||
<string name="settings_announcements_distance_frequency">Entfernungsintervall</string>
|
||||
<string name="settings_announcements_distance_frequency">Runden Länge</string>
|
||||
<string name="settings_public_api_enabled_title">Öffentliche API</string>
|
||||
<string name="track_discarding">Verwerfe Track</string>
|
||||
<string name="average_heart_rate">Durchschnittlicher Puls</string>
|
||||
@@ -618,4 +617,33 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
||||
<string name="settings_recording_min_required_accuracy_summary">Ort wird ignoriert wenn seine Genauigkeit über %1$s liegt</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_title">Bluetooth Geräte filtern</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_off">Alle verfügbaren Bluetooth Geräte anzeigen</string>
|
||||
<string name="settings_theme_switch_restart">Themenänderungen können einen manuellen Neustart erfordern.</string>
|
||||
<string name="settings_recording_idle_timeout_title">Stillstand Schwelle</string>
|
||||
<string name="settings_night_mode_option_night_oled">Nacht (OLED freundlich)</string>
|
||||
<string name="sensor_state_pressure">Barometer</string>
|
||||
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
|
||||
<string name="settings_announcements_idle_category_title">Stillstand Benachrichtigung</string>
|
||||
<string name="settings_announcements_idle_title">Stillstand</string>
|
||||
<string name="settings_ui_dynamic_colors_title">Dynamische Farben</string>
|
||||
<string name="value_internal_sensor">Interner Sensor</string>
|
||||
<string name="permission_recording_failed">OpenTracks benötigt die Berechtigubg, auf Ihren Standort sowie nahe gelegene Geräte zuzugreifen.</string>
|
||||
<string name="sensor_state_power_max">Maximale Leistung</string>
|
||||
<string name="sensor_state_power_unit">W</string>
|
||||
<string name="settings_recording_title">Aufnahme</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Verwenden Sie benutzerdefinierte dynamische Farben (nur Android 12+; erfordert einen manuellen Neustart)</string>
|
||||
<string name="value_int_seconds">%1$d s (empfohlen)</string>
|
||||
<string name="sensor_state_cadence_unit">Drehzahl</string>
|
||||
<string name="sensor_state_heart_rate_unit">bpm</string>
|
||||
<string name="activity_type_kickscooter">Roller</string>
|
||||
<string name="settings_recording_location_distance_summary">%1$s zwischen aufgezeichneten Orten</string>
|
||||
<string name="voiceSpeedMKnotsPlural">{n, plural, =1 {1 Knoten} other {{n,number,#.0} Knoten} }</string>
|
||||
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
|
||||
<string name="settings_announcements_unit">Distanz/Geschwindigkeits Einheit</string>
|
||||
<string name="power_x_watt">Leistung {x, plural, =1 {1 Watt} other {# Watt} }</string>
|
||||
<string name="power">Leistung</string>
|
||||
<string name="voiceDistance">{n, plural, =1 {1 } anderes {{n,number,#.0}} }</string>
|
||||
<string name="settings_recording_min_sampling_interval_title">Abtastzeitintervall</string>
|
||||
<string name="voiceIdle">Untätig werden.</string>
|
||||
<string name="voice_announce_lap_power_key">StimmeAnkündigungLapPower</string>
|
||||
<string name="settings_announcements_lap_power">Rundenleistung</string>
|
||||
</resources>
|
||||
@@ -37,12 +37,12 @@ limitations under the License.
|
||||
<string name="help_storage_pictures_title">¿Cómo almacena OpenTracks las imágenes que se tomaron como marcadores\?</string>
|
||||
<string name="help_storage_pictures_content">Estos se almacenan en el almacenamiento interno de la aplicación (es decir, solo se puede acceder a ellos dentro de OpenTracks).</string>
|
||||
<string name="help_track_export_title">¿Cómo puedo exportar/compartir recorridos grabados\?</string>
|
||||
<string name="help_track_export_content"> \u0020Los Tracks se pueden exportar desde la Configuración como KMZ, KML o GPX. \u0020KMZ/KML todos los datos; solo ubicaciones GPX. \u0020Las pistas compartidas a través del <i>icono de compartir</i> se comparten como KMZ. \u0020</string>
|
||||
<string name="help_track_export_content"> Las pistas se pueden exportar desde la configuración como KMZ, KML o GPX. KMZ/KML todos los datos; Ubicaciones solo GPX. Las pistas compartidas a través del <i>icono de compartir</i> se comparten como KMZ. </string>
|
||||
<string name="help_data_google_play_services_title">¿OpenTracks requiere/usa los servicios de Google Play\?</string>
|
||||
<string name="help_recording_title">¿Cómo registra OpenTracks los datos?</string>
|
||||
<string name="help_recording_content">OpenTracks utiliza el sensor GPS del dispositivo para registrar la ubicación de forma continua. Si el dispositivo GPS informa datos inexactos (por ejemplo, ubicación, velocidad, elevación), OpenTracks no puede hacer nada al respecto.</string>
|
||||
<string name="help_data_storage_title">¿Cómo almacena OpenTracks los datos de los sensores?</string>
|
||||
<string name="help_data_storage_content"> \u0020Los datos del sensor se almacenan por ubicación registrada. \u0020En términos de un sensor de frecuencia cardiaca, esto significa que sólo se almacena <i>una</i> medición de frecuencia cardiaca por ubicación. \u0020Si está parado o se encuentra en interiores, no se registrará ningún dato del sensor (pero se mostrará). \u0020</string>
|
||||
<string name="help_data_storage_content"> Los datos del sensor se almacenan por ubicación registrada. En términos de un sensor de frecuencia cardíaca, esto significa que solo se almacena <i>una</i> medición de frecuencia cardíaca por ubicación. Si está parado o está en interiores, no se registrarán los datos del sensor (pero se mostrarán). </string>
|
||||
<string name="help_gps_troubleshooting_title">¿OpenTracks no registra ubicaciones\?</string>
|
||||
<string name="help_gps_troubleshooting_content"> Lo que puedes hacer:
|
||||
\na) compruebe si el GPS está habilitado,
|
||||
@@ -520,7 +520,7 @@ limitations under the License.
|
||||
<string name="settings_gps_sensor">GPS</string>
|
||||
<string name="settings_announcements_statistics_title">Avisos de voz</string>
|
||||
<string name="settings_units_category">Unidades</string>
|
||||
<string name="settings_import_export_title">Importar/Exportar</string>
|
||||
<string name="settings_import_export_title">Importar y exportar</string>
|
||||
<string name="stats_custom_layout_default_layout">Diseño predeterminado</string>
|
||||
<string name="custom_layout_select_layout">Seleccionar diseño</string>
|
||||
<string name="custom_layout_list_edit_already_exists">El nombre de este diseño ya existe</string>
|
||||
@@ -654,4 +654,14 @@ limitations under the License.
|
||||
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
|
||||
<string name="settings_recording_min_sampling_interval_title">Intervalo de muestreo</string>
|
||||
<string name="settings_recording_location_distance_summary">%1$s entre lugares registrados</string>
|
||||
<string name="settings_theme_switch_restart">Los cambios del tema pueden requerir un reinicio manual.</string>
|
||||
<string name="settings_night_mode_option_night_oled">Oscuro (compatible con OLED)</string>
|
||||
<string name="settings_announcements_unit">Unidad de distancia/velocidad</string>
|
||||
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
|
||||
<string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string>
|
||||
<string name="settings_announcements_lap_power">Potencia por vuelta</string>
|
||||
<string name="power_x_watt">Potencia {x, plural, =1 {1 vatio} otros {# vatios} }</string>
|
||||
<string name="create_marker_error">No se ha podido crear un marcador. Inténtelo de nuevo.</string>
|
||||
<string name="voice_announce_lap_power_key">voiceAnnounceLapPower</string>
|
||||
<string name="power">Potencia</string>
|
||||
</resources>
|
||||
@@ -23,13 +23,12 @@ limitations under the License.
|
||||
<string name="about_libraries">Liburutegiak</string>
|
||||
<!-- Help -->
|
||||
<string name="help_feature_title">OpenTracks ek ezaugarri hauek ditu:</string>
|
||||
<string name="help_feature_content">
|
||||
* GPS ibilbideak sortu\n
|
||||
* erabili bihotz-maiztasuneko sentsorea (Bluetooth LE soilik)\n
|
||||
* ahots iragarpenak (denboraren edo distantziaren arabera)\n
|
||||
* Sortu markatzaileak (irudiak barne)\n
|
||||
* guztiz lineaz kanpo funtzionatzen du\n
|
||||
* Erakutsi ibilbideak mapa batean (ikusi azpian)</string>
|
||||
<string name="help_feature_content">* GPS ibilbideak sortu
|
||||
\n * erabili bihotz-maiztasuneko sentsorea (Bluetooth LE soilik)
|
||||
\n * ahots iragarpenak (denboraren edo distantziaren arabera)
|
||||
\n * Sortu markatzaileak (irudiak barne)
|
||||
\n * guztiz lineaz kanpo funtzionatzen du
|
||||
\n * Erakutsi ibilbideak mapa batean (ikusi azpian)</string>
|
||||
<string name="help_track_map_title">Nola erakutsi pista batean mapa batean?</string>
|
||||
<string name="help_track_map_content"> Erabili maparen ikono txikia.
|
||||
\n a) (zuzeneko eguneratzeak) Instalatu <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Lineako edo lineaz kanpoko mapak eskaintzen ditu <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
||||
|
||||
@@ -23,13 +23,12 @@ limitations under the License.
|
||||
<string name="about_libraries">Kirjastot</string>
|
||||
<!-- Help -->
|
||||
<string name="help_feature_title">OpenTracksissa on seuraavat ominaisuudet:</string>
|
||||
<string name="help_feature_content">
|
||||
* luo GPS-reittejä\n
|
||||
* käytä sykeanturia (vain Bluetooth LE)\n
|
||||
* ääni-ilmoitukset (ajan tai etäisyyden perusteella)\n
|
||||
* luo merkkejä (mukaan lukien kuvia)\n
|
||||
* toimii täysin ilman verkkoyhteyttä\n
|
||||
* näytä jäljet kartalla (ks. alla)</string>
|
||||
<string name="help_feature_content">* luo GPS-reittejä
|
||||
\n * käytä sykeanturia (vain Bluetooth LE)
|
||||
\n * ääni-ilmoitukset (ajan tai etäisyyden perusteella)
|
||||
\n * luo merkkejä (mukaan lukien kuvia)
|
||||
\n * toimii täysin ilman verkkoyhteyttä
|
||||
\n * näytä jäljet kartalla (ks. alla)</string>
|
||||
<string name="help_track_map_title">Miten näyttää rata kartalla\?</string>
|
||||
<string name="help_track_map_content"> Käytä pientä karttakuvaketta.
|
||||
\n a) (live-päivityksiä) Asenna <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Se tarjoaa online-tai offline-karttoja kautta <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user