Compare commits

...

134 Commits

Author SHA1 Message Date
Dennis Guse e28c385e52 Release: v3.18.3 2021-06-04 18:21:43 +02:00
Dennis Guse 8d4418a584 Revert "Bump mockito-android from 3.10.0 to 3.11.0"
This reverts commit 68c8839ebb.
2021-06-04 18:07:10 +02:00
mondstern 85c7314fa8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-06-04 17:50:39 +02:00
mondstern 06a05c82bc Translated using Weblate (Polish)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-06-04 17:50:39 +02:00
mondstern 4d08729284 Translated using Weblate (Dutch)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-06-04 17:50:39 +02:00
mondstern 28bbf14efd Translated using Weblate (Italian)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-06-04 17:50:39 +02:00
mondstern 5686322aa6 Translated using Weblate (Hungarian)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2021-06-04 17:50:39 +02:00
mondstern a6c06efb62 Translated using Weblate (Galician)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-06-04 17:50:39 +02:00
mondstern 80a5149207 Translated using Weblate (French)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-06-04 17:50:39 +02:00
mondstern 5b31213770 Translated using Weblate (Finnish)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-06-04 17:50:39 +02:00
mondstern 19cbbb90f9 Translated using Weblate (Basque)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-06-04 17:50:39 +02:00
mondstern 39e4a697c0 Translated using Weblate (Estonian)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-06-04 17:50:39 +02:00
mondstern 196de0c3d4 Translated using Weblate (Danish)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-06-04 17:50:39 +02:00
mondstern 8aa02d009d Translated using Weblate (Czech)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-06-04 17:50:39 +02:00
Nikita Epifanov 9a89295b1f Translated using Weblate (Russian)
Currently translated at 99.6% (530 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-06-04 17:50:39 +02:00
Vinicius 4c3c93511f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-06-04 17:50:39 +02:00
FTno bfd6517a61 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-06-04 17:50:39 +02:00
Dennis Guse 6334dcea04 KMLImporter: added test. 2021-06-04 17:50:34 +02:00
Dennis Guse 5b2d9a5773 Import: re-order TrackPoints by time, so statistics can be computed.
Fixes #796.
2021-06-04 17:50:34 +02:00
Dennis Guse f1ee0ea035 GPX exporter: cleanup. 2021-06-04 17:50:34 +02:00
Dennis Guse 57afedb3a8 Importer: re-organized tests.
Added test for when/gx:coords order for KML2.2 (for #790).
2021-06-04 17:50:34 +02:00
Dennis Guse 4e33ad4007 Cleanup: move instantiation of TrackImporter. 2021-06-04 17:50:34 +02:00
Dennis Guse eada954299 Cleanup: renaming Importer classes. 2021-06-04 17:50:34 +02:00
Dennis Guse 288fe27a57 Import: re-implement GPX parsing. 2021-06-04 17:50:34 +02:00
Dennis Guse 54e8fd3ee3 Import: re-implement KML parsing.
Fixes #790.
2021-06-04 17:50:34 +02:00
dependabot[bot] 68c8839ebb Bump mockito-android from 3.10.0 to 3.11.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.10.0 to 3.11.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.10.0...v3.11.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-06-04 17:49:08 +02:00
Román Martínez 12e764c44a Bugfix: EGM2008 label has to belong to stats_altitude_group to be hidden and showed according to user settings. 2021-06-01 21:04:38 +02:00
dependabot[bot] 50eff0d7e8 Bump reactivecircus/android-emulator-runner from 2.16.0 to 2.17.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.16.0 to 2.17.0.
- [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.16.0...v2.17.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-31 17:08:02 +02:00
Dennis Guse ee96226faf Bugfix: avoid ConcurrentModificationException exception in ChartView. 2021-05-31 16:52:40 +02:00
Dennis Guse f5ec91f0eb Launcher icon and splashscreen: use inset instead of separate drawable. 2021-05-29 19:36:42 +02:00
Dennis Guse b0575c0f5b Launcher icon: use vector (layer-list for API21+ and adaptive-icon for API26+). 2021-05-29 19:36:42 +02:00
Dennis Guse ccfcd3b306 TrackDataHub: do not send more TrackPoints after stop(). 2021-05-29 19:31:28 +02:00
Dennis Guse 7929d219ac Cleanup: TrackDataHub does not need started. 2021-05-29 19:31:28 +02:00
Dennis Guse d6f4d3f599 Cleanup:
TrackDataHub handles listeners itself.
2021-05-29 19:31:28 +02:00
Dennis Guse 4171d9cc4c Cleanup. 2021-05-28 07:58:57 +02:00
Dennis Guse 9366c031b3 Fix translation. 2021-05-27 18:24:47 +02:00
Allan Nordhøy 3454fe0cfa Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-05-27 10:31:04 +02:00
Pablo 3ad2d725be Translated using Weblate (Spanish)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-05-27 10:31:04 +02:00
Pablo d3400dbdfb Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2021-05-27 10:31:04 +02:00
J. Lavoie dc4d2ecd89 Translated using Weblate (French)
Currently translated at 100.0% (532 of 532 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-05-27 10:31:04 +02:00
FTno a1feb7d0d5 Update strings.xml 2021-05-27 10:30:57 +02:00
Román Martínez 5cad99b237 Cleanup unused code. 2021-05-25 20:00:37 +02:00
gordonel fb8a4de79b doc: add Rotor 2INpower cycling powermeter 2021-05-24 16:57:35 +02:00
Dennis Guse b1b0a45f53 ChartFragment: re-added isResume() check. 2021-05-23 19:52:12 +02:00
Dennis Guse e580a03115 Cleanup ChartFragment. 2021-05-23 19:52:12 +02:00
Dennis Guse 591597e4e8 Release: v3.18.2 2021-05-22 21:12:07 +02:00
Dennis Guse 1d7820469c TrackDataHub: prevent crash due to race condition.
Fixes #787.
2021-05-22 12:49:51 +02:00
Román Martínez aa34f5d55b Bugfix: category change didn't update speed/pace correctly.
Espresso test added to check that unit is properly.
2021-05-21 21:01:31 +02:00
Dennis Guse e71ab28701 TrackRecordingService: avoid race condition while stopping a recording. 2021-05-19 18:36:41 +02:00
Dennis Guse ae001c73dd Simplify TrackDataHub interface. 2021-05-19 18:36:41 +02:00
Dennis Guse 56ff7827df TrackRecordingService uses LiveData to transfer data.
Replaces pull from UI layer.

Fixes #173.
2021-05-19 18:36:41 +02:00
Dennis Guse 2cdf122e8c Release: v3.18.1 2021-05-19 18:16:45 +02:00
mondstern 495ef2fc6f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-05-19 17:15:57 +02:00
FTno ff561c1b25 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-05-19 17:15:57 +02:00
mondstern 7106f64a3c Translated using Weblate (Hungarian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2021-05-19 17:15:57 +02:00
mondstern 8420f9febb Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-05-19 17:15:57 +02:00
Xosé M 0744ba790f Translated using Weblate (Galician)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-05-19 17:15:57 +02:00
Dennis Guse 9a7f24bddc Update README. 2021-05-19 17:15:51 +02:00
dependabot[bot] ef47c86e7c Bump appcompat from 1.2.0 to 1.3.0
Bumps appcompat from 1.2.0 to 1.3.0.

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-19 07:47:14 +02:00
Román Martínez 93f203bcf1 Track deletion is handled by a service that shows a progress bar on the notification bar. Fixes #735. 2021-05-17 19:45:26 +02:00
Dennis Guse 6b0ec72b9b TrackListActivity: re-enable search.
Fixes #757.
2021-05-16 14:35:10 +02:00
Dennis Guse d46cea6150 Update AndroidManifest.xml 2021-05-16 14:34:42 +02:00
Dennis Guse 0e3ffa58f0 Disable hasFragileUserData for production.
Should fix #685 and #129.
2021-05-16 14:34:42 +02:00
Dennis Guse c00d769188 SensorDataCycling: use Distance. 2021-05-16 08:35:47 +02:00
Dennis Guse 00489a121a Bugfix: TrackStatisticsUpdater was computing distance incorrectly.
While using a cycling speed and distance sensor, the updater was not using the distance from the most recent trackpoint but rather the one considered last moving.

Fixes #762.
2021-05-16 08:35:47 +02:00
Dennis Guse d7f339c6f8 Settings: disable voice announcement setting if not TTS available.
Fixes #763.
2021-05-16 08:34:20 +02:00
Dennis Guse da28ea3e95 Cleanup. 2021-05-16 00:08:15 +02:00
Dennis Guse 9872658f81 Update AndroidStudio config files for 4.2.1 2021-05-14 19:52:12 +02:00
dependabot[bot] b6b155f19c Bump gradle from 4.2.0 to 4.2.1
Bumps gradle from 4.2.0 to 4.2.1.

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-14 19:08:37 +02:00
Dennis Guse 067f31ac21 Bugfix: ControllerFragment did not work in landscape mode.
Introduced in ed6daeb48b
2021-05-13 12:42:05 +02:00
dependabot[bot] 3f0ce27870 Bump mockito-android from 3.9.0 to 3.10.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.9.0...v3.10.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-13 08:19:07 +02:00
Dennis Guse 438f8fe273 TrackListActivity: disable search as it is not working if not shown as Action. 2021-05-13 06:37:24 +02:00
Dennis Guse 392c02a56f Cleanup. 2021-05-13 06:27:16 +02:00
Quang Trung 6dca3bea58 Translated using Weblate (Vietnamese)
Currently translated at 80.3% (426 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-05-12 07:40:32 +02:00
mondstern 13034c8bb3 Translated using Weblate (Polish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-05-12 07:40:32 +02:00
mondstern 1175ce270e Translated using Weblate (Dutch)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-05-12 07:40:32 +02:00
mondstern 02cad98a4f Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-05-12 07:40:32 +02:00
mondstern d6b38a390e Translated using Weblate (Italian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-05-12 07:40:32 +02:00
J. Lavoie 98deed8ef7 Translated using Weblate (Italian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-05-12 07:40:32 +02:00
J. Lavoie 9983168cb9 Translated using Weblate (French)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-05-12 07:40:32 +02:00
mondstern 7b5f29db09 Translated using Weblate (French)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-05-12 07:40:32 +02:00
J. Lavoie 49efd04326 Translated using Weblate (Finnish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-05-12 07:40:32 +02:00
mondstern 3159c62eb2 Translated using Weblate (Spanish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-05-12 07:40:32 +02:00
Max Schallert 0e8811d8c9 Translated using Weblate (German)
Currently translated at 97.5% (517 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
J. Lavoie 4126e36f1c Translated using Weblate (German)
Currently translated at 97.5% (517 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
mondstern b7e2df9f2f Translated using Weblate (Czech)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-05-12 07:40:32 +02:00
Max Schallert 055093772d Translated using Weblate (German)
Currently translated at 97.1% (515 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
J. Lavoie 7f8b49814b Translated using Weblate (German)
Currently translated at 97.1% (515 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
Max Schallert 5cc0e06fbb Translated using Weblate (German)
Currently translated at 95.6% (507 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
J. Lavoie 311a2550e5 Translated using Weblate (German)
Currently translated at 95.6% (507 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
J. Lavoie 4311b29833 Translated using Weblate (German)
Currently translated at 95.6% (507 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
Max Schallert 86121ac27a Translated using Weblate (German)
Currently translated at 95.6% (507 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
Dennis Guse ed6daeb48b TrackController is now ControllerFragment. 2021-05-12 07:39:47 +02:00
dependabot[bot] af72960cf6 Bump reactivecircus/android-emulator-runner from 2 to 2.16.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2 to 2.16.0.
- [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...v2.16.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 07:39:11 +02:00
dependabot[bot] 99767d0c0b Bump actions/checkout from 2 to 2.3.4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 2.3.4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v2.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 07:39:01 +02:00
Dennis Guse 1fc0a552c7 ChartView: improve performance. 2021-05-11 07:42:17 +02:00
Dennis Guse 18dd1de1f9 TrackDataHub: prevent race condition. 2021-05-08 19:31:16 +02:00
Dennis Guse ae3078304d Release: v3.18.0 2021-05-08 14:53:40 +02:00
Dennis Guse 48b6438c06 Bugfix: TrackDataHub needs to use the same TrackStatisticsUpdater for a whole track (incl. newly received TrackPoints).
Fixes #745.

Introduced in https://github.com/OpenTracksApp/OpenTracks/commit/887d4700d4f8fbf10563834e7498388a3213007e
2021-05-08 14:40:32 +02:00
Dennis Guse e822172f58 Cleanup. 2021-05-08 14:40:32 +02:00
Dennis Guse a3aee403a1 CI: run tests on more API Android levels. 2021-05-06 19:33:18 +02:00
Dennis Guse aac2ad4165 Removed invalid Russian translation (ImpliedQuantity). 2021-05-06 18:33:03 +02:00
mondstern 76d5e5780c Translated using Weblate (Finnish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-05-06 18:07:44 +02:00
mondstern 931fd7514a Translated using Weblate (Basque)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-05-06 18:07:44 +02:00
mondstern d1570ad700 Translated using Weblate (Estonian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-05-06 18:07:44 +02:00
mondstern 04fff89f68 Translated using Weblate (Danish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-05-06 18:07:44 +02:00
mondstern b57fd368ae Translated using Weblate (Russian)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-05-06 18:07:44 +02:00
chipechop 58d6a3b5a9 Update strings.xml
Hi, 
I'm new, here.

I changed some translations that didn't fit or were not correct (hr is h in italian).

I'm enthusiast of your app: it's just perfect.

Cheers!
2021-05-06 18:07:39 +02:00
dependabot[bot] 289a5273a9 Bump mockito-android from 3.8.0 to 3.9.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.8.0 to 3.9.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.8.0...v3.9.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-06 07:54:02 +02:00
Dennis Guse 120e20a0ea Avoid crash on older Android when stopping a track. 2021-05-05 21:50:26 +02:00
Dennis Guse 2843d19450 UI presents altitude in EGM2008. 2021-05-05 18:01:07 +02:00
Dennis Guse 50127e311a Introduced value object for Altitude. 2021-05-05 18:01:07 +02:00
Dennis Guse f83a1b3a4c EGM2008: add to UI in StatisticsRecordingFragment. 2021-05-05 18:01:07 +02:00
Dennis Guse 65ecbc4521 EGM2008: add reference to about activity. 2021-05-05 18:01:07 +02:00
Dennis Guse fa50db08c3 EGM2008: added undulation data and bilinear computation.
Includes data structure for caching.

Part of #224.
2021-05-05 18:01:07 +02:00
Dennis Guse b395c19832 Bugfix: voice announcements need to use KM/Miles (instead of m/feet).
Fixes #722.
2021-05-05 17:59:08 +02:00
Dennis Guse 7e6ffbabd2 Build: replace JCenter with MavenCentral; requires upgrade to AndroidStudio 4.2.X.
Fixes #653.
2021-05-05 12:45:23 +02:00
dependabot[bot] 6b43d341d0 Bump gradle from 4.1.3 to 4.2.0
Bumps gradle from 4.1.3 to 4.2.0.

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-05 12:02:45 +02:00
Dennis Guse 8af0fb3250 Bugfix: TrackDataHub did not account for recordingDistanceInterval.
Introduced in 887d4700d4
2021-05-03 22:13:23 +02:00
Dennis Guse 49219de46a GPSStatus: update initial and final status. 2021-05-03 22:13:23 +02:00
Dennis Guse 1baf652b81 Bugfix: ChartFragment needs to show elevation in M/FT. 2021-05-03 22:13:23 +02:00
Dennis Guse 6723bfb979 TrackRecordingService: provide status updates via LiveData.
Fixes #719
2021-05-03 22:13:23 +02:00
Nikita Epifanov 8357b234d2 Translated using Weblate (Russian)
Currently translated at 81.4% (431 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-05-01 20:03:23 +02:00
mondstern 9de9fbd058 Translated using Weblate (Russian)
Currently translated at 81.2% (430 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-05-01 20:03:23 +02:00
Dennis Guse 6da6fc6004 Upgrade to Gradle 7.0 2021-05-01 20:03:19 +02:00
Dennis Guse 114fc31be0 KmzImporter: process doc.kml as stream (instead of in memory copy).
Also simplified exception handling.
2021-05-01 20:01:06 +02:00
Dennis Guse ae795a76fa Removed TrackImporter interface. 2021-05-01 20:01:06 +02:00
Dennis Guse 92004b0ec7 Cleanup: refactored importers. 2021-05-01 20:01:06 +02:00
Dennis Guse f277347529 ExportImportTest: be realistic by creating actual files. 2021-05-01 20:01:06 +02:00
Dennis Guse ad1aa06407 Cleanup: extracted SAX2 import from AbstractFileTrackImporter. 2021-05-01 20:01:06 +02:00
Dennis Guse 3b92303511 Cleanup: move GPX-specific importer code to GPX importer. 2021-05-01 20:01:06 +02:00
Dennis Guse e62faa1253 Reproducible build: disable PNG crunching.
Part of #418.
2021-05-01 19:27:25 +02:00
dependabot[bot] d3900f4dad Bump espresso-core from 3.4.0-alpha05 to 3.4.0-alpha06
Bumps espresso-core from 3.4.0-alpha05 to 3.4.0-alpha06.

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-30 12:09:04 +02:00
badger40 00b8834795 Update README_TESTED_SENSORS.md 2021-04-28 07:41:00 +02:00
150 changed files with 42938 additions and 3416 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v2.3.4
with:
fetch-depth: 2
+3 -3
View File
@@ -6,14 +6,14 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
api-level: [29]
api-level: [ 23, 26, 29 ]
target: [default]
steps:
- name: checkout
uses: actions/checkout@v2
uses: actions/checkout@v2.3.4
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
uses: reactivecircus/android-emulator-runner@v2.17.0
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+1
View File
@@ -3,6 +3,7 @@
<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" />
+45 -36
View File
@@ -49,39 +49,6 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</tr>
</table>
## Features:
* __Tracking:__ track your sport and outdoor activities
* __Photos and Markers:__ mark interesting locations while tracking (also with photos)
* __Bluetooth LE sensors:__
* heart rate
* cycling: speed and cadence
* cycling: power meter
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
* __No Internet access:__ Internet is not used
* __Voice announcements__
* __No advertising__
* __Privacy:__
* _does not contain any in-app analytics_
* _does not use Google Play Services_
* _has no cloud integration_
__Only required permissions:__
* _ACCESS_FINE_LOCATION_: required to use the GPS.
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
_Please note:_
_OpenTracks_ does not contain a _map_ (as this would require internet access).
## Custom Dashboards (incl. Map)
As of v3.3.1 OpenTracks enables to use custom dashboards.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
The developer documentation is in [README_API.md](README_API.md).
Alternatively, recorded tracks can be shared as KMZ/GPX to installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
However, this is rather slow and does not provide updates while recording.
## Screenshots
<div>
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png">
@@ -96,12 +63,54 @@ However, this is rather slow and does not provide updates while recording.
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png">
</div>
## Features
* __Tracking:__ track your sport and outdoor activities
* __Voice announcements__
* __Photos and Markers:__ mark interesting locations while tracking
* __Export:__
* export tracks either as KMZ (incl. photos), KML, or GPX
* export automatically after each recording (e.g., to sync)
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
* __Altitude:__
* gain/loss via barometric sensor (if present)
* shown in EGM2008 (above mean sea level); exported as WGS84
* __Bluetooth LE sensors:__
* heart rate
* cycling: speed and distance
* cycling: cadence
* cycling: power meter
### Gadgetbridge integration
OpenTracks can be used with [Gadgetbridge](https://www.gadgetbridge.org/):
* shows statistics via notification on smart watches (requires Gadgetbridge 0.56.1 or later), and
* Gadgetbridge's GPX exporter generates `opentracks:trackid` to avoid duplication (Gadgetbridge 0.53.0 or later).
### Privacy
* __No Internet access:__ Internet is not used
* __No advertising__
* __No in-app analytics__
* __No use of Google Play Services__
__Only required permission:__
* _ACCESS_FINE_LOCATION_: required to use the GPS.
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
## Custom Dashboards (incl. map)
As of v3.3.1, OpenTracks supports custom dashboards.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
The developer documentation is in [README_API.md](README_API.md).
Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
However, this is rather slow and does not provide updates while recording.
## Project history
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
Initially, _My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html).
In 2016, [Google decided to discontinue](https://support.google.com/maps/answer/6333516) _My Tracks_ and stopped distributing it via the Google Play store in April 2016.
The [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
Then [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
Plonk42's version is available [here](https://github.com/Plonk42/mytracks).
In 2019, _OpenTracks_ was forked from Plonk42's _My Tracks_ and major rework was conducted.
@@ -114,11 +123,11 @@ Rework of _OpenTracks_ included:
* removing calorie estimation and activity estimation,
* removing support for ANT+ and Classic Bluetooth,
* adding support for Bluetooth LE heart rate sensors,
* removing Protobuf (store sensosensorr data in SQLite columns directly), and
* removing Protobuf (store sensor data in SQLite columns directly), and
* removing Android Service API for other apps.
Artwork, logos and user interface remained more or less unchanged.
More information about _Google My Tracks_:
* [Wikipedia page](https://en.wikipedia.org/wiki/MyTracks)
* [Google code archive](https://code.google.com/archive/p/mytracks/)
* [Google code archive](https://code.google.com/archive/p/mytracks/)
+3
View File
@@ -28,6 +28,8 @@ However, often only one value is provided.
* Wahoo Speed (Model: WFRPMSPD)
* CooSpo cadence and speed sensor (model: BK467)
### Cadence
* Garmin Cadence Sensor 2
@@ -48,4 +50,5 @@ Also the distance is not computed.
* QUARQ Red DZero Powermeter
* Tacx Satori Smart
* Wahoo Kickr v4.0
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is propietary @ firmware v1.061)
+12 -8
View File
@@ -3,17 +3,17 @@ apply plugin: 'com.android.application'
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.android.tools.build:gradle:4.2.1'
}
}
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}
@@ -49,10 +49,14 @@ android {
viewBinding = true
}
aaptOptions {
cruncherEnabled = false
}
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4009
versionName "v3.17.3"
versionCode 4143
versionName "v3.18.3"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -86,10 +90,10 @@ android {
}
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha05'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.3.0'
@@ -99,5 +103,5 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'org.mockito:mockito-android:3.8.0'
androidTestImplementation 'org.mockito:mockito-android:3.10.0'
}
@@ -0,0 +1,15 @@
v3.18.0: OpenTracks
Changes:
- Altitude is shown in EGM2008 (stored/exported as WGS84)
Bugfix:
- Fixed memory leak (introduced in v3.17.0)
- Charts: altitude should be in M/FT (introduced in v3.17.1)
- Charts: while recording was not correctly updated (introduced in v3.17.3)
- TrackDataHub did not account for recordingDistanceInterval (introduced in v3.17.2)
Developer:
- Disable PNG crunching (reproducible build)
- Refactored TrackImporter and optimized KMZ import
- Use LiveData for TrackRecordingService to UI communication (removing memory leak)
@@ -0,0 +1,13 @@
v3.18.1: OpenTracks
Changes:
- Deleting tracks is handled by a service
- Uninstall deletes all data (i.e., hasFragileData=false)
Bugfixes:
- Race condition: crash while recording and switching OpenTracks into background
- Search in track list was not working
- Cycling speed sensor: distance computation was wrong (only affected statistics)
Developer:
- ControllerFragment replaces TrackController
@@ -0,0 +1,7 @@
v3.18.2: OpenTracks
Bugfix:
- Crash while moving from charts to another activity/app #787
Developer:
- TrackRecordingService pushes data to UI while recording
@@ -0,0 +1,13 @@
v3.18.3: OpenTracks
Changes:
- KML parsing does not require order of when/gx:coords
- Track importer: reorders locations by timestamp
- Removed rendered launcher icons for Android < 8.0.0 (API 26)
(might look slightly different, but avoid problems for reproducible builds)
Bugfix:
- Avoid crash in charts due to race condition
Developer:
- Split GPX and KML importer (removed common inheritance)
@@ -3,9 +3,9 @@ OpenTracks is a sport tracking application that completely respects your privacy
Features:
* Sport tracking: track your sport and outdoor activities
* Photos and markers: mark interesting locations while tracking
* Supports Bluetooth LE sensors:
* heart rate
* cycling: speed and cadence
* cycling: power meter
* Export data: export tracks either as KML or GPX
* No Internet access: Internet is not used
* Altitude gain and loss: via barometric sensor
* Altitude shown in EGM2008 (above mean sea level); exported as WGS84
* Voice announcements
* Supports Bluetooth LE sensors: heart rate, speed and distance (cycling), cadence (cycling), and power meter (cycling)
* Export data: export tracks either as KMZ (incl. photos), KML, or GPX
* No Internet access: Internet is not used
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
Vendored
+104 -103
View File
@@ -1,103 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1,185 @@
package de.dennisguse.opentracks;
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.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.junit.Rule;
import org.junit.Test;
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 ActivityTestRule<TrackListActivity> mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule =
GrantPermissionRule.grant(
"android.permission.ACCESS_FINE_LOCATION");
@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,285 @@
package de.dennisguse.opentracks;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ListView;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.IdlingResource;
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.assertion.ViewAssertions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
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.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.services.TrackDeleteService;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.pressBack;
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.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;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoDeleteTrackTest {
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION");
private MyIdlingResource idlingResource;
@Before
public void registerIntentServiceIdlingResource() {
Instrumentation instrumentation
= InstrumentationRegistry.getInstrumentation();
idlingResource = new MyIdlingResource(instrumentation.getTargetContext());
Espresso.registerIdlingResources(idlingResource);
}
@After
public void unregisterIntentServiceIdlingResource() {
Espresso.unregisterIdlingResources(idlingResource);
}
@Test
public void espressoDeleteTrackTest() {
int countBefore;
{
// 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(5000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
}
// back
pressBack();
// get number of items before deleting a track
countBefore = numberOfItemsListView();
// select track
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(5000));
// open menu and delete selected track
ViewInteraction overflowMenuButton = onView(
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(R.id.toolbar),
2),
2),
isDisplayed()));
overflowMenuButton.perform(click());
ViewInteraction appCompatTextView = onView(
allOf(withId(R.id.title), withText("Delete"),
childAtPosition(
childAtPosition(
withId(R.id.content),
0),
0),
isDisplayed()));
appCompatTextView.perform(click());
ViewInteraction appCompatButton = onView(
allOf(withId(android.R.id.button1), withText("Yes"),
childAtPosition(
childAtPosition(
withId(R.id.buttonPanel),
0),
3)));
appCompatButton.perform(scrollTo(), click());
// check number of items after delete
onView(withId(R.id.track_list)).check(ViewAssertions.matches(withListSize(countBefore - 1)));
}
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 Matcher<View> withListSize (final int size) {
return new TypeSafeMatcher<View> () {
@Override public boolean matchesSafely (final View view) {
return ((ListView) view).getCount () == size;
}
@Override public void describeTo (final Description description) {
description.appendText ("ListView should have " + size + " items");
}
};
}
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);
}
};
}
private int numberOfItemsListView() {
final int[] counts = new int[1];
onView(withId(R.id.track_list)).check(matches(new TypeSafeMatcher<View>() {
@Override
public boolean matchesSafely(View view) {
ListView listView = (ListView) view;
counts[0] = listView.getCount();
return true;
}
@Override
public void describeTo(Description description) {
}
}));
return counts[0];
}
private class MyIdlingResource implements IdlingResource {
private Context context;
private ResourceCallback resourceCallback;
public MyIdlingResource(Context context) {
this.context = context;
}
@Override
public String getName() {
return MyIdlingResource.class.getName();
}
@Override
public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
@Override
public boolean isIdleNow() {
boolean idle = !isDeleteServiceRunning();
if (idle && resourceCallback != null) {
resourceCallback.onTransitionToIdle();
}
return idle;
}
private boolean isDeleteServiceRunning() {
ActivityManager manager =
(ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo info :
manager.getRunningServices(Integer.MAX_VALUE)) {
if (TrackDeleteService.class.getName().equals(
info.service.getClassName())) {
return true;
}
}
return false;
}
}
}
@@ -19,10 +19,13 @@ import org.junit.Rule;
import org.junit.Test;
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.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.anything;
@LargeTest
@RunWith(AndroidJUnit4.class)
@@ -39,13 +42,13 @@ public class EspressoUITest {
public void record_pause_resume_stop() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
// wait; stay recording
trackControllerRecordButton.perform(waitFor(5000));
@@ -72,13 +75,13 @@ public class EspressoUITest {
public void record_move_through_tabs() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
tabLayout.perform(selectTabAtIndex(1));
tabLayout.perform(waitFor(1000));
@@ -97,6 +100,13 @@ public class EspressoUITest {
}
}
@LargeTest
@Test
public void selectAndDeleteTrack() {
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(2000));
}
private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() {
@Override
@@ -54,7 +54,7 @@ public class ChartPointTest {
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
// then
assertEquals(0.05, point.getAltitude(), 0.01);
assertEquals(50, point.getAltitude(), 0.01);
assertEquals(36, point.getSpeed(), 0.01);
assertEquals(1.66, point.getPace(), 0.01);
}
@@ -1,10 +1,13 @@
package de.dennisguse.opentracks.content.data;
import android.location.Location;
import org.junit.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -25,4 +28,35 @@ public class TrackPointTest {
assertFalse(tp.isRecent());
}
@Test
public void distanceToPrevious() {
Location l1 = new Location("test");
l1.setLatitude(0);
l1.setLongitude(0.0001);
TrackPoint tp1 = new TrackPoint(l1);
Location l2 = new Location("test");
l2.setLatitude(0);
l2.setLongitude(0.0002);
TrackPoint tp2 = new TrackPoint(l2);
// without sensor distance
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
// tp1 has sensor distance
tp1.setSensorDistance(Distance.of(5));
tp1.setSensorDistance(null);
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
// tp2 has sensor distance
tp1.setSensorDistance(null);
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
// tp1 and tp2 have sensor distance
tp1.setSensorDistance(Distance.of(10));
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
}
@@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.util.UintUtils;
import static org.junit.Assert.assertEquals;
@@ -100,7 +101,7 @@ public class SensorDataCyclingTest {
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
// when
current.compute(previous, 2150);
current.compute(previous, Distance.ofMM(2150));
// then
assertEquals(2.15, current.getValue().distance.toM(), 0.01);
@@ -114,7 +115,7 @@ public class SensorDataCyclingTest {
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous, 2000);
current.compute(previous, Distance.ofMM(2000));
// then
assertEquals(2, current.getValue().distance.toM(), 0.01);
@@ -1,69 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.fragments;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.Distance;
/**
* Tests {@link ChartFragment}.
*
* @author Youtao Liu
*/
//TODO Add tests that check ChartFragment
@RunWith(AndroidJUnit4.class)
public class ChartFragmentTest {
private static final double HOURS_PER_UNIT = 60.0;
private ChartFragment chartFragment;
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() {
boolean chartByDistance = false;
chartFragment = ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setRecordingDistanceInterval(Distance.of(50));
}
@Test
public void nothing() {
//TODO
}
}
@@ -3,24 +3,26 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Looper;
import android.util.Log;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.ArrayList;
@@ -30,6 +32,7 @@ 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;
@@ -41,7 +44,6 @@ 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.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -53,7 +55,7 @@ import static org.junit.Assert.assertTrue;
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p>
*/
@RunWith(JUnit4.class)
@RunWith(AndroidJUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
@@ -71,7 +73,6 @@ public class ExportImportTest {
}
private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -79,6 +80,9 @@ public class ExportImportTest {
private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description";
private File tmpFile;
private Uri tmpFileUri;
private Track track;
private List<Marker> markers = new ArrayList<>();
private List<TrackPoint> trackPoints = new ArrayList<>();
@@ -86,6 +90,22 @@ public class ExportImportTest {
private Track.Id trackId;
private Track.Id importTrackId;
private TrackImporter trackImporter;
@Before
public void fileSetup() throws IOException {
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
tmpFileUri = Uri.fromFile(tmpFile);
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
}
@After
public void FileTearDown() {
tmpFile.deleteOnExit();
tmpFileUri = null;
}
public void setUp(boolean hasSensorDistance) throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
@@ -138,7 +158,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void kml_with_trackdetail() throws TimeoutException {
public void kml_with_trackdetail() throws TimeoutException, IOException {
setUp(false);
// given
@@ -148,14 +168,13 @@ public class ExportImportTest {
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
importTrackId = trackImporter.importFile(inputStream).get(0);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
@@ -179,7 +198,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void kml_with_trackdetail_and_sensordata() throws TimeoutException {
public void kml_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp(true);
// given
@@ -189,14 +208,52 @@ public class ExportImportTest {
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
importTrackId = trackImporter.importFile(inputStream).get(0);
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
assertTrackpoints(trackPoints, true, true, true, true, true, true);
// 2. trackstatistics
assertTrackStatistics(false, true, true);
// 4. markers
assertMarkers();
}
//TODO Does not test images
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp(true);
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
// when
// 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
KmzTrackImporter importer = new KmzTrackImporter(context, trackImporter);
importTrackId = importer.importFile(tmpFileUri).get(0);
// then
// 1. track
@@ -219,7 +276,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException {
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false);
// given
@@ -231,14 +288,12 @@ public class ExportImportTest {
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
importTrackId = trackImporter.importFile(inputStream).get(0);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
@@ -247,7 +302,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void gpx() throws TimeoutException {
public void gpx() throws TimeoutException, IOException {
setUp(true);
// given
@@ -257,14 +312,13 @@ public class ExportImportTest {
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
importTrackId = trackImporter.importFile(inputStream).get(0);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
@@ -295,7 +349,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() throws TimeoutException {
public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false);
// given
@@ -308,13 +362,12 @@ public class ExportImportTest {
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
importTrackId = trackImporter.importFile(inputStream).get(0);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
@@ -357,18 +410,18 @@ public class ExportImportTest {
// TODO Not exported for GPX/KML
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation());
assertEquals("" + i, trackPoint.hasLocation(), importedTrackPoint.hasLocation());
if (trackPoint.hasLocation()) {
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
assertEquals("" + i, trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
assertEquals("" + i, trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
}
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
assertEquals("" + i, trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
if (trackPoint.hasSpeed()) {
assertEquals(trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
assertEquals("" + i, trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
}
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
assertEquals("" + i, trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
if (trackPoint.hasAltitude()) {
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
assertEquals("" + i, trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001);
}
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
@@ -377,13 +430,13 @@ public class ExportImportTest {
}
if (verifyHeartrate) {
assertEquals("" + trackPoint, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
assertEquals("" + i, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
}
if (verifyCadence) {
assertEquals("" + trackPoint, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
assertEquals("" + i, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
}
if (verifyPower) {
assertEquals("" + trackPoint, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
assertEquals("" + i, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
}
if (verifyAltitudeGain) {
assertEquals(trackPoint.getAltitudeGain(), importedTrackPoint.getAltitudeGain(), 0.01);
@@ -438,7 +491,7 @@ public class ExportImportTest {
}
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, (double) altitude, Instant.ofEpochMilli(time));
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);
@@ -7,13 +7,16 @@ import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -28,13 +31,20 @@ import static org.junit.Assert.assertNotNull;
* Test that legacy KML/GPX formats can still be imported.
*/
@RunWith(JUnit4.class)
public class LegacyImportTest {
public class GPXImportTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private TrackImporter trackImporter;
private Track.Id importTrackId;
@Before
public void setUp() {
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
}
@After
public void tearDown() {
if (importTrackId != null) {
@@ -42,76 +52,19 @@ public class LegacyImportTest {
}
}
/**
* Check that data with statistics markers is imported and those ignored.
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
*/
@LargeTest
@Test
public void kml_with_statistics_marker() {
// given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_statistics_marker);
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("unknown", importedTrack.getCategory());
assertEquals("Test Track", importedTrack.getDescription());
assertEquals("2020-11-28 18:06", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(6, importedTrackPoints.size());
// first 3 trackpoints
assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
// created resume trackpoint with time of next valid trackpoint
assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
}
/**
* At least one valid location is required.
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
*/
@LargeTest
@Test(expected = ImportParserException.class)
public void kml_without_locations() {
// given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty);
// when
importTrackId = trackImporter.importFile(inputStream).get(0);
}
/**
* Check that data that contains pause (lat=100, lng=0) and resume (lat=200, lng=0) locations are restored to a segment break.
*/
@LargeTest
@Test
public void gpx_with_pause_resume() {
public void gpx_with_pause_resume() throws IOException {
// given
GpxFileTrackImporter trackImporter = new GpxFileTrackImporter(context);
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream).get(0);
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
@@ -137,7 +90,7 @@ public class LegacyImportTest {
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:04.103Z", 14.003, 3.0, 10.0);
}
private void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
static void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
assertEquals(type, trackPoint.getType());
@@ -151,8 +104,7 @@ public class LegacyImportTest {
if (altitude == null) {
assertFalse(trackPoint.hasAltitude());
} else {
assertEquals(altitude, (Double) trackPoint.getAltitude());
assertEquals(altitude, (Double) trackPoint.getAltitude().toM());
}
}
}
@@ -0,0 +1,191 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Test that legacy KML/GPX formats can still be imported.
*/
@RunWith(JUnit4.class)
public class KMLImportTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private TrackImporter trackImporter;
private Track.Id importTrackId;
@Before
public void setUp() {
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
}
@After
public void tearDown() {
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
}
/**
* KML does not specify the order of gx:coords and when; only count has to be the same.
*/
@LargeTest
@Test
public void kml22_order_location_and_when() throws IOException {
// given
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_order_location_and_when);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getDescription());
assertEquals("", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(5, importedTrackPoints.size());
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-05-29T18:06:21.767Z", null, null, null);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.042Z", 14.0, 3.0, 10.0);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.192Z", 14.001, 3.0, 10.0);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.318Z", 14.002, 3.0, 10.0);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.SEGMENT_END_MANUAL, "2021-05-29T18:06:22.512Z", null, null, null);
}
/**
* Coordinates / when might not be ordered by increasing time.
*/
@LargeTest
@Test
public void kml22_time_decreases() throws IOException {
// given
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_time_decreases);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getDescription());
assertEquals("", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(2, importedTrackPoints.size());
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:21.767Z", 14.0, 3.0, 10.0);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-05-29T18:06:22.042Z", null, null, null);
}
/**
* Check that data with statistics markers is imported and those ignored.
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
*/
@LargeTest
@Test
public void kml22_with_statistics_marker() throws IOException {
// given
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml22_statistics_marker);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("unknown", importedTrack.getCategory());
assertEquals("Test Track", importedTrack.getDescription());
assertEquals("2020-11-28 18:06", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(6, importedTrackPoints.size());
// first 3 trackpoints
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
// created resume trackpoint with time of next valid trackpoint
GPXImportTest.assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
GPXImportTest.assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
}
/**
* At least one valid location is required.
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
*/
@LargeTest
@Test(expected = ImportParserException.class)
public void kml_without_locations() throws IOException {
// given
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_without_locations);
// when
importTrackId = importer.importFile(inputStream).get(0);
}
@LargeTest
@Test(expected = ImportParserException.class)
public void kml_when_locations_different() throws IOException {
// given
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_when_locations_different);
// when
importTrackId = importer.importFile(inputStream).get(0);
}
}
@@ -31,6 +31,7 @@ import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -83,6 +84,10 @@ public class TrackRecordingServiceTest {
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@@ -13,6 +13,7 @@ import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -44,6 +45,7 @@ import static org.junit.Assert.assertNotNull;
* So, if it is executed on a real device, data might be lost.
*/
@RunWith(AndroidJUnit4.class)
//TODO Implement as mock test; no need to store data in database
public class TrackRecordingServiceTestLocation {
@Rule
@@ -63,6 +65,11 @@ public class TrackRecordingServiceTestLocation {
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() throws TimeoutException {
// Set up the mock content resolver
@@ -1,59 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link AnnouncementPeriodicTaskFactory}.
*
* @author Rodrigo Damazio
*/
@RunWith(AndroidJUnit4.class)
public class AnnouncementPeriodicTaskFactoryTest {
private final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Test
public void testCreate() {
PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory();
PeriodicTask task = factory.create(context);
assertTrue(task instanceof AnnouncementPeriodicTask);
}
}
@@ -0,0 +1,69 @@
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class PeriodicTaskExecutorTest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void calculateNextTaskDistance() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
PeriodicTaskExecutor periodicTaskExecutor = new PeriodicTaskExecutor(service, new AnnouncementPeriodicTask.Factory());
periodicTaskExecutor.setMetricUnits(true);
periodicTaskExecutor.setTaskFrequency(-5);
// when
TrackStatistics statistics = new TrackStatistics();
statistics.setTotalDistance(Distance.of(13000));
assertEquals(Distance.of(15000), periodicTaskExecutor.calculateNextTaskDistance(statistics));
statistics.setTotalDistance(Distance.of(15100));
assertEquals(Distance.of(20000), periodicTaskExecutor.calculateNextTaskDistance(statistics));
}
}
@@ -9,6 +9,7 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -53,8 +54,8 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -71,8 +72,8 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
// when
@@ -90,11 +91,11 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp3 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp4 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(4000));
TrackPoint tp4 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp2.setSpeed(Speed.of(5f));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
@@ -122,9 +123,9 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(Distance.of(10f));
@@ -145,6 +146,34 @@ public class TrackStatisticsUpdaterTest {
assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSensorDistance(Distance.of(5f));
TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(5, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
// when
subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE);
// then
assertEquals(59.18, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_not_moving() {
@@ -0,0 +1,229 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
@RunWith(JUnit4.class)
public class EGM2008UtilsTest {
private static final double MAX_BILINEAR_ERROR = 0.478;
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void fileVerification() throws IOException {
// given
int expectedLength = 18671444;
int expectedHeaderLength = 404;
String expectedHeader = "P5\n" +
"# Geoid file in PGM format for the GeographicLib::Geoid class\n" +
"# Description WGS84 EGM2008, 5-minute grid\n" +
"# URL http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008\n" +
"# DateTime 2009-08-29 18:45:00\n" +
"# MaxBilinearError 0.478\n" +
"# RMSBilinearError 0.012\n" +
"# MaxCubicError 0.294\n" +
"# RMSCubicError 0.005\n" +
"# Offset -108\n" +
"# Scale 0.003\n" +
"# Origin 90N 0E\n" +
"# AREA_OR_POINT Point\n" +
"# Vertical_Datum WGS84\n" +
"4320 2161\n" +
"65535" +
"\n";
// when
try (InputStream inputStream = context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA)) {
assertEquals(expectedLength, inputStream.available());
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII);
char[] data = new char[expectedHeaderLength];
int length = reader.read(data);
// then
assertEquals(expectedHeaderLength, length);
assertEquals(expectedHeader, new String(data));
}
}
@Test
public void data_Northpole() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(90);
trackPoint.setLongitude(0.1);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-14.8980, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Southpole() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(0);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Southpole2() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(180);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Southpole3() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(-180);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_0() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(0);
trackPoint.setLongitude(0);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-17.2260, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Berlin() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(52.530644);
trackPoint.setLongitude(13.383068);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-39.4865, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Berlin_Caching() throws IOException {
// given
TrackPoint trackPoint1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint1.setLatitude(52.530644);
trackPoint1.setLongitude(13.383068);
trackPoint1.setAltitude(0);
TrackPoint trackPoint2 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint2.setLatitude(52.530000);
trackPoint2.setLongitude(13.380000);
trackPoint2.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint1.getLocation());
// then
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
}
@Test
public void data_MaxUndulation() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(-8.417);
trackPoint.setLongitude(147.367);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void getIndices() {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(90);
trackPoint.setLongitude(0);
assertEquals(new EGM2008Utils.Indices(0, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
trackPoint.setLatitude(0);
trackPoint.setLongitude(0);
assertEquals(new EGM2008Utils.Indices(1080, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(180);
assertEquals(new EGM2008Utils.Indices(2160, 4320 / 2), EGM2008Utils.getIndices(trackPoint.getLocation()));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(-180);
assertEquals(new EGM2008Utils.Indices(2160, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
}
@Test
public void getUndulationRaw_ok() throws IOException {
try (DataInputStream dataInputStream = new DataInputStream(context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA))) {
assertEquals(40966, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(0, 0)));
assertEquals(41742, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(1081, 0)));
assertEquals(25950, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(2160, 4319)));
}
}
@Test(expected = EOFException.class)
public void getUndulationRaw_error() throws IOException {
try (DataInputStream dataInputStream = new DataInputStream(context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA))) {
assertEquals(0, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(2161, 4320)), 0.01);
}
}
}
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
<gx:Track>
<when>2021-05-29T18:06:21.767Z</when>
<gx:coord />
<gx:coord>14.0 3.0 10.0</gx:coord>
<when>2021-05-29T18:06:22.042Z</when>
<when>2021-05-29T18:06:22.192Z</when>
<when>2021-05-29T18:06:22.318Z</when>
<when>2021-05-29T18:06:22.512Z</when>
<gx:coord>14.001 3.0 10.0</gx:coord>
<gx:coord>14.002 3.0 10.0</gx:coord>
<gx:coord />
</gx:Track>
</gx:MultiTrack>
</Placemark>
</Document>
</kml>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
<gx:Track>
<when>2021-05-29T18:06:22.042Z</when>
<gx:coord />
<when>2021-05-29T18:06:21.767Z</when>
<gx:coord>14.0 3.0 10.0</gx:coord>
</gx:Track>
</gx:MultiTrack>
</Placemark>
</Document>
</kml>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
<gx:Track>
<when>2021-05-29T18:06:21.767Z</when>
<gx:coord />
<gx:coord>14.0 3.0 10.0</gx:coord>
</gx:Track>
</gx:MultiTrack>
</Placemark>
</Document>
</kml>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
<gx:Track />
</gx:MultiTrack>
</Placemark>
</Document>
</kml>
@@ -1,62 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
<Document>
<open>1</open>
<visibility>1</visibility>
<name><![CDATA[2020-08-24 20:57]]></name>
<atom:generator><![CDATA[OpenTracks]]></atom:generator>
<Style id="track">
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
<IconStyle>
<scale>1.3</scale>
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
</IconStyle>
</Style>
<Style id="start"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="end"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="waypoint"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Schema id="schema">
<gx:SimpleArrayField name="speed" type="float">
<displayName><![CDATA[Speed (m/s)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="power" type="float">
<displayName><![CDATA[Power (W)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="cadence" type="float">
<displayName><![CDATA[Cadence (rpm)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="heart_rate" type="float">
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
</gx:SimpleArrayField>
</Schema>
<Placemark>
<name><![CDATA[2020-08-24 20:57]]></name>
<description><![CDATA[]]></description>
<icon><![CDATA[UNKNOWN]]></icon>
<opentracks:trackid>9316e527-a5b8-47c5-bdbd-954b4be7d80e</opentracks:trackid>
<styleUrl>#track</styleUrl>
<ExtendedData>
<Data name="type"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
</gx:MultiTrack>
</Placemark>
</Document>
</kml>
+7
View File
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:hasFragileUserData="true"
tools:replace="android:hasFragileUserData" />
</manifest>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

+9 -6
View File
@@ -51,18 +51,14 @@ limitations under the License.
android:name=".Startup"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:hasFragileUserData="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/ThemeCustom">
<activity android:name=".AboutActivity" />
<activity
android:name=".TrackDeleteActivity"
android:theme="@style/TrackDeleteActivity" />
<activity
android:name=".HelpActivity"
android:label="@string/menu_help" />
@@ -350,6 +346,13 @@ limitations under the License.
android:name=".io.file.importer.ImportService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".services.TrackDeleteService"
android:enabled="true"
android:exported="false"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/delete_service" />
</application>
<queries>
@@ -1,96 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.content.Intent;
import java.util.ArrayList;
import java.util.Arrays;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.IntentUtils;
/**
* An abstract class for the following common tasks across
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
* <p>
* - share track <br>
* - delete tracks <br>
*
* @author Jimmy Shih
*/
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
private static final String TAG = AbstractListActivity.class.getSimpleName();
protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3;
protected Track.Id recordingTrackId;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) {
onTrackDeleted();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
/**
* Delete tracks.
*
* @param trackIds the track ids
*/
protected void deleteTracks(Track.Id... trackIds) {
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
}
@Override
public void onConfirmDeleteDone(Track.Id... trackIds) {
boolean stopRecording = false;
for (Track.Id trackId : trackIds) {
if (trackId.equals(recordingTrackId)) {
stopRecording = true;
break;
}
}
if (stopRecording) {
getTrackRecordingServiceConnection().stopRecording(this);
}
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
intent.putParcelableArrayListExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
startActivityForResult(intent, DELETE_REQUEST_CODE);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}
/**
* Gets the track recording service connection.
* For stopping the current recording if need to delete the current recording track.
*/
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
/**
* Called after {@link TrackDeleteActivity} returns its result.
*/
abstract protected void onTrackDeleted();
}
@@ -0,0 +1,125 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import de.dennisguse.opentracks.services.TrackDeleteService;
import de.dennisguse.opentracks.services.TrackDeleteServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
/**
* An abstract class for the following common tasks across
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
* <p>
* - delete tracks <br>
*
* @author Jimmy Shih
*/
//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
protected void onStart() {
super.onStart();
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
trackDeleteServiceConnection.bind(this);
}
@Override
protected void onStop() {
super.onStop();
if (trackDeleteServiceConnection != null) {
trackDeleteServiceConnection.unbind(this);
trackDeleteServiceConnection = null;
}
}
/**
* Delete tracks.
*
* @param trackIds the track ids
*/
protected void deleteTracks(Track.Id... trackIds) {
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
}
@Override
public void onConfirmDeleteDone(Track.Id... trackIds) {
boolean stopRecording = false;
onDeleteConfirmed();
for (Track.Id trackId : trackIds) {
if (trackId.equals(getRecordingTrackId())) {
stopRecording = true;
break;
}
}
if (stopRecording) {
getTrackRecordingServiceConnection().stopRecording(this);
}
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
trackDeleteServiceConnection.startAndBind(this, new ArrayList<>(Arrays.asList(trackIds)));
}
/**
* Gets the track recording service connection.
* For stopping the current recording if need to delete the current recording track.
*/
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return null;
}
/**
* Called every time a track is deleted.
*/
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
trackDeleteServiceConnection.unbind(this);
trackDeleteServiceConnection = null;
}
}
protected abstract void onDeleteConfirmed();
@Nullable
protected Track.Id getRecordingTrackId() {
return null;
}
@Override
public void connected() {
TrackDeleteService service = trackDeleteServiceConnection.getServiceIfBound();
if (service == null) {
return;
}
service.getDeletingStatusObservable().observe(AbstractTrackDeleteActivity.this, status -> onTrackDeleteStatus(status));
}
}
@@ -0,0 +1,259 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.annotation.SuppressLint;
import android.graphics.drawable.TransitionDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import de.dennisguse.opentracks.databinding.ControllerFragmentBinding;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Track controller for record, pause, resume, and stop.
*
* @author Jimmy Shih
*/
//TODO Move buttons into separate components
public class ControllerFragment extends Fragment implements View.OnTouchListener {
private static final String TAG = ControllerFragment.class.getSimpleName();
private Handler handlerUpdateTotalTime;
private Runnable buttonDelay;
private int buttonDelayDuration;
private ControllerFragmentBinding viewBinding;
private TransitionDrawable transition;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackRecordingService.RecordingStatus recordingStatus;
private TrackRecordingService.RecordingData recordingData;
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingStatusObservable()
.observe(ControllerFragment.this, status -> onRecordingStatusChanged(status));
service.getRecordingDataObservable()
.observe(ControllerFragment.this, recordingData -> onTotalTimeChanged(recordingData));
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
buttonDelayDuration = getResources().getInteger(R.integer.buttonDelayMillis);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
handlerUpdateTotalTime = new Handler();
recordingStatus = TrackRecordingService.STATUS_DEFAULT;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
viewBinding = ControllerFragmentBinding.inflate(inflater, container, false);
viewBinding.controllerRecord.setOnTouchListener(this);
viewBinding.controllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
return;
}
getCallback().recordStart();
});
viewBinding.controllerStop.setOnTouchListener(this);
transition = (TransitionDrawable) viewBinding.controllerContainer.getBackground();
onRecordingStatusChanged(recordingStatus);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
trackRecordingServiceConnection.startConnection(getContext());
}
@Override
public void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind(getContext());
}
@Override
public void onDestroyView() {
super.onDestroyView();
viewBinding = null;
transition = null;
}
@Override
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection = null;
handlerUpdateTotalTime = null;
recordingStatus = null;
}
//TODO Move into a separate object
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override
public boolean onTouch(final View view, MotionEvent event) {
if (!isResumed()) {
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
return false;
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
buttonDelay = null;
view.setPressed(false);
transition.resetTransition();
showStatusSetDefaultText();
return false;
}
//Trigger button pressed animation
view.setPressed(true);
//To pause a recording
if (viewBinding.controllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecordingAndNotPaused()) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(getContext(), 150);
viewBinding.controllerStatus.setText(R.string.hold_to_pause);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
getCallback().recordPause();
transition.resetTransition();
ActivityUtils.vibrate(getContext(), 1000);
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
//To stop a recording
if (viewBinding.controllerStop.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecording()) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(getContext(), 150);
viewBinding.controllerStatus.setText(R.string.hold_to_stop);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
getCallback().recordStop();
ActivityUtils.vibrate(getContext(), 1000);
transition.resetTransition();
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
return false;
}
public void hide() {
viewBinding.controllerContainer.setVisibility(View.GONE);
}
public void show() {
viewBinding.controllerContainer.setVisibility(View.VISIBLE);
}
private void showStatusSetDefaultText() {
viewBinding.controllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
}
private Callback getCallback() {
return (Callback) getActivity();
}
public interface Callback {
void recordStart();
void recordPause();
void recordStop();
}
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
viewBinding.controllerRecord.setImageResource(recordingStatus.isRecordingAndNotPaused() ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.controllerRecord.setContentDescription(getContext().getString(recordingStatus.isRecordingAndNotPaused() ? R.string.image_pause : R.string.image_record));
viewBinding.controllerStop.setEnabled(recordingStatus.isRecording());
viewBinding.controllerStatus.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
if (recordingStatus.isRecording()) {
viewBinding.controllerStatus.setTextColor(getContext().getResources().getColor(recordingStatus.isPaused() ? android.R.color.white : R.color.recording_text));
showStatusSetDefaultText();
}
viewBinding.controllerTotalTime.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
}
private void onTotalTimeChanged(TrackRecordingService.RecordingData recordingData) {
this.recordingData = recordingData;
viewBinding.controllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(this.recordingData.getTrackStatistics().getTotalTime()));
}
}
@@ -20,6 +20,7 @@ import android.app.SearchManager;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
@@ -42,7 +43,6 @@ import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -51,7 +51,7 @@ import de.dennisguse.opentracks.util.IntentUtils;
*
* @author Leif Hendrik Wilden
*/
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller, TrackRecordingServiceStatus.Listener {
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -59,8 +59,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private ContentProviderUtils contentProviderUtils;
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private Track track;
private MarkerResourceCursorAdapter resourceCursorAdapter;
@@ -74,9 +74,13 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(MarkerListActivity.this);
if (service == null) {
Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingStatusObservable()
.observe(MarkerListActivity.this, status -> onRecordingStatusChanged(status));
}
};
@@ -171,14 +175,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search);
ActivityUtils.configureSearchWidget(this, searchMenuItem, null);
ActivityUtils.configureSearchWidget(this, searchMenuItem);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingTrackId) && !recordingTrackPaused);
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingStatus.getTrackId()) && !recordingStatus.isPaused());
return super.onPrepareOptionsMenu(menu);
}
@@ -257,15 +261,16 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override
public void onBackPressed() {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
}
if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null);
return;
}
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed();
}
@@ -290,6 +295,10 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
// Do nothing
}
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
}
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
@@ -344,14 +353,4 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
resourceCursorAdapter.swapCursor(null);
}
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
recordingTrackPaused = isPaused;
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
}
}
@@ -1,246 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import java.time.Duration;
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Track controller for record, pause, resume, and stop.
*
* @author Jimmy Shih
*/
public class TrackController implements View.OnTouchListener {
private static final String TAG = TrackController.class.getSimpleName();
private static final Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
private final Activity activity;
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Handler handlerUpdateTotalTime = new Handler();
private final boolean alwaysShow;
private Runnable buttonDelay;
private final int buttonDelayDuration;
private final TrackControllerBinding viewBinding;
private final TransitionDrawable transition;
private boolean isRecording;
private boolean isRecordingPaused;
private Duration totalTime;
private boolean isResumed = false;
private final Callback callback;
// A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
if (isResumed && isRecording && !isRecordingPaused) {
updateTotalTime();
setTotalTime();
handlerUpdateTotalTime.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
}
}
};
@SuppressLint("ClickableViewAccessibility")
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, Callback callback) {
this.activity = activity;
this.viewBinding = viewBinding;
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow;
this.callback = callback;
buttonDelayDuration = activity.getResources().getInteger(R.integer.buttonDelayMillis);
viewBinding.trackControllerRecord.setOnTouchListener(this);
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || (isRecording && !isRecordingPaused)) {
return;
}
callback.recordStart();
});
viewBinding.trackControllerStop.setOnTouchListener(this);
transition = (TransitionDrawable) viewBinding.trackControllerContainer.getBackground();
}
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override
public boolean onTouch(final View view, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
buttonDelay = null;
view.setPressed(false);
transition.resetTransition();
showStatusSetDefaultText();
return false;
}
//Trigger button pressed animation
view.setPressed(true);
//To pause a recording
if (viewBinding.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording && !isRecordingPaused) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(activity, 150);
viewBinding.trackControllerStatus.setText(R.string.hold_to_pause);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
callback.recordPause();
transition.resetTransition();
ActivityUtils.vibrate(activity, 1000);
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
//To stop a recording
if (viewBinding.trackControllerStop.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(activity, 150);
viewBinding.trackControllerStatus.setText(R.string.hold_to_stop);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
callback.recordStop();
ActivityUtils.vibrate(activity, 1000);
transition.resetTransition();
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
return false;
}
public void update(boolean recording, boolean paused) {
if (!isResumed) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
isRecording = recording;
isRecordingPaused = paused;
boolean visible = alwaysShow || isRecording;
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
if (!visible) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
if (isRecording && !isRecordingPaused) {
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UI_UPDATE_INTERVAL.toMillis());
}
viewBinding.trackControllerRecord.setImageResource(isRecording && !isRecordingPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isRecordingPaused ? R.string.image_pause : R.string.image_record));
viewBinding.trackControllerStop.setEnabled(isRecording);
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isRecordingPaused ? android.R.color.white : R.color.recording_text));
showStatusSetDefaultText();
}
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
updateTotalTime();
setTotalTime();
}
}
void onResume(boolean recording, boolean paused) {
isResumed = true;
update(recording, paused);
}
void onPause() {
isResumed = false;
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
}
public void hide() {
viewBinding.trackControllerContainer.setVisibility(View.GONE);
}
public void show() {
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
}
private void updateTotalTime() {
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
totalTime = trackRecordingService.getTotalTime();
}
}
private void setTotalTime() {
if (totalTime != null) {
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
}
}
private void showStatusSetDefaultText() {
viewBinding.trackControllerStatus.setText(isRecordingPaused ? R.string.generic_paused : R.string.generic_recording);
}
public interface Callback {
void recordStart();
void recordPause();
void recordStop();
}
}
@@ -1,101 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.content.Intent;
import android.os.Bundle;
import android.os.PowerManager;
import android.view.View;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackDeleteBinding;
import de.dennisguse.opentracks.util.SystemUtils;
/**
* An activity for deleting tracks.
*
* @author Jimmy Shih
*/
public class TrackDeleteActivity extends AbstractActivity {
public static final String EXTRA_TRACK_IDS = "track_ids";
private List<Track.Id> trackIds;
private Thread deleteThread;
private TrackDeleteBinding viewBinding;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(RESULT_CANCELED);
Intent intent = getIntent();
trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
deleteThread = new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
contentProviderUtils.deleteTracks(TrackDeleteActivity.this, trackIds);
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (Thread.interrupted()) {
return;
}
runOnUiThread(TrackDeleteActivity.this::onAsyncTaskCompleted);
});
deleteThread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
@Override
protected void onStart() {
super.onStart();
if (deleteThread.getState() == Thread.State.TERMINATED) {
onAsyncTaskCompleted();
}
}
@Override
protected void onStop() {
super.onStop();
deleteThread.interrupt();
}
@Override
protected View getRootView() {
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
public void onAsyncTaskCompleted() {
viewBinding.progressbar.setVisibility(View.INVISIBLE);
setResult(RESULT_OK);
finish();
}
}
@@ -38,6 +38,7 @@ import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
@@ -55,7 +56,6 @@ import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.ActivityUtils;
@@ -71,16 +71,16 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback, TrackRecordingServiceStatus.Listener {
public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, ControllerFragment.Callback {
private static final String TAG = TrackListActivity.class.getSimpleName();
protected static final int GPS_REQUEST_CODE = 6;
// The following are set in onCreate
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackController trackController;
private ResourceCursorAdapter resourceCursorAdapter;
private GpsStatusValue gpsStatusValue;
private TrackListBinding viewBinding;
@@ -89,7 +89,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Preferences
private boolean metricUnits = true;
private boolean recordingTrackPaused = true;
private GpsStatusValue gpsStatusValue = TrackRecordingService.STATUS_GPS_DEFAULT;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@@ -118,7 +119,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu();
loaderCallbacks.restart();
trackController.update(isRecording(), recordingTrackPaused);
});
}
}
@@ -132,28 +132,27 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
// After binding changes (e.g., becomes available), update the total time in trackController.
runOnUiThread(() -> trackController.update(isRecording(), recordingTrackPaused));
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "service not available to start gps or a new recording");
gpsStatusValue = GpsStatusValue.GPS_NONE;
Log.e(TAG, "service not available to start gps or a new recording");
return;
}
service.getRecordingStatusObservable()
.observe(TrackListActivity.this, status -> onRecordingStatusChanged(status));
// Get GPS status and listen GPS status changes.
gpsStatusValue = service.getGpsStatus();
updateGpsMenuItem(true, isRecording());
service.addListener(TrackListActivity.this);
service.getGpsStatusObservable()
.observe(TrackListActivity.this, status -> onGpsStatusChanged(status));
if (isGpsStarted()) {
updateGpsMenuItem(true, recordingStatus.isRecording());
if (service.getGpsStatusObservable().getValue().isGpsStarted()) {
return;
}
//TODO Not cool to do this in a callback that might be called more than once!
service.tryStartGps();
gpsStatusValue = GpsStatusValue.GPS_ENABLED;
updateGpsMenuItem(true, isRecording());
}
};
@@ -165,23 +164,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
gpsStatusValue = GpsStatusValue.GPS_NONE;
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, this);
// Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
if (searchManager != null) {
searchManager.setOnDismissListener(() -> trackController.show());
}
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
if (isRecording() && trackId == recordingTrackId.getId()) {
if (recordingStatus.isRecording() && trackId == recordingStatus.getTrackId().getId()) {
// Is recording -> open record activity.
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId));
@@ -211,7 +200,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
boolean isRecording = trackId.equals(recordingTrackId);
boolean isRecording = trackId.equals(recordingStatus.getTrackId());
String icon = cursor.getString(iconIndex);
int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex);
@@ -222,7 +211,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingTrackPaused,
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingStatus.isPaused(),
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
startTime, true, category, description, false);
}
@@ -250,14 +239,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onPause() {
super.onPause();
// Update UI
trackController.onPause();
}
@Override
protected void onResume() {
super.onResume();
@@ -265,7 +246,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Update UI
this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
trackController.onResume(isRecording(), recordingTrackPaused);
}
@Override
@@ -298,7 +278,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
getMenuInflater().inflate(R.menu.track_list, menu);
searchMenuItem = menu.findItem(R.id.track_list_search);
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem);
searchView.setOnCloseListener(() -> {
searchView.clearFocus();
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
});
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
return super.onCreateOptionsMenu(menu);
@@ -306,7 +292,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(isGpsStarted(), isRecording());
updateGpsMenuItem(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery("", false);
@@ -322,7 +308,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else {
// Invoke trackRecordingService
if (!isGpsStarted()) {
if (!gpsStatusValue.isGpsStarted()) {
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
} else {
@@ -355,6 +341,12 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return true;
}
if (item.getItemId() == R.id.track_list_search) {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setIconified(false);
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
return super.onOptionsItemSelected(item);
}
@@ -366,17 +358,23 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return super.onKeyUp(keyCode, event);
}
@Override
public void overridePendingTransition(int enterAnim, int exitAnim) {
//Disable animations as it is weird going into searchMode; looks okay for SplashScreen.
}
@Override
public void onBackPressed() {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
}
if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null);
return;
}
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed();
}
@@ -396,25 +394,20 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
loaderCallbacks.setSearch(searchQuery);
}
@Override
public boolean onSearchRequested() {
// Hide trackController when search dialog is shown
trackController.hide();
return super.onSearchRequested();
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onTrackDeleted() {
protected void onDeleteConfirmed() {
// Do nothing
}
private boolean isGpsStarted() {
return gpsStatusValue != GpsStatusValue.GPS_NONE && gpsStatusValue != GpsStatusValue.GPS_DISABLED;
@Nullable
@Override
protected Track.Id getRecordingTrackId() {
return recordingStatus.getTrackId();
}
private void requestGPSPermissions() {
@@ -423,22 +416,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
}
/**
* Updates the menu items with not fixed icon for gps option.
*
* @param isGpsStarted true if gps is started
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isGpsStarted, boolean isRecording) {
updateGpsMenuItem(isGpsStarted, isRecording);
}
/**
* Updates the menu items with the icon specified.
*
* @param isGpsStarted true if gps is started
* @param isRecording true if recording
*/
//TODO Check if if can be avoided to call this outside of onGpsStatusChanged()
private void updateGpsMenuItem(boolean isGpsStarted, boolean isRecording) {
if (startGpsMenuItem != null) {
startGpsMenuItem.setVisible(!isRecording);
@@ -477,7 +461,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return true;
}
if (itemId == R.id.list_context_menu_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
@@ -489,6 +472,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
deleteTracks(trackIds);
return true;
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
viewBinding.trackList.setItemChecked(i, true);
@@ -554,55 +538,36 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public void recordStart() {
if (recordingTrackId == null) {
if (recordingStatus.getTrackId() == null) {
// Not recording -> Recording
updateMenuItems(false, true);
updateGpsMenuItem(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
} else if (recordingStatus.isPaused()) {
// Paused -> Resume
updateMenuItems(false, true);
updateGpsMenuItem(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
}
}
@Override
public void recordPause() {
updateMenuItems(false, true);
updateGpsMenuItem(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
@Override
public void recordStop() {
updateMenuItems(false, false);
updateGpsMenuItem(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
}
private boolean isRecording() {
return recordingTrackId != null;
}
@Override
public void onGpsStatus(GpsStatusValue newStatus) {
public void onGpsStatusChanged(GpsStatusValue newStatus) {
gpsStatusValue = newStatus;
updateGpsMenuItem(true, isRecording());
updateGpsMenuItem(true, recordingStatus.isRecording());
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
if (recordingTrackPaused != isPaused) {
trackController.update(isRecording(), isPaused);
}
recordingTrackPaused = isPaused;
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
if (!isRecording() && trackId != null) {
trackController.update(true, recordingTrackPaused);
}
recordingTrackId = trackId;
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
}
}
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
@@ -24,6 +25,7 @@ import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -32,7 +34,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayoutMediator;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
@@ -40,12 +41,13 @@ import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.services.TrackDeleteService;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* An activity to show the track detail, record a new track or resumes an existing one.
@@ -54,24 +56,25 @@ import de.dennisguse.opentracks.util.IntentUtils;
* @author Rodrigo Damazio
*/
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface, TrackRecordingServiceStatus.Listener {
public class TrackRecordedActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
public static final String VIEW_TRACK_ICON = "track_icon";
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
// The following are set in onCreate.
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackDataHub trackDataHub;
private TrackRecordedBinding viewBinding;
private Track.Id trackId;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
@@ -79,8 +82,22 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(TrackRecordedActivity.this);
if (service == null) {
Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingStatusObservable()
.observe(TrackRecordedActivity.this, status -> onRecordingStatusChanged(status));
}
};
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(TrackRecordedActivity.this, R.string.recording_distance_interval_key, key)) {
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordedActivity.this));
}
}
};
@@ -90,6 +107,8 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
super.onCreate(savedInstanceState);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
handleIntent(getIntent());
trackDataHub = new TrackDataHub(this);
@@ -110,6 +129,10 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackDataHub.start();
}
@@ -132,6 +155,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
super.onStop();
trackRecordingServiceConnection.unbind(this);
trackDataHub.stop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
@@ -162,7 +186,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.findItem(R.id.track_detail_resume_track).setVisible(recordingTrackId == null);
menu.findItem(R.id.track_detail_resume_track).setVisible(!recordingStatus.isRecording());
Track track = contentProviderUtils.getTrack(trackId);
setTitle(track != null ? track.getName() : "");
return super.onPrepareOptionsMenu(menu);
@@ -217,14 +241,22 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
return super.onOptionsItemSelected(item);
}
@Nullable
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
// Not needed.
return null;
protected Track.Id getRecordingTrackId() {
return recordingStatus.getTrackId();
}
@Override
protected void onTrackDeleted() {
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
super.onTrackDeleteStatus(deleteStatus);
if (deleteStatus.isDeleted(trackId)) {
runOnUiThread(this::finish);
}
}
@Override
protected void onDeleteConfirmed() {
runOnUiThread(this::finish);
}
@@ -238,18 +270,6 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
private void handleIntent(Intent intent) {
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
Marker.Id markerId = intent.getParcelableExtra(EXTRA_MARKER_ID);
if (markerId != null) {
// Use the trackId from the marker
Marker marker = contentProviderUtils.getMarker(markerId);
if (marker == null) {
finish();
return;
}
trackId = marker.getTrackId();
}
if (trackId == null) {
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
finish();
@@ -305,8 +325,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
startPostponedEnterTransition();
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
}
}
@@ -28,7 +28,6 @@ import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -44,7 +43,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
*/
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
//This mode of communication should be removed anyhow.
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback, TrackRecordingServiceStatus.Listener {
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, ControllerFragment.Callback {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -57,25 +56,27 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub;
private TrackController trackController;
private TrackRecordingBinding viewBinding;
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
@Deprecated
private Track.Id trackId;
private boolean recordingTrackPaused;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
// After binding changes (is available), update the total time in trackController.
runOnUiThread(() -> trackController.update(true, recordingTrackPaused));
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "could not get TrackRecordingService");
Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingStatusObservable()
.observe(TrackRecordingActivity.this, status -> onRecordingStatusChanged(status));
if (!service.isRecording()) {
if (trackId == null) {
// trackId isn't initialized -> leads a new recording.
@@ -87,12 +88,8 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// A recording track is on.
trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingTrackId(trackId);
trackController.update(true, false);
trackController.onResume(true, recordingTrackPaused);
trackDataHub.setRecordingStatus(recordingStatus);
}
service.addListener(TrackRecordingActivity.this);
}
};
@@ -108,11 +105,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_fullscreen_while_recording_key, key)) {
setFullscreenPolicy();
}
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_distance_interval_key, key)) {
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordingActivity.this));
}
if (key == null) return;
runOnUiThread(() -> {
TrackRecordingActivity.this.invalidateOptionsMenu();
trackController.update(true, recordingTrackPaused);
});
runOnUiThread(TrackRecordingActivity.this::invalidateOptionsMenu);
}
};
@@ -147,8 +145,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
if (savedInstanceState != null) {
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, this);
}
@Override
@@ -207,13 +203,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
super.onResume();
// Update UI
this.invalidateOptionsMenu();
invalidateOptionsMenu();
if (trackId != null) {
//TODO Pass recordingStatus directly to them
trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingTrackId(trackId);
trackDataHub.setRecordingTrackPaused(recordingTrackPaused);
trackController.onResume(true, recordingTrackPaused);
trackDataHub.setRecordingStatus(recordingStatus);
}
/*
@@ -232,12 +227,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
outState.putParcelable(EXTRA_TRACK_ID, trackId);
}
@Override
protected void onPause() {
super.onPause();
trackController.onPause();
}
@Override
protected void onStop() {
super.onStop();
@@ -264,7 +253,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(recordingTrackPaused);
updateMenuItems(recordingStatus.isPaused());
return super.onPrepareOptionsMenu(menu);
}
@@ -339,14 +328,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
public void recordStart() {
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
}
@Override
public void recordPause() {
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
@Override
@@ -404,24 +391,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
if (recordingTrackPaused != isPaused) {
trackController.update(true, isPaused);
trackDataHub.setRecordingTrackPaused(isPaused);
}
recordingTrackPaused = isPaused;
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
trackDataHub.setRecordingStatus(recordingStatus);
setLockscreenPolicy();
setScreenOnPolicy();
}
//TODO Why should this be interesting? This activity is only used for currently recording tracks.
@Override
public void onTrackRecordingId(Track.Id newTrackId) {
if (newTrackId != null && !newTrackId.equals(trackId)) {
trackId = newTrackId;
trackController.update(true, recordingTrackPaused);
trackDataHub.setRecordingTrackId(newTrackId);
}
}
}
@@ -29,12 +29,12 @@ public class ChartPoint {
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, double smoothedAltitude_m, boolean chartByDistance, boolean metricUnits) {
if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().to(metricUnits);
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
altitude = Distance.of(smoothedAltitude_m).to(metricUnits);
altitude = Distance.of(smoothedAltitude_m).toM_FT(metricUnits);
speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
@@ -76,4 +76,9 @@ public class ChartPoint {
public Double getPower() {
return power;
}
@Override
public String toString() {
return "ChartPoint{" + "timeOrDistance=" + timeOrDistance + '}';
}
}
@@ -26,6 +26,7 @@ import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
@@ -40,8 +41,8 @@ import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import de.dennisguse.opentracks.MarkerDetailActivity;
import de.dennisguse.opentracks.R;
@@ -114,7 +115,7 @@ public class ChartView extends View {
private int effectiveWidth = 0;
private int effectiveHeight = 0;
private final boolean chartByDistance;
private boolean chartByDistance = false;
private boolean metricUnits = true;
private boolean reportSpeed = true;
private boolean showPointer = false;
@@ -189,9 +190,9 @@ public class ChartView extends View {
}
});
public ChartView(Context context, final boolean chartByDistance) {
super(context);
this.chartByDistance = chartByDistance;
public ChartView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
@@ -366,6 +367,10 @@ public class ChartView extends View {
return true;
}
public void setChartByDistance(boolean chartByDistance) {
this.chartByDistance = chartByDistance;
}
public boolean getMetricUnits() {
return metricUnits;
}
@@ -424,7 +429,7 @@ public class ChartView extends View {
}
}
updateDimensions();
updatePaths();
updateSeries();
}
}
@@ -471,7 +476,7 @@ public class ChartView extends View {
private void zoomIn() {
if (canZoomIn()) {
zoomLevel++;
updatePaths();
updateSeries();
invalidate();
}
}
@@ -486,7 +491,7 @@ public class ChartView extends View {
scrollX = maxWidth;
scrollTo(scrollX, 0);
}
updatePaths();
updateSeries();
invalidate();
}
}
@@ -853,63 +858,49 @@ public class ChartView extends View {
}
/**
* Updates paths.
* The path needs to be updated any time after the data or the dimensions change.
*/
private void updatePaths() {
private void updateSeries() {
synchronized (chartPoints) {
for (ChartValueSeries chartValueSeries : seriesList) {
chartValueSeries.getPath().reset();
}
drawPaths();
closePaths();
seriesList.stream().forEach(this::updateSerie);
}
}
private void drawPaths() {
boolean[] hasMoved = new boolean[seriesList.size()];
private void updateSerie(ChartValueSeries series) {
final int yCorner = topBorder + effectiveHeight;
final Path path = series.getPath();
for (ChartPoint dataPoint : chartPoints) {
for (int i = 0; i < seriesList.size(); i++) {
ChartValueSeries chartValueSeries = seriesList.get(i);
boolean drawFirstPoint = false;
path.rewind();
if (!chartValueSeries.isChartPointValid(dataPoint)) {
continue;
}
Iterator<ChartPoint> iterator = chartPoints.iterator();
while (iterator.hasNext()) {
ChartPoint point = iterator.next();
if (!series.isChartPointValid(point)) {
continue;
}
double value = chartValueSeries.extractDataFromChartPoint(dataPoint);
Path path = chartValueSeries.getPath();
int x = getX(dataPoint.getTimeOrDistance());
int y = getY(chartValueSeries, value);
if (!hasMoved[i]) {
hasMoved[i] = true;
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
double value = series.extractDataFromChartPoint(point);
int x = getX(point.getTimeOrDistance());
int y = getY(series, value);
// start from lower left corner
if (!drawFirstPoint) {
path.moveTo(x, yCorner);
drawFirstPoint = true;
}
// draw graph
path.lineTo(x, y);
// last point: move to lower right
if (!iterator.hasNext()) {
path.lineTo(x, yCorner);
}
}
}
private void closePaths() {
for (ChartValueSeries chartValueSeries : seriesList) {
Optional<ChartPoint> firstValid = chartPoints.stream().filter(chartValueSeries::isChartPointValid)
.findFirst();
Optional<ChartPoint> lastValid = chartPoints.stream().filter(chartValueSeries::isChartPointValid)
.reduce((first, second) -> second);
if (firstValid.isPresent() && lastValid.isPresent()) {
Path path = chartValueSeries.getPath();
int yCorner = topBorder + effectiveHeight;
path.lineTo(getX(lastValid.get().getTimeOrDistance()), yCorner);
path.lineTo(getX(firstValid.get().getTimeOrDistance()), yCorner);
double value = chartValueSeries.extractDataFromChartPoint(firstValid.get());
path.lineTo(getX(firstValid.get().getTimeOrDistance()), getY(chartValueSeries, value));
}
}
// back to lower left corner
path.close();
}
/**
@@ -961,7 +952,7 @@ public class ChartView extends View {
width = newWidth;
height = newHeight;
updateEffectiveDimensions();
updatePaths();
updateSeries();
}
}
@@ -993,7 +984,7 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) {
if (chartByDistance) {
return marker.getLength().to(metricUnits);
return marker.getLength().toKM_Miles(metricUnits);
} else {
return marker.getDuration().toMillis();
}
@@ -27,9 +27,12 @@ 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;
@@ -39,7 +42,9 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
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.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.EGM2008Utils;
/**
* Track data hub.
@@ -67,26 +72,27 @@ public class TrackDataHub {
private static final String TAG = TrackDataHub.class.getSimpleName();
private final Context context;
private final TrackDataManager trackDataManager;
private final Set<TrackDataListener> listeners;
private final ContentProviderUtils contentProviderUtils;
private final int targetNumPoints;
private boolean started;
private EGM2008Utils.EGM2008Correction egm2008Correction;
//TODO Check if this is needed.
private HandlerThread handlerThread;
private Handler handler;
private Track.Id selectedTrackId;
@Deprecated
//TODO Is there a better way to handle this? e.g., passing information rather from Activity than from Activity to TrackDataHub
private Track.Id recordingTrackId;
@Deprecated
//TODO Is there a better way to handle this? e.g., passing information rather from Activity than from Activity to TrackDataHub
private boolean recordingTrackPaused;
private Distance recordingDistanceInterval;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
// Track points sampling state
private int numLoadedPoints;
private TrackPoint.Id firstSeenTrackPointId;
private TrackPoint.Id lastSeenTrackPointId;
private TrackStatisticsUpdater trackStatisticsUpdater;
// Registered listeners
private ContentObserver tracksTableObserver;
@@ -94,24 +100,23 @@ public class TrackDataHub {
private ContentObserver trackPointsTableObserver;
public TrackDataHub(Context context) {
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
this(context, new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
}
@VisibleForTesting
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
private TrackDataHub(Context context, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
this.context = context;
this.trackDataManager = trackDataManager;
this.listeners = new HashSet<>();
this.contentProviderUtils = contentProviderUtils;
this.targetNumPoints = targetNumPoints;
resetSamplingState();
}
public void start() {
if (started) {
if (isStarted()) {
Log.i(TAG, "TrackDataHub already started, ignoring start.");
return;
}
started = true;
handlerThread = new HandlerThread(TAG);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
@@ -121,7 +126,7 @@ public class TrackDataHub {
tracksTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
notifyTracksTableUpdate(listeners);
}
};
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
@@ -129,7 +134,7 @@ public class TrackDataHub {
markersTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
notifyMarkersTableUpdate(listeners);
}
};
contentResolver.registerContentObserver(MarkerColumns.CONTENT_URI, false, markersTableObserver);
@@ -137,26 +142,18 @@ public class TrackDataHub {
trackPointsTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
notifyTrackPointsTableUpdate(true, listeners);
}
};
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
handler.post(() -> {
if (started) {
loadDataForAll();
}
});
}
public void stop() {
if (!started) {
if (!isStarted()) {
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
return;
}
started = false;
//Unregister listeners
ContentResolver contentResolver = context.getContentResolver();
contentResolver.unregisterContentObserver(tracksTableObserver);
@@ -168,6 +165,7 @@ public class TrackDataHub {
handlerThread = null;
}
handler = null;
trackStatisticsUpdater = null;
}
public void loadTrack(final @NonNull Track.Id trackId) {
@@ -186,10 +184,10 @@ public class TrackDataHub {
*
* @param trackDataListener the track data listener
*/
public void registerTrackDataListener(final TrackDataListener trackDataListener, final boolean tracksTable, final boolean markersTable, final boolean trackPointsTable_SampleIn, final boolean trackPointsTable_SampleOut) {
public void registerTrackDataListener(final TrackDataListener trackDataListener) {
handler.post(() -> {
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, markersTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
if (started) {
listeners.add(trackDataListener);
if (isStarted()) {
loadDataForListener(trackDataListener);
}
});
@@ -201,21 +199,21 @@ public class TrackDataHub {
* @param trackDataListener the track data listener
*/
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
handler.post(() -> trackDataManager.unregisterTrackDataListener(trackDataListener));
handler.post(() -> listeners.remove(trackDataListener));
}
/**
* Returns true if the selected track is recording.
*/
public boolean isSelectedTrackRecording() {
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId);
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId());
}
/**
* Returns true if the selected track is paused.
*/
public boolean isSelectedTrackPaused() {
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId) && recordingTrackPaused;
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId()) && recordingStatus.isPaused();
}
/**
@@ -223,17 +221,17 @@ public class TrackDataHub {
*/
private void loadDataForAll() {
resetSamplingState();
if (!trackDataManager.hasListeners()) {
if (listeners.isEmpty()) {
return;
}
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
notifyTracksTableUpdate(listeners);
for (TrackDataListener listener : trackDataManager.getListenerTrackPoints_SampledIn()) {
for (TrackDataListener listener : listeners) {
listener.clearTrackPoints();
}
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
notifyTrackPointsTableUpdate(true, listeners);
notifyMarkersTableUpdate(listeners);
}
/**
@@ -244,25 +242,19 @@ public class TrackDataHub {
private void loadDataForListener(TrackDataListener trackDataListener) {
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
if (trackDataManager.listensForTracks(trackDataListener)) {
notifyTracksTableUpdate(trackDataListeners);
}
//Track
notifyTracksTableUpdate(trackDataListeners);
boolean hasSampledIn = trackDataManager.listensForTrackPoints_SampledIn(trackDataListener);
boolean hasSampledOut = trackDataManager.listensForTrackPoints_SampledOut(trackDataListener);
if (hasSampledIn || hasSampledOut) {
trackDataListener.clearTrackPoints();
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
if (isOnlyListener) {
resetSamplingState();
}
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.emptySet();
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
//TrackPoints
trackDataListener.clearTrackPoints();
boolean isOnlyListener = listeners.size() == 1;
if (isOnlyListener) {
resetSamplingState();
}
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners);
if (trackDataManager.listensForMarkers(trackDataListener)) {
notifyMarkersTableUpdate(trackDataListeners);
}
//Markers
notifyMarkersTableUpdate(trackDataListeners);
}
/**
@@ -315,11 +307,9 @@ public class TrackDataHub {
* Notifies track points table update; to be run in the {@link #handler} thread.
*
* @param updateSamplingState true to update the sampling state
* @param sampledInListeners the sampled-in listeners
* @param sampledOutListeners the sampled-out listeners
*/
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> listeners) {
if (listeners.isEmpty()) {
return;
}
@@ -327,7 +317,7 @@ public class TrackDataHub {
// Reload and resample the track at a lower frequency.
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
resetSamplingState();
for (TrackDataListener listener : sampledInListeners) {
for (TrackDataListener listener : listeners) {
listener.clearTrackPoints();
}
}
@@ -351,12 +341,17 @@ public class TrackDataHub {
next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
}
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
TrackPoint trackPoint = null;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
while (trackPointIterator.hasNext()) {
if (!isStarted()) {
break;
}
//Prevents a NPE if stop() is happening while notifyTrackPointsTableUpdate()
TrackStatisticsUpdater currentUpdater = trackStatisticsUpdater;
trackPoint = trackPointIterator.next();
TrackPoint.Id trackPointId = trackPoint.getId();
@@ -365,6 +360,8 @@ public class TrackDataHub {
break;
}
correctAltitude(trackPoint);
if (localFirstSeenTrackPointId == null) {
localFirstSeenTrackPointId = trackPointId;
}
@@ -374,16 +371,16 @@ public class TrackDataHub {
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
trackStatisticsUpdater.addTrackPoint(trackPoint, Distance.of(50)); //TODO
currentUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
// Also include the last point if the selected track is not recording.
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(trackPoint, trackStatisticsUpdater.getTrackStatistics(), trackStatisticsUpdater.getSmoothedSpeed(), trackStatisticsUpdater.getSmoothedAltitude());
for (TrackDataListener trackDataListener : listeners) {
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
}
} else {
for (TrackDataListener trackDataListener : sampledOutListeners) {
trackDataListener.onSampledOutTrackPoint(trackPoint, trackStatisticsUpdater.getTrackStatistics());
for (TrackDataListener trackDataListener : listeners) {
trackDataListener.onSampledOutTrackPoint(trackPoint, currentUpdater.getTrackStatistics());
}
}
@@ -401,11 +398,25 @@ public class TrackDataHub {
lastSeenTrackPointId = localLastSeenTrackPointIdId;
}
if (trackPoint != null) {
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone(trackPoint, trackStatisticsUpdater.getTrackStatistics());
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())));
}
/**
@@ -415,13 +426,18 @@ public class TrackDataHub {
numLoadedPoints = 0;
firstSeenTrackPointId = null;
lastSeenTrackPointId = null;
trackStatisticsUpdater = new TrackStatisticsUpdater();
}
public void setRecordingTrackId(Track.Id recordingTrackId) {
this.recordingTrackId = recordingTrackId;
private boolean isStarted() {
return handlerThread != null;
}
public void setRecordingTrackPaused(boolean recordingTrackPaused) {
this.recordingTrackPaused = recordingTrackPaused;
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
this.recordingStatus = recordingStatus;
}
public void setRecordingDistanceInterval(Distance recordingDistanceInterval) {
this.recordingDistanceInterval = recordingDistanceInterval;
}
}
@@ -62,26 +62,28 @@ public interface TrackDataListener {
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint, TrackStatistics, Speed, double)} and {@link #onSampledOutTrackPoint(TrackPoint, TrackStatistics)}.
*/
default void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint, @NonNull TrackStatistics trackStatistics) {
default void onNewTrackPointsDone() {
}
/**
* Called to clear previously sent markers.
*/
void clearMarkers();
default void clearMarkers() {
}
/**
* Called when a new marker is read.
*
* @param marker the marker
*/
void onNewMarker(Marker marker);
default void onNewMarker(@NonNull Marker marker) {
}
/**
* Called when finish sending new markers.
* This gets called after every batch of calls to {@link #clearMarkers()} and {@link #onNewMarker(Marker)}.
*/
void onNewMarkersDone();
default void onNewMarkersDone() {
}
}
@@ -1,95 +0,0 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
import java.util.HashSet;
import java.util.Set;
/**
* Manages register/unregister {@link TrackDataListener} and keeping the state for each registered listener.
*
* @author Rodrigo Damazio
*/
class TrackDataManager {
private static final String TAG = TrackDataManager.class.getSimpleName();
private final Set<TrackDataListener> dataListenerTracks = new HashSet<>();
private final Set<TrackDataListener> dataListenerMarkers = new HashSet<>();
private final Set<TrackDataListener> dataListenerTrackPoints_SampledIn = new HashSet<>();
private final Set<TrackDataListener> dataListenerTrackPoints_SampledOut = new HashSet<>();
void registerTrackDataListener(final TrackDataListener trackDataListener, boolean tracksTable, boolean markersTable, boolean trackPointsTable_SampleIn, boolean trackPointsTable_SampleOut) {
if (tracksTable) dataListenerTracks.add(trackDataListener);
if (markersTable) dataListenerMarkers.add(trackDataListener);
if (trackPointsTable_SampleIn) dataListenerTrackPoints_SampledIn.add(trackDataListener);
if (trackPointsTable_SampleOut) dataListenerTrackPoints_SampledOut.add(trackDataListener);
}
void unregisterTrackDataListener(TrackDataListener trackDataListener) {
dataListenerTracks.remove(trackDataListener);
dataListenerMarkers.remove(trackDataListener);
dataListenerTrackPoints_SampledIn.remove(trackDataListener);
dataListenerTrackPoints_SampledOut.remove(trackDataListener);
}
boolean hasListeners() {
return dataListenerTracks.size() + dataListenerMarkers.size() + dataListenerTrackPoints_SampledIn.size() + dataListenerTrackPoints_SampledOut.size() > 0;
}
int getNumberOfListeners() {
Set<TrackDataListener> listener = new HashSet<>();
listener.addAll(dataListenerTracks);
listener.addAll(dataListenerMarkers);
listener.addAll(dataListenerTrackPoints_SampledIn);
listener.addAll(dataListenerTrackPoints_SampledOut);
return listener.size();
}
boolean listensForTracks(TrackDataListener listener) {
return dataListenerTracks.contains(listener);
}
boolean listensForMarkers(TrackDataListener listener) {
return dataListenerMarkers.contains(listener);
}
boolean listensForTrackPoints_SampledIn(TrackDataListener listener) {
return dataListenerTrackPoints_SampledIn.contains(listener);
}
boolean listensForTrackPoints_SampledOut(TrackDataListener listener) {
return dataListenerTrackPoints_SampledOut.contains(listener);
}
Set<TrackDataListener> getListenerTracks() {
return dataListenerTracks;
}
Set<TrackDataListener> getListenerMarkers() {
return dataListenerMarkers;
}
Set<TrackDataListener> getListenerTrackPoints_SampledIn() {
return dataListenerTrackPoints_SampledIn;
}
Set<TrackDataListener> getListenerTrackPoints_SampledOut() {
return dataListenerTrackPoints_SampledOut;
}
}
@@ -0,0 +1,39 @@
package de.dennisguse.opentracks.content.data;
public abstract class Altitude {
private final double altitude_m;
private Altitude(double altitude_m) {
this.altitude_m = altitude_m;
}
public double toM() {
return altitude_m;
}
public static class WGS84 extends Altitude {
private WGS84(double altitude_m) {
super(altitude_m);
}
public static Altitude of(double altitude_m) {
return new WGS84(altitude_m);
}
}
public static class EGM2008 extends Altitude {
private EGM2008(double altitude_m) {
super(altitude_m);
}
public static Altitude of(double altitude_m) {
return new EGM2008(altitude_m);
}
}
}
@@ -24,6 +24,10 @@ public class Distance {
return of(distance_km * UnitConversions.KM_TO_M);
}
public static Distance ofMM(double distance_mm) {
return of(distance_mm * UnitConversions.MM_TO_M);
}
public static Distance one(boolean metricUnit) {
if (metricUnit) {
return Distance.ofKilometer(1);
@@ -98,24 +102,12 @@ public class Distance {
return toKM() * UnitConversions.KM_TO_MI;
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KM : Unit.MILES);
public double toKM_Miles(boolean metricUnit) {
return metricUnit ? toKM() : toMI();
}
public double to(Unit unit) {
switch (unit) {
case KM:
return toKM();
case MILES:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KM,
MILES
public double toM_FT(boolean metricUnit) {
return metricUnit ? toM() : toFT();
}
@Override
@@ -44,13 +44,13 @@ public final class Marker {
private String description = "";
private String category = "";
private String icon = "";
private final Track.Id trackId;
private Track.Id trackId;
private final Instant time;
private Double latitude;
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Altitude altitude;
private Float bearing;
//TODO It is the distance from the track starting point; rename to something more meaningful
@@ -74,16 +74,13 @@ public final class Marker {
if (!trackPoint.hasLocation())
throw new RuntimeException("Marker requires a trackpoint with a location.");
this.latitude = trackPoint.getLatitude();
this.longitude = trackPoint.getLongitude();
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude();
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
setTrackPoint(trackPoint);
this.length = Distance.of(0); //TODO Not cool!
this.duration = Duration.ofMillis(0); //TODO Not cool!
}
@Deprecated
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
this(trackId, trackPoint);
this.name = name;
@@ -95,6 +92,15 @@ public final class Marker {
this.photoUrl = photoUrl;
}
//TODO Is somehow part of the initialization process. Can we at least limit visibility?
public void setTrackPoint(TrackPoint trackPoint) {
this.latitude = trackPoint.getLatitude();
this.longitude = trackPoint.getLongitude();
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude();
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
}
/**
* May be null if the it was not loaded from the database.
*/
@@ -143,16 +149,19 @@ public final class Marker {
this.icon = icon;
}
@NonNull
public Track.Id getTrackId() {
return trackId;
}
@Deprecated
public void setTrackId(@NonNull Track.Id trackId) {
this.trackId = trackId;
}
public boolean hasLocation() {
return latitude != null || longitude != null;
}
@Nullable
public Location getLocation() {
Location location = new Location("");
location.setTime(time.toEpochMilli());
@@ -167,7 +176,7 @@ public final class Marker {
location.setAccuracy(accuracy);
}
if (hasAltitude()) {
location.setAltitude(altitude_m);
location.setAltitude(altitude.toM());
}
return location;
@@ -202,15 +211,15 @@ public final class Marker {
}
public boolean hasAltitude() {
return altitude_m != null;
return altitude != null;
}
public Double getAltitude() {
return altitude_m;
public Altitude getAltitude() {
return altitude;
}
public void setAltitude(double altitude_m) {
this.altitude_m = altitude_m;
public void setAltitude(Altitude altitude) {
this.altitude = altitude;
}
public boolean hasBearing() {
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
//TODO Do not default initialize attributes; might be confusing for debugging
public class Track {
private Track.Id id;
@@ -46,9 +47,6 @@ public class Track {
private TrackStatistics trackStatistics = new TrackStatistics();
public Track() {
}
/**
* May be null if the track was not loaded from the database.
*/
@@ -20,6 +20,7 @@ import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.time.Instant;
@@ -51,10 +52,10 @@ public class TrackPoint {
private Double latitude;
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Altitude altitude;
private Speed speed;
private Float bearing;
private Distance sensorDistance_m;
private Distance sensorDistance;
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
@@ -98,15 +99,21 @@ public class TrackPoint {
}
public TrackPoint(@NonNull Location location) {
this(Type.TRACKPOINT);
this(Type.TRACKPOINT, location, Instant.now());
}
public TrackPoint(@NonNull Type type, @NonNull Location location, @NonNull Instant time) {
this(type);
this.latitude = location.getLatitude();
this.longitude = location.getLongitude();
this.altitude_m = location.getAltitude();
this.altitude = Altitude.WGS84.of(location.getAltitude());
this.speed = Speed.of(location.getSpeed());
this.accuracy = location.getAccuracy();
setTime(Instant.now());
//TODO Should we copy the bearing?
setTime(time);
}
public TrackPoint(@NonNull Type type, Instant time) {
@@ -114,11 +121,11 @@ public class TrackPoint {
this.time = time;
}
public TrackPoint(double latitude, double longitude, Double altitude, Instant time) {
public TrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
this(Type.TRACKPOINT);
this.latitude = latitude;
this.longitude = longitude;
this.altitude_m = altitude;
this.altitude = altitude;
this.time = time;
}
@@ -197,6 +204,7 @@ public class TrackPoint {
this.longitude = longitude;
}
//TODO Better return null, if no location is present aka latitude == null etc.
@NonNull
public Location getLocation() {
Location location = new Location("");
@@ -212,7 +220,7 @@ public class TrackPoint {
location.setAccuracy(accuracy);
}
if (hasAltitude()) {
location.setAltitude(altitude_m);
location.setAltitude(altitude.toM());
}
return location;
@@ -257,15 +265,20 @@ public class TrackPoint {
public boolean hasAltitude() {
return altitude_m != null;
return altitude != null;
}
public double getAltitude() {
return altitude_m;
public Altitude getAltitude() {
return altitude;
}
public void setAltitude(double altitude) {
this.altitude_m = altitude;
@VisibleForTesting
public void setAltitude(double altitude_m) {
this.altitude = Altitude.WGS84.of(altitude_m);
}
public void setAltitude(Altitude altitude) {
this.altitude = altitude;
}
public boolean hasSpeed() {
@@ -308,10 +321,14 @@ public class TrackPoint {
this.accuracy = horizontalAccuracy;
}
@Nullable
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
if (!(hasLocation() && previous.hasLocation())) {
return null;
}
return Distance.of(getLocation().distanceTo(previous.getLocation()));
}
@@ -320,25 +337,27 @@ public class TrackPoint {
return hasAccuracy() && accuracy < poorAccuracy;
}
//TODO Bearing requires a location; what do we do if we don't have any?
public float bearingTo(@NonNull TrackPoint dest) {
return getLocation().bearingTo(dest.getLocation());
}
//TODO Bearing requires a location; what do we do if we don't have any?
public float bearingTo(@NonNull Location dest) {
return getLocation().bearingTo(dest);
}
// Sensor data
public boolean hasSensorDistance() {
return sensorDistance_m != null;
return sensorDistance != null;
}
public Distance getSensorDistance() {
return sensorDistance_m;
return sensorDistance;
}
public void setSensorDistance(Distance distance_m) {
this.sensorDistance_m = distance_m;
this.sensorDistance = distance_m;
}
public boolean hasSensorData() {
@@ -39,6 +39,7 @@ import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.BuildConfig;
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;
@@ -60,7 +61,6 @@ import de.dennisguse.opentracks.util.UUIDUtils;
public class ContentProviderUtils {
private static final String TAG = ContentProviderUtils.class.getSimpleName();
private static final int MAX_LATITUDE = 90000000;
// The authority (the first part of the URI) for the app's content provider.
@VisibleForTesting
@@ -160,8 +160,10 @@ public class ContentProviderUtils {
@VisibleForTesting
public void deleteAllTracks(Context context) {
//TODO Both calls should not be necessary
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
contentResolver.delete(MarkerColumns.CONTENT_URI, null, null);
// Delete tracks last since it triggers a database vaccum call
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
@@ -189,6 +191,7 @@ public class ContentProviderUtils {
}
//TODO Only use for tests; also move to tests.
@VisibleForTesting
public List<Track> getTracks() {
ArrayList<Track> tracks = new ArrayList<>();
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
@@ -202,13 +205,7 @@ public class ContentProviderUtils {
return tracks;
}
/**
* @param trackId the track id.
*/
public Track getTrack(Track.Id trackId) {
if (trackId == null) {
return null;
}
public Track getTrack(@NonNull Track.Id trackId) {
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())}, null)) {
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
@@ -217,9 +214,6 @@ public class ContentProviderUtils {
return null;
}
/**
* @param trackUUID the track uuid.
*/
public Track getTrack(@NonNull UUID trackUUID) {
String trackUUIDsearch = UUIDUtils.toHex(trackUUID);
try (Cursor cursor = getTrackCursor("hex(" + TracksColumns.UUID + ")=?", new String[]{trackUUIDsearch}, null)) {
@@ -249,6 +243,7 @@ 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));
}
@@ -320,7 +315,7 @@ public class ContentProviderUtils {
marker.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
}
if (!cursor.isNull(altitudeIndex)) {
marker.setAltitude(cursor.getFloat(altitudeIndex));
marker.setAltitude(Altitude.WGS84.of(cursor.getFloat(altitudeIndex)));
}
if (!cursor.isNull(accuracyIndex)) {
marker.setAccuracy(cursor.getFloat(accuracyIndex));
@@ -489,7 +484,7 @@ public class ContentProviderUtils {
values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6));
values.put(MarkerColumns.TIME, marker.getTime().toEpochMilli());
if (marker.hasAltitude()) {
values.put(MarkerColumns.ALTITUDE, marker.getAltitude());
values.put(MarkerColumns.ALTITUDE, marker.getAltitude().toM());
}
if (marker.hasAccuracy()) {
values.put(MarkerColumns.ACCURACY, marker.getAccuracy());
@@ -539,7 +534,7 @@ public class ContentProviderUtils {
trackPoint.setTime(Instant.ofEpochMilli(cursor.getLong(indexes.timeIndex)));
}
if (!cursor.isNull(indexes.altitudeIndex)) {
trackPoint.setAltitude(cursor.getFloat(indexes.altitudeIndex));
trackPoint.setAltitude(Altitude.WGS84.of(cursor.getFloat(indexes.altitudeIndex)));
}
if (!cursor.isNull(indexes.accuracyIndex)) {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
@@ -575,6 +570,7 @@ public class ContentProviderUtils {
}
//TODO Only used for file import; might be better to replace it.
//TODO Rename to bulkInsert
public int bulkInsertTrackPoint(List<TrackPoint> trackPoints, Track.Id trackId) {
ContentValues[] values = new ContentValues[trackPoints.size()];
for (int i = 0; i < trackPoints.size(); i++) {
@@ -583,6 +579,15 @@ public class ContentProviderUtils {
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
//TODO Set trackId in this method.
public int bulkInsertMarkers(List<Marker> markers, Track.Id trackId) {
ContentValues[] values = new ContentValues[markers.size()];
for (int i = 0; i < markers.size(); i++) {
values[i] = createContentValues(markers.get(i));
}
return contentResolver.bulkInsert(MarkerColumns.CONTENT_URI, values);
}
/**
* Gets the last location id for a track.
* Returns -1L if it doesn't exist.
@@ -608,6 +613,7 @@ public class ContentProviderUtils {
* @param location the location
* @return trackPoint id if the location is in the track. -1L otherwise.
*/
@Deprecated
public TrackPoint.Id getTrackPointId(Track.Id trackId, Location location) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(location.getTime())};
@@ -690,7 +696,7 @@ public class ContentProviderUtils {
}
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude());
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude().toM());
}
if (trackPoint.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
@@ -739,6 +745,7 @@ public class ContentProviderUtils {
return new TrackPointIterator(this, trackId, startTrackPointId);
}
@Deprecated
private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) {
try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToNext()) {
@@ -123,7 +123,7 @@ public final class SensorDataCycling {
return wheelRevolutionsTime;
}
public void compute(DistanceSpeed previous, int wheel_circumference_mm) {
public void compute(DistanceSpeed previous, Distance wheelCircumference) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
@@ -134,7 +134,7 @@ public final class SensorDataCycling {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards
Distance distance = Distance.of(wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
Distance distanceOverall = distance;
if (previous.hasValue()) {
distanceOverall = distance.plus(previous.getValue().distanceOverall);
@@ -16,28 +16,25 @@
package de.dennisguse.opentracks.fragments;
import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
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.Track;
@@ -68,7 +65,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
private SharedPreferences sharedPreferences;
private Distance recordingDistanceInterval;
private TrackDataHub trackDataHub;
@@ -80,40 +76,34 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private boolean chartByDistance;
private ChartBinding viewBinding;
// UI elements
private ChartView chartView;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
if (metricUnits != chartView.getMetricUnits()) {
chartView.setMetricUnits(metricUnits);
if (metricUnits != viewBinding.chartView.getMetricUnits()) {
viewBinding.chartView.setMetricUnits(metricUnits);
runOnUiThread(() -> {
if (isResumed()) {
chartView.requestLayout();
viewBinding.chartView.requestLayout();
}
});
}
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
if (reportSpeed != chartView.getReportSpeed()) {
chartView.setReportSpeed(reportSpeed);
chartView.applyReportSpeed();
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
viewBinding.chartView.setReportSpeed(reportSpeed);
viewBinding.chartView.applyReportSpeed();
runOnUiThread(() -> {
if (isResumed()) {
chartView.requestLayout();
viewBinding.chartView.requestLayout();
}
});
}
}
if (PreferencesUtils.isKey(getContext(), R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, getContext());
}
}
};
@@ -123,12 +113,12 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private final Runnable updateChart = new Runnable() {
@Override
public void run() {
if (!isResumed() || trackDataHub == null) {
if (!isResumed()) {
return;
}
chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate();
viewBinding.chartView.setShowPointer(isSelectedTrackRecording());
viewBinding.chartView.invalidate();
}
};
@@ -138,27 +128,15 @@ public class ChartFragment extends Fragment implements TrackDataListener {
super.onCreate(savedInstanceState);
chartByDistance = getArguments().getBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, true);
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext());
// Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
chartView = new ChartView(getContext(), chartByDistance);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = ChartBinding.inflate(inflater, container, false);
viewBinding.chartView.setChartByDistance(chartByDistance);
return viewBinding.getRoot();
}
@Override
public void onStart() {
super.onStart();
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
viewBinding.chartViewLayout.addView(chartView, layoutParams);
}
@Override
public void onResume() {
super.onResume();
@@ -178,13 +156,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
public void onStop() {
super.onStop();
viewBinding.chartViewLayout.removeView(chartView);
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -200,16 +171,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override
public void onTrackUpdated(Track track) {
if (isResumed()) {
if (track == null || track.getTrackStatistics() == null) {
if (track == null) {
category = "";
return;
}
category = track.getCategory();
boolean reportSpeed = PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category);
if (reportSpeed != chartView.getReportSpeed()) {
chartView.setReportSpeed(reportSpeed);
chartView.applyReportSpeed();
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
viewBinding.chartView.setReportSpeed(reportSpeed);
viewBinding.chartView.applyReportSpeed();
}
}
}
@@ -218,10 +189,10 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void clearTrackPoints() {
if (isResumed()) {
pendingPoints.clear();
chartView.reset();
viewBinding.chartView.reset();
runOnUiThread(() -> {
if (isResumed()) {
chartView.resetScroll();
viewBinding.chartView.resetScroll();
}
});
}
@@ -229,15 +200,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
if (isResumed()) {
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, chartView.getMetricUnits());
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, viewBinding.chartView.getMetricUnits());
pendingPoints.add(point);
}
}
@Override
public void onNewTrackPointsDone(@NonNull TrackPoint unused, @NonNull TrackStatistics alsoUnused) {
public void onNewTrackPointsDone() {
if (isResumed()) {
chartView.addChartPoints(pendingPoints);
//Avoid ConcurrentModificationException exception
viewBinding.chartView.addChartPoints(Collections.unmodifiableList(pendingPoints));
pendingPoints.clear();
runOnUiThread(updateChart);
}
@@ -246,14 +218,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override
public void clearMarkers() {
if (isResumed()) {
chartView.clearMarker();
viewBinding.chartView.clearMarker();
}
}
@Override
public void onNewMarker(Marker marker) {
if (isResumed() && marker != null) {
chartView.addMarker(marker);
public void onNewMarker(@NonNull Marker marker) {
if (isResumed()) {
viewBinding.chartView.addMarker(marker);
}
}
@@ -268,9 +240,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
* Checks the chart settings.
*/
private void checkChartSettings() {
boolean needUpdate = chartView.applyReportSpeed();
boolean needUpdate = viewBinding.chartView.applyReportSpeed();
if (needUpdate) {
chartView.postInvalidate();
viewBinding.chartView.postInvalidate();
}
}
@@ -280,7 +252,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, true, true, true, true);
trackDataHub.registerTrackDataListener(this);
}
/**
@@ -308,34 +280,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
* @param runnable the runnable
*/
private void runOnUiThread(Runnable runnable) {
FragmentActivity fragmentActivity = getActivity();
Activity fragmentActivity = getActivity();
if (fragmentActivity != null) {
fragmentActivity.runOnUiThread(runnable);
}
}
@VisibleForTesting
void setChartView(ChartView view) {
chartView = view;
}
@VisibleForTesting
void setMetricUnits(boolean value) {
chartView.setMetricUnits(value);
}
@VisibleForTesting
void setReportSpeed(boolean value) {
chartView.setReportSpeed(value);
}
@VisibleForTesting
void setChartByDistance(boolean value) {
chartByDistance = value;
}
@VisibleForTesting
void setRecordingDistanceInterval(Distance value) {
recordingDistanceInterval = value;
}
}
@@ -24,7 +24,6 @@ import de.dennisguse.opentracks.TrackActivityDataHubInterface;
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -195,7 +194,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, true, false, true, true);
trackDataHub.registerTrackDataListener(this);
}
/**
@@ -245,27 +244,12 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
}
@Override
public void onNewTrackPointsDone(@NonNull TrackPoint unused, @NonNull TrackStatistics alsoUnused) {
public void onNewTrackPointsDone() {
if (isResumed()) {
runOnUiThread(viewModel::onNewTrackPoints);
}
}
@Override
public void clearMarkers() {
// We don't care.
}
@Override
public void onNewMarker(Marker marker) {
// We don't care.
}
@Override
public void onNewMarkersDone() {
// We don't care.
}
/**
* Runs a runnable on the UI thread.
*
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
@@ -22,10 +21,6 @@ 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.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
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.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -34,7 +29,6 @@ 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.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -46,25 +40,17 @@ import de.dennisguse.opentracks.viewmodels.SensorDataModel;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class StatisticsRecordingFragment extends Fragment implements TrackDataListener {
public class StatisticsRecordingFragment extends Fragment {
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
private static final Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private TrackDataHub trackDataHub;
private Handler handlerUpdateUI;
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private TrackPoint lastTrackPoint;
@Nullable // Lazily loaded.
private Track track;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackRecordingService.RecordingData recordingData = TrackRecordingService.NOT_RECORDING;
private TrackPoint latestTrackPoint;
private StatisticsRecordingBinding viewBinding;
private SensorsAdapter sensorsAdapter;
@@ -83,9 +69,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && track != null) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && recordingData != null) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), track.getCategory());
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), recordingData.getTrackCategory());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_altitude_key, key)) {
@@ -99,39 +85,29 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
if (key != null && updateUInecessary && isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
getActivity().runOnUiThread(this::updateUI);
}
};
private final Runnable updateUIeachSecond = new Runnable() {
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && track != null) {
synchronized (this) {
if (lastTrackPoint != null && lastTrackPoint.hasLocation() && !lastTrackPoint.isRecent()) {
lastTrackPoint = null;
setLocationValues();
}
}
updateTotalTime();
updateSensorDataUI();
}
handlerUpdateUI.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingDataObservable()
.observe(StatisticsRecordingFragment.this, recordingData -> onRecordingDataChanged(recordingData));
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handlerUpdateUI = new Handler();
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
sensorsAdapter = new SensorsAdapter(getContext());
}
@@ -139,7 +115,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(track != null ? track.getCategory() : "")); //TODO "" would be useless as Track was not yet loaded.
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(recordingData.getTrackCategory()));
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
@@ -151,25 +127,18 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(getContext());
updateUIeachSecond.run();
handlerUpdateUI.post(updateUIeachSecond);
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
}
@Override
@@ -191,101 +160,12 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
sharedPreferences = null;
}
@Override
public void onTrackUpdated(final Track track) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
if ((this.track == null && track != null) || (this.track != null && track != null && !this.track.getCategory().equals(track.getCategory()))) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
this.track = track;
updateUI();
}
});
}
}
@Override
public void clearTrackPoints() {
lastTrackPoint = null;
}
@Override
public void onNewTrackPointsDone(@NonNull TrackPoint newLastTrackPoint, @NonNull TrackStatistics trackStatistics) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
synchronized (this) {
this.lastTrackPoint = newLastTrackPoint;
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
this.lastTrackPoint = null;
}
if (this.lastTrackPoint != null && this.lastTrackPoint.hasLocation() && !this.lastTrackPoint.isRecent()) {
this.lastTrackPoint = null;
}
setLocationValues();
}
}
});
}
}
@Override
public void clearMarkers() {
// We don't care.
}
@Override
public void onNewMarker(Marker marker) {
// We don't care.
}
@Override
public void onNewMarkersDone() {
// We don't care.
}
/**
* Resumes the trackDataHub.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackRecordingActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, true, false, true, true);
}
/**
* Pauses the trackDataHub.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Returns true if the selected track is recording.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Returns true if the selected track is paused.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackPaused() {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
}
private void updateUI() {
updateStats();
setLocationValues();
updateSensorDataUI();
if (isResumed()) {
updateStats();
setLocationValues();
updateSensorDataUI();
}
}
/**
@@ -293,100 +173,45 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
* Also sets altitude gain and loss.
*/
private void updateSensorDataUI() {
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService == null) {
Log.d(TAG, "Cannot get the track recording service.");
} else {
SensorDataSet sensorDataSet = trackRecordingService.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);
setSpeedSensorData(sensorDataSet);
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
if (sensorDataSet != null) {
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorDataSet.getHeartRate() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
}
//TODO Check if we can get the total altitude gain and loss via trackStatistics instead of doing some computation in the UI layer.
setTotalAltitudeGain(trackRecordingService.getAltitudeGain_m());
setTotalAltitudeLoss(trackRecordingService.getAltitudeLoss_m());
}
}
// Set altitude gain
private void setTotalAltitudeGain(Float altitudeGain_m) {
Float totalAltitudeGain = altitudeGain_m;
if (track != null && track.getTrackStatistics().hasTotalAltitudeGain()) {
if (altitudeGain_m == null) {
totalAltitudeGain = 0f;
if (sensorDataSet.getCyclingCadence() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
}
totalAltitudeGain += track.getTrackStatistics().getTotalAltitudeGain();
if (sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
}
sensorsAdapter.swapData(sensorDataList);
}
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), totalAltitudeGain, preferenceMetricUnits);
viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second);
}
// Set altitude loss
private void setTotalAltitudeLoss(Float altitudeLoss_m) {
Float totalAltitudeLoss = altitudeLoss_m;
if (track != null && track.getTrackStatistics().hasTotalAltitudeLoss()) {
if (altitudeLoss_m == null) {
totalAltitudeLoss = track.getTrackStatistics().getTotalAltitudeLoss();
} else {
totalAltitudeLoss += track.getTrackStatistics().getTotalAltitudeLoss();
}
{
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), recordingData.getTrackStatistics().getTotalAltitudeGain(), preferenceMetricUnits);
viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second);
}
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), totalAltitudeLoss, preferenceMetricUnits);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
}
private void setSpeedSensorData(SensorDataSet sensorDataSet) {
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null) {
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
if (data.hasValue() && data.isRecent()) {
setTotalDistance(data.getValue().distanceOverall);
setSpeed(data.getValue().speed);
}
if (data.hasValue() && data.isRecent()) {
setSpeed(data.getValue().speed);
}
{
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), recordingData.getTrackStatistics().getTotalAltitudeLoss(), preferenceMetricUnits);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
}
}
private void updateStats() {
setTotalDistance(Distance.of(0)); //TODO Why?
// Set activity type
if (track != null) {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), track.getCategory());
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
}
String trackIconValue = TrackIconUtils.getIconValue(getContext(), recordingData.getTrackCategory());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
// Set time
if (track != null) {
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
updateTotalTime();
}
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(recordingData.getTrackStatistics().getMovingTime()));
updateTotalTime();
// Set average speed/pace
{
Speed speed = track != null ? track.getTrackStatistics().getAverageSpeed() : null;
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);
@@ -396,7 +221,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set max speed/pace
{
Speed speed = track != null ? track.getTrackStatistics().getMaxSpeed() : null;
Speed speed = recordingData.getTrackStatistics().getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
@@ -407,7 +232,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set moving speed/pace
{
Speed speed = track != null ? track.getTrackStatistics().getAverageMovingSpeed() : null;
Speed speed = recordingData.getTrackStatistics().getAverageMovingSpeed();
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
@@ -417,34 +242,41 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
// Set altitude gain and loss
{
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
}
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
}
private void updateTotalTime() {
Duration totalTime = track.getTrackStatistics().getTotalTime();
if (isSelectedTrackRecording()) {
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
totalTime = trackRecordingService.getTotalTime();
}
}
Duration totalTime = recordingData.getTrackStatistics().getTotalTime();
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
}
private void setLocationValues() {
// Set speed/pace
Speed speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : null;
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 = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null;
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
@@ -455,23 +287,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
if (preferenceShowCoordinate) {
String latitudeText = getContext().getString(R.string.value_unknown);
String longitudeText = getContext().getString(R.string.value_unknown);
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLongitude());
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 setTotalDistance(Distance sensorDistanceSinceLastTrackpoint) {
Distance totalDistance = track != null ? (track.getTrackStatistics().getTotalDistance().plus(sensorDistanceSinceLastTrackpoint)) : Distance.invalid();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
private void setSpeed(Speed speed) {
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
@@ -479,4 +303,21 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
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();
this.recordingData = recordingData;
if (!oldCategory.equals(newCategory)) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
this.latestTrackPoint = recordingData.getLatestTrackPoint();
if (latestTrackPoint != null && latestTrackPoint.hasLocation() && !latestTrackPoint.isRecent()) {
latestTrackPoint = null;
}
updateUI();
}
}
@@ -16,6 +16,7 @@ import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
*/
public enum TrackFileFormat {
@Deprecated //Not needed anymore; we now have OSMDashboard; was only used to share with other applications
KML_ONLY_TRACK {
@Override
public TrackExporter createTrackExporter(Context context) {
@@ -198,7 +198,6 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println("xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
@@ -237,7 +236,7 @@ public class GPXTrackExporter implements TrackExporter {
if (printWriter != null) {
printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">");
if (marker.hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(marker.getAltitude()) + "</ele>");
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(marker.getAltitude().toM()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
@@ -281,7 +280,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude().toM()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
@@ -518,13 +518,12 @@ public class KMLTrackExporter implements TrackExporter {
return location.getBearing();
}
private String getCoordinates(Location location, String separator) {
StringBuilder builder = new StringBuilder();
builder.append(location.getLongitude()).append(separator).append(location.getLatitude());
private static String getCoordinates(Location location, String separator) {
String result = location.getLongitude() + separator + location.getLatitude();
if (location.hasAltitude()) {
builder.append(separator).append(location.getAltitude());
result += separator + location.getAltitude();
}
return builder.toString();
return result;
}
/**
@@ -1,584 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteConstraintException;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
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.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* Abstract class for file track importers.
*
* @author Jimmy Shih
*/
abstract class AbstractFileTrackImporter extends DefaultHandler implements TrackImporter {
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private final Distance recordingDistanceInterval;
private final List<Track.Id> trackIds = new ArrayList<>();
private final List<Marker> markers = new ArrayList<>();
// The current element content
//TODO Should be made private and getter be used by child classes.
protected String content;
protected String icon;
protected String name;
protected String description;
protected String category;
protected String latitude;
protected String longitude;
protected String altitude;
protected String time;
protected String speed;
protected String heartrate;
protected String cadence;
protected String power;
protected String markerType;
protected String photoUrl;
protected String uuid;
protected String gain;
protected String loss;
protected String distance;
// The current track data
private TrackData trackData;
// The SAX locator to get the current line information
private Locator locator;
private final SharedPreferences sharedPreferences;
AbstractFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
this.context = context;
this.contentProviderUtils = contentProviderUtils;
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void characters(char[] ch, int start, int length) {
String newContent = new String(ch, start, length);
if (content == null) {
content = newContent;
} else {
// In 99% of the cases, a single call to this method will be made for each sequence of characters we're interested in, so we'll rarely be concatenating strings, thus not justifying the use of a StringBuilder.
content += newContent;
}
}
@Override
@NonNull
public List<Track.Id> importFile(InputStream inputStream) {
try {
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, this);
return trackIds;
} catch (IOException | SAXException | ParserConfigurationException | ParsingException e) {
Log.e(TAG, "Unable to import file", e);
if (trackIds.size() > 0) {
cleanImport();
}
throw new ImportParserException(e);
} catch (SQLiteConstraintException e) {
Log.e(TAG, "Unable to import file", e);
throw new ImportAlreadyExistsException(e);
}
}
protected void onFileEnd() {
// Add markers to the last imported track
int size = trackIds.size();
if (size == 0) {
return;
}
Track.Id trackId = trackIds.get(size - 1);
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
return;
}
int markerPosition = -1;
Marker marker = null;
TrackPoint trackPoint = null;
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
// TODO We are doing in memory processing for trackpoints; so we can do this in memory as well.
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (true) {
if (marker == null) {
markerPosition++;
marker = markerPosition < markers.size() ? markers.get(markerPosition) : null;
if (marker == null) {
// No more markers
return;
}
// If marker had photo it must be translated to internal photo url (depend on track id)
if (marker.hasPhoto()) {
marker.setPhotoUrl(getInternalPhotoUrl(marker.getPhotoUrl()));
}
}
if (trackPoint == null) {
if (!trackPointIterator.hasNext()) {
// No more track points. Ignore the rest of the markers.
return;
}
trackPoint = trackPointIterator.next();
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
}
if ((marker.getTime()).isAfter(trackPoint.getTime())) {
trackPoint = null;
} else if (marker.getTime().isBefore(trackPoint.getTime())) {
Log.w(TAG, "Ignoring marker: current trackPoint was after marker.");
marker = null;
} else {
// The marker trackPoint time matches the track point time
if (!trackPoint.hasLocation()) {
// Invalid trackPoint, load the next trackPoint
trackPoint = null;
continue;
}
// Valid trackPoint
if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) {
String markerDescription = marker.getDescription();
String icon = context.getString(R.string.marker_icon_url);
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
// Insert marker
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), stats, trackPoint, marker.getPhotoUrl());
contentProviderUtils.insertMarker(newMarker);
}
// Load the next marker
marker = null;
}
}
}
}
/**
* On track start.
*/
protected void onTrackStart() throws SAXException {
trackData = new TrackData();
}
/**
* On track end.
*/
protected void onTrackEnd() {
if (name != null) {
trackData.track.setName(name);
}
try {
trackData.track.setUuid(UUID.fromString(uuid));
} catch (IllegalArgumentException | NullPointerException e) {
Log.w(TAG, "could not parse Track UUID, generating a new one.");
trackData.track.setUuid(UUID.randomUUID());
}
if (description != null) {
trackData.track.setDescription(description);
}
if (category != null) {
trackData.track.setCategory(category);
//TODO remove when GPX and KML support reading this property.
if (icon == null)
trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
}
if (icon != null) {
trackData.track.setIcon(icon);
}
TrackStatisticsUpdater statistics = new TrackStatisticsUpdater();
//TODO I guess, we should not filter by recordingDistanceInterval on import; the data is already recorded, so we should not change it.
for (TrackPoint trackPoint : trackData.bufferedTrackPoints) {
statistics.addTrackPoint(trackPoint, recordingDistanceInterval);
}
if (!statistics.isTrackInitialized()) {
throw new ImportParserException("Track did not contain any locations.");
}
trackData.track.setTrackStatistics(statistics.getTrackStatistics());
Track track = contentProviderUtils.getTrack(trackData.track.getUuid());
if (track != null) {
if (PreferencesUtils.getPreventReimportTracks(sharedPreferences, context)) {
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
}
//TODO This is a workaround until we have proper UI.
trackData.track.setUuid(UUID.randomUUID());
}
if (trackIds.size() > 0) {
// TODO Multi track is not supported yet.
cleanImport();
throw new ImportParserException("Multi track not supported");
}
Uri uri = contentProviderUtils.insertTrack(trackData.track);
Track.Id trackId = new Track.Id(Long.parseLong(uri.getLastPathSegment()));
trackIds.add(trackId);
trackData.track.setId(trackId);
flushTrackPoints();
}
protected void onTrackSegmentStart() {
trackData.numberOfSegments++;
//If not the first segment, add a pause separator if there is at least one TrackPoint in the last segment.
if (trackData.numberOfSegments > 1
&& trackData.lastLocationInCurrentSegment != null
&& (trackData.lastLocationInCurrentSegment.getType().equals(TrackPoint.Type.SEGMENT_START_MANUAL))
) {
insertTrackPoint(TrackPoint.createSegmentEndWithTime(trackData.lastLocationInCurrentSegment.getTime()));
}
trackData.lastLocationInCurrentSegment = null;
}
protected void onTrackSegmentEnd() {
TrackPoint trackPoint = trackData.lastLocationInCurrentSegment;
if (trackPoint == null) {
return;
}
if (!trackPoint.hasLocation()) {
trackPoint.setType(TrackPoint.Type.SEGMENT_END_MANUAL);
}
}
protected void addMarker() throws ParsingException {
// Markers must have a time, else cannot match to the track points
if (time == null) {
Log.w(TAG, "Marker without time ignored.");
return;
}
TrackPoint trackPoint = createTrackPoint();
if (!trackPoint.hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
return;
}
Marker marker = new Marker(null, trackPoint);
if (name != null) {
marker.setName(name);
}
if (description != null) {
marker.setDescription(description);
}
if (category != null) {
marker.setCategory(category);
}
if (photoUrl != null) {
marker.setPhotoUrl(photoUrl);
}
markers.add(marker);
}
protected TrackPoint getTrackPoint() throws ParsingException {
TrackPoint trackPoint = createTrackPoint();
if (trackPoint.hasLocation()) {
Instant time = trackPoint.getTime();
if (trackPoint.getLatitude() == 100) {
//TODO Remove by 31st December 2021.
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
trackPoint.setTime(time);
} else if (trackPoint.getLatitude() == 200) {
//TODO Remove by 31st December 2021.
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL);
trackPoint.setTime(time);
} else if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new ParsingException(createErrorMessage("Invalid location detected: " + trackPoint));
}
}
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() == null) {
if (!trackPoint.hasSpeed()) {
Duration timeDifference = Duration.between(trackData.lastLocationInCurrentSegment.getTime(), trackPoint.getTime());
// Check for negative time change
if (timeDifference.isNegative()) {
Log.w(TAG, "Time difference not positive.");
} else {
/*
* We don't have a speed and bearing in GPX, make something up from the last two points.
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
*/
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setSpeed(Speed.of(trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint), timeDifference));
}
}
}
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
}
}
trackData.lastLocationInCurrentSegment = trackPoint;
return trackPoint;
}
/**
* Creates an error message.
*
* @param message the message
*/
protected String createErrorMessage(String message) {
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
}
/**
* Gets the photo url for a file.
*
* @param externalPhotoUrl the file name
*/
protected String getInternalPhotoUrl(String externalPhotoUrl) {
if (trackData.track.getId() == null) {
Log.e(TAG, "Track id is invalid.");
return null;
}
if (externalPhotoUrl == null) {
Log.i(TAG, "External photo url is null.");
return null;
}
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
File file = FileUtils.buildInternalPhotoFile(context, trackData.track.getId(), Uri.parse(importFileName));
if (file != null) {
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
}
return null;
}
private TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
try {
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
if (latitude == null || longitude == null) {
return trackPoint;
}
try {
trackPoint.setLatitude(Double.parseDouble(latitude));
trackPoint.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
if (altitude != null) {
try {
trackPoint.setAltitude(Double.parseDouble(altitude));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
if (speed != null) {
try {
trackPoint.setSpeed(Speed.of(speed));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
}
if (heartrate != null) {
try {
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
if (cadence != null) {
try {
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
if (gain != null) {
try {
trackPoint.setAltitudeGain(Float.parseFloat(gain));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s", gain)), e);
}
}
if (loss != null) {
try {
trackPoint.setAltitudeLoss(Float.parseFloat(loss));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
}
}
if (distance != null) {
try {
trackPoint.setSensorDistance(Distance.of(distance));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
}
return trackPoint;
}
/**
* Inserts a trackPoint.
*
* @param trackPoint the trackPoint
*/
protected void insertTrackPoint(TrackPoint trackPoint) {
trackData.bufferedTrackPoints.add(trackPoint);
}
protected boolean isFirstTrackPointInSegment() {
return trackData.lastLocationInCurrentSegment == null;
}
/**
* Flushes the TrackPoints to the database.
*/
private void flushTrackPoints() {
if (trackData.bufferedTrackPoints.size() > 0) {
contentProviderUtils.bulkInsertTrackPoint(trackData.bufferedTrackPoints, trackData.track.getId());
trackData.bufferedTrackPoints.clear();
}
}
/**
* Cleans up import.
*/
private void cleanImport() {
contentProviderUtils.deleteTracks(context, trackIds);
}
/**
* Data for the current track.
*
* @author Jimmy Shih
*/
// TODO Why private inner class?
private static class TrackData {
// The current track
final Track track = new Track();
// The number of segments processed for the current track
int numberOfSegments = 0;
// The last location in the current segment; Null if the current segment doesn't have a last location
TrackPoint lastLocationInCurrentSegment;
// The buffered locations
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
}
public static class ParsingException extends RuntimeException {
private ParsingException(@NonNull String message) {
super(message);
}
private ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@NonNull
@Override
public String toString() {
return "" + getMessage();
}
}
}
@@ -1,242 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Imports a GPX file.
* Uses:
* * https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd
* * https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd
* <p>
* {@link de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter} does not export information if a segment was started automatic or manually.
* Therefore, all segments starts are marked as SEGMENT_START_AUTOMATIC.
* Thus, the {@link de.dennisguse.opentracks.stats.TrackStatistics} cannot be restored correctly.
*
* @author Jimmy Shih
*/
public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_COMMENT = "cmt";
private static final String TAG_ALTITUDE = "ele";
private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type";
private static final String TAG_MARKER = "wpt";
private static final String TAG_ID = "opentracks:trackid";
private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon";
private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
private static final String TAG_EXTENSION_GAIN = "opentracks:gain";
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
/**
* Constructor.
*
* @param context the context
*/
public GpxFileTrackImporter(Context context) {
this(context, new ContentProviderUtils(context));
}
@VisibleForTesting
GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
super(context, contentProviderUtils);
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) {
case TAG_MARKER:
onMarkerStart(attributes);
break;
case TAG_TRACK:
onTrackStart();
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentStart();
break;
case TAG_TRACK_POINT:
onTrackPointStart(attributes);
break;
}
}
@Override
public void endElement(String uri, String localName, String tag) {
switch (tag) {
case TAG_GPX:
onFileEnd();
break;
case TAG_MARKER:
onMarkerEnd();
break;
case TAG_TRACK:
onTrackEnd();
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentEnd();
break;
case TAG_TRACK_POINT:
onTrackPointEnd();
break;
case TAG_NAME:
if (content != null) {
name = content.trim();
}
break;
case TAG_DESCRIPTION:
if (content != null) {
description = content.trim();
}
break;
case TAG_TYPE:
if (content != null) {
category = content.trim();
}
break;
case TAG_TIME:
if (content != null) {
time = content.trim();
}
break;
case TAG_ALTITUDE:
if (content != null) {
altitude = content.trim();
}
break;
case TAG_COMMENT:
if (content != null) {
markerType = content.trim();
}
break;
case TAG_EXTENSION_SPEED:
if (content != null) {
speed = content.trim();
}
break;
case TAG_EXTENSION_HEARTRATE:
if (content != null) {
heartrate = content.trim();
}
break;
case TAG_EXTENSION_CADENCE:
if (content != null) {
cadence = content.trim();
}
break;
case TAG_EXTENSION_POWER:
if (content != null) {
power = content.trim();
}
break;
case TAG_ID:
if (content != null) {
uuid = content.trim();
}
break;
case TAG_EXTENSION_GAIN:
if (content != null) {
gain = content.trim();
}
break;
case TAG_EXTENSION_LOSS:
if (content != null) {
loss = content.trim();
}
break;
case TAG_EXTENSION_DISTANCE:
if (content != null) {
distance = content.trim();
}
break;
}
// Reset element content
content = null;
}
@Override
protected void onTrackStart() throws SAXException {
super.onTrackStart();
name = null;
description = null;
category = null;
}
/**
* On track point start.
*
* @param attributes the attributes
*/
private void onTrackPointStart(Attributes attributes) {
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
speed = null;
power = null;
gain = null;
loss = null;
}
private void onTrackPointEnd() {
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
TrackPoint trackPoint = getTrackPoint();
if (isFirstTrackPointInSegment) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
insertTrackPoint(trackPoint);
}
private void onMarkerStart(Attributes attributes) {
name = null;
description = null;
category = null;
photoUrl = null;
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
markerType = null;
}
private void onMarkerEnd() {
addMarker();
}
}
@@ -0,0 +1,417 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
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.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
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;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Imports a GPX file.
* Uses:
* * https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd
* * https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd
* <p>
* {@link de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter} does not export information if a segment was started automatic or manually.
* Therefore, all segments starts are marked as SEGMENT_START_AUTOMATIC.
* Thus, the {@link de.dennisguse.opentracks.stats.TrackStatistics} cannot be restored correctly.
*
* @author Jimmy Shih
*/
public class GpxTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
private static final String TAG = GpxTrackImporter.class.getSimpleName();
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_COMMENT = "cmt";
private static final String TAG_ALTITUDE = "ele";
private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type";
private static final String TAG_MARKER = "wpt";
private static final String TAG_ID = "opentracks:trackid";
private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon";
private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
private static final String TAG_EXTENSION_GAIN = "opentracks:gain";
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
private Locator locator;
private final Context context;
// Belongs to the current track
private final ArrayList<Marker> markers = new ArrayList<>();
// The current element content
private String content = "";
private String name;
private String description;
private String category;
private String latitude;
private String longitude;
private String altitude;
private String time;
private String speed;
private String heartrate;
private String cadence;
private String power;
protected String markerType;
protected String photoUrl;
protected String uuid;
protected String gain;
protected String loss;
protected String distance;
private final LinkedList<TrackPoint> currentSegment = new LinkedList<>();
private final TrackImporter trackImporter;
public GpxTrackImporter(Context context, TrackImporter trackImporter) {
this.context = context;
this.trackImporter = trackImporter;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) {
case TAG_MARKER:
onMarkerStart(attributes);
break;
case TAG_TRACK:
trackImporter.newTrack();
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentStart();
break;
case TAG_TRACK_POINT:
onTrackPointStart(attributes);
break;
}
}
@Override
public void characters(char[] ch, int start, int length) {
content += new String(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String tag) {
switch (tag) {
case TAG_GPX:
onFileEnd();
break;
case TAG_MARKER:
onMarkerEnd();
break;
case TAG_TRACK:
trackImporter.setTrack(context, name, uuid, description, category, null);
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentEnd();
break;
case TAG_TRACK_POINT:
currentSegment.add(createTrackPoint());
break;
case TAG_NAME:
if (content != null) {
name = content.trim();
}
break;
case TAG_DESCRIPTION:
if (content != null) {
description = content.trim();
}
break;
case TAG_TYPE:
if (content != null) {
category = content.trim();
}
break;
case TAG_TIME:
if (content != null) {
time = content.trim();
}
break;
case TAG_ALTITUDE:
if (content != null) {
altitude = content.trim();
}
break;
case TAG_COMMENT:
if (content != null) {
markerType = content.trim();
}
break;
case TAG_EXTENSION_SPEED:
if (content != null) {
speed = content.trim();
}
break;
case TAG_EXTENSION_HEARTRATE:
if (content != null) {
heartrate = content.trim();
}
break;
case TAG_EXTENSION_CADENCE:
if (content != null) {
cadence = content.trim();
}
break;
case TAG_EXTENSION_POWER:
if (content != null) {
power = content.trim();
}
break;
case TAG_ID:
if (content != null) {
uuid = content.trim();
}
break;
case TAG_EXTENSION_GAIN:
if (content != null) {
gain = content.trim();
}
break;
case TAG_EXTENSION_LOSS:
if (content != null) {
loss = content.trim();
}
break;
case TAG_EXTENSION_DISTANCE:
if (content != null) {
distance = content.trim();
}
break;
}
content = "";
}
private void onTrackSegmentStart() {
}
private void onTrackSegmentEnd() {
if (currentSegment.isEmpty()) {
Log.w(TAG, "No locations in current segment.");
return;
}
TrackPoint first = currentSegment.getFirst();
first.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackImporter.addTrackPoints(currentSegment);
currentSegment.clear();
}
private TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
try {
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
if (latitude == null || longitude == null) {
return trackPoint;
}
try {
trackPoint.setLatitude(Double.parseDouble(latitude));
trackPoint.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
if (altitude != null) {
try {
trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
if (speed != null) {
try {
trackPoint.setSpeed(Speed.of(speed));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
}
if (heartrate != null) {
try {
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
if (cadence != null) {
try {
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
if (gain != null) {
try {
trackPoint.setAltitudeGain(Float.parseFloat(gain));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s", gain)), e);
}
}
if (loss != null) {
try {
trackPoint.setAltitudeLoss(Float.parseFloat(loss));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
}
}
if (distance != null) {
try {
trackPoint.setSensorDistance(Distance.of(distance));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
}
return trackPoint;
}
private void onTrackPointStart(Attributes attributes) {
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
speed = null;
power = null;
gain = null;
loss = null;
}
private void onMarkerStart(Attributes attributes) {
name = null;
description = null;
category = null;
photoUrl = null;
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
markerType = null;
}
private void onMarkerEnd() {
// Markers must have a time, else cannot match to the track points
if (time == null) {
Log.w(TAG, "Marker without time; ignored.");
return;
}
TrackPoint trackPoint = createTrackPoint();
if (!trackPoint.hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
return;
}
Marker marker = new Marker(null, trackPoint);
if (name != null) {
marker.setName(name);
}
if (description != null) {
marker.setDescription(description);
}
if (category != null) {
marker.setCategory(category);
}
if (photoUrl != null) {
marker.setPhotoUrl(photoUrl);
}
markers.add(marker);
}
private String createErrorMessage(String message) {
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
}
private void onFileEnd() {
trackImporter.addMarkers(markers);
trackImporter.finish();
}
@Override
public DefaultHandler getHandler() {
return this;
}
@Override
public List<Track.Id> getImportTrackIds() {
return trackImporter.getTrackIds();
}
@Override
public void cleanImport() {
trackImporter.cleanImport();
}
}
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.io.file.importer;
import android.app.job.JobService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
@@ -13,13 +14,15 @@ import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
public class ImportService extends JobIntentService {
@@ -47,23 +50,28 @@ public class ImportService extends JobIntentService {
}
private void importFile(DocumentFile file) {
TrackImporter trackImporter;
ArrayList<Track.Id> trackIds = new ArrayList<>();
String fileExtension = FileUtils.getExtension(file);
try {
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(this);
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, this);
Distance recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, this);
boolean preventReimport = PreferencesUtils.getPreventReimportTracks(sharedPreferences, this);
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackImporter = new GpxFileTrackImporter(this);
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackImporter = new KmlFileTrackImporter(this);
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackImporter = new KmzTrackImporter(this, file.getUri());
} else {
Log.d(TAG, "Unsupported file format.");
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
return;
}
TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), recordingDistanceInterval, maxRecordingDistance, preventReimport);
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri())) {
ArrayList<Track.Id> trackIds = new ArrayList<>(trackImporter.importFile(inputStream));
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new GpxTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new KmlTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackIds.addAll(new KmzTrackImporter(this, trackImporter).importFile(file.getUri()));
} else {
Log.d(TAG, "Unsupported file format.");
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
return;
}
if (!trackIds.isEmpty()) {
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackIds, file, getString(R.string.import_file_imported, file.getName()));
@@ -17,30 +17,35 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.location.Location;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
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.List;
import java.util.Locale;
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.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Imports a KML file.
*
* @author Jimmy Shih
*/
//TODO Do not use AbstractFileTrackImporter as TrackPoint sensor generation is done differently here.
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
public class KmlTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
private static final String TAG = KmlTrackImporter.class.getSimpleName();
private static final String MARKER_STYLE = "#" + KMLTrackExporter.MARKER_STYLE;
@@ -64,24 +69,49 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String ATTRIBUTE_NAME = "name";
private boolean trackStarted = false;
private Locator locator;
private final Context context;
// Belongs to the current track
private final ArrayList<Instant> whenList = new ArrayList<>();
private final ArrayList<Location> locationList = new ArrayList<>();
private String extendedDataType;
private final ArrayList<TrackPoint> trackPoints = new ArrayList<>();
private final ArrayList<Float> speedList = new ArrayList<>();
private final ArrayList<Float> distanceList = new ArrayList<>();
private final ArrayList<Float> cadenceList = new ArrayList<>();
private final ArrayList<Float> heartRateList = new ArrayList<>();
private final ArrayList<Float> powerList = new ArrayList<>();
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
private final ArrayList<Float> sensorHeartRateList = new ArrayList<>();
private final ArrayList<Float> sensorPowerList = new ArrayList<>();
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
public KmlFileTrackImporter(Context context) {
this(context, new ContentProviderUtils(context));
private final ArrayList<Marker> markers = new ArrayList<>();
// The current element content
private String content = "";
private String icon;
private String name;
private String description;
private String category;
private String latitude;
private String longitude;
private String altitude;
private String markerType;
private String photoUrl;
private String uuid;
private final TrackImporter trackImporter;
public KmlTrackImporter(Context context, TrackImporter trackImporter) {
this.context = context;
this.trackImporter = trackImporter;
}
@VisibleForTesting
KmlFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
super(context, contentProviderUtils);
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
@@ -93,11 +123,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
onMarkerStart();
break;
case TAG_GX_MULTI_TRACK:
trackStarted = true;
onTrackStart();
trackImporter.newTrack();
break;
case TAG_GX_TRACK:
if (!trackStarted) {
if (trackImporter == null) {
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
}
onTrackSegmentStart();
@@ -108,6 +137,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
}
@Override
public void characters(char[] ch, int start, int length) {
content += new String(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
switch (tag) {
@@ -123,13 +157,13 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
onMarkerLocationEnd();
break;
case TAG_GX_MULTI_TRACK:
onTrackEnd();
trackImporter.setTrack(context, name, uuid, description, category, icon);
break;
case TAG_GX_TRACK:
onTrackSegmentEnd();
break;
case TAG_GX_COORD:
onTrackPointEnd();
onCoordEnded();
break;
case TAG_GX_VALUE:
onExtendedDataValueEnd();
@@ -161,7 +195,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
break;
case TAG_WHEN:
if (content != null) {
time = content.trim();
whenList.add(StringUtils.parseTime(content.trim()));
}
break;
@@ -178,7 +212,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
// Reset element content
content = null;
content = "";
}
private void onMarkerStart() {
@@ -191,7 +225,6 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
latitude = null;
longitude = null;
altitude = null;
time = null;
markerType = null;
}
@@ -199,7 +232,30 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (!MARKER_STYLE.equals(markerType)) {
return;
}
addMarker();
if (whenList.size() != 1) {
Log.w(TAG, "Marker without time ignored.");
return;
}
Location location = createLocation(longitude, latitude, altitude);
if (location == null) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + location);
return;
}
Marker marker = new Marker(null, new TrackPoint(TrackPoint.Type.TRACKPOINT, location, whenList.get(0))); //TODO Creating marker without need
marker.setName(name != null ? name : "");
marker.setDescription(description != null ? description : "");
marker.setCategory(category != null ? category : "");
marker.setPhotoUrl(photoUrl);
markers.add(marker);
name = null;
description = null;
category = null;
photoUrl = null;
whenList.clear();
}
private void onMarkerLocationEnd() {
@@ -214,39 +270,56 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
}
@Override
protected void onTrackSegmentStart() {
super.onTrackSegmentStart();
trackPoints.clear();
speedList.clear();
distanceList.clear();
heartRateList.clear();
cadenceList.clear();
powerList.clear();
private void onTrackSegmentStart() {
locationList.clear();
whenList.clear();
sensorSpeedList.clear();
sensorDistanceList.clear();
sensorHeartRateList.clear();
sensorCadenceList.clear();
sensorPowerList.clear();
altitudeGainList.clear();
altitudeLossList.clear();
}
protected void onTrackSegmentEnd() {
super.onTrackSegmentEnd();
// Close a track segment by inserting the segment locations
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i);
private void onTrackSegmentEnd() {
if (locationList.size() != whenList.size()) {
throw new ImportParserException("<coords> and <when> should have the same count.");
}
if (i < speedList.size() && speedList.get(i) != null) {
trackPoint.setSpeed(Speed.of(speedList.get(i)));
// Close a track segment by inserting the segment locations
for (int i = 0; i < locationList.size(); i++) {
Instant time = whenList.get(i);
Location location = locationList.get(i);
TrackPoint trackPoint;
if (i == 0) {
if (location == null) {
trackPoint = TrackPoint.createSegmentStartManualWithTime(time);
} else {
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, location, time);
}
} else if (i == locationList.size() - 1 && location == null) {
trackPoint = TrackPoint.createSegmentEndWithTime(time);
} else {
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, location, time);
}
if (i < distanceList.size() && distanceList.get(i) != null) {
trackPoint.setSensorDistance(Distance.of(distanceList.get(i)));
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
trackPoint.setSpeed(Speed.of(sensorSpeedList.get(i)));
}
if (i < heartRateList.size()) {
trackPoint.setHeartRate_bpm(heartRateList.get(i));
if (i < sensorDistanceList.size() && sensorDistanceList.get(i) != null) {
trackPoint.setSensorDistance(Distance.of(sensorDistanceList.get(i)));
}
if (i < cadenceList.size()) {
trackPoint.setCyclingCadence_rpm(cadenceList.get(i));
if (i < sensorHeartRateList.size()) {
trackPoint.setHeartRate_bpm(sensorHeartRateList.get(i));
}
if (i < powerList.size()) {
trackPoint.setPower(powerList.get(i));
if (i < sensorCadenceList.size()) {
trackPoint.setCyclingCadence_rpm(sensorCadenceList.get(i));
}
if (i < sensorPowerList.size()) {
trackPoint.setPower(sensorPowerList.get(i));
}
if (i < altitudeGainList.size()) {
trackPoint.setAltitudeGain(altitudeGainList.get(i));
@@ -255,18 +328,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
}
insertTrackPoint(trackPoint);
trackImporter.addTrackPoint(trackPoint);
}
}
/**
* On track point end. gx:coord end tag.
*/
private void onTrackPointEnd() {
// Add trackPoint to trackPoints
if (content == null) {
return;
}
private void onCoordEnded() {
String[] parts = content.trim().split(" ");
if (parts.length == 2 || parts.length == 3) {
longitude = parts[0];
@@ -274,33 +340,41 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
altitude = parts.length == 3 ? parts[2] : null;
}
// Similar to GPX
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
TrackPoint trackPoint = getTrackPoint();
if (isFirstTrackPointInSegment) {
TrackPoint.Type type = !trackPoint.hasLocation() ? TrackPoint.Type.SEGMENT_START_MANUAL : TrackPoint.Type.SEGMENT_START_AUTOMATIC;
locationList.add(createLocation(longitude, latitude, altitude));
trackPoint.setType(type);
}
trackPoints.add(trackPoint);
// Reset variables for next trackpoint (which might not have such data).
time = null;
longitude = null;
latitude = null;
altitude = null;
}
/**
* On extended data start. gx:SimpleArrayData start tag.
*/
private Location createLocation(String longitude, String latitude, String altitude) {
Location location = null;
if (longitude != null || latitude != null) {
location = new Location("import");
try {
location.setLatitude(Double.parseDouble(latitude));
location.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
if (altitude != null) {
try {
location.setAltitude(Double.parseDouble(altitude));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
}
return location;
}
private void onExtendedDataStart(Attributes attributes) {
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
}
/**
* On extended data value end. gx:value end tag.
*/
private void onExtendedDataValueEnd() throws SAXException {
Float value = null;
if (content != null) {
@@ -315,19 +389,19 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
switch (extendedDataType) {
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
speedList.add(value);
sensorSpeedList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE:
distanceList.add(value);
sensorDistanceList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER:
powerList.add(value);
sensorPowerList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE:
heartRateList.add(value);
sensorHeartRateList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE:
cadenceList.add(value);
sensorCadenceList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN:
altitudeGainList.add(value);
@@ -339,4 +413,28 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
}
}
private String createErrorMessage(String message) {
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
}
private void onFileEnd() {
trackImporter.addMarkers(markers);
trackImporter.finish();
}
@Override
public DefaultHandler getHandler() {
return this;
}
@Override
public List<Track.Id> getImportTrackIds() {
return trackImporter.getTrackIds();
}
@Override
public void cleanImport() {
trackImporter.cleanImport();
}
}
@@ -22,10 +22,9 @@ import android.util.Log;
import androidx.annotation.NonNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -46,35 +45,28 @@ import de.dennisguse.opentracks.util.FileUtils;
*
* @author Jimmy Shih
*/
public class KmzTrackImporter implements TrackImporter {
public class KmzTrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName();
private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
private static final int BUFFER_SIZE = 4096;
private final Context context;
private final Uri uriKmzFile;
private final TrackImporter trackImporter;
/**
* @param context the context
* @param uriFile URI of the kmz file.
*/
public KmzTrackImporter(Context context, Uri uriFile) {
public KmzTrackImporter(Context context, TrackImporter trackImporter) {
this.context = context;
this.uriKmzFile = uriFile;
this.trackImporter = trackImporter;
}
@Override
public @NonNull
List<Track.Id> importFile(InputStream inputStream) {
List<Track.Id> trackIds = findAndParseKmlFile(inputStream);
@NonNull
public List<Track.Id> importFile(Uri fileUri) throws IOException {
List<Track.Id> trackIds = findAndParseKmlFile(fileUri);
ArrayList<Track.Id> trackIdsWithImages = new ArrayList<>();
List<Track.Id> trackIdsWithImages = new ArrayList<>();
for (Track.Id trackId : trackIds) {
if (copyKmzImages(trackId)) {
if (copyKmzImages(fileUri, trackId)) {
trackIdsWithImages.add(trackId);
deleteOrphanImages(trackId);
} else {
@@ -90,8 +82,8 @@ public class KmzTrackImporter implements TrackImporter {
*
* @return false if there are errors or true otherwise.
*/
private boolean copyKmzImages(Track.Id trackId) {
try (InputStream inputStream = context.getContentResolver().openInputStream(uriKmzFile);
private boolean copyKmzImages(Uri uri, Track.Id trackId) throws IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
@@ -110,9 +102,6 @@ public class KmzTrackImporter implements TrackImporter {
}
return true;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
@@ -154,13 +143,9 @@ public class KmzTrackImporter implements TrackImporter {
return KMZ_IMAGES_EXT.contains(fileExt);
}
/**
* Finds KmzTrackExporter.KMZ_KML_FILE file inside kmz file (inputStream) and it parses it.
*
* @param inputStream kmz input stream.
*/
private List<Track.Id> findAndParseKmlFile(InputStream inputStream) {
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
private List<Track.Id> findAndParseKmlFile(Uri uri) throws IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
ArrayList<Track.Id> trackIds = new ArrayList<>();
@@ -190,9 +175,6 @@ public class KmzTrackImporter implements TrackImporter {
} catch (ImportParserException | ImportAlreadyExistsException e) {
Log.e(TAG, "Unable to import file", e);
throw e;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
throw new ImportParserException(e);
}
}
@@ -227,32 +209,17 @@ public class KmzTrackImporter implements TrackImporter {
}
}
private List<Track.Id> parseKml(ZipInputStream zipInputStream) {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context);
private List<Track.Id> parseKml(ZipInputStream zipInputStream) throws IOException {
XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlTrackImporter(context, trackImporter));
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
return kmlFileTrackImporter.importFile(byteArrayInputStream);
} catch (ImportParserException | ImportAlreadyExistsException e) {
throw e;
} catch (Exception e) {
throw new ImportParserException(e);
}
}
/**
* Gets the kml as byte array.
*
* @param zipInputStream the zip input stream
*/
private byte[] getKml(ZipInputStream zipInputStream) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, count);
InputStream nonClosableInputStream = new FilterInputStream(zipInputStream) {
@Override
public void close() {
// SAX2 always tries close InputStreams; but that would also close our ZIP file.
}
return byteArrayOutputStream.toByteArray();
}
};
return kmlFileTrackImporter.importFile(nonClosableInputStream);
}
/**
@@ -262,6 +229,7 @@ public class KmzTrackImporter implements TrackImporter {
* @param trackId the track's id which image belongs to.
* @param fileName the file name
*/
@Deprecated //TODO Use JDK9's inputStream.transferTo() instead of manual buffer
private void readAndSaveImageFile(ZipInputStream zipInputStream, Track.Id trackId, String fileName) throws IOException {
if (trackId == null || fileName.equals("")) {
return;
@@ -271,7 +239,7 @@ public class KmzTrackImporter implements TrackImporter {
File file = new File(dir, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE];
byte[] buffer = new byte[4096];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
@@ -0,0 +1,20 @@
package de.dennisguse.opentracks.io.file.importer;
import androidx.annotation.NonNull;
public class ParsingException extends RuntimeException {
private ParsingException(@NonNull String message) {
super(message);
}
protected ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@NonNull
@Override
public String toString() {
return "" + getMessage();
}
}
@@ -1,39 +1,301 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.io.file.importer;
import android.content.ContentUris;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.InputStream;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
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.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* Interface for a track importer.
*
* @author Jimmy Shih
* Handles logic to import:
* 1. addTrackPoints()
* 2. addMarkers();
* 3. setTrack();
* 4. newTrack(); //stores current track to databse
* 5. if needed go to 1.
* 6. finish()
* <p>
* NOTE: This class modifies the parameter.
* Do not re-use these objects anywhere else.
*/
public interface TrackImporter {
public class TrackImporter {
private static final String TAG = TrackImporter.class.getSimpleName();
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private final Distance recordingDistanceInterval;
private final Distance maxRecordingDistance;
private final boolean preventReimport;
private final List<Track.Id> trackIds = new ArrayList<>();
// Current track
private Track track;
private final List<TrackPoint> trackPoints = new LinkedList<>();
private final List<Marker> markers = new LinkedList<>();
public TrackImporter(Context context, ContentProviderUtils contentProviderUtils, Distance recordingDistanceInterval, Distance maxRecordingDistance, boolean preventReimport) {
this.context = context;
this.contentProviderUtils = contentProviderUtils;
this.recordingDistanceInterval = recordingDistanceInterval;
this.maxRecordingDistance = maxRecordingDistance;
this.preventReimport = preventReimport;
}
void newTrack() {
if (track != null) {
finishTrack();
}
track = null;
trackPoints.clear();
markers.clear();
}
void addTrackPoint(TrackPoint trackPoint) {
this.trackPoints.add(trackPoint);
}
void addTrackPoints(List<TrackPoint> trackPoints) {
this.trackPoints.addAll(trackPoints);
}
void addMarker(Marker marker) {
this.markers.add(marker);
}
void addMarkers(List<Marker> markers) {
this.markers.addAll(markers);
}
void setTrack(Context context, String name, String uuid, String description, String category, String icon) {
track = new Track();
track.setName(name != null ? name : "");
try {
track.setUuid(UUID.fromString(uuid));
} catch (IllegalArgumentException | NullPointerException e) {
Log.w(TAG, "could not parse Track UUID, generating a new one.");
track.setUuid(UUID.randomUUID());
}
track.setDescription(description != null ? description : "");
if (category != null) {
track.setCategory(category);
if (icon == null) {
icon = TrackIconUtils.getIconValue(context, category);
}
}
track.setIcon(icon != null ? icon : "");
}
void finish() {
if (track != null) {
finishTrack();
}
}
private void finishTrack() {
if (trackPoints.isEmpty()) {
throw new ImportParserException("Cannot import track without any locations.");
}
// Store Track
if (contentProviderUtils.getTrack(track.getUuid()) != null) {
if (preventReimport) {
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
}
//TODO This is a workaround until we have proper UI.
track.setUuid(UUID.randomUUID());
}
Collections.sort(trackPoints, new Comparator<TrackPoint>() {
@Override
public int compare(TrackPoint o1, TrackPoint o2) {
if (o1.getTime().isBefore(o2.getTime())) {
return -1;
}
if (o1.getTime().isAfter(o2.getTime())) {
return 1;
}
return 0;
}
});
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
updater.addTrackPoints(trackPoints, recordingDistanceInterval);
track.setTrackStatistics(updater.getTrackStatistics());
Uri trackUri = contentProviderUtils.insertTrack(track);
Track.Id trackId = new Track.Id(ContentUris.parseId(trackUri));
// Store TrackPoints
adjustTrackPoints();
contentProviderUtils.bulkInsertTrackPoint(trackPoints, trackId);
// Store Markers
matchMarkers2TrackPoints(trackId);
for (Marker marker : markers)
marker.setTrackId(trackId); //TODO Should happen in bulkInsertMarkers
contentProviderUtils.bulkInsertMarkers(markers, trackId);
//Clear up.
trackPoints.clear();
markers.clear();
trackIds.add(trackId);
}
/**
* Import a file.
*
* @param inputStream the file's input stream
* If not present: calculate data from the previous trackPoint (if present)
* NOTE: Modifies content of trackPoints.
*/
@NonNull
List<Track.Id> importFile(InputStream inputStream) throws ImportParserException;
private void adjustTrackPoints() {
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint current = trackPoints.get(i);
if (current.hasLocation()) {
Instant time = current.getTime();
if (current.getLatitude() == 100) {
//TODO Remove by 31st December 2021.
trackPoints.set(i, new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, time));
} else if (current.getLatitude() == 200) {
//TODO Remove by 31st December 2021.
trackPoints.set(i, new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, time));
//TODO Delete location
} else if (!LocationUtils.isValidLocation(current.getLocation())) {
throw new ImportParserException("Invalid location detected: " + current);
}
}
}
for (int i = 1; i < trackPoints.size(); i++) {
TrackPoint previous = trackPoints.get(i - 1);
TrackPoint current = trackPoints.get(i);
Distance distanceToPrevious = current.distanceToPrevious(previous);
if (distanceToPrevious != null) {
if (!current.hasSpeed()) {
Duration timeDifference = Duration.between(previous.getTime(), current.getTime());
current.setSpeed(Speed.of(distanceToPrevious, timeDifference));
}
if (!current.hasBearing()) {
current.setBearing(previous.bearingTo(current));
}
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
current.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
}
}
}
/**
* NOTE: Modifies content of markers (incl. removal).
*/
private void matchMarkers2TrackPoints(Track.Id trackId) {
List<TrackPoint> trackPointsWithLocation = trackPoints.stream()
.filter(TrackPoint::hasLocation)
.collect(Collectors.toList());
List<Marker> todoMarkers = new LinkedList<>(markers);
List<Marker> doneMarkers = new LinkedList<>();
for (final TrackPoint trackPoint : trackPointsWithLocation) {
if (todoMarkers.isEmpty()) {
break;
}
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
updater.addTrackPoint(trackPoint, recordingDistanceInterval);
List<Marker> matchedMarkers = todoMarkers.stream()
.filter(it -> trackPoint.getLatitude() == it.getLatitude()
&& trackPoint.getLongitude() == it.getLongitude()
&& trackPoint.getTime().equals(it.getTime())
)
.collect(Collectors.toList());
TrackStatistics statistics = updater.getTrackStatistics();
for (Marker marker : matchedMarkers) {
if (marker.hasPhoto()) {
marker.setPhotoUrl(getInternalPhotoUrl(trackId, marker.getPhotoUrl()));
}
marker.setIcon(context.getString(R.string.marker_icon_url)); //TODO Why?
marker.setLength(statistics.getTotalDistance());
marker.setDuration(statistics.getTotalTime());
marker.setTrackPoint(trackPoint);
}
todoMarkers.removeAll(matchedMarkers);
doneMarkers.addAll(matchedMarkers);
}
if (todoMarkers.isEmpty()) {
Log.w(TAG, "Some markers could not be attached to TrackPoints; those are not imported.");
}
markers.clear();
markers.addAll(doneMarkers);
}
/**
* Gets the photo url for a file.
*
* @param externalPhotoUrl the file name
*/
private String getInternalPhotoUrl(@NonNull Track.Id trackId, @NonNull String externalPhotoUrl) {
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
File file = FileUtils.buildInternalPhotoFile(context, trackId, Uri.parse(importFileName));
if (file != null) {
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
}
return null;
}
public List<Track.Id> getTrackIds() {
return Collections.unmodifiableList(trackIds);
}
public void cleanImport() {
contentProviderUtils.deleteTracks(context, trackIds);
}
}
@@ -0,0 +1,68 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.database.sqlite.SQLiteConstraintException;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.content.data.Track;
/**
* Uses SAX2 to parse XML files.
* <p>
* NOTE: SAX2 always closes InputStreams after processing.
*/
public class XMLImporter {
private static final String TAG = XMLImporter.class.getSimpleName();
private final TrackParser parser;
public XMLImporter(TrackParser parser) {
this.parser = parser;
}
@NonNull
public List<Track.Id> importFile(Context context, Uri uri) throws ImportParserException, ImportAlreadyExistsException, IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
return importFile(inputStream);
}
}
public List<Track.Id> importFile(InputStream inputStream) throws ImportParserException, ImportAlreadyExistsException, IOException {
try {
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, parser.getHandler());
return parser.getImportTrackIds();
} catch (SAXException | ParserConfigurationException | ParsingException e) {
Log.e(TAG, "Unable to import file", e);
if (parser.getImportTrackIds().size() > 0) {
parser.cleanImport();
}
throw new ImportParserException(e);
} catch (SQLiteConstraintException e) {
Log.e(TAG, "Unable to import file", e);
throw new ImportAlreadyExistsException(e);
}
}
interface TrackParser {
@Deprecated
DefaultHandler getHandler();
List<Track.Id> getImportTrackIds();
void cleanImport();
}
}
@@ -0,0 +1,171 @@
package de.dennisguse.opentracks.services;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
public class TrackDeleteService extends Service {
private static final String CHANNEL_ID = TrackDeleteService.class.getSimpleName();
private static final int NOTIFICATION_ID = 1;
static final String EXTRA_TRACK_IDS = "extra_track_ids";
private final Binder binder = new Binder();
private ExecutorService serviceExecutor;
private MutableLiveData<DeleteStatus> deleteResultObservable;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
@Override
public void onCreate() {
serviceExecutor = Executors.newSingleThreadExecutor();
deleteResultObservable = new MutableLiveData<>();
}
@Override
public void onDestroy() {
super.onDestroy();
if (serviceExecutor != null) {
serviceExecutor.shutdownNow();
serviceExecutor = null;
}
deleteResultObservable = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
ArrayList<Track.Id> trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
createAndShowNotification(trackIds.size());
deleteTracks(trackIds);
return START_NOT_STICKY;
}
private void deleteTracks(@NonNull ArrayList<Track.Id> trackIds) {
sendResult(null,0, trackIds.size());
serviceExecutor.execute(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
for (int i = 0; i < trackIds.size(); i++) {
updateNotification(i + 1, trackIds.size());
contentProviderUtils.deleteTrack(this, trackIds.get(i));
sendResult(trackIds.get(i), i + 1, trackIds.size());
}
stopSelf();
});
}
private void sendResult(Track.Id trackId, int deletes, int total) {
if (deleteResultObservable != null) {
deleteResultObservable.postValue(new DeleteStatus(trackId, deletes, total));
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public LiveData<DeleteStatus> getDeletingStatusObservable() {
return deleteResultObservable;
}
/**
* Starts and shows the notification.
*
* @param tracksToDelete number of tracks to be deleted.
*/
private void createAndShowNotification(int tracksToDelete) {
notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, this.getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
notificationChannel.setAllowBubbles(true);
}
notificationManager.createNotificationChannel(notificationChannel);
}
notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
notificationBuilder
.setContentTitle(this.getString(R.string.track_delete_progress_message))
.setContentText(this.getString(R.string.track_delete_progress, 0, tracksToDelete))
.setSmallIcon(R.drawable.ic_logo_color_24dp)
.setProgress(tracksToDelete, 0, false);
startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
/**
* Updates notification progress.
*
* @param progress number of tracks already deleted.
* @param total total of tracks to be deleted.
*/
private void updateNotification(int progress, int total) {
notificationBuilder.setProgress(total, progress, false);
notificationBuilder.setContentText(this.getString(R.string.track_delete_progress, progress, total));
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
public class Binder extends android.os.Binder {
private Binder() {
super();
}
public TrackDeleteService getService() {
return TrackDeleteService.this;
}
}
public static class DeleteStatus {
private int progress;
private int max;
private Track.Id trackId;
/**
* @param trackId Track.Id just deleted if any.
* @param progress number of deletes.
* @param max total of deletes to be done.
*/
private DeleteStatus(@Nullable Track.Id trackId, int progress, int max) {
this.trackId = trackId;
this.progress = progress;
this.max = max;
}
public boolean isFinished() {
return progress == max;
}
public boolean isDeleted(Track.Id trackId) {
return this.trackId != null && this.trackId.equals(trackId);
}
@Override
public String toString() {
return "DeleteStatus{" +
"number of deletes=" + progress +
", total=" + max +
'}';
}
}
}
@@ -0,0 +1,70 @@
package de.dennisguse.opentracks.services;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.content.data.Track;
import static de.dennisguse.opentracks.services.TrackDeleteService.EXTRA_TRACK_IDS;
public class TrackDeleteServiceConnection implements ServiceConnection {
final private Listener listener;
private TrackDeleteService trackDeleteService;
public TrackDeleteServiceConnection(@NonNull Listener listener) {
this.listener = listener;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
trackDeleteService = ((TrackDeleteService.Binder) service).getService();
listener.connected();
}
@Override
public void onServiceDisconnected(ComponentName name) {
trackDeleteService = null;
}
public void startAndBind(Context context, ArrayList<Track.Id> trackIds) {
if (trackDeleteService != null) {
return;
}
Intent intent = new Intent(context, TrackDeleteService.class)
.putParcelableArrayListExtra(EXTRA_TRACK_IDS, trackIds);
context.startService(intent);
bind(context);
}
public void bind(Context context) {
if (trackDeleteService != null) {
return;
}
context.bindService(new Intent(context, TrackDeleteService.class), this, BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0);
}
public void unbind(Context context) {
context.unbindService(this);
trackDeleteService = null;
}
public TrackDeleteService getServiceIfBound() {
return trackDeleteService;
}
public interface Listener {
void connected();
}
}
@@ -33,6 +33,8 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.TaskStackBuilder;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.time.Duration;
import java.time.Instant;
@@ -52,7 +54,7 @@ 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.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTask;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -74,11 +76,26 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private static final String TAG = TrackRecordingService.class.getSimpleName();
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
public static final RecordingStatus STATUS_DEFAULT = new RecordingStatus(null, false);
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
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 Handler handler;
private final Runnable updateRecordingData = new Runnable() {
@Override
public void run() {
updateRecordingDataWhileRecording();
handler.postDelayed(this, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
}
};
private SharedPreferences sharedPreferences;
private Distance recordingDistanceInterval;
@@ -119,16 +136,26 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private HandlerServer handlerServer;
private final TrackRecordingServiceStatus serviceStatus = new TrackRecordingServiceStatus();
private RecordingStatus recordingStatus;
private MutableLiveData<RecordingStatus> recordingStatusObservable;
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
private MutableLiveData<RecordingData> recordingDataObservable;
@Override
public void onCreate() {
super.onCreate();
handler = new Handler();
recordingStatusObservable = new MutableLiveData<>();
updateRecordingStatus(STATUS_DEFAULT);
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
recordingDataObservable = new MutableLiveData<>(NOT_RECORDING);
handlerServer = new HandlerServer(this);
contentProviderUtils = new ContentProviderUtils(this);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTask.Factory());
notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -149,7 +176,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@Override
public void onDestroy() {
serviceStatus.onStop();
handler = null;
handlerServer.stop(this);
handlerServer = null;
@@ -181,21 +208,26 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// This should be the next to last operation
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
recordingStatusObservable = null;
gpsStatusObservable = null;
recordingDataObservable = null;
super.onDestroy();
}
public boolean isRecording() {
return serviceStatus.isRecording();
return recordingStatus.isRecording();
}
public boolean isPaused() {
return serviceStatus.getRecordingTrackPaused();
return recordingStatus.isPaused();
}
public Track.Id getRecordingTrackId() {
return serviceStatus.getRecordingTrackId();
return recordingStatus.getTrackId();
}
//TODO
public TrackStatistics getTrackStatistics() {
if (trackStatisticsUpdater == null) {
return null;
@@ -223,14 +255,14 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
if (name == null) {
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(serviceStatus.getRecordingTrackId());
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(getRecordingTrackId());
if (nextMarkerNumber == null) {
nextMarkerNumber = 1;
}
name = getString(R.string.marker_name_format, nextMarkerNumber + 1);
}
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(serviceStatus.getRecordingTrackId());
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(getRecordingTrackId());
if (trackPoint == null) {
Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
return null;
@@ -244,7 +276,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
// Insert marker
Marker marker = new Marker(name, description, category, icon, serviceStatus.getRecordingTrackId(), stats, trackPoint, photoUrl);
Marker marker = new Marker(name, description, category, icon, getRecordingTrackId(), stats, trackPoint, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri));
}
@@ -266,7 +298,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
Track.Id trackId = new Track.Id(ContentUris.parseId(uri));
// Set recording status
serviceStatus.onChange(trackId, false);
updateRecordingStatus(new RecordingStatus(trackId, false));
// Update database
track.setId(trackId);
@@ -308,7 +340,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
insertTrackPoint(track, TrackPoint.createSegmentStartManual());
// Set recording status
serviceStatus.onChange(trackId, false);
updateRecordingStatus(new RecordingStatus(trackId, false));
startRecording();
}
@@ -321,10 +353,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
// Set recording status
serviceStatus.onChange(false);
updateRecordingStatus(new RecordingStatus(getRecordingTrackId(), false));
// Update database
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track != null) {
insertTrackPoint(track, TrackPoint.createSegmentStartManual());
}
@@ -340,6 +372,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start();
handler.postDelayed(updateRecordingData, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
altitudeSumManager = new AltitudeSumManager();
altitudeSumManager.start(this);
@@ -373,10 +407,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// Need to remember the recordingTrackId before setting it to null
boolean wasPause = isPaused();
Track.Id trackId = serviceStatus.getRecordingTrackId();
Track.Id trackId = getRecordingTrackId();
// Set recording status
serviceStatus.onChange(null, true);
updateRecordingStatus(STATUS_DEFAULT);
if (!wasPause) {
// Update database
@@ -404,10 +438,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
// Set recording status
serviceStatus.onChange(true);
updateRecordingStatus(new RecordingStatus(getRecordingTrackId(), true));
// Update database
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track != null) {
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
@@ -426,6 +460,13 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
* @param trackStopped true if track is stopped, false if track is paused
*/
private void endRecording(boolean trackStopped) {
handler.removeCallbacks(updateRecordingData);
if (!trackStopped) {
updateRecordingDataWhileRecording();
} else {
recordingDataObservable.postValue(NOT_RECORDING);
}
// Shutdown periodic tasks
voiceExecutor.shutdown();
@@ -490,7 +531,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track == null) {
Log.w(TAG, "Ignore newTrackPoint. No track.");
return;
@@ -564,17 +605,11 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@Override
public void newGpsStatus(GpsStatusValue gpsStatusValue) {
notificationManager.updateContent(getString(gpsStatusValue.message));
serviceStatus.onChange(gpsStatusValue);
}
public void addListener(@NonNull TrackRecordingServiceStatus.Listener listener) {
serviceStatus.addListener(listener);
}
//TODO Check that this is used everywhere, where addListener is called!
//Otherwise, we keep objects in referenced and waste memory until this service instance is terminated.
public void removeListener(@NonNull TrackRecordingServiceStatus.Listener listener) {
serviceStatus.removeListener(listener);
//TODO This check should not be necessary, but prevents a crash; somehow the shutdown is not working correctly as we should not receive a notification then.
if (gpsStatusObservable != null) {
gpsStatusObservable.postValue(gpsStatusValue);
}
}
/**
@@ -607,14 +642,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
trackPoint.setAltitudeLoss(getAltitudeLoss_m());
altitudeSumManager.reset();
}
//TODO We need a copy of the SensorDataSet as data will be reset!
SensorDataSet sensorDataSet = fillWithSensorDataSet(trackPoint);
if (remoteSensorManager != null) {
fillWithSensorDataSet(trackPoint);
remoteSensorManager.reset();
}
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
recordingDataObservable.postValue(new RecordingData(track, trackPoint, sensorDataSet));
contentProviderUtils.updateTrack(track);
} catch (SQLiteException e) {
/*
@@ -626,25 +665,26 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
voiceExecutor.update();
}
public SensorDataSet getSensorDataSet() {
private SensorDataSet fillWithSensorDataSet(TrackPoint trackPoint) {
if (remoteSensorManager == null) {
return null;
}
return remoteSensorManager.getSensorData();
}
SensorDataSet sensorData = remoteSensorManager.getSensorData();
if (sensorData == null) {
return null;
}
private void fillWithSensorDataSet(TrackPoint trackPoint) {
SensorDataSet sensorData = getSensorDataSet();
if (sensorData != null) {
if (trackPoint != null) {
sensorData.fillTrackPoint(trackPoint);
}
return sensorData;
}
/**
* Returns the relative altitude gain (since last trackpoint).
*/
public Float getAltitudeGain_m() {
private Float getAltitudeGain_m() {
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
return null;
}
@@ -655,7 +695,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
/**
* Returns the relative altitude loss (since last trackpoint).
*/
public Float getAltitudeLoss_m() {
private Float getAltitudeLoss_m() {
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
return null;
}
@@ -666,7 +706,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, serviceStatus.getRecordingTrackId());
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, getRecordingTrackId());
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackRecordingActivity.class)
.addNextIntent(intent)
@@ -702,8 +742,34 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
this.altitudeSumManager = altitudeSumManager;
}
public GpsStatusValue getGpsStatus() {
return handlerServer.getGpsStatus();
public LiveData<GpsStatusValue> getGpsStatusObservable() {
return gpsStatusObservable;
}
public MutableLiveData<RecordingData> getRecordingDataObservable() {
return recordingDataObservable;
}
private void updateRecordingDataWhileRecording() {
if (!recordingStatus.isRecording()) {
Log.w(TAG, "Currently not recording; cannot update data.");
return;
}
Track track = contentProviderUtils.getTrack(recordingStatus.getTrackId());
track.getTrackStatistics().setTotalTime(TrackRecordingService.this.getTotalTime());
SensorDataSet sensorDataSet = fillWithSensorDataSet(lastTrackPoint);
recordingDataObservable.postValue(new RecordingData(track, lastTrackPoint, sensorDataSet));
}
public LiveData<RecordingStatus> getRecordingStatusObservable() {
return recordingStatusObservable;
}
private void updateRecordingStatus(RecordingStatus status) {
Log.i(TAG, "new status " + recordingStatus + " -> " + status);
recordingStatus = status;
recordingStatusObservable.postValue(recordingStatus);
}
@Override
@@ -717,6 +783,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
public class Binder extends android.os.Binder {
private Binder() {
@@ -727,4 +794,98 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return TrackRecordingService.this;
}
}
public static class RecordingStatus {
private final Track.Id trackId;
private final boolean paused;
public RecordingStatus(Track.Id trackId, boolean paused) {
this.trackId = trackId;
this.paused = paused;
}
public Track.Id getTrackId() {
return trackId;
}
public boolean isRecording() {
return trackId != null;
}
public boolean isPaused() {
return paused;
}
public boolean isRecordingAndNotPaused() {
return isRecording() && !isPaused();
}
// TODO Use
// public RecordingStatus pause() {
// return new RecordingStatus(getTrackId(), true);
// }
//
// public RecordingStatus record(@NonNull Track.Id trackId) {
// return new RecordingStatus(trackId, false);
// }
public RecordingStatus stop() {
return STATUS_DEFAULT;
}
@Override
public String toString() {
return "RecordingStatus{" +
"trackId=" + trackId +
", paused=" + paused +
'}';
}
}
public static class RecordingData {
private final Track track;
private final TrackPoint latestTrackPoint;
private final SensorDataSet sensorDataSet;
/**
* {@link Track} and {@link TrackPoint} must be immutable (i.e., their content does not change).
*/
public RecordingData(Track track, TrackPoint lastTrackPoint, SensorDataSet sensorDataSet) {
this.track = track;
this.latestTrackPoint = lastTrackPoint;
this.sensorDataSet = sensorDataSet;
}
public Track getTrack() {
return track;
}
public String getTrackCategory() {
if (track == null) {
return "";
}
return track.getCategory();
}
@NonNull
public TrackStatistics getTrackStatistics() {
if (track == null) {
return new TrackStatistics();
}
return track.getTrackStatistics();
}
public TrackPoint getLatestTrackPoint() {
return latestTrackPoint;
}
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
}
}
}
@@ -44,6 +44,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
@Deprecated //TODO Implement proper interface that passes the current TrackRecordingService
private final Runnable callback;
private TrackRecordingService trackRecordingService;
@@ -1,83 +0,0 @@
package de.dennisguse.opentracks.services;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
/**
* This class handles the status of the recording service.
* Also offer an interface through which to know the recording Track.Id, pause status and GPS status.
*/
public class TrackRecordingServiceStatus {
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private final List<Listener> listeners = new ArrayList<>();
void addListener(@NonNull Listener listener) {
if (this.listeners.contains(listener)) {
return;
}
this.listeners.add(listener);
listener.onTrackRecordingId(recordingTrackId);
listener.onTrackRecordingPaused(recordingTrackPaused);
}
void removeListener(@NonNull Listener listener) {
this.listeners.remove(listener);
}
public boolean getRecordingTrackPaused() {
return this.recordingTrackPaused;
}
public Track.Id getRecordingTrackId() {
return this.recordingTrackId;
}
public void onStop() {
for (Listener listener : listeners) {
listener.onGpsStatus(GpsStatusValue.GPS_NONE);
listener.onTrackRecordingId(null);
listener.onTrackRecordingPaused(true);
}
listeners.clear();
}
public boolean isRecording() {
return recordingTrackId != null;
}
void onChange(Track.Id trackId, boolean paused) {
recordingTrackId = trackId;
recordingTrackPaused = paused;
for (Listener listener : listeners) {
listener.onTrackRecordingId(recordingTrackId);
listener.onTrackRecordingPaused(recordingTrackPaused);
}
}
void onChange(boolean paused) {
recordingTrackPaused = paused;
for (Listener listener : listeners) {
listener.onTrackRecordingPaused(recordingTrackPaused);
}
}
void onChange(GpsStatusValue statusValue) {
for (Listener listener : listeners) {
listener.onGpsStatus(statusValue);
}
}
public interface Listener {
default void onGpsStatus(GpsStatusValue newValue) {}
default void onTrackRecordingPaused(boolean paused) {}
default void onTrackRecordingId(Track.Id trackId) {}
}
}
@@ -14,7 +14,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* This class handle GPS status according to received locations and some thresholds.
* This class handle GPS status according to received locations` and some thresholds.
*/
//TODO should handle sharedpreference changes
class GpsStatus {
@@ -56,6 +56,7 @@ class GpsStatus {
public void stop() {
stopped = true;
sendStatus(gpsStatus, GpsStatusValue.GPS_NONE);
}
}
@@ -75,10 +76,15 @@ class GpsStatus {
gpsStatusHandler = new Handler();
}
public void start() {
client.onGpsStatusChanged(GpsStatusValue.GPS_NONE, GpsStatusValue.GPS_ENABLED);
}
/**
* The client that uses GpsStatus has to call this method to stop the Runnable if needed.
*/
public void stop() {
client.onGpsStatusChanged(gpsStatus, GpsStatusValue.GPS_NONE);
client = null;
if (gpsStatusRunner != null) {
gpsStatusRunner.stop();
@@ -20,4 +20,8 @@ public enum GpsStatusValue {
this.icon = icon;
this.message = message;
}
public boolean isGpsStarted() {
return this != GpsStatusValue.GPS_NONE && this != GpsStatusValue.GPS_DISABLED;
}
}
@@ -63,10 +63,6 @@ public class HandlerServer {
service.newGpsStatus(gpsStatusValue);
}
public GpsStatusValue getGpsStatus() {
return locationHandler.getGpsStatus();
}
public interface HandlerServerInterface {
void newTrackPoint(TrackPoint trackPoint, int gpsAccuracy);
void newGpsStatus(GpsStatusValue gpsStatusValue);
@@ -38,6 +38,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
gpsStatus = new GpsStatus(context, this);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
registerLocationListener();
gpsStatus.start();
}
@Override
@@ -25,6 +25,7 @@ import android.util.Log;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
@@ -57,7 +58,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private boolean started = false;
private final SharedPreferences sharedPreferences;
private int preferenceWheelCircumference;
private Distance preferenceWheelCircumference;
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
@@ -23,6 +23,8 @@ import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
@@ -135,12 +137,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
}
@Override
public void run(TrackRecordingService trackRecordingService) {
if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService is null.");
return;
}
public void run(@NonNull TrackRecordingService trackRecordingService) {
announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics());
}
@@ -181,7 +178,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
}
String category = track.getCategory();
//TODO Querying all TrackPoints all the time is inefficient; use TrackDataHub or something else.
//TODO Querying all TrackPoints all the time is inefficient; get infos from TrackRecordingService
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
while (trackPointIterator.hasNext()) {
@@ -231,4 +228,18 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
}
/**
* A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
*
* @author Rodrigo Damazio
*/
public static class Factory implements PeriodicTaskFactory {
@Override
@NonNull
public PeriodicTask create(Context context) {
return new AnnouncementPeriodicTask(context);
}
}
}
@@ -1,31 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
/**
* A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
*
* @author Rodrigo Damazio
*/
public class AnnouncementPeriodicTaskFactory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new AnnouncementPeriodicTask(context);
}
}
@@ -16,6 +16,8 @@
package de.dennisguse.opentracks.services.tasks;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.services.TrackRecordingService;
/**
@@ -35,7 +37,7 @@ public interface PeriodicTask {
*
* @param trackRecordingService the track recording service
*/
void run(TrackRecordingService trackRecordingService);
void run(@NonNull TrackRecordingService trackRecordingService);
/**
* Shuts down this task and clean up resources.
@@ -17,6 +17,9 @@ package de.dennisguse.opentracks.services.tasks;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.R;
@@ -52,14 +55,13 @@ public class PeriodicTaskExecutor {
private boolean metricUnits;
// The next distance for the distance periodic task
private double nextTaskDistance = Double.MAX_VALUE;
private Distance nextTaskDistance = Distance.of(Double.MAX_VALUE);
public PeriodicTaskExecutor(TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
public PeriodicTaskExecutor(@NonNull TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory;
TASK_FREQUENCY_OFF = Integer.parseInt(trackRecordingService.getBaseContext().getResources().getString(R.string.frequency_off));
TASK_FREQUENCY_OFF = Integer.parseInt(trackRecordingService.getString(R.string.frequency_off));
taskFrequency = TASK_FREQUENCY_OFF;
}
@@ -97,7 +99,7 @@ public class PeriodicTaskExecutor {
timerTaskExecutor.scheduleTask(Duration.ofMinutes(taskFrequency));
} else {
// For distance periodic task
calculateNextTaskDistance();
updateNextTaskDistance();
}
}
@@ -128,59 +130,48 @@ public class PeriodicTaskExecutor {
return;
}
Distance distance = trackStatistics.getTotalDistance();
if (distance.greaterThan(Distance.of(nextTaskDistance))) {
if (trackStatistics.getTotalDistance().greaterThan(nextTaskDistance)) {
periodicTask.run(trackRecordingService);
calculateNextTaskDistance();
updateNextTaskDistance();
}
}
/**
* Sets task frequency.
*
* @param taskFrequency the task frequency
*/
public void setTaskFrequency(int taskFrequency) {
this.taskFrequency = taskFrequency;
restore();
}
/**
* Sets metricUnits.
*
* @param metricUnits true to use metric units
*/
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextTaskDistance();
updateNextTaskDistance();
}
/**
* Calculates the next distance for the distance periodic task.
*/
private void calculateNextTaskDistance() {
private void updateNextTaskDistance() {
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused() || periodicTask == null) {
return;
}
if (!isDistanceFrequency()) {
nextTaskDistance = Distance.of(Double.MAX_VALUE);
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
TrackStatistics trackStatistics = trackRecordingService.getTrackStatistics();
if (trackStatistics == null) {
return;
}
if (!isDistanceFrequency()) {
nextTaskDistance = Double.MAX_VALUE;
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
nextTaskDistance = calculateNextTaskDistance(trackStatistics);
}
double distance = trackStatistics.getTotalDistance().to(metricUnits);
@VisibleForTesting
public Distance calculateNextTaskDistance(TrackStatistics trackStatistics) {
Distance distance = trackStatistics.getTotalDistance();
// The index will be negative since the frequency is negative.
int index = (int) (distance / taskFrequency);
index -= 1;
nextTaskDistance = taskFrequency * index;
Distance announcementInterval = Distance.one(metricUnits).multipliedBy(Math.abs(taskFrequency));
int index = (int) (distance.dividedBy(announcementInterval));
return announcementInterval.multipliedBy(index + 1);
}
/**
@@ -18,6 +18,8 @@ package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import androidx.annotation.NonNull;
/**
* An interface for classes that can create {@link PeriodicTask}.
*
@@ -30,5 +32,6 @@ interface PeriodicTaskFactory {
*
* @return the task, or null if the task is not supported
*/
@NonNull
PeriodicTask create(Context context);
}
@@ -3,10 +3,12 @@ package de.dennisguse.opentracks.settings;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.documentfile.provider.DocumentFile;
@@ -20,12 +22,10 @@ import java.util.Locale;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
@@ -93,7 +93,10 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, prefsFragment).commit();
}
public static class PrefsFragment extends PreferenceFragmentCompat implements TrackRecordingServiceStatus.Listener {
public static class PrefsFragment extends PreferenceFragmentCompat {
private TextToSpeech tts;
private int ttsInitStatus;
private SharedPreferences sharedPreferences;
@@ -107,21 +110,42 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
};
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private boolean isRecording;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private final Runnable bindServiceCallback = new Runnable() {
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(PrefsFragment.this);
if (service == null) {
Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingStatusObservable()
.observe(PrefsFragment.this, status -> onRecordingStatusChanged(status));
}
};
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog;
private Preference voiceAnnouncements;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ttsInitStatus = TextToSpeech.ERROR;
tts = new TextToSpeech(getContext(), status -> {
ttsInitStatus = status;
Log.i(TAG, "TextToSpeech initialized with status " + status);
if (voiceAnnouncements != null) {
updateVoiceAnnouncements();
}
});
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
@@ -147,6 +171,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
//Use same value for not set as Androidx ListPreference and EditTextPreference
return directory != null ? directory.getName() : getString(R.string.not_set);
});
voiceAnnouncements = findPreference(getString(R.string.voice_frequency_key));
updateVoiceAnnouncements();
}
@Override
@@ -205,6 +232,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
super.onDestroy();
trackRecordingServiceConnection.unbind(getContext());
sharedPreferences = null;
voiceAnnouncements = null;
tts = null;
}
public void setDefaultActivity(String iconValue) {
@@ -218,6 +247,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
Preference importPreference = findPreference(getString(R.string.settings_import));
Preference exportPreference = findPreference(getString(R.string.settings_export));
boolean isRecording = recordingStatus.isRecording();
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
importPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
exportPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
@@ -292,13 +322,15 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
});
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
if (trackId != null && !isRecording && isAdded()) {
isRecording = true;
private void updateVoiceAnnouncements() {
voiceAnnouncements.setEnabled(ttsInitStatus == TextToSpeech.SUCCESS);
}
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
this.recordingStatus = status;
if (!status.isRecording() && isAdded()) {
updatePrefsDependOnRecording();
}
}
}
}
@@ -22,6 +22,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
@@ -98,10 +99,13 @@ public class TrackStatisticsUpdater {
return trackInitialized;
}
public void addTrackPoints(List<TrackPoint> trackPoints, Distance minGPSDistance) {
for (TrackPoint tp : trackPoints) {
addTrackPoint(tp, minGPSDistance);
}
}
/**
* Adds a trackPoint.
*
* @param trackPoint the trackPoint
* @param minGPSDistance the min recording distance
*/
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
@@ -148,7 +152,7 @@ public class TrackStatisticsUpdater {
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
updateAbsoluteAltitude(trackPoint.getAltitude());
updateAbsoluteAltitude(trackPoint.getAltitude().toM());
}
if (lastTrackPoint == null || lastMovingTrackPoint == null) {
@@ -159,7 +163,7 @@ public class TrackStatisticsUpdater {
if (!trackPoint.hasSensorDistance()) {
// GPS-based distance/speed
Distance movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
Distance movingDistance = trackPoint.distanceToPrevious(lastMovingTrackPoint);
if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
speedBuffer_mps.reset();
lastTrackPoint = trackPoint;
@@ -23,7 +23,6 @@ import java.util.ArrayList;
import de.dennisguse.opentracks.ContextualActionModeCallback;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackController;
import static android.content.Context.VIBRATOR_SERVICE;
@@ -88,7 +87,7 @@ public class ActivityUtils {
});
}
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) {
public static SearchView configureSearchWidget(Activity activity, final MenuItem menuItem) {
final SearchView searchView = (SearchView) menuItem.getActionView();
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
if (searchManager != null) {
@@ -97,47 +96,12 @@ public class ActivityUtils {
} else {
Log.w(TAG, "Could not retrieve SearchManager.");
}
searchView.setQueryRefinementEnabled(true);
searchView.setSubmitButtonEnabled(true);
searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
// Hide and show trackController when searchable widget has focus/no focus
if (trackController != null) {
if (hasFocus) {
trackController.hide();
} else {
trackController.show();
}
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
menuItem.collapseActionView();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
@Override
public boolean onSuggestionSelect(int position) {
return false;
}
@Override
public boolean onSuggestionClick(int position) {
menuItem.collapseActionView();
return false;
}
});
return searchView;
}
public static void vibrate(Activity activity, int milliseconds) {
final Vibrator vibrator = (Vibrator) activity.getSystemService(VIBRATOR_SERVICE);
public static void vibrate(Context context, int milliseconds) {
final Vibrator vibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
@@ -49,7 +49,7 @@ public class AnnouncementUtils {
}
int totalDistanceId = isMetricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
double distanceInUnit = distance.to(isMetricUnits);
double distanceInUnit = distance.toKM_Miles(isMetricUnits);
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg;

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