Compare commits

...

97 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
119 changed files with 3191 additions and 2355 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
+2 -2
View File
@@ -10,10 +10,10 @@ jobs:
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/)
+1
View File
@@ -50,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)
+5 -5
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.0'
classpath 'com.android.tools.build:gradle:4.2.1'
}
}
@@ -55,8 +55,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4046
versionName "v3.18.0"
versionCode 4143
versionName "v3.18.3"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -93,7 +93,7 @@ dependencies {
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'
@@ -103,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.9.0'
androidTestImplementation 'org.mockito:mockito-android:3.10.0'
}
@@ -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
@@ -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
@@ -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,67 +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;
/**
* 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));
}
@Test
public void nothing() {
//TODO
}
}
@@ -90,10 +90,14 @@ 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
@@ -169,8 +173,8 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0);
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
@@ -209,8 +213,8 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0);
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
@@ -248,8 +252,8 @@ public class ExportImportTest {
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
KmzTrackImporter trackImporter = new KmzTrackImporter();
importTrackId = trackImporter.importFile(context, tmpFileUri).get(0);
KmzTrackImporter importer = new KmzTrackImporter(context, trackImporter);
importTrackId = importer.importFile(tmpFileUri).get(0);
// then
// 1. track
@@ -288,8 +292,8 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0);
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
@@ -313,8 +317,8 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
importTrackId = trackImporter.importFile(inputStream).get(0);
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
@@ -362,8 +366,8 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
importTrackId = trackImporter.importFile(inputStream).get(0);
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
importTrackId = importer.importFile(inputStream).get(0);
// then
// 1. track
@@ -406,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().toM(), importedTrackPoint.getAltitude().toM(), 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)) {
@@ -426,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);
@@ -7,6 +7,7 @@ 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;
@@ -15,6 +16,7 @@ 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;
@@ -29,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) {
@@ -43,63 +52,6 @@ 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() throws IOException {
// given
XMLImporter trackImporter = new XMLImporter(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() throws IOException {
// given
XMLImporter trackImporter = new XMLImporter(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.
*/
@@ -107,12 +59,12 @@ public class LegacyImportTest {
@Test
public void gpx_with_pause_resume() throws IOException {
// given
XMLImporter trackImporter = new XMLImporter(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
@@ -138,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());
@@ -154,6 +106,5 @@ public class LegacyImportTest {
} else {
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);
}
}
@@ -146,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,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>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_logo_color_108dp" />
</adaptive-icon>
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>
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks;
import android.content.Intent;
import androidx.annotation.Nullable;
import java.util.ArrayList;
@@ -26,33 +24,38 @@ 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;
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
*/
//TODO It is actually not a List; we need a better name for this class
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteServiceConnection.Listener {
private static final String TAG = AbstractListActivity.class.getSimpleName();
private static final String TAG = AbstractTrackDeleteActivity.class.getSimpleName();
protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3;
private TrackDeleteServiceConnection trackDeleteServiceConnection;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) {
onTrackDeleted();
} else {
super.onActivityResult(requestCode, resultCode, data);
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;
}
}
@@ -69,6 +72,8 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
public void onConfirmDeleteDone(Track.Id... trackIds) {
boolean stopRecording = false;
onDeleteConfirmed();
for (Track.Id trackId : trackIds) {
if (trackId.equals(getRecordingTrackId())) {
stopRecording = true;
@@ -79,10 +84,9 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
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);
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
trackDeleteServiceConnection.startAndBind(this, new ArrayList<>(Arrays.asList(trackIds)));
}
/**
@@ -94,12 +98,28 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
}
/**
* Called after {@link TrackDeleteActivity} returns its result.
* Called every time a track is deleted.
*/
abstract protected void onTrackDeleted();
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()));
}
}
@@ -175,7 +175,7 @@ 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);
}
@@ -261,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();
}
@@ -1,260 +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.util.Log;
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
*/
//TODO Subscribe to TrackRecordingService.recordingStatus
//TODO Could be a fragment
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 TrackRecordingService.RecordingStatus recordingStatus;
private Duration totalTime;
private final Callback callback;
// A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
if (isResumed() && recordingStatus.isRecordingAndNotPaused()) {
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 || recordingStatus.isRecordingAndNotPaused()) {
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 (!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.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecordingAndNotPaused()) {
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
&& recordingStatus.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(TrackRecordingService.RecordingStatus recordingStatus) {
if (!isResumed()) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
this.recordingStatus = recordingStatus;
boolean isRecording = recordingStatus.isRecording();
boolean isRecordingPaused = recordingStatus.isPaused();
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 (recordingStatus.isRecording()) {
updateTotalTime();
setTotalTime();
}
}
void onResume(TrackRecordingService.RecordingStatus recordingStatus) {
this.recordingStatus = recordingStatus;
update(recordingStatus);
}
void onPause() {
recordingStatus = null;
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(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
}
private boolean isResumed() {
return recordingStatus != null;
}
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();
}
}
@@ -71,14 +71,15 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback {
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 TrackListBinding viewBinding;
@@ -118,7 +119,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu();
loaderCallbacks.restart();
trackController.onResume(recordingStatus);
});
}
}
@@ -132,9 +132,6 @@ 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(recordingStatus));
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.e(TAG, "service not available to start gps or a new recording");
@@ -170,14 +167,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
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) -> {
@@ -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(recordingStatus);
}
@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(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
updateGpsMenuItem(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery("", false);
@@ -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,20 +394,13 @@ 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
}
@@ -425,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);
@@ -479,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]);
@@ -491,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);
@@ -558,25 +540,25 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
public void recordStart() {
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 (recordingStatus.isPaused()) {
// Paused -> Resume
updateMenuItems(false, true);
updateGpsMenuItem(false, true);
trackRecordingServiceConnection.resumeTrack();
}
}
@Override
public void recordPause() {
updateMenuItems(false, true);
updateGpsMenuItem(false, true);
trackRecordingServiceConnection.pauseTrack();
}
@Override
public void recordStop() {
updateMenuItems(false, false);
updateGpsMenuItem(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
}
@@ -585,10 +567,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
updateGpsMenuItem(true, recordingStatus.isRecording());
}
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
trackController.update(recordingStatus);
}
}
@@ -34,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;
@@ -42,6 +41,7 @@ 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.settings.SettingsActivity;
@@ -56,14 +56,13 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* @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 {
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";
@@ -248,9 +247,16 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
return recordingStatus.getTrackId();
}
@Override
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
super.onTrackDeleteStatus(deleteStatus);
if (deleteStatus.isDeleted(trackId)) {
runOnUiThread(this::finish);
}
}
@Override
protected void onTrackDeleted() {
protected void onDeleteConfirmed() {
runOnUiThread(this::finish);
}
@@ -264,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();
@@ -43,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 {
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, ControllerFragment.Callback {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -56,7 +56,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub;
private TrackController trackController;
private TrackRecordingBinding viewBinding;
@@ -90,8 +89,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// A recording track is on.
trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingStatus(recordingStatus);
trackController.onResume(recordingStatus);
}
}
};
@@ -148,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
@@ -214,7 +209,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
//TODO Pass recordingStatus directly to them
trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingStatus(recordingStatus);
trackController.onResume(recordingStatus);
}
/*
@@ -233,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();
@@ -406,7 +394,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
trackController.update(recordingStatus);
trackDataHub.setRecordingStatus(recordingStatus);
setLockscreenPolicy();
@@ -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();
}
}
@@ -29,6 +29,7 @@ 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;
@@ -71,13 +72,13 @@ 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 EGM2008Utils.EGM2008Correction egm2008Correction;
private boolean started;
//TODO Check if this is needed.
private HandlerThread handlerThread;
private Handler handler;
@@ -99,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());
@@ -126,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);
@@ -134,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);
@@ -142,21 +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);
}
public void stop() {
if (!started) {
if (!isStarted()) {
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
return;
}
started = false;
trackStatisticsUpdater = null;
//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,7 +199,7 @@ 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));
}
/**
@@ -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();
}
}
@@ -355,6 +345,13 @@ public class TrackDataHub {
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();
@@ -374,16 +371,16 @@ public class TrackDataHub {
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
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,7 @@ 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) {
@@ -436,6 +429,10 @@ public class TrackDataHub {
trackStatisticsUpdater = new TrackStatisticsUpdater();
}
private boolean isStarted() {
return handlerThread != null;
}
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
this.recordingStatus = recordingStatus;
}
@@ -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;
}
}
@@ -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);
@@ -44,7 +44,7 @@ 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;
@@ -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 = 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());
@@ -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.
*/
@@ -55,7 +55,7 @@ public class TrackPoint {
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)
@@ -99,7 +99,11 @@ 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();
@@ -107,7 +111,9 @@ public class TrackPoint {
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) {
@@ -198,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("");
@@ -314,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()));
}
@@ -326,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() {
@@ -61,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
@@ -161,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);
@@ -190,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)) {
@@ -203,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);
@@ -218,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)) {
@@ -250,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));
}
@@ -576,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++) {
@@ -584,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.
@@ -609,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())};
@@ -740,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,14 +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;
}
}
@@ -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().toM() : 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\"");
@@ -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,509 +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.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.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
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;
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 XMLImporter.TrackParser {
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;
}
}
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;
}
protected 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);
}
}
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();
}
}
@Override
public List<Track.Id> getImportTrackIds() {
return trackIds;
}
public 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);
}
protected ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@NonNull
@Override
public String toString() {
return "" + getMessage();
}
}
}
@@ -17,19 +17,25 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
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.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Imports a GPX file.
@@ -43,7 +49,9 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
*
* @author Jimmy Shih
*/
public class GpxFileTrackImporter extends AbstractFileTrackImporter {
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";
@@ -70,23 +78,46 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
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));
}
private Locator locator;
@VisibleForTesting
GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
super(context, contentProviderUtils);
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 DefaultHandler getHandler() {
return this;
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
@@ -96,7 +127,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
onMarkerStart(attributes);
break;
case TAG_TRACK:
onTrackStart();
trackImporter.newTrack();
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentStart();
@@ -107,6 +138,11 @@ public class GpxFileTrackImporter 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) {
switch (tag) {
@@ -117,13 +153,13 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
onMarkerEnd();
break;
case TAG_TRACK:
onTrackEnd();
trackImporter.setTrack(context, name, uuid, description, category, null);
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentEnd();
break;
case TAG_TRACK_POINT:
onTrackPointEnd();
currentSegment.add(createTrackPoint());
break;
case TAG_NAME:
if (content != null) {
@@ -197,33 +233,65 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
break;
}
// Reset element content
content = null;
content = "";
}
@Override
protected void onTrackStart() throws SAXException {
super.onTrackStart();
name = null;
description = null;
category = null;
private void onTrackSegmentStart() {
}
@Override
protected TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = super.createTrackPoint();
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 (Exception e) {
} 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 (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
@@ -239,7 +307,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
@@ -247,21 +315,21 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
if (gain != null) {
try {
trackPoint.setAltitudeGain(Float.parseFloat(gain));
} catch (Exception e) {
} 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 (Exception e) {
} 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 (Exception e) {
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
}
@@ -269,11 +337,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
return trackPoint;
}
/**
* On track point start.
*
* @param attributes the attributes
*/
private void onTrackPointStart(Attributes attributes) {
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
@@ -285,15 +348,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
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;
@@ -307,6 +361,57 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
}
private void onMarkerEnd() {
addMarker();
// 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;
@@ -16,9 +17,12 @@ import java.io.IOException;
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 {
@@ -50,13 +54,19 @@ public class ImportService extends JobIntentService {
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);
TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), recordingDistanceInterval, maxRecordingDistance, preventReimport);
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new GpxFileTrackImporter(this)).importFile(this, file.getUri()));
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 KmlFileTrackImporter(this)).importFile(this, file.getUri()));
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().importFile(this, file.getUri()));
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));
@@ -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
*/
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,29 +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<>();
@VisibleForTesting
KmlFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
super(context, contentProviderUtils);
// 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;
}
@Override
public DefaultHandler getHandler() {
return this;
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
@@ -98,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();
@@ -113,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) {
@@ -128,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();
@@ -166,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;
@@ -183,7 +212,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
// Reset element content
content = null;
content = "";
}
private void onMarkerStart() {
@@ -196,7 +225,6 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
latitude = null;
longitude = null;
altitude = null;
time = null;
markerType = null;
}
@@ -204,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() {
@@ -219,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));
@@ -260,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];
@@ -279,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) {
@@ -320,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);
@@ -344,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();
}
}
@@ -51,11 +51,16 @@ public class KmzTrackImporter {
private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
private Context context;
private final Context context;
private final TrackImporter trackImporter;
public KmzTrackImporter(Context context, TrackImporter trackImporter) {
this.context = context;
this.trackImporter = trackImporter;
}
@NonNull
public List<Track.Id> importFile(Context context, Uri fileUri) throws IOException {
this.context = context;
public List<Track.Id> importFile(Uri fileUri) throws IOException {
List<Track.Id> trackIds = findAndParseKmlFile(fileUri);
List<Track.Id> trackIdsWithImages = new ArrayList<>();
@@ -205,7 +210,7 @@ public class KmzTrackImporter {
}
private List<Track.Id> parseKml(ZipInputStream zipInputStream) throws IOException {
XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlFileTrackImporter(context));
XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlTrackImporter(context, trackImporter));
InputStream nonClosableInputStream = new FilterInputStream(zipInputStream) {
@Override
@@ -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();
}
}
@@ -0,0 +1,301 @@
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.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;
/**
* 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 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);
}
/**
* If not present: calculate data from the previous trackPoint (if present)
* NOTE: Modifies content of trackPoints.
*/
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);
}
}
@@ -45,7 +45,7 @@ public class XMLImporter {
try {
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, parser.getHandler());
return parser.getImportTrackIds();
} catch (SAXException | ParserConfigurationException | AbstractFileTrackImporter.ParsingException e) {
} catch (SAXException | ParserConfigurationException | ParsingException e) {
Log.e(TAG, "Unable to import file", e);
if (parser.getImportTrackIds().size() > 0) {
parser.cleanImport();
@@ -58,6 +58,7 @@ public class XMLImporter {
}
interface TrackParser {
@Deprecated
DefaultHandler getHandler();
List<Track.Id> getImportTrackIds();
@@ -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();
}
}
@@ -76,7 +76,10 @@ 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:
@@ -84,6 +87,15 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
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;
@@ -127,14 +139,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
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);
@@ -160,6 +176,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@Override
public void onDestroy() {
handler = null;
handlerServer.stop(this);
handlerServer = null;
@@ -192,6 +210,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
recordingStatusObservable = null;
gpsStatusObservable = null;
recordingDataObservable = null;
super.onDestroy();
}
@@ -208,6 +227,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return recordingStatus.getTrackId();
}
//TODO
public TrackStatistics getTrackStatistics() {
if (trackStatisticsUpdater == null) {
return null;
@@ -352,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);
@@ -438,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();
@@ -613,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) {
/*
@@ -632,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;
}
@@ -661,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;
}
@@ -712,6 +746,22 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
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;
}
@@ -792,4 +842,50 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
'}';
}
}
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;
@@ -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);
@@ -138,11 +138,6 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override
public void run(@NonNull TrackRecordingService trackRecordingService) {
if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService is null.");
return;
}
announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics());
}
@@ -183,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()) {
@@ -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;
@@ -93,6 +95,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
public static class PrefsFragment extends PreferenceFragmentCompat {
private TextToSpeech tts;
private int ttsInitStatus;
private SharedPreferences sharedPreferences;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
@@ -125,6 +130,22 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
// 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());
@@ -150,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
@@ -208,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) {
@@ -296,6 +322,10 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
});
}
private void updateVoiceAnnouncements() {
voiceAnnouncements.setEnabled(ttsInitStatus == TextToSpeech.SUCCESS);
}
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
this.recordingStatus = status;
if (!status.isRecording() && isAdded()) {
@@ -303,5 +333,4 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}
}
}
}
@@ -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) {
@@ -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 {
@@ -142,9 +142,9 @@ public class PreferencesUtils {
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
}
public static int getWheelCircumference(SharedPreferences sharedPreferences, Context context) {
public static Distance getWheelCircumference(SharedPreferences sharedPreferences, Context context) {
final int DEFAULT = Integer.parseInt(context.getResources().getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default));
return getInt(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT);
return Distance.ofMM(getInt(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT));
}
public static String getBluetoothCyclingPowerSensorAddress(SharedPreferences sharedPreferences, Context context) {
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_logo_color_108dp" />
<foreground>
<inset
android:inset="20dp"
android:drawable="@drawable/ic_logo_color_24dp" />
</foreground>
</adaptive-icon>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@color/ic_launcher_background" />
</shape>
</item>
<item>
<inset
android:drawable="@drawable/ic_logo_color_24dp"
android:inset="8dp" />
</item>
</layer-list>
@@ -1,33 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.56"
android:scaleY="0.56"
android:translateX="21.50"
android:translateY="24.00">
<path
android:pathData="M85.133,20.59C85.731,18.546 84.756,17.817 82.965,18.968L23.874,56.967c-1.791,1.152 -1.519,2.224 0.604,2.382L49.019,61.18c2.124,0.159 4.477,1.918 5.229,3.91l8.7,23.021c0.753,1.992 1.857,1.949 2.455,-0.094z"
android:strokeWidth="1"
android:strokeColor="#f69123">
<aapt:attr name="android:fillColor">
<gradient
android:endX="84.4913"
android:endY="19.211866"
android:startX="38.773293"
android:startY="80.33152"
android:type="linear">
<item
android:color="#FFFBB22F"
android:offset="0" />
<item
android:color="#FFF58820"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
</group>
</vector>
+6 -3
View File
@@ -8,8 +8,11 @@
<item
android:width="300dp"
android:height="300dp"
android:drawable="@drawable/ic_logo_color_108dp"
android:gravity="center_vertical|center_horizontal"
tools:targetApi="m" />
android:gravity="center"
tools:targetApi="m">
<inset
android:drawable="@drawable/ic_logo_color_24dp"
android:inset="20dp" />
</item>
</layer-list>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?><!--
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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/controller_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/controller_background"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<ImageButton
android:id="@+id/controller_record"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="6">
<TextView
android:id="@+id/controller_total_time"
style="@style/TextSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/controller_status"
style="@style/TextMicro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/controller_total_time"
android:layout_centerHorizontal="true"
android:singleLine="true" />
</RelativeLayout>
<ImageButton
android:id="@+id/controller_stop"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:id="@+id/track_controller_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/track_controller_background"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<ImageButton
android:id="@+id/track_controller_record"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="6">
<TextView
android:id="@+id/track_controller_total_time"
style="@style/TextSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/track_controller_status"
style="@style/TextMicro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/track_controller_total_time"
android:layout_centerHorizontal="true"
android:singleLine="true" />
</RelativeLayout>
<ImageButton
android:id="@+id/track_controller_stop"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
</merge>
+5 -3
View File
@@ -27,9 +27,11 @@ limitations under the License.
android:baselineAligned="false"
android:orientation="horizontal">
<include
android:id="@+id/track_controller_container"
layout="@layout/track_controller" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/controller_fragment"
android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<ListView
android:id="@+id/track_list"
+5 -3
View File
@@ -11,9 +11,11 @@
android:layout_height="match_parent"
android:orientation="horizontal">
<include
android:id="@+id/track_controller_container"
layout="@layout/track_controller" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/controller_fragment"
android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
+4 -5
View File
@@ -13,9 +13,8 @@ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/chart_view_layout"
<de.dennisguse.opentracks.chart.ChartView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/chart_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical" />
android:layout_height="match_parent" />
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?><!--
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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/controller_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/controller_background"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageButton
android:id="@+id/controller_record"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="6">
<TextView
android:id="@+id/controller_total_time"
style="@style/TextSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:gravity="center"
android:textColor="@android:color/white"
tools:text="0:12:34" />
<TextView
android:id="@+id/controller_status"
style="@style/TextMicro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/controller_total_time"
android:layout_alignParentStart="true"
android:gravity="center"
android:singleLine="true"
tools:text="@string/generic_recording" />
</RelativeLayout>
<ImageButton
android:id="@+id/controller_stop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
+1 -1
View File
@@ -269,7 +269,7 @@
android:id="@+id/stats_altitude_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="stats_altitude_horizontal_line,stats_altitude_current_label,stats_altitude_current_value,stats_altitude_current_unit,stats_altitude_gain_label,stats_altitude_gain_value,stats_altitude_gain_unit,stats_altitude_loss_label,stats_altitude_loss_value,stats_altitude_loss_unit" />
app:constraint_referenced_ids="stats_altitude_horizontal_line,stats_altitude_current_label,stats_altitude_current_value,stats_altitude_current_unit,stats_altitude_gain_label,stats_altitude_gain_value,stats_altitude_gain_unit,stats_altitude_loss_label,stats_altitude_loss_value,stats_altitude_loss_unit,stats_elevation_current_label_egm" />
<!-- altitude -->
<TextView
-75
View File
@@ -1,75 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:id="@+id/track_controller_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/track_controller_background"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageButton
android:id="@+id/track_controller_record"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="6">
<TextView
android:id="@+id/track_controller_total_time"
style="@style/TextSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:gravity="center"
android:textColor="@android:color/white"
tools:text="0:12:34" />
<TextView
android:id="@+id/track_controller_status"
style="@style/TextMicro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/track_controller_total_time"
android:layout_alignParentStart="true"
android:gravity="center"
android:singleLine="true"
tools:text="@string/generic_recording" />
</RelativeLayout>
<ImageButton
android:id="@+id/track_controller_stop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
</merge>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="100dp"
android:minWidth="100dp" />
</LinearLayout>
+5 -3
View File
@@ -83,7 +83,9 @@ limitations under the License.
</LinearLayout>
</RelativeLayout>
<include
android:id="@+id/track_controller_container"
layout="@layout/track_controller" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/controller_fragment"
android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
+5 -3
View File
@@ -17,7 +17,9 @@
android:layout_height="0dp"
android:layout_weight="1" />
<include
android:id="@+id/track_controller_container"
layout="@layout/track_controller" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/controller_fragment"
android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
+2 -2
View File
@@ -20,14 +20,14 @@ limitations under the License.
android:icon="@drawable/ic_statistics_24dp"
android:title="@string/menu_aggregated_statistics"
app:showAsAction="ifRoom" />
<!-- Icon and title is overwritten by TrackListActivity -->
<!-- Hide icon as workaround for https://github.com/OpenTracksApp/OpenTracks/issues/200 -->
<!-- Icon, title, and visibility is managed by TrackListActivity -->
<item
android:id="@+id/track_list_start_gps"
android:icon="@drawable/ic_gps_off_24dp"
android:title="@string/menu_start_gps"
android:visible="false"
app:showAsAction="ifRoom" />
<!-- TODO SearchAction is only working if it is shown as action. However, only two slots are available. See #757. -->
<item
android:id="@+id/track_list_search"
android:title="@string/menu_search"
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.6 KiB

+183 -40
View File
@@ -66,12 +66,12 @@
<string name="activity_type_walking">caminata</string>
<string name="activity_type_water_skiing">esquí acuático</string>
<string name="activity_type_wind_surfing">windsurf</string>
<string name="description_activity_type">Tipo de actividad: %1$s.</string>
<string name="description_activity_type">Tipo de actividad: %1$s</string>
<string name="description_average_moving_pace_in_minute">Velocidad promedio de movimiento: %1$s min/km (%2$s min/mi)</string>
<string name="description_average_moving_speed">Velocidad promedio de movimiento: %1$.2f km/h (%2$.1f mi/h).</string>
<string name="description_average_moving_speed">Velocidad promedio de movimiento: %1$.2f km/h (%2$.1f mi/h)</string>
<string name="description_average_pace_in_minute">Velocidad promedio: %1$s min/km (%2$s min/mi)</string>
<string name="description_average_speed">Velocidad promedio: %1$.2f km/h (%2$.1f mi/h).</string>
<string name="description_altitude_gain">Altitud: %1$d m (%2$d pies).</string>
<string name="description_average_speed">Velocidad promedio: %1$.2f km/h (%2$.1f mi/h)</string>
<string name="description_altitude_gain">Variación de altitud: %1$d m (%2$d pies)</string>
<string name="description_altitude_imperial">Elevación (pies)</string>
<string name="description_altitude_metric">Elevación (m)</string>
<string name="description_fastest_pace_in_minute">Velocidad más rápida: %1$s min/km (%2$s min/mi)</string>
@@ -81,31 +81,31 @@
<string name="description_location_latitude">Latitud (grados)</string>
<string name="description_location_longitude">Longitud (grados)</string>
<string name="description_location_speed">Velocidad (m/s)</string>
<string name="description_max_altitude">Elevación máx.: %1$d m (%2$d pies).</string>
<string name="description_max_grade">Pendiente máx.: %1$d %%.</string>
<string name="description_max_speed">Velocidad máx.: %1$.2f km/h (%2$.1f mi/h).</string>
<string name="description_min_altitude">Elevación mín.: %1$d m (%2$d pies).</string>
<string name="description_min_grade">Pendiente mín.: %1$d %%.</string>
<string name="description_moving_time">Tiempo en movimiento: %1$s.</string>
<string name="description_max_altitude">Elevación máx.: %1$d m (%2$d pies)</string>
<string name="description_max_grade">Pendiente máx.: %1$d %%</string>
<string name="description_max_speed">Velocidad máx.: %1$.2f km/h (%2$.1f mi/h)</string>
<string name="description_min_altitude">Elevación mín.: %1$d m (%2$d pies)</string>
<string name="description_min_grade">Pendiente mín.: %1$d %%</string>
<string name="description_moving_time">Tiempo en movimiento: %1$s</string>
<string name="description_pace_imperial">Velocidad (min/mi)</string>
<string name="description_pace_metric">Velocidad (min/km)</string>
<string name="description_recorded_time">Registrado: %1$s.</string>
<string name="description_recorded_time">Registrado: %1$s</string>
<string name="description_sensor_cadence">Cadencia (rpm)</string>
<string name="description_sensor_heart_rate">Frecuencia cardíaca (lpm)</string>
<string name="description_sensor_power">Potencia (W)</string>
<string name="description_speed_imperial">Velocidad (mi/h)</string>
<string name="description_speed_metric">Velocidad (km/h)</string>
<string name="description_time">Tiempo</string>
<string name="description_total_distance">Distancia total: %1$.2f km (%2$.1f mi).</string>
<string name="description_total_time">Tiempo total: %1$s.</string>
<string name="description_total_distance">Distancia total: %1$.2f km (%2$.1f mi)</string>
<string name="description_total_time">Tiempo total: %1$s</string>
<string name="description_track_point">Punto</string>
<string name="description_track_segment">Segmento</string>
<string name="export_all_option">Todo como %1$s</string>
<string name="export_all_title">Exportar a unidad de almacenamiento externa</string>
<string name="export_error">Se exportaron %1$d de %2$s a %3$s.</string>
<string name="export_error">Se exportaron %1$d de %2$s a %3$s</string>
<string name="export_option">como %1$s a %2$s</string>
<string name="export_progress_message">Exportando a %1$s…</string>
<string name="export_success">Se exportaron %1$s a %2$s.</string>
<string name="export_success">Se exportaron %1$s a %2$s</string>
<string name="export_title">Exportar a…</string>
<string name="files_few">%1$d archivos</string>
<string name="files_many">%1$d archivos</string>
@@ -134,7 +134,7 @@
<string name="gps_location_access">“Acceso a ubicaciones”</string>
<string name="gps_disabled">El GPS no está habilitado. Puntea aquí para ir a %1$s. Para volver, presiona el botón Atrás.</string>
<string name="gps_disabled_my_location">No se puede determinar la ubicación actual. Habilita más fuentes de ubicación en %1$s y asegúrate de que se pueda ver el cielo. Vuelve a intentarlo más tarde.</string>
<string name="gps_not_found">No se encontró ningún GPS en el dispositivo.</string>
<string name="gps_not_found">No se encontró ningún GPS en el dispositivo</string>
<string name="gps_starting">Iniciando GPS</string>
<string name="gps_wait_for_better_signal">Esperando una mejor señal de GPS</string>
<string name="gps_wait_for_signal">Esperando señal de GPS</string>
@@ -148,21 +148,21 @@
<string name="image_record">Grabar</string>
<string name="image_stop">Parar</string>
<string name="image_track">Seguir</string>
<string name="import_error">Se importaron %1$d de %2$s desde %3$s.</string>
<string name="import_no_directory">%1$s no existe.</string>
<string name="import_error">Se importaron %1$d de %2$s desde %3$s</string>
<string name="import_no_directory">%1$s no existe</string>
<string name="import_no_file">No hay archivos para importar. Conecta la unidad a la computadora y pasa los archivos que deseas importar a %1$s.</string>
<string name="import_no_file_title">Importar archivos</string>
<string name="import_progress_message">Importando desde %1$s…</string>
<string name="import_selection_option">Todos los archivos %1$s</string>
<string name="import_selection_title">Importar desde la unidad de almacenamiento externa</string>
<string name="import_success">Se importaron %1$s desde %2$s.</string>
<string name="import_success">Se importaron %1$s desde %2$s</string>
<string name="marker_add_error">No se puede insertar un marcador. No se recibe señal de GPS. Vuelve a intentarlo.</string>
<string name="marker_add_success">Se insertó un marcador.</string>
<string name="marker_add_canceled">Cancelado, no se agregó ningún marcador.</string>
<string name="marker_delete_all_confirm_message">Todos los marcadores de esta ruta se eliminarán del dispositivo de forma permanente.</string>
<string name="marker_delete_one_confirm_message">El marcador se eliminará del dispositivo de forma permanente.</string>
<string name="marker_add_success">Se insertó un marcador</string>
<string name="marker_add_canceled">Cancelado, no se agregó ningún marcador</string>
<string name="marker_delete_all_confirm_message">Todos los marcadores de esta ruta se eliminarán del dispositivo de forma permanente</string>
<string name="marker_delete_one_confirm_message">El marcador se eliminará del dispositivo de forma permanente</string>
<string name="marker_delete_one_confirm_title">¿Eliminar marcador?</string>
<string name="marker_delete_multiple_confirm_message">Los marcadores seleccionados se eliminarán del dispositivo de forma permanente.</string>
<string name="marker_delete_multiple_confirm_message">Los marcadores seleccionados se eliminarán del dispositivo de forma permanente</string>
<string name="marker_label_end">%1$s (Fin)</string>
<string name="marker_label_start">%1$s (Inicio)</string>
<string name="marker_name_format">Marcador %1$d</string>
@@ -171,7 +171,7 @@
<string name="marker_detail_statistics_title">Desde el último marcador de división:</string>
<string name="marker_edit_marker_type">Tipo de marcador (p. ej., cascada)</string>
<string name="marker_edit_marker_type_hint">Tipo de marcador</string>
<string name="marker_list_empty_message">No tienes ningún marcador.</string>
<string name="marker_list_empty_message">No tienes ningún marcador</string>
<string name="menu_aggregated_statistics">Estadíst. globales</string>
<string name="menu_delete">Eliminar</string>
<string name="menu_edit">Editar</string>
@@ -196,17 +196,17 @@
<string name="menu_resume_track">Reanudar ruta</string>
<string name="recording_service">Servicio de grabación</string>
<string name="search_global_description">Recorridos y marcadores</string>
<string name="search_list_empty_message">No hay rutas ni marcadores que coincidan con tu búsqueda.</string>
<string name="search_list_marker_track_location">En el recorrido “%1$s”</string>
<string name="search_list_empty_message">No hay rutas ni marcadores que coincidan con tu búsqueda</string>
<string name="search_list_marker_track_location">En el recorrido “%1$s”.</string>
<string name="sensor_state_battery">Nivel de la batería</string>
<string name="sensor_state_cadence">Cadencia</string>
<string name="sensor_state_cadence_value">%1$d rpm</string>
<string name="sensor_state_disconnected">Desconectado</string>
<string name="sensor_state_heart_rate">Frecuencia cardíaca</string>
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
<string name="sensor_state_heart_rate_value">%1$d lpm</string>
<string name="sensor_state_power">Potencia</string>
<string name="sensor_state_power_value">%1$d W</string>
<string name="settings_not_while_recording">No está disponible durante la grabación de un recorrido.</string>
<string name="settings_not_while_recording">No está disponible durante la grabación de un recorrido</string>
<string name="settings_show">Mostrar</string>
<string name="settings_advanced">Opciones avanzadas</string>
<string name="settings_chart_by_distance">Por distancia</string>
@@ -216,23 +216,23 @@
<string name="settings_recording_auto_resume_track_timeout_always_summary">Siempre reanudar la grabación después de reiniciar</string>
<string name="settings_recording_auto_resume_track_timeout_never_summary">Nunca reanudar la grabación después de reiniciar</string>
<string name="settings_recording_auto_resume_track_timeout_summary">Reanudar una grabación al reiniciar el sistema si la última ubicación se grabó en los últimos %1$s</string>
<string name="settings_recording_auto_resume_track_timeout_title">Reanudación autom.</string>
<string name="settings_recording_default_activity_title">Actividad predet.</string>
<string name="settings_recording_auto_resume_track_timeout_title">Reanudación automática</string>
<string name="settings_recording_default_activity_title">Tipo de actividad predeterminada</string>
<string name="settings_recording_location_frequency_summary">%1$s entre las ubicaciones grabadas</string>
<string name="settings_recording_max_recording_distance_summary">Se crea un nuevo tramo si la distancia entre dos ubicaciones es superior a %1$s.</string>
<string name="settings_recording_max_recording_distance_summary">Se crea un nuevo tramo si la distancia entre dos ubicaciones es superior a %1$s</string>
<string name="settings_recording_max_recording_distance_title">Distancia máxima</string>
<string name="settings_recording_min_recording_distance_title">Intervalo de distancia</string>
<string name="settings_recording_min_recording_interval_title">Intervalo de tiempo</string>
<string name="settings_recording_min_required_accuracy_summary">La ubicación se ignora si su precisión es inferior a %1$s.</string>
<string name="settings_recording_min_required_accuracy_summary">La ubicación se ignora si su precisión es inferior a %1$s</string>
<string name="settings_recording_min_required_accuracy_title">Precisión del GPS</string>
<string name="settings_recording_track_name_date_iso_8601_option">Fecha (ISO 8601)</string>
<string name="settings_recording_track_name_date_local_option">Fecha (local)</string>
<string name="settings_recording_track_name_number_option">Número</string>
<string name="settings_recording_track_name_title">Nombre de rec. predet.</string>
<string name="settings_recording_track_name_title">Nombre de recorrido predeterminado</string>
<string name="settings_reset">Restablecer</string>
<string name="settings_reset_confirm_message">Todos los parámetros de configuración se restablecerán a los valores predeterminados.</string>
<string name="settings_reset_confirm_title">¿Restablecer ahora?</string>
<string name="settings_reset_done">Se restablecieron los valores predeterminados de todas las opciones de configuración.</string>
<string name="settings_reset_done">Se restablecieron los valores predeterminados de todas las opciones de configuración</string>
<string name="settings_sensor">Sensor</string>
<string name="settings_sensor_bluetooth">Bluetooth</string>
<string name="settings_sensor_bluetooth_sensor">Sensor de Bluetooth</string>
@@ -242,7 +242,9 @@
<string name="settings_stats_units_metric">Métricas (km, m)</string>
<string name="settings_stats_units_title">Unidades preferidas</string>
<string name="share_track_share_file">Compartir el archivo del recorrido</string>
<string name="share_track_share_file_body">Creo que este recorrido podría interesarte.\n\nPara ver este recorrido, usa Google Earth u otro software de mapas.\n\n%1$s</string>
<string name="share_track_share_file_body">Creo que este recorrido podría interesarte.
\n
\n%1$s</string>
<string name="share_track_subject">Me gustaría compartir un recorrido contigo</string>
<string name="stats_average_moving_pace">Ritmo de movimiento promedio</string>
<string name="stats_average_moving_speed">Velocidad de movimiento promedio</string>
@@ -255,7 +257,7 @@
<string name="stats_latitude">Latitud</string>
<string name="stats_longitude">Longitud</string>
<string name="stats_max">Máx.</string>
<string name="stats_max_speed">Velocidad máx.</string>
<string name="stats_max_speed">Velocidad máxima</string>
<string name="stats_min">Mín.</string>
<string name="stats_moving_time">Tiempo en movimiento</string>
<string name="stats_pace">Ritmo</string>
@@ -275,7 +277,7 @@
<string name="track_edit_activity_type">Tipo de actividad (p. ej., senderismo)</string>
<string name="track_edit_activity_type_hint">Tipo de actividad</string>
<string name="track_edit_new_track_title">Nuevo recorrido</string>
<string name="track_list_empty_message">Comienza a grabar aquí tu próxima aventura.</string>
<string name="track_list_empty_message">Comienza a grabar aquí tu próxima aventura</string>
<string name="tracks_few">%1$d recorridos</string>
<string name="tracks_many">%1$d recorridos</string>
<string name="tracks_one">1 recorrido</string>
@@ -309,7 +311,7 @@
<string name="value_integer_meter_recommended">%1$d m (recomendado)</string>
<string name="value_integer_mile">%1$d millas</string>
<string name="value_integer_mile_hour">%1$d mi/h</string>
<string name="value_integer_minute">%1$d min.</string>
<string name="value_integer_minute">%1$d mín</string>
<string name="value_integer_percent">%1$d %%</string>
<string name="value_integer_second">%1$d s</string>
<string name="value_none">Ninguno</string>
@@ -416,4 +418,145 @@
<!-- Share image -->
<string name="share_image_subject">Me gustaría compartir una imagen contigo</string>
<string name="share_image_body">Creo que esta imagen podría interesarte.</string>
</resources>
<string name="about_preference_title">Acerca de OpenTracks</string>
<string name="about_description"><i> OpenTracks </i> es una aplicación de seguimiento deportivo que respeta completamente su privacidad.</string>
<string name="about_url">Página web del proyecto: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Nombre de la versión: %1$s</string>
<string name="bluetooth_disabled">Por favor habilite Bluetooth.</string>
<string name="settings_default_trackfileformat">Exportar/compartir formato de archivo</string>
<string name="settings_recording_show_on_lockscreen_while_recording_title">Mostrar estadísticas en pantalla de bloqueo</string>
<string name="permission_gps_failed">OpenTracks necesita permiso para usar el GPS.</string>
<string name="settings_recording_keep_screen_on_while_recording_title">Mantener la pantalla encendida</string>
<string name="settings_sensor_disconnected">Desconectado de %1$s</string>
<string name="instant_export_enabled_title">Exportación instantánea post-entrenamiento</string>
<string name="hold_to_stop">Mantener para parar</string>
<string name="hold_to_pause">Mantener para pausar</string>
<string name="settings_sensor_wheel_circumference">Circunferencia de la rueda (mm)</string>
<string name="sensor_state_cadence_avg">Ritmo promedio</string>
<string name="sensor_state_heart_rate_avg">Tasa cardíaca promedio</string>
<string name="sensor_state_heart_rate_max">Tasa cardíaca máxima</string>
<string name="sensor_state_cadence_max">Ritmo máximo</string>
<string name="stats_intervals">Intervalos</string>
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Mientras graba, mostrar estadísticas sin desbloquear el dispositivo.</string>
<string name="settings_recording_keep_screen_on_while_recording_summary">Mientras graba, mantener la pantalla encendida.</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Pantalla completa</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Mientras graba, presentar en pantalla completa.</string>
<string name="settings_night_mode_title">Tema de interfaz de usuario</string>
<string name="settings_night_mode_option_system">Sistema</string>
<string name="settings_night_mode_option_day">Día</string>
<string name="settings_night_mode_option_night">Noche</string>
<string name="settings_error_initial_values_restored">Lo sentimos, pero ha ocurrido un error. Todas las configuraciones se han revertido a los valores predeterminados.</string>
<string name="settings_sensor_connecting">Conectado a %1$s</string>
<string name="track_speed_notification">Velocidad: %1$s</string>
<string name="instant_export_enabled_summary">Exportar el recorrido al almacenamiento una vez finalizada la grabación</string>
<string name="toast_message_instant_track_was_exported">Último recorrido exportado</string>
<string name="description_altitude_loss">Pérdida de elevación: %1$d m (%2$d pies)</string>
<string name="export_progress_summary_new_msg">Nuevos recorridos exportados</string>
<string name="about_version_code">Código de versión: %1$d</string>
<string name="about_history">OpenTracks es una bifurcación de <a href="https://code.google.com/archive/p/mytracks/"> <i> MyTracks de Google </i> </a> (descontinuado en 2016).</string>
<string name="help_data_storage_content"> Los datos del sensor se almacenan por ubicación registrada. En términos de sensor de frecuencia cardíaca, esto significa que solo se almacena <i> una </i> medición de frecuencia cardíaca por ubicación. Si está parado o está adentro, no se registrarán los datos del sensor (pero se mostrarán). </string>
<string name="help_gps_troubleshooting_content"> Lo que puedes hacer:
\na) compruebe si el GPS está habilitado,
\nb) mejorar la recepción del GPS (por ejemplo, salir al aire libre),
\nc) verifique con la aplicación <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/"> GPSTest </a>, y
\nd) desactivar el ahorro de energía.
\n
\nSi todo no ayuda, informe un error. </string>
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
<string name="value_float_mile_hour">%1$.2f mph</string>
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
<string name="help_support_content"> Usted puede:
\na) haga correr la voz sobre OpenTracks,
\nb) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> proponer funciones </a>,
\nc) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> informar errores </a>,
\nd) ayuda en el desarrollo de software,
\ne) comprar en <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play Store </a>,
\nf) donar, y
\ng) haga lo que crea que ayuda a OpenTracks. </string>
<string name="export_error_post_workout">La exportación posterior al entrenamiento falló, consulte el directorio de exportación.</string>
<plurals name="generic_completed_with_errors">
<item quantity="one">Completado con un error</item>
<item quantity="other">Completado con %1$d errores</item>
</plurals>
<string name="import_thread_interrupted">Hilo interrumpido</string>
<string name="import_no_kml_file_found">No se ha encontrado ningún archivo doc.kml en KMZ</string>
<string name="sensor_state_power_avg">Potencia promedio</string>
<string name="sensor_not_known">El sensor %1$s es desconocido. Compruebe la configuración.</string>
<string name="sensor_could_not_scan">No se pudieron buscar dispositivos Bluetooth (error %1$i).</string>
<string name="settings_prevent_reimport_tracks_title">Evitar reimportar los recorridos</string>
<string name="track_distance_notification">Distancia: %1$s</string>
<string name="aggregated_stats_empty_message">Grabe su primer recorrido para ver estadísticas agregadas</string>
<string name="settings_default_export_uri_title">Directorio de exportación de recorrido</string>
<string name="help_track_map_content"> Utilice el icono de mapa pequeño.
\na) (actualizaciones en vivo) Instale <a href="https://github.com/OpenTracksApp/OSMDashboard"> OSMDashboard </a>. Proporciona mapas en línea o fuera de línea a través de <a href="https://www.openstreetmap.org"> OpenStreetMap </a>.
\nEn realidad, OSMDashboard es una aplicación separada que solo puede acceder (temporalmente) a los datos de las pistas que se mostrarán.
\nb) A través de una aplicación de mapas normal (p. ej., <a href="http://osmand.net/"> OsmAnd </a> y <a href="https://maps.me/"> MAPS.ME </a>). Estas aplicaciones deben admitir el formato de archivo KMZ (solo ubicaciones y marcas de tiempo). De hecho, una aplicación debe admitir <i> ACTION_VIEW </i> con MIME <i> application / vnd.google-earth.kmz</i>. </string>
<string name="voice_speed_lap">Velocidad de vuelta de %1$s</string>
<string name="voice_pace_lap">Tiempo de vuelta de %1$s</string>
<string name="delete_service">Borrar servicio</string>
<string name="about_third_party_data">Datos de terceros</string>
<string name="about_libraries">Bibliotecas</string>
<string name="help_feature_title">OpenTracks tiene las siguientes características:</string>
<string name="help_feature_content">* crear pistas de GPS
\n* utilizar un sensor de frecuencia cardíaca (solo Bluetooth LE)
\n* anuncios de voz (por tiempo o distancia)
\n* crear marcadores (incluidas imágenes)
\n* funciona completamente sin conexión
\n* mostrar pistas en un mapa (ver más abajo)</string>
<string name="help_track_map_title">¿Cómo mostrar un camino en un mapa\?</string>
<string name="help_storage_pictures_title">¿Cómo almacena OpenTracks las imágenes que se tomaron como marcadores\?</string>
<string name="help_storage_pictures_content">Estos se almacenan en el almacenamiento interno de la aplicación (es decir, solo se puede acceder a ellos dentro de OpenTracks).</string>
<string name="help_track_export_title">¿Cómo puedo exportar / compartir pistas grabadas\?</string>
<string name="help_track_export_content"> Las pistas se pueden exportar desde la configuración como KMZ, KML o GPX. KMZ / KML todos los datos; para GPX solo ubicaciones. Las pistas compartidas a través del <i> icono de compartir </i> se comparten como KMZ. </string>
<string name="help_data_google_play_services_title">¿OpenTracks requiere / usa el servicio Google Play\?</string>
<string name="help_recording_title">¿Cómo registra OpenTracks los datos\?</string>
<string name="help_recording_content">OpenTracks utiliza el sensor GPS del dispositivo para registrar la ubicación de forma continua. Si el dispositivo GPS informa datos inexactos (por ejemplo, ubicación, velocidad, elevación), OpenTracks no puede hacer nada al respecto.</string>
<string name="help_data_storage_title">¿Cómo almacena OpenTracks los datos de los sensores\?</string>
<string name="help_gps_troubleshooting_title">¿OpenTracks no registra ubicaciones\?</string>
<string name="help_support_title">¿Cómo puedo ayudar a OpenTracks\?</string>
<string name="open_track_using_dashboard">Abrir usando el Tablero OpenTracks</string>
<string name="open_track_as_KMZ">Abrir como KMZ</string>
<string name="open_track_as_GPX">Abrir como GPX</string>
<string name="open_track_as_trackfileformat">Abrir como %1$s</string>
<string name="activity_type_escooter">moto</string>
<string name="app_not_installed_show_on_map">Instale una aplicación que admita la presentación de datos de mapas.</string>
<string name="description_speed_ms">Velocidad (m/s)</string>
<string name="description_default_speed_or_pace">Por tipo de actividad</string>
<string name="export_with_photos">con fotos</string>
<string name="export_without_photos">sin fotos</string>
<string name="export_track_already_exists_msg">La pista %1$s ya existe en el directorio de destino.</string>
<string name="export_all_do_it_for_all">hacerlo para todos los conflictos</string>
<string name="export_track_errors">Recorridos no exportados</string>
<string name="export_progress_summary_overwrite_msg">Recorridos sobreescritos</string>
<string name="export_progress_summary_skip_msg">Recorridos salteados</string>
<string name="export_progress_summary_errors_msg">Errores</string>
<string name="generic_open_track">Abrir recorrido</string>
<string name="generic_overwrite">Sobreescribir</string>
<string name="generic_skip">Saltear</string>
<string name="generic_separator_done_total">/</string>
<string name="generic_show_errors">Mostrar errores</string>
<string name="generic_completed">Completado</string>
<string name="generic_click_twice_cancel">Haga nuevamente clic en atrás para cancelar</string>
<string name="gps_disabled_msg">GPS deshabilitado</string>
<string name="gps_fixed_and_ready">GPS establecido y listo</string>
<string name="import_unsupported_format">Formato de archivo no soportado</string>
<string name="import_parser_error">Error de parseo: %1$s</string>
<string name="import_unable_to_import_file">No se puede importar el archivo: %1$s</string>
<string name="import_error_info">%1$s: %2$s</string>
<string name="import_error_list_dialog_title">Archivos no importados</string>
<string name="import_file_imported">Archivo %1$s importado</string>
<string name="import_prevent_reimport">Ha configurado evitar la reimportación del recorrido</string>
<string name="import_progress_summary_ok_msg">Archivos importados</string>
<string name="import_progress_summary_exists_msg">Ya existe</string>
<string name="import_progress_summary_errors_msg">Errores</string>
<string name="marker_add_photo_canceled">Cancelado, no se agregó la foto como marcador</string>
<string name="stats_loss">Pérdida</string>
<string name="stats_split_interval">Intervalo partido</string>
<string name="track_recording_notification_accuracy">Precisión de ubicación: %1$s</string>
<string name="track_pace_notification">Ritmo: %1$s</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="track_detail_intervals_tab">Intervalos</string>
<string name="track_detail_resume_success">Ahora reanudando el recorrido</string>
<string name="interval_list_empty_message">Aún no hay ningún intervalo</string>
</resources>

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