Compare commits

..

157 Commits

Author SHA1 Message Date
Dennis Guse 5594a125d0 Release: v3.20.0 2021-08-22 22:08:23 +02:00
Román Martínez 13380afef0 TrackRecordingManager only updates track's statistics every time a new track point is inserted. Fixes #916. 2021-08-22 20:01:38 +02:00
Román Martínez 9c4c8131f5 Recording stats layout customizable.
Users can select the order, visibility and importance (primary or secondary) of all stats for recording layout from the Settings.
Also, users can select the number of columns per row in the recording layout: 1, 2 or 3.
Finally, show altitude/coordinate from settings have been removed (not needed anymore).
Fixes #659.
Fixes #302.
Fixes #820.
2021-08-21 19:40:31 +02:00
dependabot[bot] 7f10ae5c3f Bump mockito-android from 3.10.0 to 3.12.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.10.0 to 3.12.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.10.0...v3.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-20 07:37:15 +02:00
dependabot[bot] 1fa45a3393 Bump gradle from 7.0.0 to 7.0.1
Bumps gradle from 7.0.0 to 7.0.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-19 10:11:22 +02:00
Dennis Guse f1b0c83196 Merge branch 'translations' 2021-08-16 22:17:29 +02:00
Dennis Guse f0f7bca54b Translations: fix fr. 2021-08-16 22:16:58 +02:00
mondstern 8f39bf139c Translated using Weblate (Slovenian)
Currently translated at 71.5% (292 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sl/
2021-08-16 22:15:27 +02:00
mondstern 85585969bd Translated using Weblate (Russian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-08-16 22:15:27 +02:00
mondstern 976e7be370 Translated using Weblate (French)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-08-16 22:15:27 +02:00
Jaroslav Lichtblau 48dc64d4d7 Translated using Weblate (Czech)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-08-16 22:15:27 +02:00
Pablo 16e69e850b Translated using Weblate (Spanish (Latin America))
Currently translated at 98.2% (401 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2021-08-16 22:15:27 +02:00
FTno 5ece301ac0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.5% (70 of 71 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nb_NO/
2021-08-16 22:15:27 +02:00
☆Verdulo a5e500cdb1 Translated using Weblate (Esperanto)
Currently translated at 97.1% (69 of 71 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/eo/
2021-08-16 22:15:27 +02:00
Román Martínez 8433ed82fe Release: v3.19.1 2021-08-15 20:19:08 +02:00
Dennis Guse 7c7defa233 Merge pull request #912 from OpenTracksApp/raceCondition
Bugfix: LocationHandler should ignore locations after stopping.
2021-08-15 15:53:30 +02:00
Dennis Guse 1f84e44a8d Bugfix: LocationHandler should ignore locations after stopping. 2021-08-15 14:30:57 +02:00
Dennis Guse 858a8f20a8 Merge pull request #910 from OpenTracksApp/translations
Translations
2021-08-14 20:36:20 +02:00
Dennis Guse 6761c41dbf Translations: removed invalid plurals. 2021-08-14 19:46:40 +02:00
FTno 59e55be214 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.5% (70 of 71 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nb_NO/
2021-08-09 20:06:09 +02:00
☆Verdulo 1b68cba1ba Translated using Weblate (Esperanto)
Currently translated at 97.1% (69 of 71 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/eo/
2021-08-09 20:06:08 +02:00
mondstern 32f47340a9 Translated using Weblate (Latvian)
Currently translated at 99.0% (404 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/lv/
2021-08-09 20:06:07 +02:00
mondstern e25814c674 Translated using Weblate (Lithuanian)
Currently translated at 98.2% (401 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/lt/
2021-08-09 20:06:05 +02:00
FTno 522baa5193 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-08-09 11:54:21 +02:00
mondstern b597b56b84 Translated using Weblate (Romanian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ro/
2021-08-07 19:37:23 +02:00
mondstern d956e8d3f0 Translated using Weblate (Romanian)
Currently translated at 99.0% (404 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ro/
2021-08-07 19:37:23 +02:00
mondstern 274781ccfa Translated using Weblate (Romanian)
Currently translated at 83.3% (340 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ro/
2021-08-07 19:37:23 +02:00
mondstern e7e3ef23ff Translated using Weblate (Lithuanian)
Currently translated at 97.5% (398 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/lt/
2021-08-07 19:37:23 +02:00
Quang Trung 3289fa27e4 Translated using Weblate (Vietnamese)
Currently translated at 88.7% (362 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-08-07 19:37:23 +02:00
mondstern 96fc074a4b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-08-07 19:37:23 +02:00
mondstern eaab09747c Translated using Weblate (Swedish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sv/
2021-08-07 19:37:23 +02:00
mondstern bdb2a9dbc6 Translated using Weblate (Russian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-08-07 19:37:23 +02:00
mondstern 7e26571124 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-08-07 19:37:23 +02:00
mondstern 33b23a6e8f Translated using Weblate (Polish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-08-07 19:37:23 +02:00
mondstern 584adc7c57 Translated using Weblate (Dutch)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-08-07 19:37:23 +02:00
mondstern 667da76c4f Translated using Weblate (Italian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-08-07 19:37:23 +02:00
mondstern df6a3a7f04 Translated using Weblate (Hungarian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2021-08-07 19:37:23 +02:00
mondstern b254f5e40b Translated using Weblate (Galician)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-08-07 19:37:23 +02:00
mondstern 7a9c3efb3b Translated using Weblate (French)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-08-07 19:37:23 +02:00
mondstern 53dd7ada1f Translated using Weblate (Finnish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-08-07 19:37:23 +02:00
mondstern 6a5edcbdef Translated using Weblate (Basque)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-08-07 19:37:23 +02:00
mondstern 6e954261bb Translated using Weblate (Estonian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-08-07 19:37:23 +02:00
mondstern 277061ad27 Translated using Weblate (Spanish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-08-07 19:37:23 +02:00
mondstern c03f9fc792 Translated using Weblate (Danish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-08-07 19:37:23 +02:00
mondstern 657b82b57a Translated using Weblate (Czech)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-08-07 19:37:23 +02:00
mondstern 4608edec56 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/bg/
2021-08-07 19:37:23 +02:00
FTno b086c30696 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-08-07 19:37:23 +02:00
mondstern 7eca033a8e Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-08-07 19:37:23 +02:00
Dennis Guse 6f3efd9ff2 Bugfix: reset TrackRecordingManager properly.
Fixes #901.
2021-08-07 19:37:14 +02:00
A Rabbit 7a2741e508 Fix: README 2021-08-06 16:42:22 +02:00
Román Martínez 017eee8b68 Bugfix: generated fields for include and merge tags. Fixes #902. 2021-08-05 18:01:34 +02:00
Dennis Guse 024d7f8652 PlayStore: describe metadata upload. 2021-08-04 17:11:29 +02:00
Dennis Guse 05c3a89682 Release: v3.19.0 2021-08-01 18:53:12 +02:00
Dennis Guse a3872581bb Cleanup: Renamed HandlerServer to TrackPointCreator. 2021-08-01 18:40:38 +02:00
Dennis Guse e75451dab3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (407 of 407 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-08-01 18:22:52 +02:00
Dennis Guse 2897171132 Translated using Weblate (Thai)
Currently translated at 71.7% (292 of 407 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/th/
2021-08-01 18:22:51 +02:00
Dennis Guse ed2f506b7b Translated using Weblate (Serbian)
Currently translated at 71.0% (289 of 407 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sr/
2021-08-01 18:22:51 +02:00
Dennis Guse 1e2ba3798d Translated using Weblate (Malay)
Currently translated at 71.7% (292 of 407 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ms/
2021-08-01 18:22:51 +02:00
Dennis Guse a0a8ef9016 Translated using Weblate (Moldavian)
Currently translated at 71.4% (291 of 407 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ro_MD/
2021-08-01 18:22:51 +02:00
Dennis Guse e1f4406674 Translated using Weblate (Latvian)
Currently translated at 70.7% (288 of 407 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/lv/
2021-08-01 18:22:51 +02:00
Dennis Guse d9b7f77bbb Translated using Weblate (Catalan)
Currently translated at 72.2% (294 of 407 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ca/
2021-08-01 18:22:51 +02:00
Dennis Guse e60dcbe539 Translated using Weblate (Spanish (Latin America))
Currently translated at 98.5% (401 of 407 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2021-08-01 18:22:51 +02:00
Dennis Guse fc0cec6157 Share picture: if sharing a track, a share picture (PNG) is created in addition.
Fixes #363.
2021-08-01 18:22:42 +02:00
Dennis Guse 0ac34e0638 Sensor parsing: fixed test.
Part of #891.
2021-07-31 19:00:32 +02:00
Dennis Guse 242147e491 Sensor: adding Running Speed and Cadence.
Fixes #891.
2021-07-31 18:44:42 +02:00
Dennis Guse eba7a7fd89 Settings: use already available custom summary. 2021-07-31 17:54:57 +02:00
Dennis Guse 348f74c4b3 Cleanup. 2021-07-31 17:54:57 +02:00
Dennis Guse d898d51f1a Merge remote-tracking branch 'weblate/main' into main 2021-07-31 07:23:56 +02:00
Dennis Guse 6e9c95f232 Translations: removed unused ones. 2021-07-31 07:19:41 +02:00
Vinicius 9c7f458802 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (70 of 70 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/pt_BR/
2021-07-31 06:49:31 +02:00
mondstern f52b153e3a Translated using Weblate (Slovenian)
Currently translated at 100.0% (70 of 70 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/sl/
2021-07-31 06:49:30 +02:00
mondstern 145cbf870b Translated using Weblate (Swedish)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sv/
2021-07-31 06:49:30 +02:00
mondstern 8e52574378 Translated using Weblate (Slovenian)
Currently translated at 99.0% (440 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sl/
2021-07-31 06:49:30 +02:00
Dennis Guse e3ca1b7f21 Translations: fixed linting errors. 2021-07-31 06:44:02 +02:00
mondstern 2cdae4f460 Translated using Weblate (Lithuanian)
Currently translated at 100.0% (70 of 70 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/lt/
2021-07-29 22:55:05 +02:00
Allan Nordhøy 422e171406 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.5% (69 of 70 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nb_NO/
2021-07-29 22:55:05 +02:00
mondstern 130581da6f Translated using Weblate (Swedish)
Currently translated at 98.5% (69 of 70 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/sv/
2021-07-29 22:55:05 +02:00
mondstern 9cd212f6b1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-07-29 22:55:05 +02:00
mondstern 08701dcbf9 Translated using Weblate (Swedish)
Currently translated at 98.8% (439 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sv/
2021-07-29 22:55:05 +02:00
mondstern 0f7f332087 Translated using Weblate (Russian)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-07-29 22:55:05 +02:00
mondstern 9eed5aa70f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-07-29 22:55:05 +02:00
mondstern ea8cd9b6fc Translated using Weblate (Polish)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-07-29 22:55:05 +02:00
mondstern 0ef40e7fab Translated using Weblate (Dutch)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-07-29 22:55:05 +02:00
FTno 919ecbbffb Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-07-29 22:55:05 +02:00
mondstern 21369860a7 Translated using Weblate (Lithuanian)
Currently translated at 96.8% (430 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/lt/
2021-07-29 22:55:05 +02:00
mondstern 7bad6a106a Translated using Weblate (Italian)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-07-29 22:55:05 +02:00
mondstern bdf0c91714 Translated using Weblate (Hungarian)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2021-07-29 22:55:05 +02:00
Xosé M 453cbd705e Translated using Weblate (Galician)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-07-29 22:55:05 +02:00
mondstern 0669eadc01 Translated using Weblate (French)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-07-29 22:55:05 +02:00
mondstern 7a661cb521 Translated using Weblate (Finnish)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-07-29 22:55:05 +02:00
mondstern bb7cba0e47 Translated using Weblate (Basque)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-07-29 22:55:05 +02:00
mondstern 1402563702 Translated using Weblate (Estonian)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-07-29 22:55:05 +02:00
mondstern 4d8c8e4ec9 Translated using Weblate (German)
Currently translated at 99.5% (442 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-07-29 22:55:05 +02:00
mondstern ef7f36d49e Translated using Weblate (Danish)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-07-29 22:55:05 +02:00
mondstern e2ebf04621 Translated using Weblate (Czech)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-07-29 22:55:05 +02:00
mondstern 029b60efff Translated using Weblate (Bulgarian)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/bg/
2021-07-29 22:55:05 +02:00
Allan Nordhøy 70bfa3e862 Translated using Weblate (English)
Currently translated at 100.0% (70 of 70 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/en/
2021-07-29 22:55:05 +02:00
FTno 9f1087c9e6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-07-29 22:55:05 +02:00
mondstern c64bae827b Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (444 of 444 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-07-29 22:55:05 +02:00
mondstern 05bde1cf33 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-07-29 22:55:05 +02:00
mondstern 13c65b25ae Translated using Weblate (Serbian)
Currently translated at 73.0% (323 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sr/
2021-07-29 22:55:05 +02:00
mondstern 0ab0f5d7a3 Translated using Weblate (Russian)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-07-29 22:55:05 +02:00
mondstern 70ca0d50e2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-07-29 22:55:05 +02:00
mondstern 8da64ad28d Translated using Weblate (Polish)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-07-29 22:55:05 +02:00
mondstern 48d19aaa67 Translated using Weblate (Dutch)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-07-29 22:55:05 +02:00
mondstern 63bb512e3a Translated using Weblate (Italian)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-07-29 22:55:05 +02:00
mondstern 3205c9de8a Translated using Weblate (Indonesian)
Currently translated at 79.6% (352 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/id/
2021-07-29 22:55:05 +02:00
mondstern bda1603e87 Translated using Weblate (Hungarian)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2021-07-29 22:55:05 +02:00
mondstern 28d2f42096 Translated using Weblate (French)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-07-29 22:55:05 +02:00
mondstern d83bc0b32a Translated using Weblate (Finnish)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-07-29 22:55:05 +02:00
mondstern 87508fca3f Translated using Weblate (Basque)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-07-29 22:55:05 +02:00
mondstern 3250be9480 Translated using Weblate (Estonian)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-07-29 22:55:05 +02:00
mondstern 3848dd8fbd Translated using Weblate (Spanish)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-07-29 22:55:05 +02:00
mondstern 022cf5835b Translated using Weblate (Danish)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-07-29 22:55:03 +02:00
mondstern 325681c6cd Translated using Weblate (Bulgarian)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/bg/
2021-07-29 22:55:03 +02:00
Dennis Guse 54be1b74fb Lint: fix AGP7.0.0 found linting errors. 2021-07-29 22:54:57 +02:00
Dennis Guse fd1a53d1a6 CI: lint instead of CodeQL. 2021-07-29 22:54:57 +02:00
Dennis Guse 19f6a477c4 Upgrade to Android Gradle Plugin 7.0.0 (incl. Java11). 2021-07-29 22:54:57 +02:00
Dennis Guse 94babc9cd2 CI: Use JDK11. 2021-07-29 22:54:57 +02:00
Dennis Guse 26414b0c7c Upgrade to API level 31 (Android12).
Fixes #803.
2021-07-29 22:54:57 +02:00
Dennis Guse 956031264f Settings: use separate titles for cycling-only sensors. 2021-07-25 19:18:34 +02:00
Dennis Guse 7d8ac1f411 Merge remote-tracking branch 'weblate/main' into main 2021-07-25 10:29:53 +02:00
Dennis Guse a4121319fa Tests: close cursor. 2021-07-25 10:27:22 +02:00
Xosé M 1fdf1dd607 Translated using Weblate (Galician)
Currently translated at 100.0% (442 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-07-22 17:18:41 +02:00
☆Verdulo 7759d1f9b0 Translated using Weblate (Esperanto)
Currently translated at 98.5% (69 of 70 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/eo/
2021-07-22 17:18:41 +02:00
Quang Trung 43b114b64b Translated using Weblate (Vietnamese)
Currently translated at 77.3% (342 of 442 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-07-22 17:18:41 +02:00
Dennis Guse a808c52d48 Bugfix: recording layout constraint was not correct. 2021-07-22 17:18:31 +02:00
Dennis Guse bb90c53057 Simplified voice announcement interface.
Part of #822.
2021-07-22 17:18:31 +02:00
Dennis Guse bd5bf530f0 TrackRecordingService: extracted storing data into TrackRecordingManager.
Part of #822.
2021-07-22 17:18:31 +02:00
Dennis Guse a4580fc84e Cleanup. 2021-07-22 17:18:31 +02:00
Dennis Guse 9ab34eb846 TrackRecordingService: EGM2008 computation should only be done for UI.
Reverting 16483d4261ff780f940fe4ea223957c898023e14
2021-07-22 17:18:31 +02:00
Dennis Guse 997ddbb4f1 ExportImportTest: made tests deterministic (simulate GPS/sensors).
Fixes #761.
2021-07-22 17:18:31 +02:00
Dennis Guse 86a0054c24 Bugfix: TrackStatistics' total time was not restored correctly, when having pause/stop and then resume. 2021-07-22 17:18:31 +02:00
Dennis Guse 3bd00c906a LocationHandler.onStop(): reset state. 2021-07-22 17:18:31 +02:00
Dennis Guse 684419e86e Version info: show only git describe --tags.
The commitId is included if there are commits after this tag.
2021-07-22 17:18:31 +02:00
Dennis Guse d42d1214ea While recording: UI updates altitude, speed, and bearing to most recently received value.
Fixes #866.
2021-07-22 17:18:31 +02:00
Dennis Guse db24639829 Cleanup: horizontal accuracy is now a Distance (and named consistently). 2021-07-22 17:18:31 +02:00
Dennis Guse 0f6480197b Cleanup. 2021-07-22 17:18:31 +02:00
Dennis Guse cce62c4c47 CI: store logs as artifact. 2021-07-22 17:18:31 +02:00
Dennis Guse bfdc04fbd9 CI: use orchestrator to continue tests after crash + set overall timeout. 2021-07-22 17:18:31 +02:00
Dennis Guse 18702bb6b3 Cleanup. 2021-07-22 17:18:31 +02:00
Dennis Guse 65e920e8b6 ExportImportTest: made tests deterministic.
Fixes #761.
2021-07-22 17:18:31 +02:00
Dennis Guse 7d24262b38 TrackRecordingService: generate timestamps for TrackPoints in HandlerServer.
Part of #822.
2021-07-22 17:18:31 +02:00
Dennis Guse 223ad70d36 TrackRecordingService: move BluetoothRemoteSensorManager and AltitudeSumManager to HandlerServer.
Part of #822.
2021-07-22 17:18:31 +02:00
Dennis Guse 7f618da7a6 TrackPointAssert: provide better error messages on assertion failure. 2021-07-22 17:18:31 +02:00
Dennis Guse 0332b9eb87 ExportImportTest: only cleanup if necessary. 2021-07-22 17:18:31 +02:00
Dennis Guse 29aefff788 Cleanup: remove HandlerInterface. 2021-07-22 17:18:31 +02:00
Dennis Guse c9478ea38d HandlerServer: disable serviceExecutor to simplify testing for now.
Part of #882.
2021-07-22 17:18:31 +02:00
Dennis Guse 1255ec8111 TrackRecordingService: extract EGM2008CorrectionManager into HandlerServer.
Part of #882.
2021-07-22 17:18:31 +02:00
Dennis Guse f9d3108cb2 Extract EGM2008CorrectionManager.
Part of #882.
2021-07-22 17:18:31 +02:00
dependabot[bot] e18d6b942d Bump appcompat from 1.3.0 to 1.3.1
Bumps appcompat from 1.3.0 to 1.3.1.

---
updated-dependencies:
- dependency-name: androidx.appcompat:appcompat
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-22 07:45:57 +02:00
dependabot[bot] cc5ced6383 Bump reactivecircus/android-emulator-runner from 2.19.0 to 2.19.1
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.19.0 to 2.19.1.
- [Release notes](https://github.com/reactivecircus/android-emulator-runner/releases)
- [Changelog](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/CHANGELOG.md)
- [Commits](https://github.com/reactivecircus/android-emulator-runner/compare/v2.19.0...v2.19.1)

---
updated-dependencies:
- dependency-name: reactivecircus/android-emulator-runner
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-21 07:50:54 +02:00
Dennis Guse 163879487c CI: Disable API level 29 as this creates timeouts. 2021-07-20 22:54:02 +02:00
Dennis Guse d7dd9f7c1b CI: only build on push.
Avoid triggering two builds.
2021-07-20 22:53:37 +02:00
Dennis Guse 1259c42d28 TrackRecordingService: notification should not vibrate.
Fixes #611.
2021-07-11 23:05:05 +02:00
Dennis Guse 9bccbe79a4 Typo in italian translation.
Fixes #879.
2021-07-11 16:54:24 +02:00
Dennis Guse 401e07d7e7 Cleanup. 2021-07-09 23:17:18 +02:00
237 changed files with 5739 additions and 4485 deletions
-31
View File
@@ -1,31 +0,0 @@
name: "CodeQL"
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'
jobs:
analyse:
name: Analyse
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2.3.4
with:
fetch-depth: 2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: java
- run: |
./gradlew build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+19
View File
@@ -0,0 +1,19 @@
name: Linting
on: [ push ]
jobs:
analyse:
name: Analyse
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2.3.4
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- run: ./gradlew lint
+17 -3
View File
@@ -1,19 +1,25 @@
#https://github.com/marketplace/actions/android-emulator-runner
name: Test
on: [push, pull_request]
on: [ push ]
jobs:
test:
timeout-minutes: 45
runs-on: macos-latest
strategy:
matrix:
api-level: [ 23, 26, 29 ]
api-level: [ 23, 26 ] #TODO Re-enable 29
target: [ default ]
steps:
- name: checkout
uses: actions/checkout@v2.3.4
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2.19.0
uses: reactivecircus/android-emulator-runner@v2.19.1
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
@@ -21,3 +27,11 @@ jobs:
profile: Nexus 6
script: ./gradlew connectedCheck
- name: Archive
if: always()
uses: actions/upload-artifact@v2
with:
name: code-coverage-report
path: build/reports/androidTests/
retention-days: 7
-13
View File
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
+10 -8
View File
@@ -51,16 +51,16 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
## Screenshots
<div>
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot2.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot3.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot4.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot3.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot4.png">
</div>
<div>
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot5.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot6.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot7.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot5.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot6.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot7.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot8.png">
</div>
## Features
@@ -79,6 +79,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* cycling: speed and distance
* cycling: cadence
* cycling: power meter
* running: speed and cadence
An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md)
### Gadgetbridge integration
+4 -3
View File
@@ -28,9 +28,10 @@ Requirement: `gem install fastlane`
(requires keystore)
2. Upload signed apk to [Google Play Console](https://play.google.com/apps/publish)
3. Update store meta data (e.g., description and screenshots)
`fastlane supply --version_code VERSION_CODE --skip_upload_apk`
(use the version code from the manifest)
(requires API key)
`fastlane metadata`
(requires API key configured in `Appfile`)
Supported languages: https://support.google.com/googleplay/android-developer/table/4419860
## Translations (localization)
+5
View File
@@ -50,3 +50,8 @@ Also the distance is not computed.
* Wahoo Kickr v4.0
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is propietary @ firmware v1.061)
## RSC: running cadence and speed
We do not support type of movement (i.e., walking vs running).
* Polar Stride
+14 -17
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath 'com.android.tools.build:gradle:7.0.1'
}
}
@@ -24,13 +24,8 @@ def getVersionName = { ->
commandLine 'git', 'describe', '--tags'
standardOutput = stdout
}
exec {
commandLine 'git', 'log', '--format="%h"', '-n 1'
standardOutput = stdout
}
return stdout.toString().replace('\n', '-').replace('"', '').trim()
}
catch (ignored) {
return stdout.toString().trim()
} catch (ignored) {
return null
}
}
@@ -39,11 +34,11 @@ android {
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
compileSdkVersion 30
compileSdkVersion 31
buildFeatures {
viewBinding = true
@@ -55,17 +50,17 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4253
versionName "v3.18.6"
versionCode 4410
versionName "v3.20.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 30
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
buildTypes {
@@ -92,7 +87,7 @@ android {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.4.0'
@@ -103,5 +98,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'org.mockito:mockito-android:3.10.0'
androidTestImplementation 'org.mockito:mockito-android:3.12.0'
androidTestUtil 'androidx.test:orchestrator:1.4.0'
}
+20 -1
View File
@@ -43,7 +43,6 @@ class TrackPoint {
- sensor_power
}
class Marker {
- id
- trackId
@@ -58,6 +57,26 @@ class Marker {
- photoUrl
}
class DataField {
- key
- title
- isVisible
- isPrimary
- isWide
}
class StatisticData {
- value
- unit
- description
}
StatisticData o-- DataField
class Layout {
- profile
}
Layout "1" o-- "many" DataField
note left of TrackPoint
As of OpenTracks version 3.15.0, all times are using device time.
Before that TrackPoint.time contained GPS time (determined by GPS hardware).
+3
View File
@@ -0,0 +1,3 @@
README.md
Appfile
report.xml
+6 -18
View File
@@ -16,23 +16,11 @@
default_platform(:android)
platform :android do
desc "Runs all the tests"
lane :test do
gradle(task: "test")
end
desc "Submit a new Beta Build to Crashlytics Beta"
lane :beta do
gradle(task: "clean assembleRelease")
crashlytics
# sh "your_script.sh"
# You can also use other beta testing services here
end
desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(task: "clean assembleRelease")
upload_to_play_store
desc "Upload metadata"
lane :metadata do
upload_to_play_store(
skip_upload_apk: true,
skip_upload_changelogs: true
)
end
end
@@ -2,4 +2,3 @@ v3.13.2: OpenTracks
Bugfix:
- Crash in landscape mode
@@ -15,4 +15,3 @@ Developer:
- Replaced communication via SharedPreferences
- Use non-static service binder
- Use altitude in the code base (instead of elevation)
@@ -8,4 +8,3 @@ Bugfix:
- Crash if using pace for voice announcements (introduced in v3.17.0)
- Statistics showed pace initially (introduced in v3.17.0)
- Rare crash when deleting tracks
@@ -0,0 +1,16 @@
v3.19.0: OpenTracks
Changes:
- Support for Running Speed and Cadence (RCS) sensors implemented
- When sharing a track: a share picture is created
- No vibration when starting a recording
- While recording: UI shows most recently received altitude, speed, and bearing (even if not stored)
Bugfix:
- Computation of total duration should not include paused durations (requires re-import)
- (again) Use custom summaries if present
Developer:
- Upgrade to SDK31 and AGP7.0.0 (requires JDK11)
- TrackRecordingService: extacted sensor fusion (TrackPointCreator) and TrackPoint storage (TrackRecordingManager)
- ExportImportTest is now deterministics/ constant
@@ -0,0 +1,8 @@
v3.19.1: OpenTracks
Changes:
- Updated translations.
Bugfix:
- Export All Tracks bug fixed.
- Resume Track bug fixed.
@@ -0,0 +1,9 @@
v3.20.0: OpenTracks
Changes:
- While recording: statistics UI is now configurable
Bugfix:
- Changing track name/ category while recording was ignored (introduced in v3.19.0)

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 574 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 456 KiB

Before

Width:  |  Height:  |  Size: 630 KiB

After

Width:  |  Height:  |  Size: 630 KiB

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 416 KiB

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 508 KiB

Before

Width:  |  Height:  |  Size: 798 KiB

After

Width:  |  Height:  |  Size: 798 KiB

@@ -1,11 +1,11 @@
OpenTracks estas sporto spuranta aplikon kiu tute respektas vian privatecon.
OpenTracks estas aplikaĵo por registri vian sportan aktivecon, kiu plene respektas vian privatecon.
Karakterizaĵoj:
* Sporto spurado: spuri via sporto kaj subĉielaj aktivecoj
* Fotoj kaj signoj: mark interesaj lokoj dum spurado
* Altitudo akiro kaj perdo: tra barometric sentilo
* Altitudo montrita en EGM2008 (supre signifas marnivelo); eksportita kiel WGS84
* # Vo? o anoncoj
* Elportas Bluetooth LE sentiloj: koro imposto, rapido kaj distanco (biciklado), kadenco (biciklado), kaj potenco metro (biciklado)
* Eksportado datumoj: eksporti vojetoj aŭ kiel KMZ (incl. fotoj), KML, aŭ GPX
* Neniu aliro al Interreto: Internet ne estas uzita
Eblaĵoj:
• registri vian sportan kaj alian aktivecon eksterdome
• fotoj kaj markoj: marki interesajn lokojn dum registri aktivecon
• registri ŝanĝon de altitudo uzante barometron
• montri altitudon en EGM2008 (super averaĝa marnivelo); elporti ilin kiel WGS84
• voĉaj anoncoj
• subteno por sentiloj BLE por registri vian pulson, rapidon, veturigitan distancon, pedal-rapidon, povumon (biciklado)
• elporti datumojn al dosieroj KMZ (kun fotoj), KML aŭ GPX
• funkcias malkonekte: interreta permeso ne estas uzata
@@ -1 +1 @@
Sporto spuristo ke tute respektas vian privatecon.
Registrilo de via sporta aktiveco, kiu respektas vian privatecon.
@@ -0,0 +1,16 @@
v3.18.6: „OpenTracks“
Pakeitimai:
- „Rodyti žemėlapyje“ funkcionalumas per KML, o ne KMZ
- Pranešime balsu nėra programos pavadinimo
- Įrašant: aukščio padidėjimas / praradimas atnaujinamas kas sekundę
- Išversti parduotuvės aprašymai
Bufiksai:
- Ištaisyti neteisingi balso pranešimų vertimai
Programuotojas:
- „Android12“ leidimai (BLUETOOTH_SCAN)
- „Android12“ leidimai (ACCESS_COARSE_LOCATION)
- Tvirtinimas „TrackPoints“ (testavimas)
- Pašalinti nepanaudoti vertimai
@@ -0,0 +1,11 @@
„OpenTracks“ yra sporto stebėjimo programa, visiškai gerbianti jūsų privatumą.
Funkcijos:
* Sporto sekimas: stebėkite savo sportinę ir lauko veiklą
* Nuotraukos ir žymekliai: stebėdami stebėkite įdomias vietas
* Aukščio padidėjimas ir praradimas: per barometrinį jutiklį
* EGM2008 nurodytas aukštis (virš vidutinio jūros lygio); eksportuota kaip WGS84
* Balso pranešimai
* Palaiko „Bluetooth LE“ jutiklius: širdies ritmą, greitį ir atstumą (važiuojant dviračiu), ritmą (dviračiu) ir galios matuoklį (važiuojant dviračiu)
* Eksportuoti duomenis: eksportuoti takelius kaip KMZ (įskaitant nuotraukas), KML arba GPX
* Nėra interneto prieigos: internetas nenaudojamas
@@ -0,0 +1 @@
Sporto sekimo priemonė, visiškai gerbianti jūsų privatumą.
+1
View File
@@ -0,0 +1 @@
OpenTracks
@@ -0,0 +1,16 @@
v3.18.6: OpenTracks
Endringer:
- «Vis på kart»-funksjonalitet via KML istedenfor KMZ
- Stemmekunngjøringer inkluderer ikke programnavn
- Under opptak: Høydeendringer registrert hvert sekund
- Butikk-oversettelser oversatt
Feilfikser:
- Fikset ugyldige stemmekunngjøringsoversettelser
Utviklere:
- Android12-tilganger (Blåtannsskanning)
- Android12-tilganger (tilgang til grovposisjon)
- Fastsetting for TrackPoints (testing)
- Fjernet ubrukte oversettelser
@@ -0,0 +1,11 @@
OpenTracks er en sportsporingsapp som respekterer ditt personvern.
Funksjoner:
* Sportsporing: Registrer dine sports- og utendørsaktiviteter
* Bilder og markører: Marker interessante steder under sporing
* Total stigning og nedstigning: via barometersensor
* Høyde vist i EGM2008 (over havnivå); eksportert som WGS84
* Talemeldinger
* Støtter Blåtann LE-sensorer: puls, hastighet og distanse (sykling), kadens (sykling) og wattmåler (sykling)
* Eksport av data: Enten som KMZ (inkl. bilder), KML eller GPX
* Ingen internettilgang: Internett brukes ikke
@@ -0,0 +1 @@
En sportsregistrerer som respekterer ditt personvern.
@@ -0,0 +1 @@
OpenTracks
@@ -0,0 +1,16 @@
v3.18.6: OpenTracks
Mudanças:
- Funcionalidade "Mostrar no mapa" via KML em vez de KMZ
- O anúncio de voz não inclui o nome do aplicativo
- Durante a gravação: o ganho / perda de elevação é atualizado a cada segundo
- Descrições da loja traduzidas
Correções:
- Correções de traduções inválidas no anúncios de voz
Desenvolvedores:
- Permissões Android12 (BLUETOOTH_SCAN)
- Permissões Android12 (ACCESS_COARSE_LOCATION)
- Confirmação de TrackPoints (em teste)
- Removidas as traduções não utilizadas
@@ -7,5 +7,5 @@ Características:
* Altitude mostrada no EGM2008 (acima do nível médio do mar); exportado como WGS84
* Anúncios de voz
* Suporta sensores Bluetooth LE: frequência cardíaca, velocidade e distância (ciclismo), cadência (ciclismo) e medidor de potência (ciclismo)
* Exportar dados: exporte trilhas como KMZ (incluindo fotos), KML ou GPX
* Exportar dados: exporte trilhas como KMZ (incluindo fotos), KML, ou GPX
* Sem acesso à Internet: a Internet não é usada
@@ -0,0 +1,16 @@
v3.18.6: OpenTracks
Spremembe:
- "Prikaži na zemljevidu"-funkcionalnost prek KML namesto KMZ
- Glasovno obvestilo ne vključuje imena aplikacije
- Med snemanjem: pridobitev/izguba nadmorske višine se posodobi vsako sekundo
- Prevedeni opisi trgovin
Bufiksi:
- Vključene so bile naslednje možnosti: popravljeni neveljavni prevodi glasovnih napovedi
Razvijalec:
- Android12 (BLUETOOTH_SCAN)
- Android12 dovoljenja (ACCESS_COARSE_LOCATION)
- Zagotavljanje za TrackPoints (testiranje)
@@ -1,7 +1,7 @@
OpenTracks je aplikacija za sledenje športu, ki popolnoma spoštuje vašo zasebnost.
Značilnosti:
* Športno sledenje: sledite svojim športnim aktivnostim in aktivnostim na prostem.
* Sledenje športu: sledite svojim športnim aktivnostim in aktivnostim na prostem.
* Fotografije in oznake: med sledenjem označite zanimive lokacije
* Pridobivanje in izgubljanje nadmorske višine: prek barometričnega senzorja
* Nadmorska višina je prikazana v EGM2008 (nad srednjo morsko gladino); izvozi se kot WGS84
@@ -0,0 +1,16 @@
v3.18.6: OpenTracks
Förändring:
- "Visa på kartan" - funktionalitet via KML istället för KMZ
- Röstmeddelande inkluderar inte appnamn
- Under inspelning: höjd vinst / förlust uppdateras varje sekund
- Store beskrivningar översatt
Bufixar:
- Fast ogiltig röst tillkännagivande översättningar
Utvecklare:
- Android12 behörigheter (BLUETOOTH_SCAN)
- Android12 behörigheter (ACCESS_COARSE_LOCATION)
- Bedömning av spårpunkter (provning)
- Bort oanvända översättningar
+3 -2
View File
@@ -1,5 +1,6 @@
#Wed Jul 28 23:10:13 CEST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
@@ -1,191 +0,0 @@
package de.dennisguse.opentracks;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.test.espresso.DataInteraction;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.is;
/**
* Espresso test that checks the unit for activity category selected.
* Also it checks that unit is properly when user change from a pace activity to an speed one.
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoCategoryUnitTest {
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public TestRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
@Rule
public TestRule mLocaleRule = new PreferenceMetricUnitRule(context, true);
@Test
public void changeBetweenCategoryUpdateProperlyUnitTest() {
// Start record.
ViewInteraction appCompatImageButton = onView(
allOf(withId(R.id.controller_record), withContentDescription("Record"),
childAtPosition(
allOf(withId(R.id.controller_container),
childAtPosition(
withId(R.id.controller_fragment),
0)),
0),
isDisplayed()));
appCompatImageButton.perform(click());
{
// Change to running activity (pace activity).
ViewInteraction appCompatImageView = onView(
allOf(withId(R.id.stats_activity_type_icon),
childAtPosition(
childAtPosition(
withClassName(is("android.widget.ScrollView")),
0),
7)));
appCompatImageView.perform(scrollTo(), click());
DataInteraction imageView = onData(anything())
.inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view),
childAtPosition(
withClassName(is("android.widget.LinearLayout")),
0)))
.atPosition(1);
imageView.perform(click());
}
// Pace (min/km) in all fields.
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit)));
textView.perform(waitFor(2000));
textView.check(matches(withText("min/km")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit)));
textView.check(matches(withText("min/km")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit)));
textView.check(matches(withText("min/km")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit)));
textView.check(matches(withText("min/km")));
}
{
// Change to biking activity (speed activity).
ViewInteraction appCompatImageView = onView(
allOf(withId(R.id.stats_activity_type_icon),
childAtPosition(
childAtPosition(
withClassName(is("android.widget.ScrollView")),
0),
7)));
appCompatImageView.perform(scrollTo(), click());
DataInteraction imageView = onData(anything())
.inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view),
childAtPosition(
withClassName(is("android.widget.LinearLayout")),
0)))
.atPosition(9);
imageView.perform(click());
}
// Speed (km/h) in all fields.
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit)));
textView.perform(waitFor(2000));
textView.check(matches(withText("km/h")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit)));
textView.check(matches(withText("km/h")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit)));
textView.check(matches(withText("km/h")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit)));
textView.check(matches(withText("km/h")));
}
}
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
private static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Wait for milliseconds.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
}
};
}
}
@@ -0,0 +1,138 @@
package de.dennisguse.opentracks;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewAssertion;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.runner.AndroidJUnit4;
import android.content.Context;
import android.content.SharedPreferences;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.util.PreferencesUtils;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hamcrest.CoreMatchers.is;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoCustomLayoutTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Rule
public ActivityTestRule<TrackListActivity> mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule =
GrantPermissionRule.grant(
"android.permission.ACCESS_FINE_LOCATION");
@Test
public void customLayoutTest() {
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click());
// Get custom layout preferences and check all data fields are showed.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
Layout layout = PreferencesUtils.getCustomLayout(sharedPreferences, context);
onView(withId(R.id.stats_recycler_view)).check(new RecyclerViewItemCountAssertion((int) layout.getFields().stream().filter(DataField::isVisible).count()));
// stop recording
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
trackControllerStopButton.perform(veryLongTouch(1600));
}
private static class RecyclerViewItemCountAssertion implements ViewAssertion {
private final int expectedCount;
public RecyclerViewItemCountAssertion(int expectedCount) {
this.expectedCount = expectedCount;
}
@Override
public void check(View view, NoMatchingViewException noViewFoundException) {
if (noViewFoundException != null) {
throw noViewFoundException;
}
RecyclerView recyclerView = (RecyclerView) view;
RecyclerView.Adapter adapter = recyclerView.getAdapter();
assertThat(adapter.getItemCount(), is(expectedCount));
}
}
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Perform long touch.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
// Get view absolute position
int[] location = new int[2];
view.getLocationOnScreen(location);
// Offset coordinates by view position
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
// Send down event, pause, and send up
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
uiController.loopMainThreadForAtLeast(duration_ms);
MotionEvents.sendUp(uiController, down, coordinates);
}
};
}
}
@@ -0,0 +1,261 @@
package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.core.IsInstanceOf;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeoutException;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoEditTrackRecordingTest {
@Rule
public ActivityTestRule<TrackListActivity> mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule =
GrantPermissionRule.grant(
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION");
@LargeTest
@Test
public void espressoEditTrackRecordingTest() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
// wait; stay recording
trackControllerRecordButton.perform(waitFor(15000));
// open menu
ViewInteraction overflowMenuButton = onView(
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(R.id.toolbar),
2),
2),
isDisplayed()));
overflowMenuButton.perform(click());
// click on edit
ViewInteraction appCompatTextView = onView(
allOf(withId(R.id.title), withText("Edit"),
childAtPosition(
childAtPosition(
withId(R.id.content),
0),
0),
isDisplayed()));
appCompatTextView.perform(click());
// change name for "New Name"
ViewInteraction textInputEditText = onView(
allOf(withId(R.id.track_edit_name),
childAtPosition(
childAtPosition(
withClassName(is("com.google.android.material.textfield.TextInputLayout")),
0),
1)));
textInputEditText.perform(scrollTo(), replaceText("New Name"));
ViewInteraction textInputEditText2 = onView(
allOf(withId(R.id.track_edit_name), withText("New Name"),
childAtPosition(
childAtPosition(
withClassName(is("com.google.android.material.textfield.TextInputLayout")),
0),
1),
isDisplayed()));
textInputEditText2.perform(closeSoftKeyboard());
// save edition
ViewInteraction appCompatButton = onView(
allOf(withId(R.id.track_edit_save),
childAtPosition(
childAtPosition(
withClassName(is("android.widget.LinearLayout")),
3),
1),
isDisplayed()));
appCompatButton.perform(click());
//pause
trackControllerRecordButton.perform(veryLongTouch(1600));
// wait; stay paused
trackControllerRecordButton.perform(waitFor(1000));
// resume
trackControllerRecordButton.perform(click());
// wait; stay recording
trackControllerRecordButton.perform(waitFor(1000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
// check track's name is "New Name"
ViewInteraction textView = onView(
allOf(withId(R.id.stats_name_value),
withParent(withParent(IsInstanceOf.<View>instanceOf(android.widget.ScrollView.class))),
isDisplayed()));
textView.check(matches(withText("New Name")));
}
}
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
public static ViewAction waitId(final int viewId, final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
}
@Override
public void perform(final UiController uiController, final View view) {
uiController.loopMainThreadUntilIdle();
final long startTime = System.currentTimeMillis();
final long endTime = startTime + millis;
final Matcher<View> viewMatcher = withId(viewId);
do {
for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
// found view with required ID
if (viewMatcher.matches(child)) {
return;
}
}
uiController.loopMainThreadForAtLeast(50);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(new TimeoutException())
.build();
}
};
}
private static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Wait for milliseconds.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
}
};
}
private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Perform long touch.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
// Get view absolute position
int[] location = new int[2];
view.getLocationOnScreen(location);
// Offset coordinates by view position
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
// Send down event, pause, and send up
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
uiController.loopMainThreadForAtLeast(duration_ms);
MotionEvents.sendUp(uiController, down, coordinates);
}
};
}
}
@@ -112,12 +112,11 @@ public class TestDataUtil {
}
public static TrackPoint createTrackPoint(int i) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(i + 1));
trackPoint.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
trackPoint.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
trackPoint.setAccuracy((float) i / 100.0f);
trackPoint.setHorizontalAccuracy(Distance.of(i / 100.0f));
trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
trackPoint.setTime(Instant.ofEpochSecond(i + 1));
trackPoint.setSpeed(Speed.of(5f + (i / 10f)));
trackPoint.setHeartRate_bpm(100f + i);
@@ -160,13 +159,14 @@ public class TestDataUtil {
}
public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null);
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
while (trackPointIterator.hasNext()) {
trackPoints.add(trackPointIterator.next());
}
return trackPoints;
}
}
public static Pair<Track.Id, TrackStatistics> buildTrackWithTrackPoints(ContentProviderUtils contentProviderUtils, int numberOfPoints) {
Track dummyTrack = new Track();
@@ -12,16 +12,15 @@ public class TestSensorDataUtil {
public void add(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
TrackPoint tp = new TrackPoint(type);
TrackPoint tp = new TrackPoint(type, time);
int i = trackPointList.size() + 1;
tp.setLatitude(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0);
tp.setLongitude(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0);
tp.setHeartRate_bpm(hr);
tp.setCyclingCadence_rpm(cadence);
tp.setPower(power);
tp.setAccuracy(1f);
tp.setHorizontalAccuracy(Distance.of(1f));
tp.setAltitude(1f);
tp.setTime(time);
tp.setSpeed(Speed.of(5f + (i / 10f)));
tp.setAltitudeGain(3f);
tp.setAltitudeLoss(3f);
@@ -13,27 +13,25 @@ public class TrackPointTest {
@Test
public void isRecent_true() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
tp.setTime(Instant.now());
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.now());
assertTrue(tp.isRecent());
}
@Test
public void isRecent_false() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
tp.setTime(Instant.now().minus(2, ChronoUnit.MINUTES));
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.now().minus(2, ChronoUnit.MINUTES));
assertFalse(tp.isRecent());
}
@Test
public void distanceToPrevious() {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT)
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0))
.setLatitude(0)
.setLongitude(0.0001);
TrackPoint tp2 = new TrackPoint(TrackPoint.Type.TRACKPOINT)
TrackPoint tp2 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(1))
.setLatitude(0)
.setLongitude(0.0002);
@@ -135,10 +135,10 @@ public class CustomContentProviderUtilsTest {
List<TrackPoint> trackPoints = new ArrayList<>(numPoints);
for (int i = 0; i < numPoints; ++i) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT)
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(i))
.setLatitude(37.0 + (double) i / 10000.0)
.setLongitude(57.0 - (double) i / 10000.0)
.setAccuracy((float) i / 100.0f)
.setHorizontalAccuracy(Distance.of(i / 100.0f))
.setAltitude(i * 2.5);
trackPoints.add(trackPoint);
}
@@ -104,8 +104,8 @@ public class SensorDataCyclingTest {
current.compute(previous, Distance.ofMM(2150));
// then
assertEquals(2.15, current.getValue().distance.toM(), 0.01);
assertEquals(1.20, current.getValue().speed.toMPS(), 0.01);
assertEquals(2.15, current.getValue().getDistance().toM(), 0.01);
assertEquals(1.20, current.getValue().getSpeed().toMPS(), 0.01);
}
@Test
@@ -118,8 +118,8 @@ public class SensorDataCyclingTest {
current.compute(previous, Distance.ofMM(2000));
// then
assertEquals(2, current.getValue().distance.toM(), 0.01);
assertEquals(2, current.getValue().speed.toMPS(), 0.01);
assertEquals(2, current.getValue().getDistance().toM(), 0.01);
assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
}
@Test
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.fragments;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -32,7 +33,7 @@ public class TrackStubUtils {
public static final long INITIAL_TIME = 1000L;
private static final double INITIAL_LATITUDE = 22;
private static final double INITIAL_LONGITUDE = 22;
private static final float INITIAL_ACCURACY = 5;
private static final Distance INITIAL_ACCURACY = Distance.of(5);
private static final Speed INITIAL_SPEED = Speed.of(10);
private static final float INITIAL_BEARING = 3.0f;
@@ -55,7 +56,7 @@ public class TrackStubUtils {
.setLatitude(latitude)
.setLongitude(longitude)
.setAltitude(altitude)
.setAccuracy(INITIAL_ACCURACY)
.setHorizontalAccuracy(INITIAL_ACCURACY)
.setSpeed(INITIAL_SPEED)
.setBearing(INITIAL_BEARING);
}
@@ -1,11 +1,17 @@
package de.dennisguse.opentracks.io.file.importer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.net.Uri;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -19,11 +25,15 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -31,7 +41,6 @@ import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
@@ -39,25 +48,24 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p>
*/
@RunWith(AndroidJUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@@ -81,7 +89,6 @@ public class ExportImportTest {
private File tmpFile;
private Uri tmpFileUri;
private Track track;
private List<Marker> markers = new ArrayList<>();
private List<TrackPoint> trackPoints = new ArrayList<>();
@@ -99,35 +106,50 @@ public class ExportImportTest {
}
@After
public void FileTearDown() {
public void FileTearDown() throws TimeoutException {
tmpFile.deleteOnExit();
tmpFileUri = null;
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getHandlerServer().setClock(Clock.systemUTC());
}
public void setUp(boolean hasSensorDistance) throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
TrackPointCreator handlerServer = service.getHandlerServer();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
trackId = service.startNewTrack();
Distance sensorDistance = hasSensorDistance ? Distance.of(5) : null;
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance), 0);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:03Z"), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:04Z"), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:05Z"), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:06Z"), ZoneId.of("CET")));
handlerServer.setRemoteSensorManager(new BluetoothRemoteSensorManager(context));
service.pauseCurrentTrack();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:20Z"), ZoneId.of("CET")));
service.resumeCurrentTrack();
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:21Z"), ZoneId.of("CET")));
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:21Z"), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:22Z"), 3, 16, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:23Z"), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:24Z"), ZoneId.of("CET")));
handlerServer.setRemoteSensorManager(new BluetoothRemoteSensorManager(context));
service.endCurrentTrack();
track = contentProviderUtils.getTrack(trackId);
Track track = contentProviderUtils.getTrack(trackId);
track.setIcon(TRACK_ICON);
track.setCategory(TRACK_CATEGORY);
track.setDescription(TRACK_DESCRIPTION);
@@ -142,53 +164,14 @@ public class ExportImportTest {
@After
public void tearDown() {
if (trackId != null) {
contentProviderUtils.deleteTrack(context, trackId);
}
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
}
@LargeTest
@Test
public void kml_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp(true);
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
// when
// 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. trackpoints
TrackPointAssert a = new TrackPointAssert()
.noAccuracy();
a.assertEquals(trackPoints, TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId));
// 2. trackstatistics
assertTrackStatistics(false, true);
// 4. markers
assertMarkers();
}
//TODO Does not test images
@LargeTest
@Test
@@ -223,8 +206,30 @@ public class ExportImportTest {
.noAccuracy();
a.assertEquals(trackPoints, TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId));
// 2. trackstatistics
assertTrackStatistics(false, true);
// 3. trackstatistics
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
// Time
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:24Z"), importedTrackStatistics.getStopTime());
assertEquals(track.getTrackStatistics().getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(8), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(Distance.of(30), importedTrackStatistics.getTotalDistance());
// Speed
assertEquals(Speed.of(15), importedTrackStatistics.getMaxSpeed());
assertEquals(Speed.of(3.75), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(7.5), importedTrackStatistics.getAverageMovingSpeed());
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(10, importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
// 4. markers
assertMarkers();
@@ -278,14 +283,14 @@ public class ExportImportTest {
// then
// 1. track
Track trackImported = contentProviderUtils.getTrack(importTrackId);
assertNotNull(trackImported);
assertEquals(track.getCategory(), trackImported.getCategory());
assertEquals(track.getDescription(), trackImported.getDescription());
assertEquals(track.getName(), trackImported.getName());
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
//TODO exporting and importing a track icon is not yet supported by GpxTrackWriter.
//assertEquals(track.getIcon(), trackImported.getIcon());
//assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. trackpoints
// The GPX exporter does not support exporting TrackPoints without lat/lng.
@@ -300,7 +305,29 @@ public class ExportImportTest {
a.assertEquals(trackPointsWithCoordinates, TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId));
// 3. trackstatistics
assertTrackStatistics(true, false);
TrackStatistics trackStatistics = track.getTrackStatistics();
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
// Time
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Duration.ofSeconds(20), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(19), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(Distance.of(30), importedTrackStatistics.getTotalDistance());
// Speed
assertEquals(Speed.of(15), importedTrackStatistics.getMaxSpeed());
assertEquals(Speed.of(1.5), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(1.5789473684210527), importedTrackStatistics.getAverageMovingSpeed());
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(10, importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
// 4. markers
assertMarkers();
@@ -353,56 +380,49 @@ public class ExportImportTest {
}
}
private void assertTrackStatistics(boolean isGpx, boolean verifyDistance) {
double delta = isGpx ? 0.1 : 0.01;
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
private void sendLocation(TrackPointCreator handlerServer, Instant time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
location.setAltitude(altitude);
assertNotNull(importedTrack.getTrackStatistics());
TrackStatistics trackStatistics = track.getTrackStatistics();
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
// Time
assertTrue(trackStatistics.getStartTime().isBefore(trackStatistics.getStopTime())); //Just to be sure.
if (!isGpx) {
assertEquals(trackStatistics.getStartTime(), importedTrackStatistics.getStartTime());
assertEquals(trackStatistics.getStopTime(), importedTrackStatistics.getStopTime());
assertEquals(trackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(trackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
// Distance
if (verifyDistance) {
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
handlerServer.setAltitudeSumManager(new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(altitudeGain);
trackPoint.setAltitudeLoss(altitudeGain);
}
});
// Speed
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
}
handlerServer.setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
@Override
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataCyclingPower("power", "power", power));
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", heartRate));
// Altitude
assertEquals(trackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), delta);
if (isGpx) {
assertEquals(trackStatistics.getMaxAltitude(), importedTrackStatistics.getMaxAltitude(), 2);
} else {
assertEquals(trackStatistics.getMaxAltitude(), importedTrackStatistics.getMaxAltitude(), delta);
}
assertEquals(trackStatistics.getTotalAltitudeGain(), importedTrackStatistics.getTotalAltitudeGain(), delta);
assertEquals(trackStatistics.getTotalAltitudeLoss(), importedTrackStatistics.getTotalAltitudeLoss(), delta);
}
SensorDataCycling.Cadence cadence = Mockito.mock(SensorDataCycling.Cadence.class);
Mockito.when(cadence.hasValue()).thenReturn(true);
Mockito.when(cadence.getValue()).thenReturn(cyclingCadence);
sensorDataSet.set(cadence);
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) {
TrackPoint tp = new TrackPoint(latitude, longitude, Altitude.WGS84.of(altitude), Instant.ofEpochMilli(time));
tp.setAccuracy(accuracy);
tp.setSpeed(Speed.of(speed));
tp.setHeartRate_bpm(heartRate);
tp.setCyclingCadence_rpm(cyclingCadence);
tp.setPower(power);
tp.setAltitudeGain(altitudeGain);
tp.setAltitudeLoss(altitudeGain); //TODO
tp.setSensorDistance(distance);
return tp;
SensorDataCycling.DistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCycling.DistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCycling.DistanceSpeed distanceSpeed = Mockito.mock(SensorDataCycling.DistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed);
sensorDataSet.fillTrackPoint(trackPoint);
return sensorDataSet;
}
});
handlerServer.setClock(Clock.fixed(time, ZoneId.of("CET")));
handlerServer.getLocationHandler().onLocationChanged(location);
}
}
@@ -50,12 +50,12 @@ public class TrackPointAssert {
}
if (assertAccuracy) {
Assert.assertEquals(expected.hasAccuracy(), actual.hasAccuracy());
if (expected.hasAccuracy()) {
Assert.assertEquals(expected.getAccuracy(), actual.getAccuracy(), delta);
Assert.assertEquals(expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
if (expected.hasHorizontalAccuracy()) {
Assert.assertEquals(expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
}
} else {
Assert.assertFalse(actual.hasAccuracy());
Assert.assertFalse(actual.hasHorizontalAccuracy());
}
Assert.assertEquals(expected.hasSensorDistance(), actual.hasSensorDistance());
@@ -80,6 +80,19 @@ public class TrackPointAssert {
}
public void assertEquals(List<TrackPoint> expected, List<TrackPoint> actual) {
try {
Assert.assertEquals(expected.size(), actual.size());
} catch (AssertionError e) {
throw new AssertionError("Expected: " + expected + " actual: " + actual);
}
for (int i = 0; i < expected.size(); i++) {
try {
assertEquals(expected.get(i), actual.get(i));
} catch (AssertionError e) {
throw new AssertionError("Expected: " + expected.get(i) + " actual: " + actual.get(i), e);
}
}
Assert.assertEquals(expected.size(), actual.size());
for (int i = 0; i < expected.size(); i++) {
@@ -40,20 +40,21 @@ public class TrackRecordingServiceNotificationManagerTest {
@Test
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
when(trackPointMock.hasAccuracy()).thenReturn(true);
when(trackPointMock.getAccuracy()).thenReturn(999f);
when(trackPointMock.hasHorizontalAccuracy()).thenReturn(true);
when(trackPointMock.getHorizontalAccuracy()).thenReturn(Distance.of(999f));
when(trackPointMock.getSpeed()).thenReturn(Speed.of(0));
when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean())).thenReturn(notificationCompatBuilder);
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean()))
.thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
subject.setMetricUnits(true);
// when
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, 100);
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, 100);
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, 1000);
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, 100);
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(1000));
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
@@ -15,6 +15,13 @@
*/
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.ContentProvider;
import android.content.Context;
import android.content.Intent;
@@ -22,6 +29,7 @@ import android.content.SharedPreferences;
import android.os.IBinder;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
@@ -37,13 +45,16 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -51,16 +62,12 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
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;
/**
* Tests for the track recording service.
*
@@ -99,6 +106,14 @@ public class TrackRecordingServiceTest {
return new Intent(context, TrackRecordingService.class);
}
private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(0f);
trackPoint.setAltitudeLoss(0f);
}
};
@Before
public void setUp() {
// Set up the mock content resolver
@@ -123,6 +138,7 @@ public class TrackRecordingServiceTest {
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
service.getHandlerServer().setClock(Clock.systemUTC());
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
@@ -189,40 +205,34 @@ public class TrackRecordingServiceTest {
assertTrue(service.isRecording());
}
@MediumTest
@Test
public void testRecording_start() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
// when
Track.Id trackId = service.startNewTrack();
// then
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(1, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
}
@MediumTest
@Test
public void testRecording_stop() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator handlerServer = service.getHandlerServer();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
handlerServer.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager);
// when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.endCurrentTrack();
// then
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(2, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:03Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@@ -231,52 +241,79 @@ public class TrackRecordingServiceTest {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator handlerServer = service.getHandlerServer();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
handlerServer.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager);
// when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.pauseCurrentTrack();
// then
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
//when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.resumeTrack(trackId);
// then
assertTrue(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(3, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(2).getType());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:03Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:04Z"))
), trackPoints);
}
@MediumTest
@Test
public void testRecording_resumeStoppedTrack() throws TimeoutException, InterruptedException {
public void testRecording_resumeStoppedTrack() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator handlerServer = service.getHandlerServer();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
handlerServer.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.endCurrentTrack();
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
// when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.resumeTrack(trackId);
newTrackPoint(service);
handlerServer.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager);
handlerServer.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z")), Distance.of(50));
// then
assertTrue(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(4, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(2).getType());
assertEquals(TrackPoint.Type.TRACKPOINT, trackPoints.get(3).getType());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:03Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:04Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@@ -285,22 +322,32 @@ public class TrackRecordingServiceTest {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator handlerServer = service.getHandlerServer();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
handlerServer.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.pauseCurrentTrack();
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
// when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(2, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:03Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@@ -353,13 +400,23 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws TimeoutException, InterruptedException {
public void testInsertWaypointMarker_validWaypoint() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator handlerServer = service.getHandlerServer();
handlerServer.stopGPS();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
newTrackPoint(service);
handlerServer.onNewTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:03Z"))
.setLatitude(10)
.setLongitude(10)
, Distance.of(50));
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
@@ -395,11 +452,7 @@ public class TrackRecordingServiceTest {
addTrack(dummyTrack);
}
private static void newTrackPoint(TrackRecordingService trackRecordingService) throws InterruptedException {
newTrackPoint(trackRecordingService, 45.0f, 35f, 5, 10, System.currentTimeMillis());
}
static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException {
static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed) {
newTrackPoint(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis());
}
@@ -410,9 +463,10 @@ public class TrackRecordingServiceTest {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(time))
.setLongitude(longitude)
.setLatitude(latitude)
.setAccuracy(accuracy)
.setHorizontalAccuracy(Distance.of(accuracy))
.setSpeed(Speed.of(speed))
.setBearing(3.0f);
trackRecordingService.newTrackPoint(trackPoint, 50);
trackRecordingService.getHandlerServer().onNewTrackPoint(trackPoint, Distance.of(50));
}
}
@@ -24,6 +24,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
@@ -95,6 +96,7 @@ public class TrackRecordingServiceTestLocation {
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
service.getHandlerServer().stopGPS();
}
@After
@@ -108,7 +110,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_movingAccurate() throws Exception {
// given
Track.Id trackId = service.startNewTrack();
service.setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
@@ -127,50 +129,50 @@ public class TrackRecordingServiceTestLocation {
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(1)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0001)
.setLongitude(35)
.setAccuracy(2)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0001)
.setLongitude(35)
.setAccuracy(3)
.setHorizontalAccuracy(Distance.of(3))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0002)
.setLongitude(35)
.setAccuracy(4)
.setHorizontalAccuracy(Distance.of(4))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0003)
.setLongitude(35)
.setAccuracy(5)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0004)
.setLongitude(35)
.setAccuracy(6)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL)
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
@@ -181,7 +183,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_slowMovingAccurate() throws Exception {
// given
Track.Id trackId = service.startNewTrack();
service.setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
@@ -200,22 +202,22 @@ public class TrackRecordingServiceTestLocation {
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(1)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0005)
.setLongitude(35)
.setAccuracy(6)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL)
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
@@ -226,7 +228,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_idle() throws Exception {
// given
Track.Id trackId = service.startNewTrack();
service.setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
@@ -245,29 +247,29 @@ public class TrackRecordingServiceTestLocation {
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(1)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT) //TODO Why is this added? Systems is idle and not moving at all.
new TrackPoint(TrackPoint.Type.TRACKPOINT, null) //TODO Why is this added? Systems is idle and not moving at all.
.setLatitude(45)
.setLongitude(35)
.setAccuracy(2)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(6)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL)
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
@@ -278,7 +280,8 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_idle_withMovement() throws Exception {
// given
Track.Id trackId = service.startNewTrack();
service.setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().stopGPS();
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
@@ -297,36 +300,36 @@ public class TrackRecordingServiceTestLocation {
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(1)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT) //TODO Check why this trackPoint is inserted.
new TrackPoint(TrackPoint.Type.TRACKPOINT, null) //TODO Check why this trackPoint is inserted.
.setLatitude(45)
.setLongitude(35)
.setAccuracy(2)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT) //TODO Check why this trackPoint is inserted.
new TrackPoint(TrackPoint.Type.TRACKPOINT, null) //TODO Check why this trackPoint is inserted.
.setLatitude(45)
.setLongitude(35)
.setAccuracy(5)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(6)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL)
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
@@ -337,8 +340,8 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_idle_withSensorData() throws Exception {
// given
Track.Id trackId = service.startNewTrack();
service.setAltitudeSumManager(altitudeSumManager);
service.setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
@Override
public boolean isEnabled() {
@@ -346,10 +349,11 @@ public class TrackRecordingServiceTestLocation {
}
@Override
public void fill(@NonNull TrackPoint trackPoint) {
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataHeartRate("sensorName", "sensorAddress", 5f));
sensorDataSet.fillTrackPoint(trackPoint);
return sensorDataSet;
}
});
@@ -370,56 +374,56 @@ public class TrackRecordingServiceTestLocation {
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(1)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(2)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(3)
.setHorizontalAccuracy(Distance.of(3))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(4)
.setHorizontalAccuracy(Distance.of(4))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(5)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(6)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL)
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f)
@@ -431,7 +435,7 @@ public class TrackRecordingServiceTestLocation {
public void testOnLocationChangedAsync_segment() throws Exception {
// given
Track.Id trackId = service.startNewTrack();
service.setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
@@ -450,45 +454,45 @@ public class TrackRecordingServiceTestLocation {
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setAccuracy(1)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC)
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, null)
.setLatitude(45.1)
.setLongitude(35)
.setAccuracy(2)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.1)
.setLongitude(35)
.setAccuracy(3)
.setHorizontalAccuracy(Distance.of(3))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC)
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, null)
.setLatitude(45.2)
.setLongitude(35)
.setAccuracy(4)
.setHorizontalAccuracy(Distance.of(4))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT)
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.2)
.setLongitude(35)
.setAccuracy(5)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL)
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
@@ -25,6 +25,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -234,15 +235,16 @@ public class TrackRecordingServiceTestLooper {
// Insert a few points, markers and statistics.
for (int i = 0; i < 30; i++) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT)
//TODO Should send locations to LocationHandler instead of TrackPoints to HandlerServer?
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, service.getHandlerServer().createNow())
.setLongitude(35.0f + i / 10.0f)
.setLatitude(45.0f - i / 5.0f)
.setAccuracy(5)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(10))
.setBearing(3.0f);
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(sharedPreferences, context);
service.newTrackPoint(trackPoint, prefAccuracy);
Distance prefAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy(sharedPreferences, context);
service.getHandlerServer().onNewTrackPoint(trackPoint, prefAccuracy);
if (i % 7 == 0) {
service.insertMarker(null, null, null, null);
@@ -1,5 +1,9 @@
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.content.SharedPreferences;
import android.location.Location;
@@ -13,16 +17,15 @@ 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 de.dennisguse.opentracks.R;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class LocationHandlerTest {
@@ -30,7 +33,7 @@ public class LocationHandlerTest {
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@Mock
private HandlerServer handlerServer;
private TrackPointCreator handlerServer;
@InjectMocks
private LocationHandler locationHandler;
@@ -46,9 +49,13 @@ public class LocationHandlerTest {
// Let's use default values.
sharedPreferences.edit().clear().commit();
locationHandler.onSharedPreferenceChanged(context, sharedPreferences, context.getString(R.string.recording_gps_accuracy_key));
locationHandler.onSharedPreferenceChanged(context, sharedPreferences, context.getString(R.string.min_recording_interval_key));
locationHandler.onStart(context);
Mockito.when(handlerServer.createNow())
.thenReturn(Instant.now());
//TODO REMOVE
// locationHandler.onSharedPreferenceChanged(context, sharedPreferences, context.getString(R.string.recording_gps_accuracy_key));
// locationHandler.onSharedPreferenceChanged(context, sharedPreferences, context.getString(R.string.min_recording_interval_key));
locationHandler.onStart(context, sharedPreferences);
}
/**
@@ -60,7 +67,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis()));
// then
verify(handlerServer, times(1)).sendTrackPoint(any(TrackPoint.class), any(Integer.class));
verify(handlerServer, times(1)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
}
/**
@@ -76,7 +83,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis()));
// then
verify(handlerServer, times(0)).sendTrackPoint(any(TrackPoint.class), any(Integer.class));
verify(handlerServer, times(0)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
}
/**
@@ -85,14 +92,14 @@ public class LocationHandlerTest {
@Test
public void testOnLocationChanged_poorAccuracy() {
// given
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(sharedPreferences, context);
Distance prefAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy(sharedPreferences, context);
// when
locationHandler.onLocationChanged(createLocation(45f, 35f, prefAccuracy + 1, 5, System.currentTimeMillis()));
locationHandler.onLocationChanged(createLocation(45f, 35f, (float) (prefAccuracy.toM() + 1), 5, System.currentTimeMillis()));
// then
// no newTrackPoint called
verify(handlerServer, times(0)).sendTrackPoint(any(TrackPoint.class), any(Integer.class));
verify(handlerServer, times(0)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
}
@Test
@@ -105,7 +112,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
// then
verify(handlerServer, times(1)).sendTrackPoint(any(TrackPoint.class), any(Integer.class));
verify(handlerServer, times(1)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
}
/**
@@ -1,27 +1,29 @@
package de.dennisguse.opentracks.services.handlers;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.content.SharedPreferences;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class HandlerServerTest {
public class TrackPointCreatorTest {
@Mock
private Context context;
@Mock
private HandlerServer.HandlerServerInterface server;
private TrackPointCreator.Callback server;
@Mock
private LocationHandler locationHandler;
@@ -29,39 +31,31 @@ public class HandlerServerTest {
@Mock
private SharedPreferences sharedPreferences;
private HandlerServer subject;
private TrackPointCreator subject;
@Before
public void setUp() {
subject = new HandlerServer(locationHandler, server);
subject = new TrackPointCreator(locationHandler, server);
subject.start(context);
}
@After
public void tearDown() {
subject.stop(context);
}
@Test
public void onSharedPreferenceChanged() {
// when
subject.onSharedPreferenceChanged(context, sharedPreferences, null);
// then
verify(locationHandler).onSharedPreferenceChanged(context, sharedPreferences, null);
subject.stop();
}
@Ignore("ServiceExecutor disabled for #822")
@Test
public void sendTrackPoint() throws InterruptedException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
int accuracy = 50;
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, null);
Distance horizontalAccuracyThreshold = Distance.of(50);
// when
subject.sendTrackPoint(trackPoint, accuracy);
subject.onNewTrackPoint(trackPoint, horizontalAccuracyThreshold);
// then
Thread.sleep(10); // Wait for executor service
verify(server).newTrackPoint(trackPoint, accuracy);
verify(server).newTrackPoint(trackPoint, horizontalAccuracyThreshold);
}
}
@@ -1,13 +1,16 @@
package de.dennisguse.opentracks.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
public class BluetoothUtilsTest {
@@ -87,4 +90,18 @@ public class BluetoothUtilsTest {
// then
assertEquals(40, power_w);
}
@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, 12, 1});
// when
SensorDataRunning sensor = BluetoothUtils.parseRunningSpeedAndCadence("address", "sensorName", characteristic);
// then
assertEquals(Speed.of(5), sensor.getSpeed());
assertEquals(80, sensor.getCadence(), 0.01);
assertEquals(Distance.of(26.8), sensor.getTotalDistance());
}
}
@@ -10,9 +10,14 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class PreferencesUtilsTest {
@@ -64,4 +69,131 @@ public class PreferencesUtilsTest {
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
}
@Test
public void testGetCustomLayout_default() {
// given
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear();
editor.commit();
// when
Layout layout = PreferencesUtils.getCustomLayout(sharedPreferences, context);
// then
assertTrue(layout.getFields().size() > 0);
assertEquals(layout.getProfile(), context.getString(R.string.default_activity_default));
assertTrue(layout.getFields().stream().anyMatch(DataField::isVisible));
}
@Test
public void testGetCustomLayout_1() {
// given
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(
context.getString(R.string.stats_custom_layout_fields_key),
"run;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;");
editor.apply();
// when
Layout layout = PreferencesUtils.getCustomLayout(sharedPreferences, context);
// then
assertEquals(layout.getFields().size(), 4);
assertEquals(layout.getProfile(), "run");
assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(layout.getFields().get(0).isVisible());
assertTrue(layout.getFields().get(0).isPrimary());
assertEquals(layout.getFields().get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertTrue(layout.getFields().get(1).isVisible());
assertFalse(layout.getFields().get(1).isPrimary());
assertEquals(layout.getFields().get(2).getKey(), context.getString(R.string.stats_custom_layout_average_moving_speed_key));
assertFalse(layout.getFields().get(2).isVisible());
assertTrue(layout.getFields().get(2).isPrimary());
assertEquals(layout.getFields().get(3).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertFalse(layout.getFields().get(3).isVisible());
assertFalse(layout.getFields().get(3).isPrimary());
}
@Test
public void testGetCustomLayout_coordinatesIsWide() {
// given
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(
context.getString(R.string.stats_custom_layout_fields_key),
"walking;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_coordinates_key) + ",0,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;");
editor.apply();
// when
Layout layout = PreferencesUtils.getCustomLayout(sharedPreferences, context);
// then
assertEquals(layout.getFields().size(), 4);
assertEquals(layout.getProfile(), "walking");
assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(layout.getFields().get(0).isVisible());
assertTrue(layout.getFields().get(0).isPrimary());
assertEquals(layout.getFields().get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertTrue(layout.getFields().get(1).isVisible());
assertFalse(layout.getFields().get(1).isPrimary());
assertEquals(layout.getFields().get(2).getKey(), context.getString(R.string.stats_custom_layout_coordinates_key));
assertFalse(layout.getFields().get(2).isVisible());
assertTrue(layout.getFields().get(2).isPrimary());
assertTrue(layout.getFields().get(2).isWide());
assertEquals(layout.getFields().get(3).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertFalse(layout.getFields().get(3).isVisible());
assertFalse(layout.getFields().get(3).isPrimary());
}
@Test
public void testSetCustomLayout() {
// given
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
Layout layoutSrc = new Layout("road cycling");
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false));
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, false, false));
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_average_moving_speed_key), context.getString(R.string.stats_average_moving_speed), false, true, false));
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), false, false, false));
// when
PreferencesUtils.setCustomLayout(sharedPreferences, context, layoutSrc);
// then
String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layout_fields_key), null);
assertNotNull(csv);
assertEquals(csv,
"road cycling;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;");
Layout layoutDst = PreferencesUtils.getCustomLayout(sharedPreferences, context);
assertEquals(layoutSrc.getProfile(), layoutDst.getProfile());
assertEquals(layoutSrc.getFields().size(), layoutDst.getFields().size());
for (int i = 0; i < layoutSrc.getFields().size(); i++) {
assertEquals(layoutSrc.getFields().get(i).getKey(), layoutDst.getFields().get(i).getKey());
assertEquals(layoutSrc.getFields().get(i).isVisible(), layoutDst.getFields().get(i).isVisible());
assertEquals(layoutSrc.getFields().get(i).isPrimary(), layoutDst.getFields().get(i).isPrimary());
}
}
}
@@ -0,0 +1,68 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
@RunWith(AndroidJUnit4.class)
public class StatisticsUtilsTest extends TestCase {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void testEmptyValue() {
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_distance_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_total_time_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_moving_time_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_max_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_altitude_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_gain_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_loss_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_fastest_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_coordinates), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_coordinates_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_heart_rate_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_cadence_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_power_key)));
}
@Test
public void testFilterVisible() {
// given
Layout layout = new Layout("profile");
layout.addField(new DataField("key1", "title1", true, true, false));
layout.addField(new DataField("key2", "title2", false, true, false));
layout.addField(new DataField("key3", "title3", true, true, false));
layout.addField(new DataField("key4", "title4", false, true, false));
layout.addField(new DataField("key5", "title5", true, true, false));
// when
Layout resultTrue = StatisticsUtils.filterVisible(layout, true);
Layout resultFalse = StatisticsUtils.filterVisible(layout, false);
// then
assertEquals(resultTrue.getFields().size(), 3);
assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key1")));
assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key3")));
assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key5")));
assertEquals(resultFalse.getFields().size(), 2);
assertTrue(resultFalse.getFields().stream().anyMatch(f -> f.getKey().equals("key2")));
assertTrue(resultFalse.getFields().stream().anyMatch(f -> f.getKey().equals("key4")));
}
}
@@ -0,0 +1,195 @@
package de.dennisguse.opentracks.viewmodels;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class StatisticDataBuilderTest extends TestCase {
private final Context context = ApplicationProvider.getApplicationContext();
@Mock
private Track trackMock;
@Mock
private TrackStatistics trackStatisticsMock;
@Mock
private TrackRecordingService.RecordingData recordingDataMock;
@Mock
private SensorStatistics sensorStatisticsMock;
@Test
public void testFromRecordingData() {
when(trackMock.getTrackStatistics()).thenReturn(trackStatisticsMock);
when(trackStatisticsMock.getTotalTime()).thenReturn(Duration.ofMillis(0));
when(trackStatisticsMock.getMovingTime()).thenReturn(Duration.ofMillis(0));
when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
when(recordingDataMock.getTrackStatistics()).thenReturn(trackStatisticsMock);
// given
Layout layout = new Layout(context.getString(R.string.default_activity_default));
layout.addField(context.getString(R.string.stats_custom_layout_total_time_key), context.getString(R.string.stats_total_time), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), false, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), false, true, false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true);
// then
assertEquals(statisticDataList.size(), 3);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_total_time))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_moving_time))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_distance))));
}
@Test
public void testFromRecordingData_allFields() {
when(trackMock.getTrackStatistics()).thenReturn(trackStatisticsMock);
when(trackStatisticsMock.getTotalTime()).thenReturn(Duration.ofMillis(0));
when(trackStatisticsMock.getMovingTime()).thenReturn(Duration.ofMillis(0));
when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
when(recordingDataMock.getTrackStatistics()).thenReturn(trackStatisticsMock);
// given
Layout layout = new Layout(context.getString(R.string.default_activity_default));
layout.addField(context.getString(R.string.stats_custom_layout_total_time_key), context.getString(R.string.stats_total_time), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_average_moving_speed_key), context.getString(R.string.stats_average_moving_speed), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_average_speed_key), context.getString(R.string.stats_average_speed), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_pace_key), context.getString(R.string.stats_pace), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_average_moving_pace_key), context.getString(R.string.stats_average_moving_pace), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_average_pace_key), context.getString(R.string.stats_average_pace), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_fastest_pace_key), context.getString(R.string.stats_fastest_pace), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_altitude_key), context.getString(R.string.stats_altitude), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_gain_key), context.getString(R.string.stats_gain), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_loss_key), context.getString(R.string.stats_loss), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_coordinates_key), context.getString(R.string.stats_coordinates), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_heart_rate_key), context.getString(R.string.stats_sensors_heart_rate), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_cadence_key), context.getString(R.string.stats_sensors_cadence), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_power_key), context.getString(R.string.stats_sensors_power), true, true, false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true);
// then
assertEquals(statisticDataList.size(), 18);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_total_time))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_moving_time))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_distance))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_speed))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_moving_speed))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_speed))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_max_speed))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_pace))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_moving_pace))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_pace))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_fastest_pace))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_altitude))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_gain))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_loss))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_coordinates))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_heart_rate))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_cadence))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_power))));
}
@Test
public void testFromSensorStatistics_onlyHeartRate() {
when(sensorStatisticsMock.hasHeartRate()).thenReturn(true);
when(sensorStatisticsMock.getMaxHeartRate()).thenReturn(200f);
when(sensorStatisticsMock.getAvgHeartRate()).thenReturn(150f);
when(sensorStatisticsMock.hasCadence()).thenReturn(false);
when(sensorStatisticsMock.hasPower()).thenReturn(false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock);
// then
assertEquals(statisticDataList.size(), 2);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_max))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_avg))));
}
@Test
public void testFromSensorStatistics_onlyCadence() {
when(sensorStatisticsMock.hasHeartRate()).thenReturn(false);
when(sensorStatisticsMock.hasCadence()).thenReturn(true);
when(sensorStatisticsMock.getAvgCadence()).thenReturn(90f);
when(sensorStatisticsMock.getMaxCadence()).thenReturn(110f);
when(sensorStatisticsMock.hasPower()).thenReturn(false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock);
// then
assertEquals(statisticDataList.size(), 2);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_max))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_avg))));
}
@Test
public void testFromSensorStatistics_onlyPower() {
when(sensorStatisticsMock.hasHeartRate()).thenReturn(false);
when(sensorStatisticsMock.hasCadence()).thenReturn(false);
when(sensorStatisticsMock.hasPower()).thenReturn(true);
when(sensorStatisticsMock.getAvgPower()).thenReturn(300f);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_power_avg))));
// then
assertEquals(statisticDataList.size(), 1);
}
@Test
public void testFromSensorStatistics() {
when(sensorStatisticsMock.hasHeartRate()).thenReturn(true);
when(sensorStatisticsMock.getMaxHeartRate()).thenReturn(200f);
when(sensorStatisticsMock.getAvgHeartRate()).thenReturn(150f);
when(sensorStatisticsMock.hasCadence()).thenReturn(true);
when(sensorStatisticsMock.getAvgCadence()).thenReturn(90f);
when(sensorStatisticsMock.getMaxCadence()).thenReturn(110f);
when(sensorStatisticsMock.hasPower()).thenReturn(true);
when(sensorStatisticsMock.getAvgPower()).thenReturn(300f);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock);
// then
assertEquals(statisticDataList.size(), 5);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_max))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_avg))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_max))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_avg))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_power_avg))));
}
}
+18 -4
View File
@@ -73,17 +73,21 @@ limitations under the License.
<activity android:name=".io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity" />
<activity android:name=".io.file.importer.ImportActivity">
<activity
android:name=".io.file.importer.ImportActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx+xml" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx" />
</intent-filter>
@@ -142,6 +146,7 @@ limitations under the License.
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kmz" />
</intent-filter>
@@ -257,7 +262,9 @@ limitations under the License.
</intent-filter>
</activity>
<activity android:name=".MarkerDetailActivity">
<activity
android:name=".MarkerDetailActivity"
android:exported="true">
<intent-filter>
<action android:name="de.dennisguse.opentracks.MarkerDetails" />
<category android:name="android.intent.category.DEFAULT" />
@@ -268,6 +275,7 @@ limitations under the License.
<activity
android:name=".MarkerListActivity"
android:exported="true"
android:label="@string/menu_markers"
android:launchMode="singleTop">
<intent-filter>
@@ -280,6 +288,7 @@ limitations under the License.
<activity
android:name=".TrackListActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/SplashTheme">
<intent-filter>
@@ -307,14 +316,19 @@ limitations under the License.
<activity android:name=".io.file.exporter.ExportActivity" />
<activity android:name=".settings.SettingsActivity" />
<activity android:name=".settings.SettingsCustomLayoutActivity" />
<activity android:name=".ShowOnMapProxyActivity$KML">
<activity
android:name=".ShowOnMapProxyActivity$KML"
android:exported="true">
<intent-filter android:label="@string/open_track_as_KML">
<action android:name="Intent.OpenTracks-Dashboard" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".ShowOnMapProxyActivity$GPX">
<activity
android:name=".ShowOnMapProxyActivity$GPX"
android:exported="true">
<intent-filter android:label="@string/open_track_as_GPX">
<action android:name="Intent.OpenTracks-Dashboard" />
<category android:name="android.intent.category.DEFAULT" />
@@ -18,8 +18,6 @@ package de.dennisguse.opentracks;
import android.widget.Toast;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Collectors;
@@ -41,8 +39,6 @@ import de.dennisguse.opentracks.services.TrackDeleteServiceConnection;
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteServiceConnection.Listener {
private static final String TAG = AbstractTrackDeleteActivity.class.getSimpleName();
private TrackDeleteServiceConnection trackDeleteServiceConnection;
@Override
@@ -97,10 +93,7 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
protected abstract void onDeleteConfirmed();
@Nullable
protected Track.Id getRecordingTrackId() {
return null;
}
protected abstract Track.Id getRecordingTrackId();
@Override
public void connected() {
@@ -27,7 +27,6 @@ import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
@@ -393,24 +392,22 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
private void requestGPSPermissions() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
return;
}
ActivityResultLauncher<String[]> locationPermissionRequest = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
Boolean fineLocationGranted = result.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false);
if (fineLocationGranted == null || !fineLocationGranted) {
Boolean coarseLocationGranted = result.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false);
if (fineLocationGranted == null || !fineLocationGranted
|| coarseLocationGranted == null || !coarseLocationGranted) {
Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show();
finish();
}
}
);
String[] permissions;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
permissions = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};
} else {
permissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
}
String[] permissions = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};
locationPermissionRequest.launch(permissions);
}
@@ -0,0 +1,16 @@
package de.dennisguse.opentracks.adapters;
enum CustomLayoutFieldType {
SHORT(0),
WIDE(1);
private final int value;
CustomLayoutFieldType(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
@@ -50,7 +50,7 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
final long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
final int cacheSize = (int) (Runtime.getRuntime().maxMemory() - usedMemory) / 8;
memoryCache = new LruCache<String, Bitmap>(cacheSize) {
memoryCache = new LruCache<>(cacheSize) {
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap bitmap) {
return bitmap.getByteCount();
@@ -1,91 +0,0 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<SensorDataModel> sensorDataList;
private final Context context;
public SensorsAdapter(Context context) {
this.context = context;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.sensor_item, parent, false);
return new SensorsAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
SensorDataModel sensorDataModel = sensorDataList.get(position);
viewHolder.setData(sensorDataModel);
}
@Override
public int getItemCount() {
if (sensorDataList == null) {
return 0;
} else {
return sensorDataList.size();
}
}
public List<SensorDataModel> swapData(List<SensorDataModel> data) {
if (sensorDataList == data) {
return null;
}
sensorDataList = data;
if (data != null) {
this.notifyDataSetChanged();
}
return data;
}
private class ViewHolder extends RecyclerView.ViewHolder {
final TextView label;
final TextView sensorValue;
final TextView value;
final TextView unit;
public ViewHolder(@NonNull View itemView) {
super(itemView);
label = itemView.findViewById(R.id.stats_sensor_label);
sensorValue = itemView.findViewById(R.id.stats_sensor_sensor_value);
value = itemView.findViewById(R.id.stats_sensor_value);
unit = itemView.findViewById(R.id.stats_sensor_unit);
}
public void setData(SensorDataModel sensorDataModel) {
String sensorName = sensorDataModel.getSensorName();
String sensorValue = sensorDataModel.hasValue() ? sensorDataModel.getSensorValue() : context.getString(R.string.value_unknown);
this.label.setText(context.getString(sensorDataModel.getLabelId()));
if (sensorName == null) {
this.sensorValue.setVisibility(View.GONE);
} else {
this.sensorValue.setText(sensorName);
}
this.value.setText(sensorValue);
this.unit.setText(context.getString(sensorDataModel.getUnitId()));
}
}
}
@@ -0,0 +1,118 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Optional;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.util.StatisticsUtils;
public class SettingsCustomLayoutAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Layout layout;
private final Context context;
private final SettingsCustomLayoutItemClickListener itemClickListener;
public SettingsCustomLayoutAdapter(Context context, SettingsCustomLayoutItemClickListener itemClickListener, Layout layout) {
this.context = context;
this.itemClickListener = itemClickListener;
this.layout = layout;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.custom_stats_item, parent, false);
return new SettingsCustomLayoutAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SettingsCustomLayoutAdapter.ViewHolder viewHolder = (SettingsCustomLayoutAdapter.ViewHolder) holder;
DataField field = layout.getFields().get(position);
viewHolder.itemView.setTag(field.getKey());
viewHolder.title.setText(field.getTitle());
viewHolder.value.setText(StatisticsUtils.emptyValue(context, field.getKey()));
viewHolder.title.setTextAppearance(context, field.isVisible() ? (field.isPrimary() ? R.style.StatsPrimaryLabel : R.style.StatsSecondaryLabel) : R.style.StatsLabelHidden);
viewHolder.value.setTextAppearance(context, field.isVisible() ? (field.isPrimary() ? R.style.StatsPrimaryValue : R.style.StatsSecondaryValue) : R.style.StatsValueHidden);
viewHolder.statusIcon.setVisibility(field.isVisible() ? View.GONE : View.VISIBLE);
viewHolder.statusIcon.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_baseline_visibility_off_24));
viewHolder.moveIcon.setVisibility(field.isVisible() ? View.VISIBLE : View.GONE);
}
@Override
public int getItemCount() {
if (layout == null) {
return 0;
} else {
return layout.getFields().size();
}
}
@Override
public int getItemViewType(int position) {
return layout.getFields().get(position).isWide() ? CustomLayoutFieldType.WIDE.value() : CustomLayoutFieldType.SHORT.value();
}
public boolean isItemWide(int position) {
return getItemViewType(position) == CustomLayoutFieldType.WIDE.value();
}
public DataField getItem(int position) {
return layout.getFields().get(position);
}
public void swapValues(Layout data) {
this.layout = data;
if (this.layout != null) {
this.notifyDataSetChanged();
}
}
public Layout move(int fromPosition, int toPosition) {
layout.moveField(fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
return layout;
}
private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView title;
final TextView value;
final TextView unit;
final ImageView statusIcon;
final ImageView moveIcon;
public ViewHolder(@NonNull View itemView) {
super(itemView);
title = itemView.findViewById(R.id.stats_description_main);
value = itemView.findViewById(R.id.stats_value);
unit = itemView.findViewById(R.id.stats_unit);
statusIcon = itemView.findViewById(R.id.stats_icon_show_status);
moveIcon = itemView.findViewById(R.id.stats_iv_drag_indicator);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
String statTitle = (String) view.getTag();
Optional<DataField> optionalField = layout.getFields().stream().filter(f -> f.getKey().equals(statTitle)).findFirst();
optionalField.ifPresent(itemClickListener::onSettingsCustomLayoutItemClicked);
}
}
public interface SettingsCustomLayoutItemClickListener {
void onSettingsCustomLayoutItemClicked(@NonNull DataField field);
}
}
@@ -0,0 +1,140 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.viewmodels.StatisticData;
public class StatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<StatisticData> statisticDataList;
private final Context context;
public StatisticsAdapter(Context context) {
this.context = context;
this.statisticDataList = new ArrayList<>();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item, parent, false);
return new StatisticsAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
StatisticsAdapter.ViewHolder viewHolder = (StatisticsAdapter.ViewHolder) holder;
StatisticData statisticData = statisticDataList.get(position);
viewHolder.setData(statisticData);
}
@Override
public int getItemCount() {
if (statisticDataList == null) {
return 0;
} else {
return statisticDataList.size();
}
}
@Override
public int getItemViewType(int position) {
return statisticDataList.get(position).getField().isWide() ? CustomLayoutFieldType.WIDE.value() : CustomLayoutFieldType.SHORT.value();
}
public boolean isItemWide(int position) {
return getItemViewType(position) == CustomLayoutFieldType.WIDE.value();
}
public List<StatisticData> swapData(List<StatisticData> data) {
if (statisticDataList == data) {
return null;
}
statisticDataList = data;
if (data != null) {
this.notifyDataSetChanged();
}
return data;
}
public static class WithRecordedLayout extends StatisticsAdapter {
public WithRecordedLayout(Context context) {
super(context);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_recorded_item, parent, false);
return new StatisticsAdapter.ViewRecordedHolder(view);
}
}
private class ViewHolder extends RecyclerView.ViewHolder {
final TextView value;
final TextView unit;
final TextView descMain;
final TextView descSecondary;
public ViewHolder(@NonNull View itemView) {
super(itemView);
value = itemView.findViewById(R.id.stats_value);
unit = itemView.findViewById(R.id.stats_unit);
descMain = itemView.findViewById(R.id.stats_description_main);
descSecondary = itemView.findViewById(R.id.stats_description_secondary);
}
public void setData(StatisticData statisticData) {
if (statisticData == null) {
return;
}
this.value.setText(statisticData.hasValue() ? statisticData.getValue() : context.getString(R.string.value_unknown));
this.value.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.StatsPrimaryValue : R.style.StatsSecondaryValue);
this.unit.setText(statisticData.getUnit());
this.descMain.setText(statisticData.getField().getTitle());
this.descMain.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.StatsPrimaryLabel : R.style.StatsSecondaryLabel);
if (statisticData.hasDescription()) {
this.descSecondary.setVisibility(View.VISIBLE);
this.descSecondary.setText(statisticData.getDescription());
} else {
this.descSecondary.setVisibility(View.GONE);
}
}
}
private class ViewRecordedHolder extends ViewHolder {
public ViewRecordedHolder(@NonNull View itemView) {
super(itemView);
}
public void setData(StatisticData statisticData) {
if (statisticData == null) {
return;
}
this.value.setText(statisticData.hasValue() ? statisticData.getValue() : context.getString(R.string.value_unknown));
this.unit.setText(statisticData.getUnit());
this.descMain.setText(statisticData.getField().getTitle());
if (statisticData.hasDescription()) {
this.descSecondary.setVisibility(View.VISIBLE);
this.descSecondary.setText(statisticData.getDescription());
} else {
this.descSecondary.setVisibility(View.GONE);
}
}
}
}
@@ -27,12 +27,10 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
@@ -43,8 +41,8 @@ import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.EGM2008Utils;
/**
* Track data hub.
@@ -76,7 +74,7 @@ public class TrackDataHub {
private final ContentProviderUtils contentProviderUtils;
private final int targetNumPoints;
private EGM2008Utils.EGM2008Correction egm2008Correction;
private final EGM2008CorrectionManager egm2008Correction = new EGM2008CorrectionManager();
//TODO Check if this is needed.
private HandlerThread handlerThread;
@@ -209,13 +207,6 @@ public class TrackDataHub {
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId());
}
/**
* Returns true if the selected track is paused.
*/
public boolean isSelectedTrackPaused() {
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId()) && recordingStatus.isPaused();
}
/**
* Loads data for all listeners. To be run in the {@link #handler} thread.
*/
@@ -360,7 +351,7 @@ public class TrackDataHub {
break;
}
correctAltitude(trackPoint);
egm2008Correction.correctAltitude(context, trackPoint);
if (localFirstSeenTrackPointId == null) {
localFirstSeenTrackPointId = trackPointId;
@@ -401,23 +392,7 @@ public class TrackDataHub {
listeners.stream().forEach(TrackDataListener::onNewTrackPointsDone);
}
private void correctAltitude(TrackPoint trackPoint) {
if (!trackPoint.hasLocation() || !trackPoint.hasAltitude()) {
Log.d(TAG, "No altitude correction necessary.");
return;
}
if (egm2008Correction == null || !egm2008Correction.canCorrect(trackPoint.getLocation())) {
try {
egm2008Correction = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
} catch (IOException e) {
Log.e(TAG, "Could not load altitude correction for " + trackPoint, e);
return;
}
}
trackPoint.setAltitude(Altitude.EGM2008.of(egm2008Correction.correctAltitude(trackPoint.getLocation())));
}
/**
* Resets the track points sampling states.
@@ -0,0 +1,101 @@
package de.dennisguse.opentracks.content.data;
import android.content.Context;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.CsvConstants;
public class DataField {
public static final String YES_VALUE = "1";
public static final String NOT_VALUE = "0";
private final String key;
private final String title;
private boolean isVisible;
private boolean isPrimary;
private final boolean isWide;
public DataField(String key, String title, boolean isVisible, boolean isPrimary, boolean isWide) {
this.key = key;
this.title = title;
this.isVisible = isVisible;
this.isPrimary = isPrimary;
this.isWide = isWide;
}
public String getKey() {
return key;
}
public String getTitle() {
return title;
}
public boolean isVisible() {
return isVisible;
}
public void toggleVisibility() {
isVisible = !isVisible;
}
public boolean isPrimary() {
return isPrimary;
}
public void togglePrimary() {
isPrimary = !isPrimary;
}
public boolean isWide() {
return isWide;
}
public String toCsv() {
String visible = this.isVisible ? YES_VALUE : NOT_VALUE;
String primary = this.isPrimary ? YES_VALUE : NOT_VALUE;
return key + CsvConstants.ITEM_SEPARATOR + visible + CsvConstants.ITEM_SEPARATOR + primary;
}
public static String getTitleByKey(Context context, String key) {
if (key.equals(context.getString(R.string.stats_custom_layout_total_time_key))) {
return context.getString(R.string.stats_total_time);
} else if (key.equals(context.getString(R.string.stats_custom_layout_moving_time_key))) {
return context.getString(R.string.stats_moving_time);
} else if (key.equals(context.getString(R.string.stats_custom_layout_distance_key))) {
return context.getString(R.string.stats_distance);
} else if (key.equals(context.getString(R.string.stats_custom_layout_speed_key))) {
return context.getString(R.string.stats_speed);
} else if (key.equals(context.getString(R.string.stats_custom_layout_pace_key))) {
return context.getString(R.string.stats_pace);
} else if (key.equals(context.getString(R.string.stats_custom_layout_average_moving_speed_key))) {
return context.getString(R.string.stats_average_moving_speed);
} else if (key.equals(context.getString(R.string.stats_custom_layout_average_speed_key))) {
return context.getString(R.string.stats_average_speed);
} else if (key.equals(context.getString(R.string.stats_custom_layout_max_speed_key))) {
return context.getString(R.string.stats_max_speed);
} else if (key.equals(context.getString(R.string.stats_custom_layout_average_moving_pace_key))) {
return context.getString(R.string.stats_average_moving_pace);
} else if (key.equals(context.getString(R.string.stats_custom_layout_average_pace_key))) {
return context.getString(R.string.stats_average_pace);
} else if (key.equals(context.getString(R.string.stats_custom_layout_fastest_pace_key))) {
return context.getString(R.string.stats_fastest_pace);
} else if (key.equals(context.getString(R.string.stats_custom_layout_altitude_key))) {
return context.getString(R.string.stats_altitude);
} else if (key.equals(context.getString(R.string.stats_custom_layout_gain_key))) {
return context.getString(R.string.stats_gain);
} else if (key.equals(context.getString(R.string.stats_custom_layout_loss_key))) {
return context.getString(R.string.stats_loss);
} else if (key.equals(context.getString(R.string.stats_custom_layout_coordinates_key))) {
return context.getString(R.string.stats_coordinates);
} else if (key.equals(context.getString(R.string.stats_custom_layout_heart_rate_key))) {
return context.getString(R.string.stats_sensors_heart_rate);
} else if (key.equals(context.getString(R.string.stats_custom_layout_cadence_key))) {
return context.getString(R.string.stats_sensors_cadence);
} else if (key.equals(context.getString(R.string.stats_custom_layout_power_key))) {
return context.getString(R.string.stats_sensors_power);
} else {
throw new RuntimeException("It doesn't exists a field with key: " + key);
}
}
}
@@ -25,7 +25,15 @@ public class Distance {
}
public static Distance ofMM(double distance_mm) {
return of(distance_mm * UnitConversions.MM_TO_M);
return of(0.001 * distance_mm);
}
public static Distance ofCM(double distance_cm) {
return of(0.01 * distance_cm);
}
public static Distance ofDM(double distance_dm) {
return of(0.1 * distance_dm);
}
public static Distance one(boolean metricUnit) {
@@ -0,0 +1,42 @@
package de.dennisguse.opentracks.content.data;
import java.util.ArrayList;
import java.util.List;
public class Layout {
private final String profile;
private final List<DataField> dataFields = new ArrayList<>();
public Layout(String profile) {
this.profile = profile;
}
public void addField(String key, String title, boolean visible, boolean primary, boolean isWide) {
dataFields.add(new DataField(key, title, visible, primary, isWide));
}
public void addField(DataField dataField) {
dataFields.add(dataField);
}
public void addFields(List<DataField> dataFields) {
this.dataFields.addAll(dataFields);
}
public void removeField(DataField dataField) {
dataFields.remove(dataField);
}
public List<DataField> getFields() {
return new ArrayList<>(dataFields);
}
public void moveField(int from, int to) {
DataField dataFieldToMove = dataFields.remove(from);
dataFields.add(to, dataFieldToMove);
}
public String getProfile() {
return profile;
}
}
@@ -49,7 +49,8 @@ public final class Marker {
private final Instant time;
private Double latitude;
private Double longitude;
private Float accuracy;
@Deprecated //Not needed
private Distance accuracy;
private Altitude altitude;
private Float bearing;
@@ -96,7 +97,7 @@ public final class Marker {
public void setTrackPoint(TrackPoint trackPoint) {
this.latitude = trackPoint.getLatitude();
this.longitude = trackPoint.getLongitude();
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
if (trackPoint.hasHorizontalAccuracy()) this.accuracy = trackPoint.getHorizontalAccuracy();
if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude();
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
}
@@ -173,7 +174,7 @@ public final class Marker {
location.setBearing(bearing);
}
if (hasAccuracy()) {
location.setAccuracy(accuracy);
location.setAccuracy((float) accuracy.toM());
}
if (hasAltitude()) {
location.setAltitude(altitude.toM());
@@ -202,11 +203,11 @@ public final class Marker {
return accuracy != null;
}
public Float getAccuracy() {
public Distance getAccuracy() {
return accuracy;
}
public void setAccuracy(Float accuracy) {
public void setAccuracy(Distance accuracy) {
this.accuracy = accuracy;
}
@@ -308,7 +309,7 @@ public final class Marker {
parcel.writeLong(id);
}
public static final Parcelable.Creator<Id> CREATOR = new Parcelable.Creator<Id>() {
public static final Parcelable.Creator<Id> CREATOR = new Parcelable.Creator<>() {
public Id createFromParcel(Parcel in) {
return new Id(in.readLong());
}
@@ -23,10 +23,6 @@ public class Speed {
return of(Float.parseFloat(speed_mps));
}
public static Speed invalid() {
return of(Double.NaN);
}
public static Speed zero() {
return of(0.0);
}
@@ -167,7 +167,7 @@ public class Track {
parcel.writeLong(id);
}
public static final Parcelable.Creator<Track.Id> CREATOR = new Parcelable.Creator<Track.Id>() {
public static final Parcelable.Creator<Track.Id> CREATOR = new Parcelable.Creator<>() {
public Track.Id createFromParcel(Parcel in) {
return new Track.Id(in.readLong());
}
@@ -49,11 +49,11 @@ public class TrackPoint {
private TrackPoint.Id id;
@NonNull
private Instant time;
private final Instant time;
private Double latitude;
private Double longitude;
private Float accuracy;
private Distance horizontalAccuracy;
private Altitude altitude;
private Speed speed;
private Float bearing;
@@ -96,18 +96,13 @@ public class TrackPoint {
private Float altitudeGain_m = null;
private Float altitudeLoss_m = null;
public TrackPoint(@NonNull Type type) {
public TrackPoint(@NonNull Type type, @NonNull Instant time) {
this.type = type;
this.time = Instant.now(); //TODO This may result in #800
}
public TrackPoint(@NonNull Type type, Instant time) {
this(type);
this.time = time;
}
public TrackPoint(@NonNull Location location) {
this(Type.TRACKPOINT, location, Instant.now()); //TODO This may result in #800
public TrackPoint(@NonNull Location location, @NonNull Instant time) {
this(Type.TRACKPOINT, location, time);
}
public TrackPoint(@NonNull Type type, @NonNull Location location, @NonNull Instant time) {
@@ -117,7 +112,7 @@ public class TrackPoint {
this.longitude = location.getLongitude();
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
this.accuracy = location.hasAccuracy() ? location.getAccuracy() : null;
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null;
//TODO Should we copy the bearing?
}
@@ -130,28 +125,10 @@ public class TrackPoint {
this.altitude = altitude;
}
@Deprecated //See #316
public static TrackPoint createSegmentStartManual() {
return createSegmentStartManualWithTime(Instant.now());
}
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_START_MANUAL, time);
}
@Deprecated //See #316
public static TrackPoint createSegmentStartAutomatic() {
return createSegmentStartAutomaticWithTime(Instant.now());
}
public static TrackPoint createSegmentStartAutomaticWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_START_AUTOMATIC, time);
}
public static TrackPoint createSegmentEnd() {
return createSegmentEndWithTime(Instant.now());
}
public static TrackPoint createSegmentEndWithTime(Instant time) {
return new TrackPoint(Type.SEGMENT_END_MANUAL, time);
}
@@ -220,12 +197,15 @@ public class TrackPoint {
if (hasBearing()) {
location.setBearing(bearing);
}
if (hasAccuracy()) {
location.setAccuracy(accuracy);
if (hasHorizontalAccuracy()) {
location.setAccuracy((float) horizontalAccuracy.toM());
}
if (hasAltitude()) {
location.setAltitude(altitude.toM());
}
if (hasSpeed()) {
location.setSpeed((float) speed.toMPS());
}
return location;
}
@@ -256,15 +236,11 @@ public class TrackPoint {
return this;
}
@NonNull
public Instant getTime() {
return time;
}
public TrackPoint setTime(Instant time) {
this.time = time;
return this;
}
public boolean isRecent() {
return Instant.now()
.isBefore(time.plus(MAX_LOCATION_AGE));
@@ -320,33 +296,33 @@ public class TrackPoint {
return this;
}
public boolean hasAccuracy() {
return accuracy != null;
public boolean hasHorizontalAccuracy() {
return horizontalAccuracy != null;
}
public float getAccuracy() {
return accuracy;
public Distance getHorizontalAccuracy() {
return horizontalAccuracy;
}
public TrackPoint setAccuracy(float horizontalAccuracy) {
this.accuracy = horizontalAccuracy;
public TrackPoint setHorizontalAccuracy(Distance horizontalAccuracy) {
this.horizontalAccuracy = horizontalAccuracy;
return this;
}
@Nullable
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
public Distance distanceToPrevious(TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
if (!(hasLocation() && previous.hasLocation())) {
if (previous == null || !(hasLocation() && previous.hasLocation())) {
return null;
}
return Distance.of(getLocation().distanceTo(previous.getLocation()));
}
public boolean fulfillsAccuracy(int poorAccuracy) {
return hasAccuracy() && accuracy < poorAccuracy;
public boolean fulfillsAccuracy(Distance thresholdHorizontalAccuracy) {
return hasHorizontalAccuracy() && horizontalAccuracy.lessThan(thresholdHorizontalAccuracy);
}
//TODO Bearing requires a location; what do we do if we don't have any?
@@ -424,11 +400,11 @@ public class TrackPoint {
return result;
}
result += ": lat=" + getLatitude() + " lng=" + getLongitude();
if (!hasAccuracy()) {
if (!hasHorizontalAccuracy()) {
return result;
}
return result + " acc=" + getAccuracy();
return result + " acc=" + getHorizontalAccuracy();
}
public static class Id {
@@ -45,7 +45,7 @@ public interface TrackPointsColumns extends BaseColumns {
String TIME = "time";
String ALTITUDE = "elevation";
String ACCURACY = "accuracy";
String HORIZONTAL_ACCURACY = "accuracy";
String SPEED = "speed";
String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate";
@@ -69,7 +69,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ LATITUDE + " INTEGER, "
+ TIME + " INTEGER, "
+ ALTITUDE + " FLOAT, "
+ ACCURACY + " FLOAT, "
+ HORIZONTAL_ACCURACY + " FLOAT, "
+ SPEED + " FLOAT, "
+ BEARING + " FLOAT, "
+ SENSOR_HEARTRATE + " FLOAT, "
@@ -31,7 +31,7 @@ class CachedTrackPointsIndexes {
latitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE);
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE);
accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY);
accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.HORIZONTAL_ACCURACY);
speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
@@ -243,9 +243,9 @@ public class ContentProviderUtils {
* @param track the track
* @return the content provider URI of the inserted track.
*/
///TODO Return Track.Id
public Uri insertTrack(Track track) {
return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track));
public Track.Id insertTrack(Track track) {
Uri uri = contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track));
return new Track.Id(ContentUris.parseId(uri));
}
/**
@@ -290,6 +290,31 @@ public class ContentProviderUtils {
return values;
}
public void updateTrackStatistics(@NonNull Track.Id trackId, @NonNull TrackStatistics trackStatistics) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
}
private ContentValues createContentValues(TrackStatistics trackStatistics) {
ContentValues values = new ContentValues();
if (trackStatistics.getStartTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
}
if (trackStatistics.getStopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
}
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed().toMPS());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed().toMPS());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed().toMPS());
values.put(TracksColumns.MIN_ALTITUDE, trackStatistics.getMinAltitude());
values.put(TracksColumns.MAX_ALTITUDE, trackStatistics.getMaxAltitude());
values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.getTotalAltitudeGain());
values.put(TracksColumns.ALTITUDE_LOSS, trackStatistics.getTotalAltitudeLoss());
return values;
}
public Marker createMarker(Cursor cursor) {
int idIndex = cursor.getColumnIndexOrThrow(MarkerColumns._ID);
int nameIndex = cursor.getColumnIndexOrThrow(MarkerColumns.NAME);
@@ -318,7 +343,7 @@ public class ContentProviderUtils {
marker.setAltitude(Altitude.WGS84.of(cursor.getFloat(altitudeIndex)));
}
if (!cursor.isNull(accuracyIndex)) {
marker.setAccuracy(cursor.getFloat(accuracyIndex));
marker.setAccuracy(Distance.of(cursor.getFloat(accuracyIndex)));
}
if (!cursor.isNull(bearingIndex)) {
marker.setBearing(cursor.getFloat(bearingIndex));
@@ -472,7 +497,7 @@ public class ContentProviderUtils {
values.put(MarkerColumns.ALTITUDE, marker.getAltitude().toM());
}
if (marker.hasAccuracy()) {
values.put(MarkerColumns.ACCURACY, marker.getAccuracy());
values.put(MarkerColumns.ACCURACY, marker.getAccuracy().toM());
}
if (marker.hasBearing()) {
values.put(MarkerColumns.BEARING, marker.getBearing());
@@ -506,7 +531,8 @@ public class ContentProviderUtils {
* @param indexes the cached trackPoints indexes
*/
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.getById(cursor.getInt(indexes.typeIndex)));
Instant time = Instant.ofEpochMilli(cursor.getLong(indexes.timeIndex));
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.getById(cursor.getInt(indexes.typeIndex)), time);
trackPoint.setId(new TrackPoint.Id(cursor.getInt(indexes.idIndex)));
if (!cursor.isNull(indexes.longitudeIndex)) {
@@ -515,14 +541,11 @@ public class ContentProviderUtils {
if (!cursor.isNull(indexes.latitudeIndex)) {
trackPoint.setLatitude(((double) cursor.getInt(indexes.latitudeIndex)) / 1E6);
}
if (!cursor.isNull(indexes.timeIndex)) {
trackPoint.setTime(Instant.ofEpochMilli(cursor.getLong(indexes.timeIndex)));
}
if (!cursor.isNull(indexes.altitudeIndex)) {
trackPoint.setAltitude(Altitude.WGS84.of(cursor.getFloat(indexes.altitudeIndex)));
}
if (!cursor.isNull(indexes.accuracyIndex)) {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
trackPoint.setHorizontalAccuracy(Distance.of(cursor.getFloat(indexes.accuracyIndex)));
}
if (!cursor.isNull(indexes.speedIndex)) {
trackPoint.setSpeed(Speed.of(cursor.getFloat(indexes.speedIndex)));
@@ -683,8 +706,8 @@ public class ContentProviderUtils {
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude().toM());
}
if (trackPoint.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
if (trackPoint.hasHorizontalAccuracy()) {
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.getHorizontalAccuracy().toM());
}
if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed().toMPS());
@@ -49,15 +49,13 @@ public class ShareContentProvider extends CustomContentProvider {
private static final String TAG = ShareContentProvider.class.getSimpleName();
private static final int URI_GPX = 0;
private static final int URI_KML_ONLY = 1;
private static final int URI_KML_WITH_TRACKDETAIL = 2;
private static final int URI_KML_WITH_TRACKDETAIL_SENSORDATA = 3;
private static final int URI_KMZ_ONLY_TRACK = 4;
private static final int URI_KMZ_WITH_TRACKDETAIL = 5;
private static final int URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA = 6;
private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7;
private static final int URI_SHARE_PICTURE = 8;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final String TRACKID_DELIMITER = "_";
@@ -68,14 +66,22 @@ public class ShareContentProvider extends CustomContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.SHARE_PICTURE_PNG.getName() + "/*/*", URI_SHARE_PICTURE);
}
/**
* @return An URI for one file containing this tracks.
*/
public static Pair<Uri, String> createURI(Track.Id trackId, String trackName, @NonNull TrackFileFormat trackFileFormat) {
Set<Track.Id> trackIds = new HashSet<>(1);
trackIds.add(trackId);
return createURI(trackIds, trackName, trackFileFormat);
}
/**
* @return An URI for one file containing all tracks.
*/
public static Pair<Uri, String> createURI(Set<Track.Id> trackIds, String trackName, @NonNull TrackFileFormat trackFileFormat) {
if (trackIds.isEmpty()) {
throw new UnsupportedOperationException();
@@ -142,6 +148,9 @@ public class ShareContentProvider extends CustomContentProvider {
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
case URI_SHARE_PICTURE:
return TrackFileFormat.SHARE_PICTURE_PNG;
default:
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
}
@@ -167,12 +176,15 @@ public class ShareContentProvider extends CustomContentProvider {
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
switch (col) {
case OpenableColumns.DISPLAY_NAME:
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = uri.getLastPathSegment();
} else if (OpenableColumns.SIZE.equals(col)) {
break;
case OpenableColumns.SIZE:
cols[i] = OpenableColumns.SIZE;
values[i++] = -1;
values[i++] = -1; //Report unknown size; if applications need to know, one need to generate the file here also (count bytes that are written to OutputStream.
break;
}
}
@@ -213,6 +225,7 @@ public class ShareContentProvider extends CustomContentProvider {
PipeDataWriter<String> pipeDataWriter = (output, uri1, mimeType, opts, args) -> {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
// TODO handle failure (i.e., do not export an empty file)
trackExporter.writeTrack(tracks.toArray(new Track[0]), fileOutputStream);
} catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e);
@@ -169,9 +169,9 @@ public final class SensorDataCycling {
}
public static class Data {
public final Distance distance;
public final Distance distanceOverall;
public final Speed speed;
private final Distance distance;
private final Distance distanceOverall;
private final Speed speed;
private Data(Distance distance, Distance distanceOverall, Speed speed) {
this.distance = distance;
@@ -179,12 +179,24 @@ public final class SensorDataCycling {
this.speed = speed;
}
public Distance getDistance() {
return distance;
}
public Distance getDistanceOverall() {
return distanceOverall;
}
public Speed getSpeed() {
return speed;
}
@Override
public String toString() {
return "Data{" +
"distance_m=" + distance +
", distance_overall_m=" + distanceOverall +
", speed_mps=" + speed +
"distance=" + getDistance() +
", distance_overall=" + getDistanceOverall() +
", speed=" + getSpeed() +
'}';
}
}
@@ -0,0 +1,109 @@
package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
/**
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors.
*/
public final class SensorDataRunning extends SensorData<SensorDataRunning.Data> {
private static final String TAG = SensorDataRunning.class.getSimpleName();
private final Speed speed;
private final Float cadence;
private final Distance totalDistance;
public SensorDataRunning(String sensorAddress) {
super(sensorAddress);
this.speed = null;
this.cadence = null;
this.totalDistance = null;
}
public SensorDataRunning(String sensorAddress, String sensorName, Speed speed, Float cadence, Distance totalDistance) {
super(sensorAddress, sensorName);
this.speed = speed;
this.cadence = cadence;
this.totalDistance = totalDistance;
}
private boolean hasTotalDistance() {
return totalDistance != null;
}
public Float getCadence() {
return cadence;
}
@VisibleForTesting
public Speed getSpeed() {
return speed;
}
@VisibleForTesting
public Distance getTotalDistance() {
return totalDistance;
}
public void compute(SensorDataRunning previous) {
Distance overallDistance = null;
if (hasTotalDistance() && previous != null && previous.hasTotalDistance()) {
overallDistance = this.totalDistance.minus(previous.totalDistance);
if (previous.hasValue() && previous.getValue().getDistance() != null) {
overallDistance = overallDistance.plus(previous.getValue().getDistance());
}
}
value = new Data(speed, cadence, overallDistance);
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.speed, value.cadence, Distance.of(0));
}
}
public static class Data {
private final Speed speed;
private final Float cadence;
@Nullable
private final Distance distance;
public Data(Speed speed, Float cadence, @Nullable Distance distance) {
this.speed = speed;
this.cadence = cadence;
this.distance = distance;
}
public Speed getSpeed() {
return speed;
}
public Float getCadence() {
return cadence;
}
public Distance getDistance() {
return distance;
}
@Override
public String toString() {
return "Data{" +
"speed=" + speed +
", cadence=" + cadence +
", distance=" + distance +
'}';
}
}
}
@@ -14,6 +14,8 @@ public final class SensorDataSet {
private SensorDataCyclingPower cyclingPower;
private SensorDataRunning runningDistanceSpeedCadence;
public SensorDataSet() {
}
@@ -22,6 +24,7 @@ public final class SensorDataSet {
this.cyclingCadence = toCopy.cyclingCadence;
this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed;
this.cyclingPower = toCopy.cyclingPower;
this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence;
}
public SensorDataHeartRate getHeartRate() {
@@ -40,6 +43,10 @@ public final class SensorDataSet {
return cyclingPower;
}
public SensorDataRunning getRunningDistanceSpeedCadence() {
return runningDistanceSpeedCadence;
}
public void set(SensorData<?> data) {
set(data, data);
}
@@ -53,6 +60,7 @@ public final class SensorDataSet {
this.cyclingCadence = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
this.runningDistanceSpeedCadence = null;
}
public void fillTrackPoint(TrackPoint trackPoint) {
@@ -65,13 +73,19 @@ public final class SensorDataSet {
}
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distanceOverall);
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed);
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().getDistanceOverall());
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().getSpeed());
}
if (cyclingPower != null && cyclingPower.hasValue()) {
trackPoint.setPower(cyclingPower.getValue());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) {
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().getDistance());
trackPoint.setSpeed(runningDistanceSpeedCadence.getValue().getSpeed());
trackPoint.setCyclingCadence_rpm(runningDistanceSpeedCadence.getValue().getCadence());
}
}
public void reset() {
@@ -79,6 +93,8 @@ public final class SensorDataSet {
if (cyclingCadence != null) cyclingCadence.reset();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset();
if (cyclingPower != null) cyclingPower.reset();
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset();
}
@NonNull
@@ -87,7 +103,8 @@ public final class SensorDataSet {
return (getHeartRate() != null ? "" + getHeartRate() : "")
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
+ (getCyclingDistanceSpeed() != null ? " " + getCyclingDistanceSpeed() : "")
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
+ (getCyclingPower() != null ? " " + getCyclingPower() : "")
+ (getRunningDistanceSpeedCadence() != null ? " " + getRunningDistanceSpeedCadence() : "");
}
private void set(@NonNull SensorData<?> type, SensorData<?> data) {
@@ -110,6 +127,11 @@ public final class SensorDataSet {
return;
}
if (type instanceof SensorDataRunning) {
this.runningDistanceSpeedCadence = (SensorDataRunning) data;
return;
}
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
}
}
@@ -100,7 +100,7 @@ public class IntervalsFragment extends Fragment {
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
viewBinding.intervalList.setAdapter(adapter);
spinnerAdapter = new ArrayAdapter<IntervalStatisticsModel.IntervalOption>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
@@ -254,9 +254,9 @@ public class MarkerDetailFragment extends Fragment {
private void setLocation(boolean addShadow) {
String value = "[" + getString(R.string.stats_latitude) + " "
+ StringUtils.formatCoordinate(marker.getLatitude()) + ", "
+ StringUtils.formatCoordinate(getContext(), marker.getLatitude()) + ", "
+ getString(R.string.stats_longitude) + " "
+ StringUtils.formatCoordinate(marker.getLongitude()) + "]";
+ StringUtils.formatCoordinate(getContext(), marker.getLongitude()) + "]";
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerLocation, value, addShadow);
}
@@ -31,12 +31,11 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.adapters.SensorsAdapter;
import de.dennisguse.opentracks.adapters.StatisticsAdapter;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
@@ -45,7 +44,8 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
import de.dennisguse.opentracks.viewmodels.StatisticData;
import de.dennisguse.opentracks.viewmodels.StatisticDataBuilder;
/**
* A fragment to display track statistics to the user for a recorded {@link Track}.
@@ -69,7 +69,7 @@ public class StatisticsRecordedFragment extends Fragment {
}
private SensorStatistics sensorStatistics;
private SensorsAdapter sensorsAdapter;
private StatisticsAdapter.WithRecordedLayout sensorsAdapter;
private Track.Id trackId;
@Nullable // Lazily loaded.
@@ -114,7 +114,7 @@ public class StatisticsRecordedFragment extends Fragment {
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sensorsAdapter = new SensorsAdapter(getContext());
sensorsAdapter = new StatisticsAdapter.WithRecordedLayout(getContext());
}
@Override
@@ -238,10 +238,6 @@ public class StatisticsRecordedFragment extends Fragment {
// Set altitude gain and loss
{
// Make altitude visible?
boolean show = PreferencesUtils.isShowStatsAltitude(sharedPreferences, getContext());
viewBinding.statsAltitudeGroup.setVisibility(show ? View.VISIBLE : View.GONE);
Float altitudeGain_m = trackStatistics.getTotalAltitudeGain();
Float altitudeLoss_m = trackStatistics.getTotalAltitudeLoss();
@@ -254,6 +250,9 @@ public class StatisticsRecordedFragment extends Fragment {
parts = StringUtils.getAltitudeParts(getContext(), altitudeLoss_m, preferenceMetricUnits);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
boolean show = altitudeGain_m != null && altitudeLoss_m != null;
viewBinding.statsAltitudeGroup.setVisibility(show ? View.VISIBLE : View.GONE);
}
}
@@ -262,18 +261,7 @@ public class StatisticsRecordedFragment extends Fragment {
return;
}
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorStatistics.hasHeartRate()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_max, R.string.sensor_unit_beats_per_minute, sensorStatistics.getMaxHeartRate()));
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_avg, R.string.sensor_unit_beats_per_minute, sensorStatistics.getAvgHeartRate()));
}
if (sensorStatistics.hasCadence()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_max, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getMaxCadence()));
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_avg, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getAvgCadence()));
}
if (sensorStatistics.hasPower()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_power_avg, R.string.sensor_unit_power, sensorStatistics.getAvgPower()));
}
List<StatisticData> sensorDataList = StatisticDataBuilder.fromSensorStatistics(getContext(), sensorStatistics);
if (sensorDataList.size() > 0) {
sensorsAdapter.swapData(sensorDataList);
}
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -11,28 +10,25 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.adapters.SensorsAdapter;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.adapters.StatisticsAdapter;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
import de.dennisguse.opentracks.viewmodels.StatisticData;
import de.dennisguse.opentracks.viewmodels.StatisticsDataModel;
/**
* A fragment to display track statistics to the user for a currently recording {@link Track}.
@@ -51,15 +47,16 @@ public class StatisticsRecordingFragment extends Fragment {
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackRecordingService.RecordingData recordingData = TrackRecordingService.NOT_RECORDING;
private TrackPoint latestTrackPoint;
private Layout layout;
private StatisticsRecordingBinding viewBinding;
private SensorsAdapter sensorsAdapter;
private StatisticsAdapter statisticsAdapter;
private GridLayoutManager gridLayoutManager;
private StatisticsDataModel viewModel;
private LiveData<List<StatisticData>> statisticsLiveData;
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private boolean preferenceShowAltitude;
private boolean preferenceShowCoordinate;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
@@ -69,19 +66,13 @@ public class StatisticsRecordingFragment extends Fragment {
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && recordingData != null) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_custom_layout_fields_key, key)) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), recordingData.getTrackCategory());
layout = PreferencesUtils.getCustomLayout(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_altitude_key, key)) {
updateUInecessary = true;
preferenceShowAltitude = PreferencesUtils.isShowStatsAltitude(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_coordinate_key, key)) {
updateUInecessary = true;
preferenceShowCoordinate = PreferencesUtils.isStatsShowCoordinate(sharedPreferences, getContext());
if (PreferencesUtils.isKey(getContext(), R.string.stats_custom_layout_columns_key, key)) {
gridLayoutManager.setSpanCount(PreferencesUtils.getLayoutColumns(sharedPreferences, getContext()));
}
if (key != null && updateUInecessary && isResumed()) {
@@ -109,17 +100,28 @@ public class StatisticsRecordingFragment extends Fragment {
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
sensorsAdapter = new SensorsAdapter(getContext());
statisticsAdapter = new StatisticsAdapter(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(recordingData.getTrackCategory()));
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
RecyclerView recyclerView = viewBinding.statsRecyclerView;
recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), RecyclerView.VERTICAL));
final int numColumns = PreferencesUtils.getLayoutColumns(sharedPreferences, getContext());
gridLayoutManager = new GridLayoutManager(getContext(), numColumns);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (statisticsAdapter.isItemWide(position)) {
return numColumns;
}
return 1;
}
});
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.setAdapter(statisticsAdapter);
return viewBinding.getRoot();
}
@@ -131,13 +133,16 @@ public class StatisticsRecordingFragment extends Fragment {
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
viewModel = new ViewModelProvider(getActivity()).get(StatisticsDataModel.class);
statisticsLiveData = viewModel.getStatsData();
statisticsLiveData.observe(getActivity(), statsDataList -> statisticsAdapter.swapData(statsDataList));
trackRecordingServiceConnection.startConnection(getContext());
}
@Override
public void onPause() {
super.onPause();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@@ -158,160 +163,16 @@ public class StatisticsRecordingFragment extends Fragment {
super.onDestroy();
trackRecordingServiceConnection = null;
sharedPreferences = null;
viewModel = null;
statisticsLiveData.removeObservers(getActivity());
}
private void updateUI() {
if (isResumed()) {
updateStats();
setLocationValues();
updateSensorDataUI();
viewModel.update(recordingData, layout, preferenceMetricUnits);
}
}
/**
* Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}.
* Also sets altitude gain and loss.
*/
private void updateSensorDataUI() {
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
if (sensorDataSet != null) {
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorDataSet.getHeartRate() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
}
if (sensorDataSet.getCyclingCadence() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
}
if (sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
}
sensorsAdapter.swapData(sensorDataList);
}
{
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), recordingData.getTrackStatistics().getTotalAltitudeGain(), preferenceMetricUnits);
viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second);
}
{
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), recordingData.getTrackStatistics().getTotalAltitudeLoss(), preferenceMetricUnits);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
}
}
private void updateStats() {
// Set activity type
String trackIconValue = TrackIconUtils.getIconValue(getContext(), recordingData.getTrackCategory());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
// Set time
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(recordingData.getTrackStatistics().getMovingTime()));
updateTotalTime();
// Set average speed/pace
{
Speed speed = recordingData.getTrackStatistics().getAverageSpeed();
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
// Set max speed/pace
{
Speed speed = recordingData.getTrackStatistics().getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
// Set moving speed/pace
{
Speed speed = recordingData.getTrackStatistics().getAverageMovingSpeed();
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
// Set altitude gain and loss
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
}
private void updateTotalTime() {
Duration totalTime = recordingData.getTrackStatistics().getTotalTime();
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
}
private void setLocationValues() {
// Set speed/pace
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null) {
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
if (data.hasValue() && data.isRecent()) {
speed = data.getValue().speed;
}
}
setSpeed(speed);
// Set distance
{
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), recordingData.getTrackStatistics().getTotalDistance(), preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
// Set altitude
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
if (preferenceShowAltitude) {
// Current altitude
Float altitude = null;
int labelId = R.string.value_unknown;
if (latestTrackPoint != null && latestTrackPoint.hasAltitude()) {
altitude = (float) latestTrackPoint.getAltitude().toM();
labelId = latestTrackPoint.getAltitude().getLabelId();
}
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
viewBinding.statsAltitudeCurrentLabelEgm.setText(labelId);
}
// Set coordinate
viewBinding.statsCoordinateGroup.setVisibility(preferenceShowCoordinate ? View.VISIBLE : View.GONE);
if (preferenceShowCoordinate) {
String latitudeText = getContext().getString(R.string.value_unknown);
String longitudeText = getContext().getString(R.string.value_unknown);
if (latestTrackPoint != null && latestTrackPoint.hasLocation()) {
latitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLatitude());
longitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLongitude());
}
viewBinding.statsLatitudeValue.setText(latitudeText);
viewBinding.statsLongitudeValue.setText(longitudeText);
}
}
private void setSpeed(Speed speed) {
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsSpeedValue.setText(parts.first);
viewBinding.statsSpeedUnit.setText(parts.second);
}
private void onRecordingDataChanged(TrackRecordingService.RecordingData recordingData) {
String oldCategory = this.recordingData.getTrackCategory();
String newCategory = recordingData.getTrackCategory();
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.io.file;
import android.content.Context;
import android.graphics.Bitmap;
import java.util.Locale;
@@ -9,6 +10,7 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.SharePictureTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
/**
@@ -85,6 +87,7 @@ public enum TrackFileFormat {
}
},
GPX {
@Override
public TrackExporter createTrackExporter(Context context) {
@@ -99,6 +102,22 @@ public enum TrackFileFormat {
public String getExtension() {
return "gpx";
}
},
SHARE_PICTURE_PNG {
@Override
public TrackExporter createTrackExporter(Context context) {
return new SharePictureTrackExporter(context, Bitmap.CompressFormat.PNG);
}
@Override
public String getMimeType() {
return "image/png";
}
public String getExtension() {
return "png";
}
};
private static final String MIME_KMZ = "application/vnd.google-earth.kmz";
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.ExportActivityBinding;
import de.dennisguse.opentracks.databinding.ToolbarBinding;
import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.ExportUtils;
@@ -134,6 +135,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = ExportActivityBinding.inflate(getLayoutInflater());
ToolbarBinding mergedBinding = ToolbarBinding.bind(viewBinding.getRoot());
setContentView(viewBinding.getRoot());
directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
@@ -144,7 +146,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri);
String directoryDisplayName = FileUtils.getPath(documentFile);
viewBinding.toolbar.toolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
mergedBinding.toolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
resultReceiver = new ExportServiceResultReceiver(new Handler(), this);
@@ -105,7 +105,7 @@ public class KMLTrackExporter implements TrackExporter {
Instant startTime = tracks[0].getTrackStatistics().getStartTime();
for (Track track : tracks) {
Duration offset = Duration.between(track.getTrackStatistics().getStartTime(), startTime);
writeLocations(track, offset);
writeLocations(track);
}
if (hasMultipleTracks) {
writeMultiTrackEnd();
@@ -144,7 +144,7 @@ public class KMLTrackExporter implements TrackExporter {
}
}
private void writeLocations(Track track, Duration offset) throws InterruptedException {
private void writeLocations(Track track) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
@@ -229,7 +229,7 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<atom:generator>" + StringUtils.formatCData(context.getString(R.string.app_name)) + "</atom:generator>");
writeTrackStyle();
writePlacemarkerStyle(MARKER_STYLE, MARKER_ICON, 20, 2);
writePlacemarkerStyle();
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_SPEED, context.getString(R.string.description_speed_ms));
@@ -263,7 +263,7 @@ public class KMLTrackExporter implements TrackExporter {
float heading = getHeading(marker.getTrackId(), marker.getLocation());
writePhotoOverlay(marker, heading);
} else {
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), MARKER_STYLE, marker.getLocation());
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation());
}
}
}
@@ -406,16 +406,15 @@ public class KMLTrackExporter implements TrackExporter {
* @param name the name
* @param category the category
* @param description the description
* @param styleName the style name
* @param location the location
*/
private void writePlacemark(String name, String category, String description, String styleName, Location location) {
private void writePlacemark(String name, String category, String description, Location location) {
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(location) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
printWriter.println("<styleUrl>#" + KMLTrackExporter.MARKER_STYLE + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
@@ -521,17 +520,12 @@ public class KMLTrackExporter implements TrackExporter {
/**
* Writes a placemarker style.
*
* @param name the name of the style
* @param url the url of the style icon
* @param x the x position of the hotspot
* @param y the y position of the hotspot
*/
private void writePlacemarkerStyle(String name, String url, int x, int y) {
printWriter.println("<Style id=\"" + name + "\"><IconStyle>");
private void writePlacemarkerStyle() {
printWriter.println("<Style id=\"" + KMLTrackExporter.MARKER_STYLE + "\"><IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + url + "</href></Icon>");
printWriter.println("<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("<Icon><href>" + KMLTrackExporter.MARKER_ICON + "</href></Icon>");
printWriter.println("<hotSpot x=\"" + 20 + "\" y=\"" + 2 + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("</IconStyle></Style>");
}
@@ -0,0 +1,88 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.databinding.TrackSharePictureBinding;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Create a share picture for a track.
* Here a layout is inflated and converted to PNG.
* <p>
* TODO
* If this classes is used with non-visual context (e.g., ApplicationContext), it may not work in the future.
* API31 introduces `ncorrectContextUseViolation`, which may thrown during runtime.
* This code was tested on API30.
* Some more information: https://stackoverflow.com/questions/68539083/inflate-layout-from-applicationcontext-i-e-in-a-contentprovider/68567186#68567186
* <p>
* Future plan: draw directly on a view.
*/
public class SharePictureTrackExporter implements TrackExporter {
private static final String TAG = SharePictureTrackExporter.class.getSimpleName();
private final static int imageWidth = 1024;
private final Context context;
private final Bitmap.CompressFormat compressFormat;
public SharePictureTrackExporter(Context context, Bitmap.CompressFormat compressFormat) {
this.context = context;
this.compressFormat = compressFormat;
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
if (tracks.length > 1) {
Log.e(TAG, "Not yet implemented.");
return false;
}
return writeTrack(tracks[0], outputStream);
}
@Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
int imageHeight = imageWidth / 4 * 3;
TrackSharePictureBinding viewBinding = TrackSharePictureBinding.inflate(LayoutInflater.from(context));
viewBinding.sharePictureCategory.setText(track.getCategory());
boolean metricUnits = PreferencesUtils.isMetricUnits(PreferencesUtils.getSharedPreferences(context), context);
TrackStatistics trackStatistics = track.getTrackStatistics();
viewBinding.sharePictureTotalTime.setText(StringUtils.formatElapsedTimeWithHour(track.getTrackStatistics().getTotalTime()));
viewBinding.sharePictureTotalDistance.setText(StringUtils.formatDistance(context, track.getTrackStatistics().getTotalDistance(), metricUnits));
viewBinding.sharePictureGain.setText(StringUtils.formatAltitude(context, trackStatistics.getTotalAltitudeGain(), metricUnits));
// prepare rendering
View view = viewBinding.getRoot();
int widthSpec = View.MeasureSpec.makeMeasureSpec(imageWidth, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(imageHeight, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
view.layout(0, 0, imageWidth, imageHeight);
// draw
Canvas canvas = new Canvas();
Bitmap bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888);
canvas.setBitmap(bitmap);
view.draw(canvas);
// store
return bitmap.compress(compressFormat, 100, outputStream);
}
}
@@ -113,7 +113,7 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
Uri directoryUri = resultData.getData();
PreferencesUtils.setDefaultExportDirectoryUri(sharedPreferences, this, directoryUri);
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), resultData, directoryUri);
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), directoryUri);
break;
case RESULT_CANCELED:
PreferencesUtils.setDefaultExportDirectoryUri(sharedPreferences, this, null);
@@ -21,9 +21,9 @@ import android.util.Log;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -121,7 +121,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
public void startElement(String uri, String localName, String tag, Attributes attributes) {
switch (tag) {
case TAG_MARKER:
onMarkerStart(attributes);
@@ -251,14 +251,14 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
Instant parsedTime = null;
try {
trackPoint.setTime(StringUtils.parseTime(time));
parsedTime = StringUtils.parseTime(time);
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, parsedTime);
if (latitude == null || longitude == null) {
return trackPoint;
}
@@ -1,6 +1,5 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.ContentUris;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
@@ -86,10 +85,6 @@ public class TrackImporter {
this.trackPoints.addAll(trackPoints);
}
void addMarker(Marker marker) {
this.markers.add(marker);
}
void addMarkers(List<Marker> markers) {
this.markers.addAll(markers);
}
@@ -153,8 +148,7 @@ public class TrackImporter {
updater.addTrackPoints(trackPoints, recordingDistanceInterval);
track.setTrackStatistics(updater.getTrackStatistics());
Uri trackUri = contentProviderUtils.insertTrack(track);
Track.Id trackId = new Track.Id(ContentUris.parseId(trackUri));
Track.Id trackId = contentProviderUtils.insertTrack(track);
// Store TrackPoints
adjustTrackPoints();
@@ -160,6 +160,7 @@ public class TrackDeleteService extends Service {
return this.trackId != null && this.trackId.equals(trackId);
}
@NonNull
@Override
public String toString() {
return "DeleteStatus{" +
@@ -0,0 +1,295 @@
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.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackNameUtils;
class TrackRecordingManager {
private static final String TAG = TrackRecordingManager.class.getSimpleName();
private final ContentProviderUtils contentProviderUtils;
private final Context context;
private Distance recordingDistanceInterval;
private Distance maxRecordingDistance;
private Track track;
private TrackStatisticsUpdater trackStatisticsUpdater;
private TrackPoint lastTrackPoint;
private TrackPoint lastValidTrackPoint;
private boolean isIdle;
TrackRecordingManager(Context context) {
this.context = context;
contentProviderUtils = new ContentProviderUtils(context);
}
Track.Id start(TrackPoint segmentStartTrackPoint) {
// Insert a track
track = new Track();
track.setId(contentProviderUtils.insertTrack(track));
trackStatisticsUpdater = new TrackStatisticsUpdater();
insertTrackPoint(track, segmentStartTrackPoint);
//TODO Pass TrackPoint
track.setName(TrackNameUtils.getTrackName(context, track.getId(), segmentStartTrackPoint.getTime()));
String category = PreferencesUtils.getDefaultActivity(PreferencesUtils.getSharedPreferences(context), context); //TODO Re-use sharedpreferences
track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(context, category));
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track); //TODO Could we do this in the insert?
return track.getId();
}
//TODO Handle non-existing trackId? Start a new track or exception?
void resume(@NonNull Track.Id trackId, @NonNull TrackPoint segmentStartTrackPoint) {
track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "Ignore resumeTrack. Track " + trackId.getId() + " does not exists.");
return;
}
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(track, segmentStartTrackPoint);
}
void pause(TrackPointCreator handlerServer) {
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
}
insertTrackPoint(track, handlerServer.createSegmentEnd());
}
void end(TrackPointCreator handlerServer) {
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
}
TrackPoint segmentEnd = handlerServer.createSegmentEnd();
insertTrackPoint(track, segmentEnd);
track = null;
trackStatisticsUpdater = null;
lastTrackPoint = null;
lastValidTrackPoint = null;
isIdle = false;
}
Pair<Track, Pair<TrackPoint, SensorDataSet>> get(TrackPointCreator handlerServer) {
if (handlerServer == null) {
return null;
}
TrackStatisticsUpdater tmpTrackStatisticsUpdater = getTrackStatisticsUpdater();
Pair<TrackPoint, SensorDataSet> current = handlerServer.createCurrentTrackPoint(lastTrackPoint);
tmpTrackStatisticsUpdater.addTrackPoint(current.first, recordingDistanceInterval);
Track track = getTrack(); //Get copy
if (track == null) {
Log.w(TAG, "Requesting data if not recording is taking place, should not be done.");
return null;
}
track.setTrackStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
return new Pair<>(track, current);
}
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
if (name == null) {
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(track.getId());
if (nextMarkerNumber == null) {
nextMarkerNumber = 1;
}
name = context.getString(R.string.marker_name_format, nextMarkerNumber + 1);
}
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
if (trackPoint == 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, track.getId(), getTrackStatistics(), trackPoint, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri));
}
void onNewTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) {
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
//Storing trackPoint
// Always insert the first segment location
if (!currentSegmentHasTrackPoint()) {
insertTrackPoint(track, trackPoint);
lastTrackPoint = trackPoint;
return;
}
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation != null) {
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
insertTrackPointIfNewer(track, lastTrackPoint);
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
if (trackPoint.hasSensorData() || distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
}
if (!isIdle && !trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = true;
lastTrackPoint = trackPoint;
return;
}
if (isIdle && trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
Log.d(TAG, "Not recording TrackPoint, idle");
lastTrackPoint = trackPoint;
}
Track getTrack() {
return contentProviderUtils.getTrack(track.getId()); //Copy in memory; not via DB
}
//Functionality that uses this method should happen here.
@Deprecated
TrackStatisticsUpdater getTrackStatisticsUpdater() {
return new TrackStatisticsUpdater(trackStatisticsUpdater);
}
TrackStatistics getTrackStatistics() {
return trackStatisticsUpdater.getTrackStatistics();
}
/**
* Gets the last valid track point in the current segment.
*
* @param trackId the track id
* @return the location or null
*/
@Deprecated
//Use lastValidTrackPoint
private TrackPoint getLastValidTrackPointInCurrentSegment(Track.Id trackId) {
if (!currentSegmentHasTrackPoint()) {
return null;
}
return contentProviderUtils.getLastValidTrackPoint(trackId);
}
private boolean currentSegmentHasTrackPoint() {
return lastTrackPoint != null;
}
/**
* Inserts a trackPoint if this trackPoint is different than lastValidTrackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPointIfNewer(@NonNull Track track, @NonNull TrackPoint trackPoint) {
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
if (lastValidTrackPoint != null && trackPoint.getTime().equals(lastValidTrackPoint.getTime())) {
// Do not insert if inserted already
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid track point time.");
return;
}
insertTrackPoint(track, trackPoint);
}
/**
* Inserts a trackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPoint(@NonNull Track track, @NonNull TrackPoint trackPoint) {
try {
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrackStatistics(track.getId(), track.getTrackStatistics());
} catch (SQLiteException e) {
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
* This is expected to happen extremely rarely (if our listener gets invoked twice at about the same time).
*/
Log.w(TAG, "SQLiteException", e);
}
}
public void onSharedPreferenceChanged(@NonNull SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
}
if (PreferencesUtils.isKey(context, R.string.max_recording_distance_key, key)) {
maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context);
}
}
}
@@ -18,17 +18,15 @@ package de.dennisguse.opentracks.services;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -36,38 +34,30 @@ import androidx.core.app.TaskStackBuilder;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver;
import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.HandlerServer;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTask;
import de.dennisguse.opentracks.services.tasks.PeriodicTask;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.EGM2008Utils;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackNameUtils;
/**
* A background service that registers a location listener and records track points.
@@ -75,7 +65,7 @@ import de.dennisguse.opentracks.util.TrackNameUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackRecordingService extends Service implements HandlerServer.HandlerServerInterface, ExportServiceResultReceiver.Receiver {
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, ExportServiceResultReceiver.Receiver {
private static final String TAG = TrackRecordingService.class.getSimpleName();
@@ -86,10 +76,13 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
// The following variables are set in onCreate:
private ContentProviderUtils contentProviderUtils;
private PeriodicTaskExecutor voiceExecutor;
private TrackRecordingServiceNotificationManager notificationManager;
private TrackRecordingManager trackRecordingManager;
private final EGM2008CorrectionManager egm2008CorrectionManager = new EGM2008CorrectionManager();
private Handler handler;
private final Runnable updateRecordingData = new Runnable() {
@Override
@@ -107,8 +100,6 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private SharedPreferences sharedPreferences;
private Distance recordingDistanceInterval;
private Distance maxRecordingDistance;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
@@ -121,31 +112,19 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(sharedPreferences, context));
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
}
if (PreferencesUtils.isKey(context, R.string.max_recording_distance_key, key)) {
maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context);
}
handlerServer.onSharedPreferenceChanged(context, sharedPreferences, key);
handlerServer.onSharedPreferenceChanged(sharedPreferences, key);
trackRecordingManager.onSharedPreferenceChanged(sharedPreferences, key);
}
};
// The following variables are set when recording:
private WakeLock wakeLock;
private BluetoothRemoteSensorManager remoteSensorManager;
private AltitudeSumManager altitudeSumManager;
private EGM2008Utils.EGM2008Correction egm2008Correction;
private TrackStatisticsUpdater trackStatisticsUpdater;
private TrackPoint lastTrackPoint;
private boolean isIdle;
private final Binder binder = new Binder();
private HandlerServer handlerServer;
private TrackPointCreator handlerServer; //TODO Move to TrackRecordingManager?
private RecordingStatus recordingStatus;
private MutableLiveData<RecordingStatus> recordingStatusObservable;
@@ -163,9 +142,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
recordingDataObservable = new MutableLiveData<>(NOT_RECORDING);
handlerServer = new HandlerServer(this);
trackRecordingManager = new TrackRecordingManager(this);
handlerServer = new TrackPointCreator(this);
contentProviderUtils = new ContentProviderUtils(this);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTask.Factory());
notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -189,19 +168,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
public void onDestroy() {
handler = null;
handlerServer.stop(this);
handlerServer.stop();
handlerServer = null;
if (remoteSensorManager != null) {
remoteSensorManager.stop();
remoteSensorManager = null;
}
if (altitudeSumManager != null) {
altitudeSumManager.stop(this);
altitudeSumManager = null;
}
// Reverse order from onCreate
showNotification(false); //TODO Why?
@@ -214,8 +183,6 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
voiceExecutor = null;
}
contentProviderUtils = null;
// This should be the next to last operation
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
@@ -234,48 +201,16 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return recordingStatus.isPaused();
}
public Track.Id getRecordingTrackId() {
return recordingStatus.getTrackId();
}
//TODO
public TrackStatistics getTrackStatistics() {
if (trackStatisticsUpdater == null) {
return null;
}
return trackStatisticsUpdater.getTrackStatistics();
}
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
if (!isRecording() || isPaused()) {
return null;
}
if (name == null) {
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(getRecordingTrackId());
if (nextMarkerNumber == null) {
nextMarkerNumber = 1;
}
name = getString(R.string.marker_name_format, nextMarkerNumber + 1);
return trackRecordingManager.insertMarker(name, category, description, photoUrl);
}
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(getRecordingTrackId());
if (trackPoint == 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 = getString(R.string.marker_icon_url);
photoUrl = photoUrl != null ? photoUrl : "";
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
// Insert marker
Marker marker = new Marker(name, description, category, icon, getRecordingTrackId(), stats, trackPoint, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri));
public void run(@NonNull PeriodicTask periodicTask) {
periodicTask.run(recordingStatus.getTrackId(), trackRecordingManager.getTrackStatistics());
}
/**
@@ -289,30 +224,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return null;
}
// Insert a track
Track track = new Track();
Uri uri = contentProviderUtils.insertTrack(track);
Track.Id trackId = new Track.Id(ContentUris.parseId(uri));
// Set recording status
Track.Id trackId = trackRecordingManager.start(handlerServer.createSegmentStartManual());
updateRecordingStatus(new RecordingStatus(trackId, false));
// Update database
track.setId(trackId);
TrackPoint segmentStartTrackPoint = TrackPoint.createSegmentStartManual();
trackStatisticsUpdater = new TrackStatisticsUpdater();
insertTrackPoint(track, segmentStartTrackPoint);
//TODO Pass TrackPoint
track.setName(TrackNameUtils.getTrackName(this, trackId, segmentStartTrackPoint.getTime()));
String category = PreferencesUtils.getDefaultActivity(sharedPreferences, this);
track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(this, category));
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track);
startRecording();
return trackId;
}
@@ -324,17 +239,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
* @param trackId the id of the track to be resumed.
*/
public void resumeTrack(Track.Id trackId) {
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "Ignore resumeTrack. Track " + trackId.getId() + " does not exists.");
return;
}
// Sync the real time setting the stop time with current time.
track.getTrackStatistics().setStopTime(Instant.now());
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(track, TrackPoint.createSegmentStartManual());
handlerServer.resetSensorData();
trackRecordingManager.resume(trackId, handlerServer.createSegmentStartManual());
// Set recording status
updateRecordingStatus(new RecordingStatus(trackId, false));
@@ -349,34 +255,16 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
// Set recording status
updateRecordingStatus(new RecordingStatus(getRecordingTrackId(), false));
// Update database
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track != null) {
insertTrackPoint(track, TrackPoint.createSegmentStartManual());
}
startRecording();
resumeTrack(recordingStatus.getTrackId());
}
/**
* Common code for starting a new track, resuming a track, or restarting after phone reboot.
* Common code for starting a new track or resuming a track.
*/
private void startRecording() {
// Update instance variables
remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start();
altitudeSumManager = new AltitudeSumManager();
altitudeSumManager.start(this);
handler.postDelayed(updateRecordingData, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
lastTrackPoint = null;
isIdle = false;
startGps();
// Restore periodic tasks
@@ -404,25 +292,16 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// Need to remember the recordingTrackId before setting it to null
boolean wasPause = isPaused();
Track.Id trackId = getRecordingTrackId();
Track.Id trackId = recordingStatus.getTrackId();
// Set recording status
updateRecordingStatus(STATUS_DEFAULT);
if (!wasPause) {
// Update database
Track track = contentProviderUtils.getTrack(trackId);
if (track != null) {
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
trackRecordingManager.end(handlerServer);
}
insertTrackPoint(track, TrackPoint.createSegmentEnd());
}
}
Track track = contentProviderUtils.getTrack(trackId);
ExportUtils.postWorkoutExport(this, track, new ExportServiceResultReceiver(new Handler(), this));
ExportUtils.postWorkoutExport(this, trackId, new ExportServiceResultReceiver(new Handler(), this));
endRecording(true);
}
@@ -435,16 +314,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
// Set recording status
updateRecordingStatus(new RecordingStatus(getRecordingTrackId(), true));
updateRecordingStatus(new RecordingStatus(recordingStatus.getTrackId(), true));
// Update database
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track != null) {
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
}
insertTrackPoint(track, TrackPoint.createSegmentEnd());
}
trackRecordingManager.pause(handlerServer);
endRecording(false);
@@ -468,18 +340,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
voiceExecutor.shutdown();
// Update instance variables
if (remoteSensorManager != null) {
remoteSensorManager.stop();
remoteSensorManager = null;
}
if (altitudeSumManager != null) {
altitudeSumManager.stop(this);
altitudeSumManager = null;
}
lastTrackPoint = null;
handlerServer.stop(this);
handlerServer.stop();
stopGps(trackStopped);
}
@@ -496,7 +357,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
void stopGps(boolean shutdown) {
if (!isRecording()) return;
handlerServer.stop(this);
handlerServer.stop();
showNotification(false);
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (shutdown) {
@@ -504,120 +365,16 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
/**
* Gets the last valid track point in the current segment.
*
* @param trackId the track id
* @return the location or null
*/
private TrackPoint getLastValidTrackPointInCurrentSegment(Track.Id trackId) {
if (!currentSegmentHasTrackPoint()) {
return null;
}
return contentProviderUtils.getLastValidTrackPoint(trackId);
}
private boolean currentSegmentHasTrackPoint() {
return lastTrackPoint != null;
}
@Override
public void newTrackPoint(TrackPoint trackPoint, int recordingGpsAccuracy) {
public void newTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) {
if (!isRecording() || isPaused()) {
Log.w(TAG, "Ignore newTrackPoint. Not recording or paused.");
return;
}
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track == null) {
Log.w(TAG, "Ignore newTrackPoint. No track.");
return;
}
remoteSensorManager.fill(trackPoint);
correctAltitude(trackPoint);
notificationManager.updateTrackPoint(this, track.getTrackStatistics(), trackPoint, recordingGpsAccuracy);
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
//Storing trackPoint
// Always insert the first segment location
if (!currentSegmentHasTrackPoint()) {
insertTrackPoint(track, trackPoint);
lastTrackPoint = trackPoint;
return;
}
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
insertTrackPointIfNewer(track, lastTrackPoint);
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
if (trackPoint.hasSensorData() || distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
if (!isIdle && !trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = true;
lastTrackPoint = trackPoint;
return;
}
if (isIdle && trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
Log.d(TAG, "Not recording TrackPoint, idle");
lastTrackPoint = trackPoint;
}
//TODO Identical to TrackDataHub
private void correctAltitude(TrackPoint trackPoint) {
if (!trackPoint.hasLocation() || !trackPoint.hasAltitude()) {
Log.d(TAG, "No altitude correction necessary.");
return;
}
if (egm2008Correction == null || !egm2008Correction.canCorrect(trackPoint.getLocation())) {
try {
egm2008Correction = EGM2008Utils.createCorrection(this, trackPoint.getLocation());
} catch (IOException e) {
Log.e(TAG, "Could not load altitude correction for " + trackPoint, e);
return;
}
}
trackPoint.setAltitude(Altitude.EGM2008.of(egm2008Correction.correctAltitude(trackPoint.getLocation())));
trackRecordingManager.onNewTrackPoint(trackPoint, thresholdHorizontalAccuracy);
notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy);
voiceExecutor.update(recordingStatus.getTrackId(), trackRecordingManager.getTrackStatistics());
}
@Override
@@ -630,58 +387,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
/**
* Inserts a trackPoint if this trackPoint is different than lastValidTrackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPointIfNewer(@NonNull Track track, @NonNull TrackPoint trackPoint) {
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
if (lastValidTrackPoint != null && trackPoint.getTime().equals(lastValidTrackPoint.getTime())) {
// Do not insert if inserted already
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid track point time.");
return;
}
insertTrackPoint(track, trackPoint);
}
/**
* Inserts a trackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPoint(@NonNull Track track, @NonNull TrackPoint trackPoint) {
try {
if (!TrackPoint.Type.SEGMENT_START_MANUAL.equals(trackPoint.getType())) {
altitudeSumManager.fill(trackPoint);
altitudeSumManager.reset();
remoteSensorManager.fill(trackPoint);
remoteSensorManager.reset();
}
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track);
} catch (SQLiteException e) {
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
* This is expected to happen extremely rarely (if our listener gets invoked twice at about the same time).
*/
Log.w(TAG, "SQLiteException", e);
}
voiceExecutor.update();
}
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, getRecordingTrackId());
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, recordingStatus.getTrackId());
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackRecordingActivity.class)
.addNextIntent(intent)
@@ -707,18 +416,14 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
// This is used to modify the state of this service while testing; must be called after startNewTrack().
@Deprecated
@VisibleForTesting
public void setRemoteSensorManager(@NonNull BluetoothRemoteSensorManager remoteSensorManager) {
this.remoteSensorManager = remoteSensorManager;
public Duration getTotalTime() {
return trackRecordingManager.getTrackStatistics().getTotalTime();
}
// This is used to modify the state of this service while testing; must be called after startNewTrack().
@Deprecated
@VisibleForTesting
public void setAltitudeSumManager(@NonNull AltitudeSumManager altitudeSumManager) {
this.altitudeSumManager = altitudeSumManager;
public TrackPointCreator getHandlerServer() {
return handlerServer;
}
public LiveData<GpsStatusValue> getGpsStatusObservable() {
@@ -734,35 +439,25 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
Log.w(TAG, "Currently not recording; cannot update data.");
return;
}
Track track = contentProviderUtils.getTrack(recordingStatus.getTrackId());
// Compute temporary track statistics using sensorData and update time.
//TODO This somehow should happen in the HandlerServer as we create a new TrackPoint.
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater);
TrackPoint tmpLastTrackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
//TODO Should happen in TrackPoint? via constructor
tmpLastTrackPoint.setSpeed(lastTrackPoint.getSpeed());
tmpLastTrackPoint.setAltitude(lastTrackPoint.getAltitude());
tmpLastTrackPoint.setLongitude(lastTrackPoint.getLongitude());
tmpLastTrackPoint.setLatitude(lastTrackPoint.getLatitude());
}
BluetoothRemoteSensorManager localRemoteSensorManager = this.remoteSensorManager;
AltitudeSumManager localAltitudeSumManager = this.altitudeSumManager;
if (localAltitudeSumManager == null || localRemoteSensorManager == null) {
TrackPointCreator localHandlerServer = this.handlerServer;
if (localHandlerServer == null) {
// when this happens, no recording is running and we should not send any notifications.
//TODO This implementation is not a good idea; rather solve the issue for this properly
return;
}
localAltitudeSumManager.fill(tmpLastTrackPoint);
SensorDataSet sensorDataSet = localRemoteSensorManager.getSensorDataSet();
sensorDataSet.fillTrackPoint(tmpLastTrackPoint);
tmpTrackStatisticsUpdater.addTrackPoint(tmpLastTrackPoint, recordingDistanceInterval);
track.setTrackStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
recordingDataObservable.postValue(new RecordingData(track, tmpLastTrackPoint, sensorDataSet));
Pair<Track, Pair<TrackPoint, SensorDataSet>> data = trackRecordingManager.get(handlerServer);
if (data == null) {
Log.w(TAG, "Requesting data if not recording is taking place, should not be done.");
return;
}
TrackPoint trackPoint = data.second.first;
egm2008CorrectionManager.correctAltitude(this, trackPoint);
recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second));
}
public LiveData<RecordingStatus> getRecordingStatusObservable() {

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