Compare commits

...

143 Commits

Author SHA1 Message Date
Dennis Guse e5d1e421a4 Release: v4.0.8 2022-09-03 10:21:38 +02:00
Dennis Guse c3b30815c8 SplashScreen: use Androidx library.
Improvement for Android 12+.
Fixes #1365.
2022-08-28 17:01:10 +02:00
Dennis Guse 5a4919efd2 PublicAPI: support setting the icon.
Fixes #1282.
2022-08-28 15:17:42 +02:00
dependabot[bot] bb549fdc93 Bump desugar_jdk_libs from 1.2.0 to 1.2.2
Bumps [desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 1.2.0 to 1.2.2.
- [Release notes](https://github.com/google/desugar_jdk_libs/releases)
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-25 21:58:51 +02:00
Dennis Guse 73e59cbba2 UI: show timezone if different than local.
Fixes #1296.
2022-08-22 18:43:53 +02:00
Dennis Guse 27727b6dcf Merge pull request #1310 from OpenTracksApp/time
Refactoring things regarding Duration, Speed, and Distance
2022-08-21 23:25:44 +02:00
Dennis Guse 06d2957407 Desugar: upgrade to AGP 7.3.0-rc01
Required for desugar 1.2.0)
2022-08-21 23:25:06 +02:00
Dennis Guse 8b08af6bcf Upgrade to Gradlew 7.5.1 2022-08-21 22:02:49 +02:00
Dennis Guse 78dad95ef6 Altitude: show always in m/ft.
Fixes #1319.
2022-08-21 22:02:49 +02:00
Dennis Guse be9d56721e Preferences: nicer formattings for distances. 2022-08-21 22:02:49 +02:00
Dennis Guse e5785dd3ac Distance now does all conversions itself. 2022-08-21 22:02:49 +02:00
Dennis Guse 11d912d14d Speed: use Distance for conversions. 2022-08-21 22:02:49 +02:00
Dennis Guse 807ac3fc9f Remove UnitConversions for time/durations. 2022-08-21 22:02:49 +02:00
Dennis Guse 45874d3749 Release: v4.0.7 2022-08-21 21:55:19 +02:00
Peter Storch 594f69320a Fix dependencies to lifecycle-viewmodel 2022-08-21 19:28:07 +02:00
dependabot[bot] d4927c9444 Bump appcompat from 1.4.2 to 1.5.0
Bumps appcompat from 1.4.2 to 1.5.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-21 19:28:07 +02:00
Alexander Ivanov 00dbd056d6 Translated using Weblate (Russian)
Currently translated at 99.0% (496 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2022-08-21 18:53:26 +02:00
M K 8f19ad8f66 Translated using Weblate (Arabic)
Currently translated at 53.6% (269 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ar/
2022-08-21 18:53:26 +02:00
mondstern 2fdad54a7c Translated using Weblate (German)
Currently translated at 94.8% (475 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2022-08-21 18:53:26 +02:00
Peter Storch 5a5f040ed5 Fix #1340 mass track deletion time 2022-08-21 18:53:20 +02:00
Dennis Guse 0ada9a2463 Android13: request NOTIFICATION_POST permission.
Fixes #1353.
Part of #1210.
2022-08-18 08:07:37 +02:00
Dennis Guse 719bb981ec Bugfix: Public API stop does not start service in foreground.
Fixes #1354.
2022-08-17 18:43:27 +02:00
dependabot[bot] 3c95d8744d Bump mockito-android from 4.6.1 to 4.7.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.6.1 to 4.7.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.6.1...v4.7.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>
2022-08-15 07:59:40 +02:00
Dennis Guse 63d1c9ee5e GPS: set signal lost threshold to 30s. 2022-08-14 20:18:37 +02:00
Dennis Guse 1ccd2e3c0a Release: v4.0.6 2022-08-07 19:47:54 +02:00
Dennis Guse cdbddabc1b Cleanup: use ContextCompat.getColor() 2022-08-04 22:11:44 +02:00
Dennis Guse 9f6ca83f27 Bug: PublicAPI needs to call startForegroundService (SDK API26+).
Fixes #1301.
2022-08-04 22:11:44 +02:00
dependabot[bot] f2ee99a477 Bump gradle from 7.2.1 to 7.2.2
Bumps gradle from 7.2.1 to 7.2.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-04 06:44:02 +02:00
Dennis Guse 7009ede095 EGM2008 offset lookup for negative longitudes was wrong. 2022-08-03 17:30:31 +02:00
Dennis Guse 2b530a1c53 Cleanup: unused imports. 2022-08-02 18:44:15 +02:00
Poussinou 33673f5921 Update FUNDING.yml 2022-07-31 14:59:56 +02:00
Dennis Guse a7398d43e3 Release: v4.0.5 2022-07-29 17:58:32 +02:00
Dennis Guse 59b016a4a3 PlayStore: use an inverted icon (app launcher only).
Part of #1329.
2022-07-29 17:56:28 +02:00
Dennis Guse 78aa5d51c6 Intervals: if gain/loss is not available show "-" instead of null.
Fixes #1338.
2022-07-26 22:30:08 +02:00
Quang Trung 95f584b554 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (501 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2022-07-21 15:04:36 +02:00
Dennis Guse cf759d526d README_TESTED_SENSORS.md: we now store sensor data without movement.
Part of #1088.
2022-07-18 07:35:46 +02:00
Dennis Guse 4c44ea12a4 Release: v4.0.4 2022-07-15 21:41:46 +02:00
dependabot[bot] 9d09865907 Bump reactivecircus/android-emulator-runner from 2.24.0 to 2.25.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.24.0 to 2.25.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.24.0...v2.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-13 07:39:30 +02:00
Dennis Guse 5667d2d83e Store sensor data without distance.
Fixes #1088.
2022-07-11 22:56:11 +02:00
Dennis Guse 386f017fb7 VoiceAnnouncement: avoid using deprecated API. 2022-07-10 21:46:15 +02:00
Román Ginés 87594e822d Bugfixed: adding a mark with a photo after rotating in camera resulted in a crash. Fixes #1321. 2022-07-10 19:47:45 +02:00
Dennis Guse ee832c7bd2 Revert upgrade ot AGP 7.3.0-beta. 2022-07-10 17:59:32 +02:00
Dennis Guse 30042d5bc3 Merge branch 'translations' 2022-07-09 19:49:28 +02:00
Hiajen 73cbb7a29c Translated using Weblate (German)
Currently translated at 75.5% (68 of 90 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/de/
2022-07-09 19:48:52 +02:00
Hiajen c100fdde4f Translated using Weblate (German)
Currently translated at 94.8% (475 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2022-07-09 19:48:52 +02:00
Dennis Guse 04e1496084 Cleanup: adjusted Interval uses constructor.
Fixes #1320.
2022-07-09 15:52:57 +02:00
Dennis Guse 21d1852505 Test: import+export results in same timezone.
Part of #1296.
2022-07-08 00:16:48 +02:00
Dennis Guse 1502b0cfa7 Gain/loss: handle events in our TrackRecordingService.handler (avoids potential race conditions). 2022-07-07 18:27:56 +02:00
Dennis Guse e7d6f65508 Gain/loss: use 5s as sensor interval to save some energy. 2022-07-07 18:27:56 +02:00
Dennis Guse 0e69a444a5 Add a comma. 2022-07-07 18:26:27 +02:00
Román Ginés 3b62999fde Announcement computes intervals from last track point. Fixes #645.
Bugfixed: intervals distance wasn't computed correctly.
2022-07-06 22:54:45 +02:00
Dennis Guse 97431233ac Fix translations. 2022-07-05 21:01:09 +02:00
Uglješa d49675afc8 Translated using Weblate (Serbian)
Currently translated at 56.6% (284 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sr/
2022-07-03 18:29:42 +02:00
vsevjednom-cz c1f7ab36a8 Translated using Weblate (Czech)
Currently translated at 94.2% (472 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2022-07-03 18:29:42 +02:00
FTno eb14cc7bc6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (500 of 500 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-07-03 18:29:42 +02:00
Xosé M a47b9164af Translated using Weblate (Galician)
Currently translated at 100.0% (500 of 500 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2022-07-03 18:29:42 +02:00
Surfoo 502e3ecb21 Translated using Weblate (French)
Currently translated at 100.0% (500 of 500 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-07-03 18:29:42 +02:00
metezd 71903f174a Translated using Weblate (Turkish)
Currently translated at 98.0% (490 of 500 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-07-03 18:29:41 +02:00
Dennis Guse ab6606a295 Github actions: rename APK build job. 2022-07-03 18:29:38 +02:00
Dennis Guse c4e2023992 Public API: allow setting track properties (name, category, description) on StartRecording.
Fixes #1282.
2022-07-03 17:32:21 +02:00
Dennis Guse cb10bdda3f Settings: show package id in Public API settings. 2022-07-03 10:28:43 +02:00
Dennis Guse f913472aae Settings: idle speed threshold has now a summary. 2022-07-03 10:28:31 +02:00
Dennis Guse af4826c578 TrackStoppedActivity: update track data on resume track.
Fixes #1249.
2022-07-02 08:51:31 +02:00
Dennis Guse 63172ec66f Github Actions: downgrade triggers. 2022-07-01 20:40:38 +02:00
Dennis Guse 3d6c339509 Android13: use new Image picker.
Part of #1210.
2022-07-01 20:24:20 +02:00
Dennis Guse 49f86c4eb9 Android13: upgrade target sdk to 33.
Part of #1210.
2022-07-01 20:24:20 +02:00
Dennis Guse 7664bd248d Release: v4.0.3 2022-07-01 13:27:50 +02:00
Dennis Guse 73615aa0d6 Statistics: remove speed filtering (fix tests).
Part of #1003.
2022-07-01 13:24:18 +02:00
Dennis Guse 81bb6699d8 Statistics: remove speed filtering.
Part of #1003.
2022-06-30 14:03:33 +02:00
dependabot[bot] 93c67e3728 Bump desugar_jdk_libs from 1.1.5 to 1.1.6
Bumps [desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 1.1.5 to 1.1.6.
- [Release notes](https://github.com/google/desugar_jdk_libs/releases)
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-29 08:45:59 +02:00
Dennis Guse 993816a34a Github issue template for data not recorded.
Fixes #1295.
2022-06-28 16:18:41 +02:00
Šimon Hořánek 3c0999c3a0 Translated using Weblate (Czech)
Currently translated at 94.3% (471 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2022-06-27 23:03:27 +02:00
metezd 4528e7148f Translated using Weblate (Turkish)
Currently translated at 98.1% (490 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-06-27 23:03:27 +02:00
FTno 74042f5d85 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (499 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-06-27 23:03:27 +02:00
Dennis Guse 92f6fbe04a Prevent crash if no dashboard is installed. 2022-06-27 23:03:23 +02:00
Dennis Guse 5fa50250de Cleanup tests for StringUtils.parseTime().
Part of #1295.
2022-06-27 19:30:23 +02:00
Dennis Guse 8ee9daf7af Github issue template for data not recorded.
Fixes #1295.
2022-06-27 12:58:17 +02:00
Dennis Guse a54c8c1d25 Use Set.of(). 2022-06-22 20:31:41 +02:00
Dennis Guse 8137ec8de2 GPX exporter: only export trkseq if it actually contains data.
Fixes #1291.
2022-06-21 19:56:19 +02:00
Dennis Guse ae2a97576b Release: v4.0.2 2022-06-20 22:06:01 +02:00
Šimon Hořánek 3e830a086e Replace Start with Stop 2022-06-19 21:03:56 +02:00
Surfoo 618a1df3ea Translated using Weblate (French)
Currently translated at 82.9% (73 of 88 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/fr/
2022-06-19 17:34:42 +02:00
Surfoo 90cfc67c59 Translated using Weblate (French)
Currently translated at 100.0% (487 of 487 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-06-19 17:34:42 +02:00
Dennis Guse cecc586868 Bugfix: export was not checking properly if file exists.
Fixes #1283.
2022-06-19 17:24:18 +02:00
Dennis Guse f956e7fab3 Duplicate variable name. 2022-06-19 17:21:56 +02:00
William Pettersson 128b95cd83 Fix voice announcement rounding problem
We extracted integral parts before rounding, but String.format does
rounding which caused occasional issues. To alleviate, we need to round
before passing to String.format. All this rounding logic has now been
moved into appendDecimalUnit, and tests have been added to check that
rounding works correctly.

Fixes: #1285
2022-06-19 16:39:46 +02:00
Román Ginés 895a5c2b2a Bugfixed: DistanceFormatter was using the wrong unitSystem. Fixes #1288. 2022-06-18 23:18:04 +02:00
Dennis Guse 91ce5cf8d7 Add nautical miles (with feet for altitude).
Tests were broken.

Fixes #1280.
2022-06-18 10:01:09 +02:00
Dennis Guse a283464331 Add nautical miles (with feet for altitude).
Fixes #1280.
2022-06-17 19:57:40 +02:00
Dennis Guse 1c3a2bd2e4 Removed altitude smoothing completely: now it will just be the plain GPS-reported value.
Was only used for statistics in exports.

Follow-up to #1241.
2022-06-11 14:41:56 +02:00
Dennis Guse 0b326a77c2 Bugfix: Elevation in charts smoothing removed as it was broken.
Fixes #1241.
2022-06-11 14:24:41 +02:00
Dennis Guse d68008941e Release: v4.0.1 2022-06-09 22:55:36 +02:00
Dennis Guse b822d05a78 Fix voice announcement tests. 2022-06-09 22:33:43 +02:00
Dennis Guse 74bfed768c Translations: removed obsolete ones. 2022-06-09 21:01:08 +02:00
arjan-s ebfcce6d8d Translated using Weblate (Dutch)
Currently translated at 100.0% (487 of 487 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2022-06-09 16:04:29 +02:00
Hiajen d97d8f85e4 Translated using Weblate (German)
Currently translated at 96.9% (472 of 487 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2022-06-09 16:04:28 +02:00
FTno 37c2c5029e Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (482 of 482 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-06-07 09:55:18 +02:00
mondstern 105ff89fd8 Translated using Weblate (Romanian)
Currently translated at 84.4% (407 of 482 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ro/
2022-06-07 09:55:18 +02:00
mondstern 0ac033106c Translated using Weblate (Hungarian)
Currently translated at 91.7% (442 of 482 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2022-06-07 09:55:18 +02:00
mondstern 21ec9fc568 Translated using Weblate (Finnish)
Currently translated at 96.6% (466 of 482 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2022-06-07 09:55:18 +02:00
mondstern 2e3a7a31d4 Translated using Weblate (Hungarian)
Currently translated at 82.5% (398 of 482 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2022-06-07 09:55:17 +02:00
Román Ginés ecd7405a8c TrackStoppedActivity: added discard button option. Fixes #1267. 2022-06-07 09:55:09 +02:00
Dennis Guse 28abe360cb Merge pull request #1276 from OpenTracksApp/voiceConfig#1275
Added settings for what to announce
2022-06-06 17:04:15 +02:00
Dennis Guse 9cf88c00b0 Voice announcements: time is moving time (settings). 2022-06-06 17:02:31 +02:00
Dennis Guse 8bbde4aacd Voice announcements: fix translations 2022-06-06 17:02:31 +02:00
Jonas Kalderstam 00351136f2 Voice announcements: added settings for what to announce 2022-06-06 17:02:31 +02:00
dependabot[bot] 30b85bd215 Bump mockito-android from 4.6.0 to 4.6.1
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.6.0...v4.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-04 09:40:55 +02:00
Dennis Guse 14b705b43e README: add large translation status; removed Amazon App Store link. 2022-06-04 09:22:51 +02:00
Dennis Guse 31fc0adabc Fullscreen: now more fullscreen. 2022-06-03 17:26:33 +02:00
Dennis Guse 1714da5d3b Translations: fix quantities (fr). 2022-06-02 22:12:00 +02:00
mondstern 6ae47b5e01 Translated using Weblate (Slovenian)
Currently translated at 93.3% (450 of 482 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sl/
2022-06-02 22:06:14 +02:00
metezd df8e4c2e2e Translated using Weblate (Turkish)
Currently translated at 99.1% (478 of 482 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-06-02 20:00:22 +02:00
Surfoo 6f9fa3719a Translated using Weblate (French)
Currently translated at 100.0% (482 of 482 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-06-02 20:00:21 +02:00
vsevjednom-cz b9014c7219 Translated using Weblate (Czech)
Currently translated at 98.7% (483 of 489 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2022-06-02 20:00:21 +02:00
FTno d44ab44620 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-06-02 20:00:21 +02:00
dependabot[bot] bcffe05744 Bump appcompat from 1.4.1 to 1.4.2
Bumps appcompat from 1.4.1 to 1.4.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-02 20:00:06 +02:00
Dennis Guse ed60734868 Donations: we do not accept Bitcoin anymore (too much effort). 2022-06-01 20:17:02 +02:00
dependabot[bot] e314ff8f43 Bump material from 1.6.0 to 1.6.1
Bumps [material](https://github.com/material-components/material-components-android) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.6.0...1.6.1)

---
updated-dependencies:
- dependency-name: com.google.android.material:material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-31 22:48:36 +02:00
Peter Storch 1108684d06 Add "show on map" format selection dialog 2022-05-30 07:51:00 +02:00
dependabot[bot] 6c0033c107 Bump mockito-android from 4.5.1 to 4.6.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.5.1 to 4.6.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.5.1...v4.6.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>
2022-05-30 07:35:00 +02:00
Dennis Guse 8a3e50ef9b BLE sensors: faster connection. 2022-05-29 22:50:55 +02:00
Román Ginés 05b572253e Bugfixes: buttons colors were handcoded. Fixes #1260. 2022-05-29 20:25:18 +02:00
Dennis Guse f755a086cd Cleanup: removed unused translations. 2022-05-29 20:22:50 +02:00
Dennis Guse b0605323da Track list: RTL fix. 2022-05-29 18:15:41 +02:00
Dennis Guse 585c2234eb Bugfix: request permissions in TrackListActivity.
Introduced in 9d64989ef1
2022-05-29 18:06:00 +02:00
Román Ginés c5d7ea4245 Bugfixes: TrackStoppedActivity on smaller devices. Fixes #1257. 2022-05-29 18:02:57 +02:00
Dennis Guse 281cd8a861 Merge pull request #1248 from OpenTracksApp/reset#1247
Allow settings reset and import/export while recording
2022-05-28 13:48:56 +02:00
Dennis Guse 36400c8ad0 Fix EspressoUITest for API24. 2022-05-26 23:39:35 +02:00
Dennis Guse fb00f697c5 Fix EspressoDeleteTrackTest.
Fixes #817.
2022-05-26 23:39:35 +02:00
Dennis Guse f529726218 KML: icons (start, stop, markers) use default icon.
Until now we used external URLs from Google to fetch proper icons.

Fixes #126.
2022-05-26 10:23:27 +02:00
Dennis Guse 059f3d8f43 Cleanup: align test classes name to tested class. 2022-05-26 10:23:27 +02:00
dependabot[bot] cae716925f Bump gradle from 7.2.0 to 7.2.1
Bumps gradle from 7.2.0 to 7.2.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-26 08:22:07 +02:00
Dennis Guse d29ee6da3d Settings: allow import/export settings while recording.
Fixes #1247.
2022-05-25 22:03:16 +02:00
Dennis Guse 033b5211ea Settings: allow reset settings while recording.
Fixes #1247.
2022-05-25 22:01:51 +02:00
Dennis Guse 5f4f5baba4 Remove unused settings (formerly used in settings). 2022-05-25 07:38:12 +02:00
Dennis Guse 4421046556 Remove unused settings (incl. deprecated ones). 2022-05-25 07:38:12 +02:00
Dennis Guse 30482d5a34 Record/stop buttons: add contentDescription. 2022-05-25 07:38:12 +02:00
Dennis Guse f12094c953 Remove unused translations (were used for voice announcements). 2022-05-25 07:38:12 +02:00
Dennis Guse c5147e7664 UI tests: use longClick() instead of veryLongClick(). 2022-05-25 07:38:12 +02:00
Dennis Guse 03b66bc6a2 Remove unused xml configuration. 2022-05-25 07:38:12 +02:00
Dennis Guse 7b40f2c949 Remove unused buttons.
Replaced in da62d26409
2022-05-25 07:38:12 +02:00
Dennis Guse 785a922d03 Remove empty translations. 2022-05-25 07:38:12 +02:00
Dennis Guse 5d2414977b TrackRecordingService: remove pause functionality.
Fixes #1233.
2022-05-23 07:41:50 +02:00
198 changed files with 2719 additions and 3451 deletions
@@ -0,0 +1,24 @@
---
name: Data not recorded
about: Somehow OpenTracks did not record GPS data
title: ''
labels: potential bug
assignees: ''
---
**Please describe the problem and how to reproduce it:**
...
**Checklist**
- [ ] Consulted [https://dontkillmyapp.com/]
- [ ] Power management: battery optimization is disabled for OpenTracks
**Technical information**
- Device: [e.g. Nexus 7 (2013)]
- OS: [e.g. LineageOS 15.1]
- OpenTracks version: [e.g. v3.2.4]
- OpenTracks commit id (only for nightly builds): [e.g. 4b333bb]
@@ -1,9 +1,10 @@
#https://github.com/marketplace/actions/android-emulator-runner
name: Build nightly
on: [ push ]
name: Build APK
on:
push
jobs:
nightly-build:
apk-build:
runs-on: ubuntu-latest
steps:
- name: checkout
@@ -14,7 +15,7 @@ jobs:
distribution: 'adopt'
java-version: '11'
- name: Assemble nighlty APK
- name: Assemble APK
shell: bash
run: |
echo ${NIGHTLY_STORE_FILE} | base64 -d > KEY_NIGHTLY.jks
@@ -28,7 +29,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v3
with:
name: OpenTracks-nightly.apk
name: OpenTracks-APK.apk
path: ./build/outputs/apk/nightly/*.apk
retention-days: 7
+1 -1
View File
@@ -2,7 +2,7 @@ name: Linting
on:
push:
pull_request_target:
pull_request:
jobs:
analyse:
+2 -2
View File
@@ -2,7 +2,7 @@
name: Test
on:
push:
pull_request_target:
pull_request:
jobs:
test:
@@ -23,7 +23,7 @@ jobs:
java-version: '11'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2.24.0
uses: reactivecircus/android-emulator-runner@v2.25.0
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
+29 -36
View File
@@ -9,7 +9,6 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<th>Free (F-Droid)</th>
<th>Free (Nightly for F-Droid)</th>
<th>Donations</th>
<th>Translations</th>
</tr>
<tr>
<td align="center">
@@ -27,47 +26,26 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
</a>
</td>
<td align="center">
<a href="https://weblate.bubu1.eu/engage/OpenTracks/">
<img src="https://weblate.bubu1.eu/widgets/OpenTracks/-/opentracks/svg-badge.svg" alt="Translation status" />
</a>
</td>
</tr>
<tr>
<td align="center">
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.svg" align="middle" >
</td>
<td></td>
<td align="center">
<a href="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc">
<img alt="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc" src="promotion/donations-bitcoin.png" height="90" align="middle">
</a>
</td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td align="center">
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="60" align="middle">
</a>
</td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td align="center">
<a href="https://www.amazon.de/dp/B09K2KNQ7X/">
<img alt="Get it on Amazon App Store" src="https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-appstore-badge-english-white.png" height="40" align="middle">
</a>
</td>
<td></td>
</tr>
</table>
<a href="https://weblate.bubu1.eu/engage/OpenTracks/">
<img src="https://weblate.bubu1.eu/widgets/OpenTracks/-/opentracks/horizontal-auto.svg" alt="Translation status" />
</a>
## Screenshots
<div>
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.png">
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.png">
@@ -117,7 +95,7 @@ OpenTracks can be used with [Gadgetbridge](https://www.gadgetbridge.org/):
__Only required permission:__
* _ACCESS_FINE_LOCATION_: required to use the GPS.
### API
### Public API
OpenTracks includes an API for starting/stopping recording by another installed application (e.g., [Automate](https://llamalab.com/automate/), [Tasker](https://tasker.joaoapps.com), or [Easer](https://github.com/renyuneyun/Easer)).
The API can be invoked by sending an explicit Intent to start an activity.
@@ -129,20 +107,35 @@ The API can be invoked by sending an explicit Intent to start an activity.
* Nightly: `de.dennisguse.opentracks.nightly`
`Class`:
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording`
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
* **Stop a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
For testing via adb: `adb shell am start -n "package/class"`
For testing via adb: `adb shell am start -e someParameter someValue -n "package/class"`
The Start/Stop API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
`StartRecording` supports the following parameters:
## Custom Dashboards (incl. map)
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live tracks.
* Set track data: `TRACK_NAME`, `TRACK_DESCRIPTION`, `TRACK_CATEGORY`, and `TRACK_ICON` (non-localized identifier see [/src/main/java/de/dennisguse/opentracks/util/TrackIconUtils.java#L38](/src/main/java/de/dennisguse/opentracks/util/TrackIconUtils.java#L38)).
NOTE: if `TRACK_ICON` is not present, `TRACK_CATEGORY` will be used to determine the icon (localized).
* Send recorded data to another application via _Dashboard API_: `STATS_TARGET_PACKAGE` and `STATS_TARGET_CLASS`
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track statistics on supported wearables.
The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
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.
__IMPORTANT__: triggering `StartRecording` does not check if Android permissions (location or Bluetooth) were granted. If they are not granted, nothing will be recorded.
## Dashboard API (incl. map)
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live
tracks.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which
presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also
used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track
statistics on supported wearables.
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.
The developer documentation is in [README_API.md](README_API.md).
-4
View File
@@ -38,10 +38,6 @@ However, often only one value is provided.
We only support _Instantaneous Power_.
_WARNING_: we only support physically movements with valid GPS coordinates.
Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
Also the distance is not computed.
For cycling:
* Assioma Duo Powermeter
* Elite drivo II
+16 -10
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath 'com.android.tools.build:gradle:7.3.0-rc01'
}
}
@@ -51,7 +51,7 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
compileSdkVersion 32
compileSdk 33
buildFeatures {
viewBinding true
@@ -59,13 +59,13 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4874
versionName "v4.0.0"
versionCode 5017
versionName "v4.0.8"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 32
minSdk 21
targetSdk 33
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
@@ -81,6 +81,8 @@ android {
}
}
namespace 'de.dennisguse.opentracks'
buildTypes {
debug {
applicationIdSuffix ".debug"
@@ -100,6 +102,7 @@ android {
releasePlayStore {
applicationIdSuffix ".playstore"
versionNameSuffix "-PlayStore"
signingConfig signingConfigs.debug
}
}
@@ -117,20 +120,23 @@ android {
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.6.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-splashscreen:1.0.0'
androidTestImplementation 'androidx.test:core:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'org.mockito:mockito-android:4.5.1'
androidTestImplementation 'org.mockito:mockito-android:4.7.0'
androidTestUtil 'androidx.test:orchestrator:1.4.1'
}
@@ -7,4 +7,4 @@ Funktionen & Features:
* zeichnet deine Herzfrequenz auf (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren sowie Leistungsmesser (Bluetooth LE)
* exportiert deine Daten als KML oder GPX
* funktioniert ganz ohne Internet
* funktioniert ganz ohne Internet
@@ -1 +1 @@
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
@@ -0,0 +1,13 @@
v4.0.1: OpenTracks
Changes:
- Voice announcements are now configurable
- Fullscreen mode now also hides Android's navigation bar
- Connection to BLE sensors should now be faster
- Allow to reset settings while recording
- Allow to change import/export settings while recording
- KML/KMZ: do not use icons to be loaded from Google
Bugfix:
- TrackStoppedActivity now works on smaller screens
- Requests permissions on track list
@@ -0,0 +1,10 @@
v4.0.2: OpenTracks
Changes:
- Add nautical miles
- Settings can be reset while recording
Bugfix:
- Voice announcement rounding
- Chart: elevation was weirdly smoothed
- Export was not checking if file exists
@@ -0,0 +1,8 @@
v4.0.3: OpenTracks
Changes:
- Max. speed as reported by sensor (GPS or BLE)
Bugfix:
- Prevent crash if OSM Dashboard is not installed
- GPX track export: do not export empty track segments
@@ -0,0 +1,13 @@
v4.0.4: OpenTracks
Changes:
- Store BLE sensor data even without distance (every 10s)
- Public API: allows setting name, category, and description
Bugfix:
- Crash when taking a picture and rotating the phone
-
Developer:
- Target SDK 33
- Barometer uses interval of 5s
@@ -0,0 +1,8 @@
v4.0.5: OpenTracks
Changes:
- PlayStore relase get's another logo
Bugfix:
- Interval list shows "-" if gain/loss is not present
@@ -0,0 +1,6 @@
v4.0.6: OpenTracks
Bugfix:
- EGM2008 offset computed wrong for negative longitudes
- PublicAPI crashed if OpenTracks was in background (API26+)
@@ -0,0 +1,11 @@
v4.0.7: OpenTracks
Changes:
- GPS signal lost timeout: 30s
- Android13: request permission to show notifications
- Faster deletion of tracks (vacuum database only if needed)
Bugfix:
- PublicAPI stop command resulted in crash (introduced in v4.0.6)
- Permissions: request all permissions during on startup
@@ -0,0 +1,11 @@
v4.0.8: OpenTracks
Changes:
- Altitude always shown in m/ft
- Show track timezone if different than device timezone
- PublicAPI: support setting the track icon explicitly
Developer:
- Introduced DistanceFormatter
- Upgrade to AGP 7.3/desugar for Duration.class
- Android12+: do splashscreen via Androidx
@@ -0,0 +1,11 @@
v3.20.2 : OpenTracks
Changements :
- Le gain et la perte d'altitude sont des entiers au lieu de floats
- Demander la permission avant d'utiliser Bluetooth par un message Toast
Correction d'un bug :
- Définir FLAG_IMMUTABLE pour PendingIntents pour éviter un crash sur Android 12
- Le capteur de cadence renvoie 0 au lieu de None lorsque l'utilisateur ne pédale pas.
- Le capteur de vitesse renvoie 0 au lieu de la dernière vitesse connue quand c'est arrêté
- Avec les capteurs, le temps de déplacement s'arrête lorsqu'il n'est pas en mouvement
@@ -7,5 +7,11 @@ Fonctionnalités :
* Dénivelé positif et négatif: via capteur de pression barométrique
* Synthèse vocale
* Prise en charge des capteurs Bluetooth LE : rythme cardiaque, vitesse et distance (vélo), cadence de pédalage (vélo), et capteur de puissance (vélo)
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
* Thème sombre ou claire, avec le respect des paramètres du système
* Pas de publicités
Logiciel Open Source
Vous pouvez utiliser, étudier, changer et partager le code source.
Licence Apache 2.0
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -3,48 +3,25 @@ package de.dennisguse.opentracks;
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.action.ViewActions.longClick;
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;
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
import static de.dennisguse.opentracks.util.EspressoUtils.veryLongTouch;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import static de.dennisguse.opentracks.util.EspressoUtils.withListSize;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.content.Context;
import android.view.View;
import android.widget.ListView;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.assertion.ViewAssertions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.services.TrackDeleteService;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoDeleteTrackTest {
@@ -55,149 +32,33 @@ public class EspressoDeleteTrackTest {
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private MyIdlingResource idlingResource;
@Before
public void registerIntentServiceIdlingResource() {
Instrumentation instrumentation
= InstrumentationRegistry.getInstrumentation();
idlingResource = new MyIdlingResource(instrumentation.getTargetContext());
IdlingRegistry.getInstance().register(idlingResource);
}
@After
public void unregisterIntentServiceIdlingResource() {
IdlingRegistry.getInstance().unregister(idlingResource);
}
@Ignore("Test fails permanently")
@Deprecated
@Test
public void espressoDeleteTrackTest() {
int countBefore;
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action)).perform(click());
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
trackControllerRecordButton.perform(click());
}
// TrackRecordingActivity
onView(withId(R.id.track_recording_fab_action))
// wait; stay recording
.perform(waitFor(5000))
// stop;
.perform(longClick());
{
// TrackRecordingActivity
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
// wait; stay recording
trackControllerStopButton.perform(waitFor(5000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
}
// back
pressBack();
// get number of items before deleting a track
countBefore = numberOfItemsListView();
// TrackStoppedActivity
onView(withId(R.id.finish_button)).perform(click());
// select track
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(5000));
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(longClick());
// open menu and delete selected track
ViewInteraction overflowMenuButton = onView(
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(R.id.bottom_app_bar),
2),
2),
isDisplayed()));
overflowMenuButton.perform(click());
onView(allOf(withContentDescription("More options"), isDisplayed()))
.perform(click());
ViewInteraction appCompatTextView = onView(
allOf(withId(R.id.title), withText("Delete"),
childAtPosition(
childAtPosition(
withId(R.id.content),
0),
0),
isDisplayed()));
appCompatTextView.perform(click());
onView(withText("Delete")).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());
onView(withText("OK")).perform(click());
// check number of items after delete
onView(withId(R.id.track_list)).check(ViewAssertions.matches(withListSize(countBefore - 1)));
}
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 static class MyIdlingResource implements IdlingResource {
private final 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;
}
// tracklist is empty now
onView(allOf(withText("Start recording your next adventure here"), isDisplayed()));
}
}
@@ -5,25 +5,20 @@ import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.view.MotionEvent;
import android.view.View;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -53,7 +48,7 @@ public class EspressoUITest {
.perform(longClick());
// TrackStoppedActivity: resume
onView(withId(R.id.resume_button))
onView(allOf(withId(R.id.resume_button), isClickable()))
.perform(click());
// TrackRecordingActivity: wait and then stop
@@ -93,7 +88,7 @@ public class EspressoUITest {
tabLayout.perform(waitFor(1000));
// stop
trackControllerStopButton.perform(veryLongTouch(1600));
trackControllerStopButton.perform(longClick());
}
}
@@ -101,36 +96,6 @@ public class EspressoUITest {
@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));
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(longClick());
}
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);
}
};
}
}
@@ -27,7 +27,7 @@ public class ChartPointTest {
statistics.setTotalTime(Duration.ofSeconds(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, UnitSystem.IMPERIAL);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), false, UnitSystem.IMPERIAL);
// then
assertEquals(1000000, (long) point.getTimeOrDistance());
@@ -40,7 +40,7 @@ public class ChartPointTest {
statistics.setTotalDistance(Distance.of(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, UnitSystem.METRIC);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), true, UnitSystem.METRIC);
// then
assertEquals(1, (long) point.getTimeOrDistance());
@@ -50,9 +50,11 @@ public class ChartPointTest {
public void create_get_altitude_speed_and_pace() {
// given
TrackStatistics statistics = new TrackStatistics();
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
.setAltitude(Altitude.EGM2008.of(50));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertEquals(50, point.getAltitude(), 0.01);
@@ -64,9 +66,10 @@ public class ChartPointTest {
public void create_sensorNotAvailable() {
// given
TrackStatistics statistics = new TrackStatistics();
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
.setAltitude(Altitude.EGM2008.of(50));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertNull(point.getHeartRate());
@@ -77,15 +80,16 @@ public class ChartPointTest {
@Test
public void create_sensorAvailable() {
// given
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setHeartRate(100f);
trackPoint.setCadence(101f);
trackPoint.setPower(102f);
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
.setAltitude(Altitude.EGM2008.of(50))
.setHeartRate(100f)
.setCadence(101f)
.setPower(102f);
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
// then
assertEquals(100.0, point.getHeartRate(), 0.01);
@@ -15,6 +15,10 @@
*/
package de.dennisguse.opentracks.chart;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -25,10 +29,6 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests {@link ChartValueSeries}.
*
@@ -47,6 +47,7 @@ public class ChartValueSeriesTest {
new int[]{1, 100, 1000},
R.string.description_altitude_metric,
R.string.description_altitude_imperial,
R.string.description_altitude_imperial,
R.color.chart_altitude_fill,
R.color.chart_altitude_border,
15,
@@ -7,7 +7,6 @@ import android.util.Pair;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
import de.dennisguse.opentracks.data.models.Track;
@@ -24,10 +23,7 @@ public class ShareContentProviderTest {
@Test
public void testCreateAndParseURI_valid() {
Set<Track.Id> trackIds = new HashSet<>();
trackIds.add(new Track.Id(1));
trackIds.add(new Track.Id(3));
trackIds.add(new Track.Id(5));
Set<Track.Id> trackIds = Set.of(new Track.Id(1), new Track.Id(3), new Track.Id(5));
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "filename.suffix", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
@@ -36,9 +32,7 @@ public class ShareContentProviderTest {
@Test
public void testCreateURIescapeFilename() {
Set<Track.Id> trackIds = new HashSet<>();
trackIds.add(new Track.Id(1));
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "../../&1=1", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(Set.of(new Track.Id(1)), "../../&1=1", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/KML_WITH_TRACKDETAIL_AND_SENSORDATA/1/..%2F..%2F%261%3D1"), shareURIandMIME.first);
}
@@ -7,6 +7,7 @@ import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.Track;
@@ -105,7 +106,7 @@ public class TrackSelectionTest extends TestCase {
long oneDay = 24 * 60 * 60 * 1000;
TrackSelection filterWrong1 = new TrackSelection().addDateRange(instant, null);
TrackSelection filterWrong2 = new TrackSelection().addDateRange(null, instant);
TrackSelection filterOk = new TrackSelection().addDateRange(instant, instant.plusMillis(oneDay));
TrackSelection filterOk = new TrackSelection().addDateRange(instant, instant.plus(Duration.ofDays(1)));
// when
SelectionData selectionWrong1 = filterWrong1.buildSelection();
@@ -12,6 +12,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.settings.UnitSystem;
//TODO Parametrized tests
@RunWith(AndroidJUnit4.class)
public class DistanceFormatterTest {
@@ -42,4 +43,17 @@ public class DistanceFormatterTest {
// A small number in imperial
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
}
@Test
public void testFormatDistance_nautical() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.build(context);
// A large number in nautical
assertEquals("2.70 NM", formatter.formatDistance(Distance.of(5000)));
// A small number in nautical
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
}
}
@@ -12,6 +12,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.settings.UnitSystem;
//TODO Use make parametrized tests?
@RunWith(AndroidJUnit4.class)
public class SpeedFormatterTest {
@@ -39,12 +40,22 @@ public class SpeedFormatterTest {
.setReportSpeedOrPace(false)
.build(context);
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.34)).first);
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.33)).first);
assertEquals("8:02", formatter.getSpeedParts(Speed.of(3.34)).first);
assertEquals("min/mi", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_pace_nautical() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.setReportSpeedOrPace(false)
.build(context);
assertEquals("9:14", formatter.getSpeedParts(Speed.of(3.34)).first);
assertEquals("min/NM", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_speed_metric() {
SpeedFormatter formatter = SpeedFormatter.Builder()
@@ -69,6 +80,18 @@ public class SpeedFormatterTest {
assertEquals("mph", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testGetSpeedParts_speed_nautical() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
.setReportSpeedOrPace(true)
.build(context);
assertEquals("6.5", formatter.getSpeedParts(Speed.of(3.34)).first);
assertEquals("kn", formatter.getSpeedParts(Speed.zero()).second);
}
@Test
public void testFormatSpeed() {
SpeedFormatter formatter = SpeedFormatter.Builder()
@@ -155,26 +155,31 @@ public class ExportImportTest {
mockAltitudeChange(trackPointCreator, 1);
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:05Z");
trackPointCreator.setClock("2020-02-02T02:02:14Z");
mockBLESensorData(trackPointCreator, 15f, null, 66f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:15Z");
mockBLESensorData(trackPointCreator, null, null, 66f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:16Z");
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:06Z");
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
service.pauseCurrentTrack();
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:02:20Z");
service.resumeCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
service.resumeTrack(trackId);
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 27, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:02:24Z");
trackPointCreator.setClock("2020-02-02T02:03:24Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
service.endCurrentTrack();
@@ -187,7 +192,7 @@ public class ExportImportTest {
track = contentProviderUtils.getTrack(trackId);
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
markers = contentProviderUtils.getMarkers(trackId);
assertEquals(10, trackPoints.size());
assertEquals(12, trackPoints.size());
assertEquals(2, markers.size());
}
@@ -231,29 +236,29 @@ public class ExportImportTest {
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:24Z"), importedTrackStatistics.getStopTime());
assertEquals(Instant.parse("2020-02-02T02:03:24Z"), importedTrackStatistics.getStopTime());
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(7), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(19), importedTrackStatistics.getTotalTime());
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(17.59, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(6.58, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
// Altitude
assertEquals(originalTrackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
@@ -345,19 +350,19 @@ public class ExportImportTest {
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:17Z"))
.setLatitude(3)
.setLongitude(14.001)
.setAltitude(10)
.setSpeed(Speed.of(5))
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setSensorDistance(Distance.of(12))
.setSensorDistance(Distance.of(14))
.setHeartRate(66f)
.setPower(50f)
.setCadence(3f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z"))
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:21Z"))
.setLatitude(3)
.setLongitude(14.002)
.setAltitude(10)
@@ -365,7 +370,7 @@ public class ExportImportTest {
.setAltitudeGain(0f)
.setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:22Z"))
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:22Z"))
.setLatitude(3)
.setLongitude(16)
.setAltitude(10)
@@ -373,7 +378,7 @@ public class ExportImportTest {
.setAltitudeGain(0f)
.setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:23Z"))
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:23Z"))
.setLatitude(3)
.setLongitude(16.001)
.setAltitude(10)
@@ -390,18 +395,18 @@ public class ExportImportTest {
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Instant.parse("2020-02-02T02:03:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(41.05, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(41.05, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(8.34, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(8.34, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(8.34, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
@@ -458,7 +463,7 @@ public class ExportImportTest {
contentProviderUtils.deleteTrack(context, trackId);
// then
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.csv_export);
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.csv_export);
String expectedText = new BufferedReader(new InputStreamReader(expected, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
@@ -15,11 +15,16 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -27,13 +32,15 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Test that legacy KML/GPX formats can still be imported.
*/
@RunWith(JUnit4.class)
public class GPXImportTest {
public class GPXTrackImporterTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -62,7 +69,7 @@ public class GPXImportTest {
public void gpx_with_pause_resume() throws IOException {
// given
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);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.legacy_gpx_pause_resume);
// when
// 1. import
@@ -116,7 +123,7 @@ public class GPXImportTest {
public void gpx_without_speed() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx11_without_speed);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_without_speed);
// when
// 1. import
@@ -164,7 +171,7 @@ public class GPXImportTest {
public void gpx_speed_no_namespace() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx11_with_speed_no_namespace);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_with_speed_no_namespace);
// when
// 1. import
@@ -181,7 +188,7 @@ public class GPXImportTest {
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
assertEquals(4, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(4.0, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
// 4. trackpoints
@@ -208,4 +215,31 @@ public class GPXImportTest {
.setSpeed(Speed.of(3))
), importedTrackPoints);
}
@LargeTest
@Test
public void importExportTest_timezone() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
trackExporter.writeTrack(importedTrack, outputStream);
// then
String expected = new BufferedReader(
new InputStreamReader(inputStreamExpected, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n")) + "\n";
assertEquals(expected, outputStream.toString()); //TODO inputStream.readAllBytes() ?
}
}
@@ -31,7 +31,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
* Test that legacy KML/GPX formats can still be imported.
*/
@RunWith(JUnit4.class)
public class KMLImportTest {
public class KMLTrackImporterTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -60,7 +60,7 @@ public class KMLImportTest {
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);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_order_location_and_when);
// when
// 1. import
@@ -109,7 +109,7 @@ public class KMLImportTest {
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);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_time_decreases);
// when
// 1. import
@@ -149,7 +149,7 @@ public class KMLImportTest {
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);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.legacy_kml22_statistics_marker);
// when
// 1. import
@@ -223,7 +223,7 @@ public class KMLImportTest {
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);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_without_locations);
// when
importTrackId = importer.importFile(inputStream).get(0);
@@ -234,7 +234,7 @@ public class KMLImportTest {
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);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_when_locations_different);
// when
importTrackId = importer.importFile(inputStream).get(0);
@@ -31,12 +31,14 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -159,7 +161,7 @@ public class TrackRecordingServiceTestRecording {
// when
String pauseTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(pauseTime);
service.pauseCurrentTrack();
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, pauseTime, 0, 1, 0, 0, 0f, 0f)
@@ -205,7 +207,7 @@ public class TrackRecordingServiceTestRecording {
String pauseTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(pauseTime);
service.pauseCurrentTrack();
service.endCurrentTrack();
// when
trackPointCreator.setClock("2020-02-02T02:02:04Z");
@@ -257,6 +259,47 @@ public class TrackRecordingServiceTestRecording {
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_blesensor_only_no_distance() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager();
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored
String sensor3 = "2020-02-02T02:02:13Z";
trackPointCreator.setClock(sensor3);
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(7)));
String stopTime = "2020-02-02T02:02:15Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate(HeartRate.of(7)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate(HeartRate.of(7))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_above() {
@@ -143,7 +143,7 @@ public class TrackRecordingServiceTestStateMachine {
@MediumTest
@Test
public void recording_startPauseStopResume_no_data() throws InterruptedException {
public void recording_startStopResume_no_data() throws InterruptedException {
// given
assertFalse(service.isRecording());
@@ -153,31 +153,19 @@ public class TrackRecordingServiceTestStateMachine {
// then
assertTrue(service.isRecording());
assertEquals(new RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
service.endCurrentTrack();
// when
service.pauseCurrentTrack();
service.resumeTrack(trackId);
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertTrue(service.isPaused());
assertEquals(new RecordingStatus(trackId, true), service.getRecordingStatusObservable().getValue());
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); //TODO The GPS is kept in pause, right? Might be a bug.
// when
service.resumeCurrentTrack();
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertFalse(service.isPaused());
assertEquals(new RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
@@ -188,7 +176,6 @@ public class TrackRecordingServiceTestStateMachine {
// then
assertFalse(service.isRecording());
assertFalse(service.isPaused());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
@@ -200,8 +187,7 @@ public class TrackRecordingServiceTestStateMachine {
// then
assertTrue(service.isRecording());
assertFalse(service.isPaused());
assertEquals(new RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
@@ -209,29 +195,31 @@ public class TrackRecordingServiceTestStateMachine {
@MediumTest
@Test
public void invalidStateChanges() throws InterruptedException {
public void cannotResume_non_existing_track() throws InterruptedException {
// given
assertFalse(service.isRecording());
// when
service.pauseCurrentTrack();
Thread.sleep(2000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when
service.resumeCurrentTrack();
Thread.sleep(1000);
service.resumeTrack(new Track.Id(-1));
// then
assertFalse(service.isRecording());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
}
@MediumTest
@Test
public void cannotEnd_without_starting() throws InterruptedException {
// given
assertFalse(service.isRecording());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when
service.endCurrentTrack();
@@ -257,8 +245,7 @@ public class TrackRecordingServiceTestStateMachine {
// then
assertTrue(service.isRecording());
assertFalse(service.isPaused());
assertEquals(new RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
@@ -42,7 +42,13 @@ public class VoiceAnnouncementUtilsTest {
@Before
public void setUp() {
contentProviderUtils = new ContentProviderUtils(context);
PreferencesUtils.setVoiceAnnounceHeartRate(false);
PreferencesUtils.setVoiceAnnounceLapHeartRate(false);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
PreferencesUtils.setVoiceAnnounceTotalDistance(true);
PreferencesUtils.setVoiceAnnounceMovingTime(true);
PreferencesUtils.setVoiceAnnounceAverageSpeedPace(true);
PreferencesUtils.setVoiceAnnounceLapSpeedPace(true);
}
@Test
@@ -61,6 +67,54 @@ public class VoiceAnnouncementUtilsTest {
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_speed_rounding_check() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusSeconds(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 1 second. Speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_rounding_check() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(19999));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_rounding_check_two() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(19990));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 19.99 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_withInterval_metric_speed() {
// given
@@ -199,7 +253,8 @@ public class VoiceAnnouncementUtilsTest {
@Test
public void getAnnouncement_heart_rate_and_sensor_statistics() {
PreferencesUtils.setVoiceAnnounceHeartRate(true);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(true);
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
@@ -221,4 +276,34 @@ public class VoiceAnnouncementUtilsTest {
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
}
@Test
public void getAnnouncement_only_lap_heart_rate() {
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
PreferencesUtils.setVoiceAnnounceTotalDistance(false);
PreferencesUtils.setVoiceAnnounceMovingTime(false);
PreferencesUtils.setVoiceAnnounceAverageSpeedPace(false);
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
Track.Id trackId = trackWithStats.first;
TrackStatistics stats = trackWithStats.second;
IntervalStatistics.Interval lastInterval;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPointIterator);
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
// then
assertEquals(" Current heart rate 133 bpm.", announcement);
}
}
@@ -99,7 +99,7 @@ public class DescriptionGeneratorTest {
+ "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@@ -135,7 +135,7 @@ public class DescriptionGeneratorTest {
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@@ -11,6 +11,7 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.models.Altitude;
@@ -86,7 +87,7 @@ public class TrackStatisticsUpdaterTest {
assertEquals(Duration.ofSeconds(9), statistics.getMovingTime());
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
assertEquals(28.75, statistics.getMaxAltitude(), 0.01);
assertEquals(32.5, statistics.getMaxAltitude(), 0.01);
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
@@ -245,6 +246,70 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_speed_from_GPS_moving_and_sensor_speed() {
}
@Test
public void addTrackPoint_maxSpeed_ignore_above_acceleration() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
// Ignore as we set max speed if two consecutive trackpoints were considered moving
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(1f)));
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
// Update max speed
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(1f)));
assertEquals(Speed.of(1f), subject.getTrackStatistics().getMaxSpeed());
// Update max speed
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(12))
.setSpeed(Speed.of(50f)));
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
// Ignore; we were getting slower
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(13))
.setSpeed(Speed.of(5f)));
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
// Ignore acceleration above 2g
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(14))
.setSpeed(Speed.of(500f)));
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
}
@Test
public void addTrackPoint_maxSpeed_multiple_segments() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
subject.addTrackPoints(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(2f)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(2f)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(4))
));
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
// when
subject.addTrackPoints(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(5)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6))
.setSpeed(Speed.of(1f)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7))
.setSpeed(Speed.of(1f)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(8))
));
// then
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
}
@Test
public void copy_constructor() {
// given
@@ -3,10 +3,10 @@ package de.dennisguse.opentracks.ui.customRecordingLayout;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.CoreMatchers.is;
import static de.dennisguse.opentracks.util.EspressoUtils.veryLongTouch;
import android.view.View;
@@ -51,7 +51,7 @@ public class EspressoCustomLayoutTest {
// stop recording
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
trackControllerStopButton.perform(veryLongTouch(1600));
trackControllerStopButton.perform(longClick());
}
private static class RecyclerViewItemCountAssertion implements ViewAssertion {
@@ -21,7 +21,6 @@ import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
@@ -180,7 +179,7 @@ public class IntervalStatisticsTest {
// then
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
assertEquals(trackStatistics.getTotalTime().toSeconds(), totalTime, 0.01);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
if (totalGain != null) {
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
@@ -142,7 +142,7 @@ public class EGM2008UtilsTest {
}
@Test
public void data_Berlin() throws IOException {
public void data_Berlin_Germany_() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(52.530644);
@@ -157,7 +157,7 @@ public class EGM2008UtilsTest {
}
@Test
public void data_Berlin_Caching() throws IOException {
public void data_Berlin_Germany_Caching() throws IOException {
// given
TrackPoint trackPoint1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint1.setLatitude(52.530644);
@@ -176,6 +176,22 @@ public class EGM2008UtilsTest {
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
}
@Test
public void data_Seattle_USA() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(47.63153);
trackPoint.setLongitude(-122.30938);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(22.99, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_MaxUndulation() throws IOException {
// given
@@ -191,6 +207,7 @@ public class EGM2008UtilsTest {
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void getIndices() {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
@@ -208,7 +225,7 @@ public class EGM2008UtilsTest {
trackPoint.setLatitude(-90);
trackPoint.setLongitude(-180);
assertEquals(new EGM2008Utils.Indices(2160, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
assertEquals(new EGM2008Utils.Indices(2160, 2160), EGM2008Utils.getIndices(trackPoint.getLocation()));
}
@Test
@@ -1,21 +1,14 @@
package de.dennisguse.opentracks.util;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.util.TreeIterables.breadthFirstViewTraversal;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ListView;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.espresso.util.HumanReadables;
import com.google.android.material.tabs.TabLayout;
@@ -23,8 +16,6 @@ import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import java.util.concurrent.TimeoutException;
public class EspressoUtils {
public static Matcher<View> childAtPosition(
@@ -80,76 +71,6 @@ public class EspressoUtils {
};
}
public 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);
}
};
}
public static ViewAction waitId(final int viewId, final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
}
@Override
public void perform(final UiController uiController, final View view) {
uiController.loopMainThreadUntilIdle();
final long startTime = System.currentTimeMillis();
final long endTime = startTime + millis;
final Matcher<View> viewMatcher = withId(viewId);
do {
for (View child : breadthFirstViewTraversal(view)) {
// found view with required ID
if (viewMatcher.matches(child)) {
return;
}
}
uiController.loopMainThreadForAtLeast(50);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(new TimeoutException())
.build();
}
};
}
public static ViewAction selectTabAtIndex(final int index) {
return new ViewAction() {
@Override
@@ -1,63 +0,0 @@
package de.dennisguse.opentracks.util;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RunWith(JUnit4.class)
public class ExportUtilsTest extends TestCase {
@Test
public void testIsExportFileExists_exists() {
// given
UUID uuid = UUID.randomUUID();
String format = "kmz";
List<String> fileNames = new ArrayList<>();
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
fileNames.add(uuid.toString().substring(0, 8) + "_other name name.kmz");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertTrue(exists);
}
@Test
public void testIsExportFileExists_not_exists1() {
// given
UUID uuid = UUID.randomUUID();
String format = "gpx";
List<String> fileNames = new ArrayList<>();
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_name.gpx");
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertFalse(exists);
}
@Test
public void testIsExportFileExists_not_exists2() {
// given
UUID uuid = UUID.randomUUID();
String format = "kmz";
List<String> fileNames = new ArrayList<>();
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertFalse(exists);
}
}
@@ -41,12 +41,12 @@ import java.time.format.TextStyle;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.TimezoneRule;
/**
* Tests for {@link StringUtils}.
@@ -59,6 +59,9 @@ public class StringUtilsTest {
@Rule
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
@Rule
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
private final Context context = ApplicationProvider.getApplicationContext();
/**
@@ -99,51 +102,13 @@ public class StringUtilsTest {
}
@Test
public void testGetTime_fractional() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 352);
}
public void testParseTime() {
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352").toInstant());
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352Z").toInstant());
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352+00:00").toInstant());
@Test
public void testGetTime_timezone() {
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01+00:00", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01-00:00", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01+01:00", 2010, 5, 4, 2, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01+10:30", 2010, 5, 3, 16, 32, 1, 0);
assertGetTime("2010-05-04T03:02:01-09:30", 2010, 5, 4, 12, 32, 1, 0);
assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0);
}
@Test
public void testGetTime_fractionalAndTimezone() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.47+00:00", 2010, 5, 4, 3, 2, 1, 470);
assertGetTime("2010-05-04T03:02:01.5791+03:00", 2010, 5, 4, 0, 2, 1, 579);
assertGetTime("2010-05-04T03:02:01.8-05:30", 2010, 5, 4, 8, 32, 1, 800);
}
/**
* Asserts the {@link StringUtils#parseTime(String)} returns the expected values.
*
* @param xmlDateTime the xml date time string
* @param year the expected year
* @param month the expected month
* @param day the expected day
* @param hour the expected hour
* @param minute the expected minute
* @param second the expected second
* @param millisecond the expected milliseconds
*/
private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, int second, int millisecond) {
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
calendar.set(year, month - 1, day, hour, minute, second);
calendar.set(GregorianCalendar.MILLISECOND, millisecond);
// This comparision tends to be flaky (difference of 1ms)
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toInstant().toEpochMilli()) <= 1);
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T01:00:00.352+01:00").toInstant());
assertEquals(Instant.ofEpochMilli(352).plus(Duration.ofHours(1)), StringUtils.parseTime("1970-01-01T00:00:00.352-01:00").toInstant());
}
@Test
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx
version="1.1"
creator="OpenTracks (Debug)"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:topografix="http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1"
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v2"
xmlns:gpxtrkx="http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
xmlns:cluetrust="http://www.cluetrust.com/Schemas/"
xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.garmin.com/xmlschemas/TrackStatsExtension/v1 http://www.cluetrust.com/Schemas http://www.cluetrust.com/Schemas/gpxdata10.xsd http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
<wpt lat="3" lon="14">
<ele>10</ele>
<time>2021-01-07T21:51:59.179+01:15</time>
<name><![CDATA[Marker 1]]></name>
<desc><![CDATA[Marker 1 desc]]></desc>
<type><![CDATA[Marker 1 category]]></type>
</wpt>
<trk>
<name><![CDATA[2021-01-07 22:51]]></name>
<desc><![CDATA[the description]]></desc>
<type><![CDATA[the category]]></type>
<extensions>
<topografix:color>c0c0c0</topografix:color>
<opentracks:trackid>7002101e-4198-4613-8c24-544e01ca3981</opentracks:trackid>
<gpxtrkx:TrackStatsExtension>
<gpxtrkx:Distance>0.0</gpxtrkx:Distance>
<gpxtrkx:TimerTime>0</gpxtrkx:TimerTime>
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
<gpxtrkx:Ascent>null</gpxtrkx:Ascent>
<gpxtrkx:Descent>null</gpxtrkx:Descent>
</gpxtrkx:TrackStatsExtension>
</extensions>
<trkseg>
<trkpt lat="3" lon="14">
<ele>10</ele>
<time>2021-01-07T21:51:59.179+01:15</time>
</trkpt>
</trkseg>
</trk>
</gpx>
+2 -18
View File
@@ -15,7 +15,6 @@ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.dennisguse.opentracks"
android:installLocation="auto">
<supports-screens
@@ -59,6 +58,8 @@ limitations under the License.
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".Startup"
android:allowBackup="false"
@@ -347,23 +348,6 @@ limitations under the License.
<activity android:name=".settings.SettingsCustomLayoutListActivity" />
<activity android:name=".settings.SettingsCustomLayoutEditActivity" />
<activity
android:name=".share.ShowOnMapProxyActivity$KML"
android:exported="true">
<intent-filter android:label="@string/open_track_as_KML">
<action android:name="Intent.OpenTracks-Dashboard" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".share.ShowOnMapProxyActivity$GPX"
android:exported="true">
<intent-filter android:label="@string/open_track_as_GPX">
<action android:name="Intent.OpenTracks-Dashboard" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ShowErrorActivity"
android:excludeFromRecents="true"
@@ -84,15 +84,18 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
/**
* Called every time a track is deleted.
*/
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
if (trackDeleteServiceConnection != null) {
trackDeleteServiceConnection.unbind(this);
trackDeleteServiceConnection = null;
onDeleteFinished();
}
}
protected abstract void onDeleteConfirmed();
protected abstract void onDeleteFinished();
protected abstract Track.Id getRecordingTrackId();
@Override
@@ -3,19 +3,15 @@ package de.dennisguse.opentracks;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import de.dennisguse.opentracks.databinding.ActivityShowErrorBinding;
import de.dennisguse.opentracks.databinding.TrackListBinding;
public class ShowErrorActivity extends AbstractActivity {
@@ -38,6 +38,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.core.splashscreen.SplashScreen;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
@@ -70,6 +71,7 @@ import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.ListItemUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -157,12 +159,13 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
@Override
protected void onCreate(Bundle savedInstanceState) {
// Reset theme after splash
setTheme(R.style.ThemeCustom);
SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
requestRequiredPermissions();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
@@ -238,7 +241,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
startActivity(newIntent);
connection.unbind(this);
}).startAndBind(this);
}).startAndBind(this, true);
});
viewBinding.trackListFabAction.setOnLongClickListener((view) -> {
if (!recordingStatus.isRecording()) {
@@ -259,6 +262,10 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
loadData(getIntent());
}
private void requestRequiredPermissions() {
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
}
@Override
protected void onStart() {
super.onStart();
@@ -310,8 +317,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
searchView.findViewById(R.id.search_edit_frame).setPadding(0, 0, 48, 0);
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(R.id.search_src_text);
searchAutoComplete.setHintTextColor(getResources().getColor(android.R.color.white));
searchAutoComplete.setTextColor(getResources().getColor(android.R.color.white));
searchAutoComplete.setHintTextColor(ContextCompat.getColor(this, android.R.color.white));
searchAutoComplete.setTextColor(ContextCompat.getColor(this, android.R.color.white));
searchView.setOnCloseListener(() -> {
searchView.clearFocus();
@@ -432,6 +439,11 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
// Do nothing
}
@Override
protected void onDeleteFinished() {
// Do nothing
}
@Nullable
@Override
protected Track.Id getRecordingTrackId() {
@@ -472,7 +484,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
if (itemId == R.id.list_context_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, false, trackIds);
IntentDashboardUtils.showTrackOnMap(this, false, trackIds);
return true;
}
@@ -179,7 +179,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, false, trackId);
IntentDashboardUtils.showTrackOnMap(this, false, trackId);
return true;
}
@@ -213,7 +213,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
connection.unbind(this);
finish();
}).startAndBind(this);
}).startAndBind(this, true);
return true;
}
@@ -232,9 +232,9 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
@Override
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
super.onTrackDeleteStatus(deleteStatus);
if (deleteStatus.isDeleted(trackId)) {
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
super.onTrackDeleteStatus(deletionFinishedStatus);
if (deletionFinishedStatus.isDeleted(trackId)) {
runOnUiThread(this::finish);
}
}
@@ -244,6 +244,11 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
runOnUiThread(this::finish);
}
@Override
protected void onDeleteFinished() {
// Do nothing
}
/**
* Gets the {@link TrackDataHub}.
*/
@@ -16,6 +16,9 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
@@ -44,7 +47,6 @@ import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
@@ -131,11 +133,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
requestGPSPermissions();
viewBinding.trackRecordingFabAction.setImageResource(R.drawable.ic_baseline_stop_24);
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setBackgroundColor(getResources().getColor(R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setBackgroundColor(ContextCompat.getColor(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> {
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
@@ -185,9 +185,20 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
private void setFullscreenPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldUseFullscreen();
boolean fullscreen = PreferencesUtils.shouldUseFullscreen();
if (keepScreenOn) {
WindowInsetsControllerCompat windowInsetsController = ViewCompat.getWindowInsetsController(getWindow().getDecorView());
if (windowInsetsController != null) {
windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
if (fullscreen) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
} else {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars());
}
return;
}
if (fullscreen) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
@@ -256,7 +267,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, true, trackId);
IntentDashboardUtils.showTrackOnMap(this, true, trackId);
return true;
}
@@ -371,14 +382,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
setScreenOnPolicy();
}
private void requestGPSPermissions() {
if (PermissionUtils.hasGPSPermission(this)) {
return;
}
PermissionUtils.requestGPSPermission(this, null, () -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
}
private void onGpsStatusChanged(GpsStatusValue gpsStatusValue) {
if (gpsStatusValue.isGpsStarted() && snackbar != null && snackbar.isShown()) {
snackbar.dismiss();
@@ -15,13 +15,14 @@ import de.dennisguse.opentracks.databinding.TrackStoppedBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
public class TrackStoppedActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
private static final String TAG = TrackStoppedActivity.class.getSimpleName();
@@ -31,6 +32,8 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
private Track.Id trackId;
private boolean isDiscarding = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -44,26 +47,26 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Track track = contentProviderUtils.getTrack(trackId);
viewBinding.fields.trackEditName.setText(track.getName());
viewBinding.trackEditName.setText(track.getName());
viewBinding.fields.trackEditActivityType.setText(track.getCategory());
viewBinding.trackEditActivityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.fields.trackEditActivityType.setAdapter(adapter);
viewBinding.fields.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.fields.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.fields.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
viewBinding.trackEditActivityType.setAdapter(adapter);
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackStoppedActivity.this, viewBinding.fields.trackEditActivityType.getText().toString()));
TrackStoppedActivity.this, viewBinding.trackEditActivityType.getText().toString()));
}
});
String iconValue = track.getIcon();
setActivityTypeIcon(iconValue);
viewBinding.fields.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.fields.trackEditActivityType.getText().toString()));
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
viewBinding.fields.trackEditDescription.setText(track.getDescription());
viewBinding.trackEditDescription.setText(track.getDescription());
viewBinding.time.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
@@ -87,20 +90,30 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
}
viewBinding.finishButton.setOnClickListener(v -> {
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.fields.trackEditName.getText().toString(),
viewBinding.fields.trackEditActivityType.getText().toString(), viewBinding.fields.trackEditDescription.getText().toString(),
contentProviderUtils);
storeTrackMetaData(contentProviderUtils, track);
ExportUtils.postWorkoutExport(this, trackId);
finish();
});
viewBinding.resumeButton.setOnClickListener(v -> {
storeTrackMetaData(contentProviderUtils, track);
resumeTrackAndFinish();
});
viewBinding.discardButton.setOnClickListener(v -> ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackId));
}
private void storeTrackMetaData(ContentProviderUtils contentProviderUtils, Track track) {
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.trackEditName.getText().toString(),
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
contentProviderUtils);
}
@Override
public void onBackPressed() {
if (isDiscarding) {
return;
}
super.onBackPressed();
resumeTrackAndFinish();
}
@@ -112,13 +125,13 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
}
private void setActivityTypeIcon(String iconValue) {
viewBinding.fields.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
setActivityTypeIcon(iconValue);
viewBinding.fields.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(iconValue)));
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(iconValue)));
}
private void resumeTrackAndFinish() {
@@ -132,6 +145,24 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
connection.unbind(this);
finish();
}).startAndBind(this);
}).startAndBind(this, true);
}
}
@Override
protected void onDeleteConfirmed() {
isDiscarding = true;
viewBinding.loadingLayout.loadingText.setText(getString(R.string.track_discarding));
viewBinding.contentLinearLayout.setVisibility(View.GONE);
viewBinding.loadingLayout.loadingIndeterminate.setVisibility(View.VISIBLE);
}
@Override
protected void onDeleteFinished() {
finish();
}
@Override
protected Track.Id getRecordingTrackId() {
return null;
}
}
@@ -32,9 +32,7 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding;
@@ -188,9 +186,9 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
}
}
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
if (isResumed()) {
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getUnitSystem());
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, trackPoint.getSpeed(), chartByDistance, viewBinding.chartView.getUnitSystem());
pendingPoints.add(point);
}
}
@@ -3,11 +3,9 @@ package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -29,20 +27,20 @@ public class ChartPoint {
this.altitude = altitude;
}
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, UnitSystem unitSystem) {
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, boolean chartByDistance, UnitSystem unitSystem) {
if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
if (smoothedAltitude != null) {
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(unitSystem);
if (trackPoint.hasAltitude()) {
altitude = Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem);
}
if (smoothedSpeed != null) {
speed = smoothedSpeed.to(unitSystem);
pace = smoothedSpeed.toPace(unitSystem).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
pace = smoothedSpeed.toPace(unitSystem).toSeconds() / 60d;
}
if (trackPoint.hasHeartRate()) {
heartRate = (double) trackPoint.getHeartRate().getBPM();
@@ -24,6 +24,7 @@ import android.graphics.Paint.Style;
import android.graphics.Path;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import java.text.NumberFormat;
@@ -44,6 +45,7 @@ abstract class ChartValueSeries {
private final int[] intervalValues;
private final int metricTitleId;
private final int imperialTitleId;
private final int nauticalTitleId;
private final Paint fillPaint;
private final Paint strokePaint;
private final Paint titlePaint;
@@ -57,33 +59,22 @@ abstract class ChartValueSeries {
private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS;
private boolean enabled = true;
/**
* Constructor.
*
* @param context the context
* @param absoluteMin the absolute min value
* @param absoluteMax the absolute max value
* @param intervalValues the list of interval values
* @param metricTitleId the metric title id
* @param imperialTitleId the imperial title id
* @param fillColor the fill color
* @param strokeColor the stroke color
*/
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int nauticalTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues;
this.metricTitleId = metricTitleId;
this.imperialTitleId = imperialTitleId;
this.nauticalTitleId = nauticalTitleId;
fillPaint = new Paint();
fillPaint.setStyle(Style.FILL);
fillPaint.setColor(context.getResources().getColor(fillColor));
fillPaint.setColor(ContextCompat.getColor(context, fillColor));
fillPaint.setAntiAlias(true);
strokePaint = new Paint();
strokePaint.setStyle(Style.STROKE);
strokePaint.setColor(context.getResources().getColor(strokeColor));
strokePaint.setColor(ContextCompat.getColor(context, strokeColor));
strokePaint.setAntiAlias(true);
// Make copies of the stroke paint with the default thickness
@@ -224,39 +215,27 @@ abstract class ChartValueSeries {
return metricTitleId;
case IMPERIAL:
return imperialTitleId;
case NAUTICAL_IMPERIAL:
return nauticalTitleId;
default:
throw new RuntimeException("Not implemented");
}
}
/**
* Gets the title paint.
*/
Paint getTitlePaint() {
return titlePaint;
}
/**
* Gets the marker paint.
*/
Paint getMarkerPaint() {
return markerPaint;
}
/**
* Gets the largest marker.
*/
String getLargestMarker() {
String minMarker = numberFormat.format(getMinMarkerValue());
String maxMarker = numberFormat.format(getMaxMarkerValue());
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
}
/**
* Formats a marker value.
*
* @param value the value
*/
String formatMarker(int value) {
return numberFormat.format(value);
}
@@ -203,6 +203,7 @@ public class ChartView extends View {
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
R.string.description_altitude_metric,
R.string.description_altitude_imperial,
R.string.description_altitude_imperial,
R.color.chart_altitude_fill,
R.color.chart_altitude_border,
fontSizeSmall,
@@ -224,6 +225,7 @@ public class ChartView extends View {
new int[]{1, 5, 10, 20, 50, 100},
R.string.description_speed_metric,
R.string.description_speed_imperial,
R.string.description_speed_nautical,
R.color.chart_speed_fill,
R.color.chart_speed_border,
fontSizeSmall,
@@ -246,6 +248,7 @@ public class ChartView extends View {
new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120},
R.string.description_pace_metric,
R.string.description_pace_imperial,
R.string.description_pace_nautical,
R.color.chart_pace_fill,
R.color.chart_pace_border,
fontSizeSmall,
@@ -268,6 +271,7 @@ public class ChartView extends View {
new int[]{25, 50},
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.color.chart_heart_rate_fill,
R.color.chart_heart_rate_border,
fontSizeSmall,
@@ -289,6 +293,7 @@ public class ChartView extends View {
new int[]{5, 10, 25, 50},
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.color.chart_cadence_fill,
R.color.chart_cadence_border,
fontSizeSmall,
@@ -309,6 +314,7 @@ public class ChartView extends View {
new int[]{5, 50, 100, 200},
R.string.description_sensor_power,
R.string.description_sensor_power,
R.string.description_sensor_power,
R.color.chart_power_fill,
R.color.chart_power_border,
fontSizeSmall,
@@ -732,6 +738,8 @@ public class ChartView extends View {
return context.getString(R.string.unit_kilometer);
case IMPERIAL:
return context.getString(R.string.unit_mile);
case NAUTICAL_IMPERIAL:
return context.getString(R.string.unit_nautical_mile);
default:
throw new RuntimeException("Not implemented");
}
@@ -187,7 +187,6 @@ public class ContentProviderUtils {
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
}
// Delete track last since it triggers a database vacuum call
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new));
}
@@ -195,8 +194,6 @@ public class ContentProviderUtils {
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
// Delete track folder resources.
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
// Delete track last since it triggers a database vacuum call
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
}
@@ -27,6 +27,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@@ -39,6 +40,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
@@ -53,6 +55,8 @@ public class CustomContentProvider extends ContentProvider {
private static final String SQL_LIST_DELIMITER = ",";
private static final int TOTAL_DELETED_ROWS_VACUUM_THRESHOLD = 10000;
private final UriMatcher uriMatcher;
private SQLiteDatabase db;
@@ -131,14 +135,12 @@ public class CustomContentProvider extends ContentProvider {
@Override
public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
String table;
boolean shouldVacuum = false;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
shouldVacuum = true;
break;
case MARKERS:
table = MarkerColumns.TABLE_NAME;
@@ -147,23 +149,40 @@ public class CustomContentProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown URL " + url);
}
Log.w(TAG, "Deleting table " + table);
int count;
Log.w(TAG, "Deleting from table " + table);
int totalChangesBefore = getTotalChanges();
int deletedRowsFromTable;
try {
db.beginTransaction();
count = db.delete(table, where, selectionArgs);
deletedRowsFromTable = db.delete(table, where, selectionArgs);
Log.i(TAG, "Deleted " + deletedRowsFromTable + " rows of table " + table);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
if (shouldVacuum) {
// If a potentially large amount of data was deleted, reclaim its space.
Log.i(TAG, "Vacuuming the database.");
int totalChanges = getTotalChanges() - totalChangesBefore;
Log.i(TAG, "Deleted " + totalChanges + " total rows from database");
PreferencesUtils.addTotalRowsDeleted(totalChanges);
int totalRowsDeleted = PreferencesUtils.getTotalRowsDeleted();
if (totalRowsDeleted > TOTAL_DELETED_ROWS_VACUUM_THRESHOLD) {
Log.i(TAG, "TotalRowsDeleted " + totalRowsDeleted + ", starting to vacuum the database.");
db.execSQL("VACUUM");
PreferencesUtils.resetTotalRowsDeleted();
}
return count;
return deletedRowsFromTable;
}
private int getTotalChanges() {
int totalCount;
try (Cursor cursor = db.rawQuery("SELECT total_changes()", null)) {
cursor.moveToNext();
totalCount = cursor.getInt(0);
}
return totalCount;
}
@Override
@@ -24,6 +24,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
@@ -72,9 +73,7 @@ public class ShareContentProvider extends CustomContentProvider {
* @return An URI for one file containing this tracks.
*/
public static Pair<Uri, String> createURI(Track.Id trackId, String trackName, @NonNull TrackFileFormat trackFileFormat) {
Set<Track.Id> trackIds = new HashSet<>(1);
trackIds.add(trackId);
return createURI(trackIds, trackName, trackFileFormat);
return createURI(Set.of(trackId), trackName, trackFileFormat);
}
/**
@@ -108,11 +107,7 @@ public class ShareContentProvider extends CustomContentProvider {
String[] uriTrackIds = uriPaths.get(2).split(TRACKID_DELIMITER);
Set<Track.Id> trackIds = new HashSet<>();
for (String uriTrackId : uriTrackIds) {
trackIds.add(new Track.Id(Long.parseLong(uriTrackId)));
}
return trackIds;
return Arrays.stream(uriTrackIds).map(uriTrackId -> new Track.Id(Long.parseLong(uriTrackId))).collect(Collectors.toSet());
}
/**
@@ -25,16 +25,13 @@ import android.os.HandlerThread;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
@@ -367,7 +364,7 @@ public class TrackDataHub {
// Also include the last point if the selected track is not recording.
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
for (Listener trackDataListener : listeners) {
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics());
}
} else {
for (Listener trackDataListener : listeners) {
@@ -432,7 +429,7 @@ public class TrackDataHub {
*
* @param trackPoint the trackPoint
*/
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, @Nullable Altitude smoothedAltitude_m) {
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
}
/**
@@ -2,22 +2,35 @@ package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull;
import java.time.Duration;
import java.util.Objects;
public class Cadence {
public static Cadence of(float value) {
return new Cadence(value);
public static Cadence of(float value, Duration duration) {
if (duration.isZero()) {
return zero();
}
return new Cadence(value / (duration.toMillis() / (float) Duration.ofMinutes(1).toMillis()));
}
private final float value;
public static Cadence of(float value_rpm) {
return new Cadence(value_rpm);
}
public static Cadence zero() {
return of(0.0f);
}
private final float value_rpm;
private Cadence(float value) {
this.value = value;
this.value_rpm = value;
}
public float getRPM() {
return value;
return value_rpm;
}
@Override
@@ -25,19 +38,19 @@ public class Cadence {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Cadence cadence = (Cadence) o;
return Float.compare(cadence.value, value) == 0;
return Float.compare(cadence.value_rpm, value_rpm) == 0;
}
@Override
public int hashCode() {
return Objects.hash(value);
return Objects.hash(value_rpm);
}
@NonNull
@Override
public String toString() {
return "Cadence{" +
"value=" + value + " rpm" +
"value=" + value_rpm + " rpm" +
'}';
}
}
@@ -13,6 +13,14 @@ public class Distance {
return new Distance(distance_m);
}
public static Distance of(Double distance_m) {
if (distance_m == null) {
return Distance.of(Double.NaN);
} else {
return Distance.of((double) distance_m);
}
}
public static Distance of(String distance_m) {
return of(Float.parseFloat(distance_m));
}
@@ -26,11 +34,15 @@ public class Distance {
}
public static Distance ofMile(double distance_mile) {
return of(distance_mile * UnitConversions.MI_TO_M);
return of(distance_mile * MI_TO_M);
}
public static Distance ofNauticalMile(double distance_mile) {
return of(distance_mile * NAUTICAL_MILE_TO_M);
}
public static Distance ofKilometer(double distance_km) {
return of(distance_km * UnitConversions.KM_TO_M);
return of(distance_km * KM_TO_M);
}
public static Distance ofMM(double distance_mm) {
@@ -51,6 +63,8 @@ public class Distance {
return Distance.ofKilometer(1);
case IMPERIAL:
return Distance.ofMile(1);
case NAUTICAL_IMPERIAL:
return Distance.ofNauticalMile(1);
default:
throw new RuntimeException("Not implemented");
}
@@ -103,15 +117,19 @@ public class Distance {
}
public double toKM() {
return distance_m * UnitConversions.M_TO_KM;
return distance_m * M_TO_KM;
}
public double toFT() {
return distance_m * UnitConversions.M_TO_FT;
return distance_m * M_TO_FT;
}
public double toMI() {
return toKM() * UnitConversions.KM_TO_MI;
return distance_m * M_TO_MI;
}
public double toNauticalMiles() {
return distance_m * M_TO_NAUTICAL_MILE;
}
public double toKM_Miles(UnitSystem unitSystem) {
@@ -120,6 +138,8 @@ public class Distance {
return toKM();
case IMPERIAL:
return toMI();
case NAUTICAL_IMPERIAL:
return toNauticalMiles();
default:
throw new RuntimeException("Not implemented");
}
@@ -129,6 +149,7 @@ public class Distance {
switch (unitSystem) {
case METRIC:
return toM();
case NAUTICAL_IMPERIAL:
case IMPERIAL:
return toFT();
default:
@@ -156,4 +177,17 @@ public class Distance {
"distance_m=" + distance_m +
'}';
}
// multiplication factors for conversion
private static final double KM_TO_M = 1000.0;
private static final double M_TO_KM = 1 / KM_TO_M;
public static final double MI_TO_M = 1609.344;
public static final double M_TO_MI = 1 / MI_TO_M;
private static final double MI_TO_FT = 5280.0;
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
private static final double NAUTICAL_MILE_TO_M = 1852.0;
private static final double M_TO_NAUTICAL_MILE = 1 / NAUTICAL_MILE_TO_M;
}
@@ -16,11 +16,14 @@ public class DistanceFormatter {
private final int decimalCount;
private final double threshold;
private final UnitSystem unitSystem;
private DistanceFormatter(Resources resources, int decimalCount, UnitSystem unitSystem) {
private DistanceFormatter(Resources resources, int decimalCount, double threshold, UnitSystem unitSystem) {
this.resources = resources;
this.decimalCount = decimalCount;
this.threshold = threshold;
this.unitSystem = unitSystem;
assert unitSystem != null;
}
@@ -42,11 +45,13 @@ public class DistanceFormatter {
*/
public Pair<String, String> getDistanceParts(Distance distance) {
if (distance.isInvalid()) {
String valueUnknown = resources.getString(R.string.value_unknown);
switch (unitSystem) {
case METRIC:
return new Pair<>(null, resources.getString(R.string.unit_meter));
return new Pair<>(valueUnknown, resources.getString(R.string.unit_meter));
case IMPERIAL:
return new Pair<>(null, resources.getString(R.string.unit_feet));
case NAUTICAL_IMPERIAL:
return new Pair<>(valueUnknown, resources.getString(R.string.unit_feet));
default:
throw new RuntimeException("Not implemented");
}
@@ -54,17 +59,23 @@ public class DistanceFormatter {
switch (unitSystem) {
case METRIC:
if (distance.greaterThan(Distance.of(500))) {
if (distance.greaterThan(Distance.ofKilometer(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
}
case IMPERIAL:
if (distance.greaterThan(Distance.ofMile(0.5))) {
if (distance.greaterThan(Distance.ofMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
case NAUTICAL_IMPERIAL:
if (distance.greaterThan(Distance.ofNauticalMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
default:
throw new RuntimeException("Not implemented");
}
@@ -80,8 +91,11 @@ public class DistanceFormatter {
private UnitSystem unitSystem;
private double threshold;
public Builder() {
decimalCount = 2;
threshold = 0.5;
}
public Builder setDecimalCount(int decimalCount) {
@@ -94,8 +108,13 @@ public class DistanceFormatter {
return this;
}
public Builder setThreshold(double threshold) {
this.threshold = threshold;
return this;
}
public DistanceFormatter build(Resources resource) {
return new DistanceFormatter(resource, decimalCount, unitSystem);
return new DistanceFormatter(resource, decimalCount, threshold, unitSystem);
}
public DistanceFormatter build(Context context) {
@@ -15,7 +15,7 @@ public class Speed {
return zero();
}
return new Speed(distance.toM() / (duration.toMillis() * UnitConversions.MS_TO_S));
return new Speed(distance.toM() / (duration.toMillis() / (double) Duration.ofSeconds(1).toMillis()));
}
public static Speed of(double speed_mps) {
@@ -85,12 +85,23 @@ public class Speed {
return speed_mps;
}
/**
* We interpret {@link Speed} here as a {@link Distance} over 1h.
*/
private Distance toH() {
return Distance.of(speed_mps * Duration.ofHours(1).toSeconds());
}
public double toKMH() {
return speed_mps * UnitConversions.MPS_TO_KMH;
return toH().toKM();
}
public double toMPH() {
return toKMH() * UnitConversions.KM_TO_MI;
return toH().toMI();
}
public double toKnots() {
return toH().toNauticalMiles();
}
public Duration toPace(UnitSystem unitSystem) {
@@ -98,17 +109,7 @@ public class Speed {
return Duration.ofSeconds(0);
}
double distance = speed_mps;
switch (unitSystem) { //TODO Can we use to(UnitSystem)?
case METRIC:
distance *= UnitConversions.M_TO_KM;
break;
case IMPERIAL:
distance *= UnitConversions.M_TO_MI;
break;
default:
throw new RuntimeException("Not implemented");
}
double distance = Distance.of(speed_mps).toKM_Miles(unitSystem);
return Duration.ofSeconds(Math.round(1 / distance));
}
@@ -119,6 +120,8 @@ public class Speed {
return toKMH();
case IMPERIAL:
return toMPH();
case NAUTICAL_IMPERIAL:
return toKnots();
default:
throw new RuntimeException("Not implemented");
}
@@ -47,6 +47,9 @@ public class SpeedFormatter {
case IMPERIAL:
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
break;
case NAUTICAL_IMPERIAL:
unitId = reportSpeedOrPace ? R.string.unit_knots : R.string.unit_minute_per_nautical_mile;
break;
default:
throw new RuntimeException("Not implemented");
}
@@ -46,7 +46,7 @@ public class TrackPoint {
private Double longitude;
private Distance horizontalAccuracy;
private Distance verticalAccuracy;
private Altitude altitude;
private Altitude altitude; //TODO use Altitude.WGS84
private Speed speed;
private Float bearing;
private Distance sensorDistance;
@@ -56,7 +56,7 @@ public class TrackPoint {
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
TRACKPOINT(0), //Just GPS data and may contain BLE sensor data
SENSORPOINT(2), //Just BLE sensor data; required to have speed and sensorDistance
SENSORPOINT(2), //Just BLE sensor data; may have speed and sensorDistance
SEGMENT_END_MANUAL(1); //End of a segment
@@ -1,65 +0,0 @@
/*
* Copyright 2008 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.data.models;
/**
* Unit conversion constants.
*
* @author Sandor Dornbush
*/
public class UnitConversions {
// Time //TODO Use Duration
// multiplication factor to convert seconds to milliseconds
public static final long S_TO_MS = 1000;
// multiplication factor to convert milliseconds to seconds
@Deprecated
public static final double MS_TO_S = 1d / S_TO_MS;
// multiplication factor to convert minutes to seconds
@Deprecated
public static final double MIN_TO_S = 60.0;
// multiplication factor to convert seconds to minutes
@Deprecated
public static final double S_TO_MIN = 1 / MIN_TO_S;
// multiplication factor to convert minutes to hours
@Deprecated
public static final double MIN_TO_HR = 1 / 60.0;
// multiplication factor to convert kilometers to miles
public static final double KM_TO_MI = 0.621371192;
// Distance //TODO Make private to Distance class!
// multiplication factor to convert miles to feet
private static final double MI_TO_FT = 5280.0;
// multiplication factor to covert kilometers to meters
public static final double KM_TO_M = 1000.0;
// multiplication factor to convert meters to kilometers
public static final double M_TO_KM = 1 / KM_TO_M;
// multiplication factor to convert meters to miles
public static final double M_TO_MI = M_TO_KM * KM_TO_MI;
// multiplication factor to convert meters to feet
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
// multiplication factor to convert meters per second to kilometers per hour
public static final double MPS_TO_KMH = M_TO_KM / (S_TO_MIN * MIN_TO_HR);
// multiplication factor to convert miles to km
public static final double MI_TO_KM = 1 / KM_TO_MI;
// multiplication factor to convert miles to m
public static final double MI_TO_M = MI_TO_KM * KM_TO_M;
private UnitConversions() {
}
}
@@ -182,7 +182,7 @@ public class StatisticsRecordedFragment extends Fragment {
private void loadTrackDescription(@NonNull Track track) {
viewBinding.statsNameValue.setText(track.getName());
viewBinding.statsDescriptionValue.setText(track.getDescription());
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTime(track.getStartTime()));
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTimeWithOffsetIfDifferent(track.getStartTime()));
}
private void updateUI() {
@@ -1,6 +1,12 @@
package de.dennisguse.opentracks.io.file;
import android.content.Context;
import android.content.res.Resources;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -128,6 +134,23 @@ public enum TrackFileFormat {
this.preferenceId = preferenceId;
}
public static Map<String, String> toPreferenceIdLabelMap(final Resources resources, final TrackFileFormat ... trackFileFormats) {
Map<String, String> preferenceIdLabelMap = new LinkedHashMap<>(trackFileFormats.length);
for (TrackFileFormat trackFileFormat : trackFileFormats) {
String trackFileFormatUpperCase = trackFileFormat.getExtension().toUpperCase(Locale.US); //ASCII upper case
int photoMessageId = trackFileFormat.includesPhotos() ? R.string.export_with_photos : R.string.export_without_photos;
preferenceIdLabelMap.put(trackFileFormat.getPreferenceId(), String.format("%s (%s)", trackFileFormatUpperCase, resources.getString(photoMessageId)));
}
return preferenceIdLabelMap;
}
public static TrackFileFormat valueOfPreferenceId(final String preferenceId) {
return Arrays.stream(values())
.filter(trackFileFormat -> trackFileFormat.getPreferenceId().equals(preferenceId))
.findFirst()
.orElse(null);
}
/**
* Returns the mime type for each format.
*/
@@ -39,6 +39,7 @@ import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.ExportActivityBinding;
import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.FileUtils;
@@ -234,7 +235,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* @param conflictResolution conflict resolution to be applied if needed.
*/
private void export(Track track, int conflictResolution) {
boolean fileExists = ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles);
boolean fileExists = exportFileExists(track);
if (fileExists && conflictResolution == CONFLICT_NONE) {
conflict(track);
@@ -250,6 +251,11 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
export(track, autoConflict);
}
private boolean exportFileExists(Track track) {
String filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
return directoryFiles.stream().anyMatch(filename::equals);
}
private void setConflictVisibility(int visibility) {
viewBinding.exportProgressAlertIcon.setVisibility(visibility);
viewBinding.exportProgressAlertMsg.setVisibility(visibility);
@@ -315,7 +321,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
public void onExportSuccess(Track.Id trackId) {
Track track = contentProviderUtils.getTrack(trackId);
if (ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles)) {
if (exportFileExists(track)) {
trackExportOverwrittenCount++;
} else {
trackExportSuccessCount++;
@@ -146,13 +146,10 @@ public class GPXTrackExporter implements TrackExporter {
switch (trackPoint.getType()) {
case SEGMENT_START_MANUAL:
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
wroteSegment = true;
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
break;
case SEGMENT_END_MANUAL:
writeCloseSegment();
if (wroteSegment) writeCloseSegment();
wroteSegment = false;
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_END_MANUAL.name() + " is not supported.");
break;
@@ -68,9 +68,6 @@ public class KMLTrackExporter implements TrackExporter {
public static final String EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL = "accuracy_horizontal";
public static final String EXTENDED_DATA_TYPE_ACCURACY_VERTICAL = "accuracy_vertical";
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private static final NumberFormat SENSOR_DATA_FORMAT = NumberFormat.getInstance(Locale.US);
static {
@@ -536,7 +533,7 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
printWriter.println("<IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>");
printWriter.println("<Icon />");
printWriter.println("</IconStyle>");
printWriter.println("</Style>");
}
@@ -546,9 +543,7 @@ public class KMLTrackExporter implements TrackExporter {
*/
private void writePlacemarkerStyle() {
printWriter.println("<Style id=\"" + KMLTrackExporter.MARKER_STYLE + "\"><IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + KMLTrackExporter.MARKER_ICON + "</href></Icon>");
printWriter.println("<hotSpot x=\"" + 20 + "\" y=\"" + 2 + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("<Icon />");
printWriter.println("</IconStyle></Style>");
}
@@ -17,8 +17,8 @@ import androidx.documentfile.provider.DocumentFile;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.exporter.ExportActivity;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.IntentUtils;
public abstract class DirectoryChooserActivity extends AppCompatActivity {
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
@@ -290,4 +291,5 @@ public class TrackImporter {
public void cleanImport() {
contentProviderUtils.deleteTracks(context, trackIds);
}
}
@@ -6,6 +6,7 @@ import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.splashscreen.SplashScreen;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.services.TrackRecordingService;
@@ -30,11 +31,14 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
splashScreen.setKeepOnScreenCondition(() -> true );
if (PreferencesUtils.isPublicAPIenabled()) {
Log.i(TAG, "Received and trying to execute requested action.");
new TrackRecordingServiceConnection(serviceConnectedCallback)
.startAndBind(this);
.startAndBind(this, isStartServiceForeground());
} else {
Toast.makeText(this, getString(R.string.settings_public_api_disabled_toast), Toast.LENGTH_LONG).show();
Log.w(TAG, "Public API is disabled; ignoring request.");
@@ -42,6 +46,10 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
}
}
protected boolean isStartServiceForeground() {
return false;
}
protected abstract void execute(TrackRecordingService service);
protected abstract boolean isPostExecuteStopService();
@@ -2,28 +2,68 @@ package de.dennisguse.opentracks.publicapi;
import android.os.Bundle;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.TrackUtils;
public class StartRecording extends AbstractAPIActivity {
public static final String EXTRA_TRACK_NAME = "TRACK_NAME";
public static final String EXTRA_TRACK_CATEGORY = "TRACK_CATEGORY";
public static final String EXTRA_TRACK_ICON = "TRACK_ICON";
public static final String EXTRA_TRACK_DESCRIPTION = "TRACK_DESCRIPTION";
public static final String EXTRA_STATS_TARGET_PACKAGE = "STATS_TARGET_PACKAGE";
public static final String EXTRA_STATS_TARGET_CLASS = "STATS_TARGET_CLASS";
private static final String TAG = StartRecording.class.getSimpleName();
protected void execute(TrackRecordingService service) {
Track.Id trackId = service.startNewTrack();
if ((trackId != null) && (PreferencesUtils.isPublicAPIDashboardEnabled())) {
if (trackId != null) {
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
String targetPackage = bundle.getString("STATS_TARGET_PACKAGE", null);
String targetClass = bundle.getString("STATS_TARGET_CLASS", null);
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
updateTrackMetadata(trackId, bundle);
if (PreferencesUtils.isPublicAPIDashboardEnabled()) {
startDashboardAPI(trackId, bundle);
}
}
}
}
private void updateTrackMetadata(@NonNull Track.Id trackId, @NonNull Bundle bundle) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Track track = contentProviderUtils.getTrack(trackId);
TrackUtils.updateTrack(this, track,
bundle.getString(EXTRA_TRACK_NAME, null),
bundle.getString(EXTRA_TRACK_CATEGORY, null),
bundle.getString(EXTRA_TRACK_ICON, null),
bundle.getString(EXTRA_TRACK_DESCRIPTION, null),
contentProviderUtils);
}
private void startDashboardAPI(@NonNull Track.Id trackId, @NonNull Bundle bundle) {
String targetPackage = bundle.getString(EXTRA_STATS_TARGET_PACKAGE, null);
String targetClass = bundle.getString(EXTRA_STATS_TARGET_CLASS, null);
if (targetClass != null && targetPackage != null) {
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
}
}
@Override
protected boolean isPostExecuteStopService() {
return false;
}
@Override
protected boolean isStartServiceForeground() {
return true;
}
}
@@ -5,12 +5,15 @@ import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.concurrent.TimeUnit;
import de.dennisguse.opentracks.data.models.TrackPoint;
/**
@@ -29,7 +32,7 @@ public class AltitudeSumManager implements SensorEventListener {
private Float altitudeGain_m;
private Float altitudeLoss_m;
public void start(Context context) {
public void start(Context context, Handler handler) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
@@ -37,7 +40,7 @@ public class AltitudeSumManager implements SensorEventListener {
Log.w(TAG, "No pressure sensor available.");
isConnected = false;
} else {
isConnected = sensorManager.registerListener(this, pressureSensor, SensorManager.SENSOR_DELAY_FASTEST);
isConnected = sensorManager.registerListener(this, pressureSensor, (int) TimeUnit.SECONDS.toMicros(5), handler);
}
lastAcceptedPressureValue_hPa = Float.NaN;
@@ -136,9 +136,9 @@ public abstract class BluetoothConnectionManager<DataType> {
Log.d(TAG, "Connecting to: " + device);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
bluetoothGatt = device.connectGatt(context, true, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, this.observer.getHandler());
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, this.observer.getHandler());
} else {
bluetoothGatt = device.connectGatt(context, true, connectCallback);
bluetoothGatt = device.connectGatt(context, false, connectCallback);
}
SensorData<?> sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
observer.onChanged(sensorData);
@@ -35,7 +35,7 @@ import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
/**
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
@@ -162,7 +162,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
} else {
connectionManager.disconnect();
}
if (!PermissionUtils.hasBluetoothPermissions(context)) {
if (!PermissionRequester.BLUETOOTH.hasPermission(context)) {
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
}
@@ -11,7 +11,6 @@ import java.time.Duration;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.UnitConversions;
import de.dennisguse.opentracks.sensors.UintUtils;
/**
@@ -68,8 +67,9 @@ public final class SensorDataCycling {
public void compute(CyclingCadence previous) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null;
return;
@@ -82,8 +82,7 @@ public final class SensorDataCycling {
}
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
float cadence_ms = crankDiff / timeDiff_ms;
value = Cadence.of((float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN));
value = Cadence.of(crankDiff, timeDiff);
}
}
@@ -147,7 +146,7 @@ public final class SensorDataCycling {
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;
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute speed.");
@@ -9,12 +9,10 @@ import de.dennisguse.opentracks.data.models.Track;
public class RecordingStatus {
private final Track.Id trackId;
private final boolean paused;
@VisibleForTesting
RecordingStatus(Track.Id trackId, boolean paused) {
RecordingStatus(Track.Id trackId) {
this.trackId = trackId;
this.paused = paused;
}
public Track.Id getTrackId() {
@@ -25,24 +23,12 @@ public class RecordingStatus {
return trackId != null;
}
public boolean isPaused() {
return paused;
}
public boolean isRecordingAndNotPaused() {
return isRecording() && !isPaused();
}
static RecordingStatus notRecording() {
return new RecordingStatus(null, false);
return new RecordingStatus(null);
}
static RecordingStatus record(@NonNull Track.Id trackId) {
return new RecordingStatus(trackId, false);
}
RecordingStatus pause() {
return new RecordingStatus(getTrackId(), true);
return new RecordingStatus(trackId);
}
public RecordingStatus stop() {
@@ -54,7 +40,6 @@ public class RecordingStatus {
public String toString() {
return "RecordingStatus{" +
"trackId=" + trackId +
", paused=" + paused +
'}';
}
@@ -63,11 +48,11 @@ public class RecordingStatus {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RecordingStatus that = (RecordingStatus) o;
return paused == that.paused && Objects.equals(trackId, that.trackId);
return Objects.equals(trackId, that.trackId);
}
@Override
public int hashCode() {
return Objects.hash(trackId, paused);
return Objects.hash(trackId);
}
}
@@ -15,12 +15,14 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class TrackDeleteService extends Service {
@@ -31,7 +33,7 @@ public class TrackDeleteService extends Service {
private final Binder binder = new Binder();
private ExecutorService serviceExecutor;
private MutableLiveData<DeleteStatus> deleteResultObservable;
private MutableLiveData<DeletionFinishedStatus> deleteResultObservable;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
@@ -59,22 +61,18 @@ public class TrackDeleteService extends Service {
return START_NOT_STICKY;
}
private void deleteTracks(@NonNull ArrayList<Track.Id> trackIds) {
sendResult(null,0, trackIds.size());
private void deleteTracks(@NonNull List<Track.Id> trackIds) {
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());
}
contentProviderUtils.deleteTracks(this, trackIds);
sendResult(trackIds);
stopSelf();
});
}
private void sendResult(Track.Id trackId, int deletes, int total) {
private void sendResult(List<Track.Id> trackIds) {
if (deleteResultObservable != null) {
deleteResultObservable.postValue(new DeleteStatus(trackId, deletes, total));
deleteResultObservable.postValue(new DeletionFinishedStatus(trackIds));
}
}
@@ -83,7 +81,7 @@ public class TrackDeleteService extends Service {
return binder;
}
public LiveData<DeleteStatus> getDeletingStatusObservable() {
public LiveData<DeletionFinishedStatus> getDeletingStatusObservable() {
return deleteResultObservable;
}
@@ -106,25 +104,13 @@ public class TrackDeleteService extends Service {
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))
.setContentText(this.getString(R.string.track_delete_number_of_tracks, tracksToDelete))
.setSmallIcon(R.drawable.ic_logo_color_24dp)
.setProgress(tracksToDelete, 0, false);
.setProgress(0, 0, true);
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() {
@@ -136,36 +122,25 @@ public class TrackDeleteService extends Service {
}
}
public static class DeleteStatus {
private final int progress;
private final int max;
private final Track.Id trackId;
public static class DeletionFinishedStatus {
private final List<Track.Id> trackIds;
/**
* @param trackId Track.Id just deleted if any.
* @param progress number of deletes.
* @param max total of deletes to be done.
* @param trackIds List of deleted Track.Ids.
*/
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;
private DeletionFinishedStatus(@Nullable List<Track.Id> trackIds) {
this.trackIds = trackIds;
}
public boolean isDeleted(Track.Id trackId) {
return this.trackId != null && this.trackId.equals(trackId);
return this.trackIds != null && this.trackIds.contains(trackId);
}
@NonNull
@Override
public String toString() {
return "DeleteStatus{" +
"number of deletes=" + progress +
", total=" + max +
"trackIds=" + trackIds +
'}';
}
}
@@ -10,6 +10,7 @@ import android.util.Pair;
import androidx.annotation.NonNull;
import java.time.Duration;
import java.time.ZoneOffset;
import de.dennisguse.opentracks.R;
@@ -82,25 +83,23 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
return trackId;
}
//TODO Handle non-existing trackId? Start a new track or exception?
void resumeExistingTrack(@NonNull Track.Id resumeTrackId, @NonNull TrackPointCreator trackPointCreator) {
/**
* @return if the recording could be started.
*/
boolean resumeExistingTrack(@NonNull Track.Id resumeTrackId, @NonNull TrackPointCreator trackPointCreator) {
trackId = resumeTrackId;
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "Ignore resumeTrack. Track " + trackId.getId() + " does not exists.");
return;
return false;
}
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
onNewTrackPoint(trackPointCreator.createSegmentStartManual());
reset();
}
void pause(TrackPointCreator trackPointCreator) {
insertTrackPoint(trackPointCreator.createSegmentEnd(), true);
reset();
return true;
}
void end(TrackPointCreator trackPointCreator) {
@@ -182,8 +181,16 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
}
if (!trackPoint.hasLocation() && !trackPoint.hasSensorDistance()) {
Log.d(TAG, "Ignoring TrackPoint as it has no distance.");
return false;
Duration minStorageInterval = Duration.ofSeconds(10); // TODO Should be configurable.
boolean shouldStore = lastStoredTrackPoint.getTime().plus(minStorageInterval)
.isBefore(trackPoint.getTime());
if (!shouldStore) {
Log.d(TAG, "Ignoring TrackPoint as it has no distance (and sensor data is not new enough).");
return false;
} else {
insertTrackPoint(trackPoint, true);
return true;
}
}
Distance distanceToLastStoredTrackPoint;
@@ -30,7 +30,6 @@ import androidx.lifecycle.MutableLiveData;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
@@ -130,7 +129,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
trackRecordingManager = null;
// Reverse order from onCreate
showNotification(false); //TODO Why?
stopForeground(true);
notificationManager.stop();
notificationManager = null;
@@ -178,23 +178,16 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public void resumeTrack(Track.Id trackId) {
trackPointCreator.reset();
trackRecordingManager.resumeExistingTrack(trackId, trackPointCreator);
if (!trackRecordingManager.resumeExistingTrack(trackId, trackPointCreator)) {
Log.w(TAG, "Cannot resume a non-existing track.");
return;
}
updateRecordingStatus(RecordingStatus.record(trackId));
startRecording();
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void resumeCurrentTrack() {
if (!isRecording() || !isPaused()) {
Log.w(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
return;
}
resumeTrack(recordingStatus.getTrackId());
}
private void startRecording() {
// Update instance variables
handler.postDelayed(updateRecordingData, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
@@ -213,7 +206,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private void startSensors() {
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
trackPointCreator.start(this, handler);
showNotification(true);
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
}
public void endCurrentTrack() {
@@ -222,46 +215,18 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return;
}
// Need to remember the recordingTrackId before setting it to null
boolean wasPause = isPaused();
Track.Id trackId = recordingStatus.getTrackId();
// Set recording status
updateRecordingStatus(STATUS_DEFAULT);
if (!wasPause) {
trackRecordingManager.end(trackPointCreator);
}
endRecording(true);
trackRecordingManager.end(trackPointCreator);
endRecording();
stopSelf();
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void pauseCurrentTrack() {
if (!isRecording() || isPaused()) {
Log.w(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
return;
}
// Set recording status
updateRecordingStatus(recordingStatus.pause());
trackRecordingManager.pause(trackPointCreator);
endRecording(false);
notificationManager.updateContent(getString(R.string.generic_paused));
}
private void endRecording(boolean trackStopped) {
private void endRecording() {
stopUpdateRecordingData();
if (!trackStopped) {
updateRecordingDataWhileRecording();
} else {
recordingDataObservable.postValue(NOT_RECORDING);
}
recordingDataObservable.postValue(NOT_RECORDING);
voiceAnnouncementManager.stop();
@@ -283,14 +248,15 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
if (!isRecording()) return;
trackPointCreator.stop();
showNotification(false);
stopForeground(true);
notificationManager.cancelNotification();
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
}
@Override
public boolean newTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) {
if (!isRecording() || isPaused()) {
Log.w(TAG, "Ignore newTrackPoint. Not recording or paused.");
if (!isRecording()) {
Log.w(TAG, "Ignore newTrackPoint. Not recording.");
return false;
}
@@ -309,21 +275,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
}
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this, recordingStatus.getTrackId()));
}
if (!isRecording() && isGpsStarted) {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
}
if (!isRecording() && !isGpsStarted) {
stopForeground(true);
notificationManager.cancelNotification();
}
}
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
if (!isRecording() || isPaused()) {
if (!isRecording()) {
return null;
}
@@ -398,9 +351,4 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public boolean isRecording() {
return recordingStatus.isRecording();
}
@VisibleForTesting
public boolean isPaused() {
return recordingStatus.isPaused();
}
}
@@ -28,6 +28,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R;
@@ -66,15 +67,21 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
/**
* Starts and binds the service.
*
* @param foreground is the service expected to call `startForeground()`?
*/
public void startAndBind(Context context) {
public void startAndBind(Context context, boolean foreground) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
Log.i(TAG, "Starting the service.");
context.startService(new Intent(context, TrackRecordingService.class));
if (foreground) {
ContextCompat.startForegroundService(context, new Intent(context, TrackRecordingService.class));
} else {
context.startService(new Intent(context, TrackRecordingService.class));
}
startConnection(context);
}
@@ -88,7 +95,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
@Deprecated
public void startAndBindWithCallback(Context context) {
if (trackRecordingService == null) {
startAndBind(context);
startAndBind(context, false);
return;
}
if (callback != null) {
@@ -167,17 +174,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
setTrackRecordingService(null);
}
public void resumeTrack() {
if (trackRecordingService != null) {
trackRecordingService.resumeCurrentTrack();
}
}
public void pauseTrack() {
if (trackRecordingService != null) {
trackRecordingService.pauseCurrentTrack();
}
}
@Nullable
public Marker.Id addMarker(Context context, String name, String category, String description, String photoUrl) {
TrackRecordingService trackRecordingService = getServiceIfBound();
@@ -68,6 +68,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
}
void stop() {
cancelNotification();
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(this);
}
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.services.announcement;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.speech.tts.TextToSpeech;
@@ -34,8 +35,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -56,6 +57,10 @@ public class VoiceAnnouncement {
private final AudioManager audioManager;
private final ContentProviderUtils contentProviderUtils;
private TrackPoint.Id startTrackPointId = null;
private IntervalStatistics intervalStatistics;
private Distance intervalDistance;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
@@ -116,6 +121,8 @@ public class VoiceAnnouncement {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
contentProviderUtils = new ContentProviderUtils(context);
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
intervalStatistics = new IntervalStatistics(intervalDistance);
}
public void start() {
@@ -132,7 +139,10 @@ public class VoiceAnnouncement {
if (ttsFallback == null) {
Log.w(TAG, "MediaPlayer for ttsFallback could not be created.");
} else {
ttsFallback.setAudioStreamType(AUDIO_STREAM);
ttsFallback.setAudioAttributes(
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build());
ttsFallback.setLooping(false);
}
}
@@ -165,24 +175,27 @@ public class VoiceAnnouncement {
return;
}
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
if (currentIntervalDistance != intervalDistance) {
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
intervalDistance = currentIntervalDistance;
startTrackPointId = null;
}
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
boolean isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
//TODO Do not load all trackpoints for every announcement
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(unitSystem));
intervalStatistics.addTrackPoints(trackPointIterator);
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
if (track.getId() != null) {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics);
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track.getCategory()), lastInterval, sensorStatistics);
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
if (announcement.length() > 0) {
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
}
}
public void stop() {
@@ -1,7 +1,12 @@
package de.dennisguse.opentracks.services.announcement;
import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceHeartRate;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageHeartRate;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceMovingTime;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
import android.content.Context;
import android.text.Spannable;
@@ -52,68 +57,68 @@ class VoiceAnnouncementUtils {
unitDistanceTTS = "mile";
unitSpeedTTS = "mile per hour";
break;
case NAUTICAL_IMPERIAL:
perUnitStringId = R.string.voice_per_nautical_mile;
distanceId = R.plurals.voiceDistanceNauticalMiles;
speedId = R.plurals.voiceSpeedMKnots;
unitDistanceTTS = "nautical mile";
unitSpeedTTS = "knots";
break;
default:
throw new RuntimeException("Not implemented");
}
double distanceInUnit = distance.toKM_Miles(unitSystem);
builder.append(context.getString(R.string.total_distance));
long distanceIntegerPart = (long) distanceInUnit;
// Extract the decimal part
String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2);
// Units should always be english singular for TTS.
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, unitDistanceTTS);
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
if (shouldVoiceAnnounceTotalDistance()) {
builder.append(context.getString(R.string.total_distance));
// Units should always be english singular for TTS.
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 2, unitDistanceTTS);
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
}
if (distance.isZero()) {
return builder;
}
// Announce time
Duration movingTime = trackStatistics.getMovingTime();
if (!movingTime.isZero()) {
if (shouldVoiceAnnounceMovingTime() && !movingTime.isZero()) {
appendDuration(context, builder, movingTime);
builder.append(".");
}
if (isReportSpeed) {
double speedInUnit = distancePerTime.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.speed));
long speedIntegerPart = (long) speedInUnit;
// Extract the decimal part
String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, unitSpeedTTS);
builder.append(".");
if (currentDistancePerTime != null) {
if (shouldVoiceAnnounceAverageSpeedPace()) {
double speedInUnit = distancePerTime.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.speed));
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
builder.append(".");
}
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) {
builder.append(" ")
.append(context.getString(R.string.lap_speed));
long currentDistanceIntegerPart = (long) currentDistancePerTimeInUnit;
// Extract the decimal part
String currentDistanceFractionalPart = String.format("%.1f", (currentDistancePerTimeInUnit - currentDistanceIntegerPart)).substring(2);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, unitSpeedTTS);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
builder.append(".");
}
}
} else {
Duration time = distancePerTime.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.pace));
appendDuration(context, builder, time);
builder.append(" ")
.append(context.getString(perUnitStringId))
.append(".");
if (shouldVoiceAnnounceAverageSpeedPace()) {
Duration time = distancePerTime.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.pace));
appendDuration(context, builder, time);
builder.append(" ")
.append(context.getString(perUnitStringId))
.append(".");
}
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(unitSystem) : Duration.ofMillis(0);
if (!currentTime.isZero()) {
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
Duration currentTime = currentDistancePerTime.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.lap_time));
appendDuration(context, builder, currentTime);
@@ -123,24 +128,21 @@ class VoiceAnnouncementUtils {
}
}
if (shouldVoiceAnnounceHeartRate()) {
if (sensorStatistics != null && sensorStatistics.hasHeartRate()) {
int averageHeartRate = Math.round(sensorStatistics.getAvgHeartRate().getBPM());
if (shouldVoiceAnnounceAverageHeartRate() && sensorStatistics != null && sensorStatistics.hasHeartRate()) {
int averageHeartRate = Math.round(sensorStatistics.getAvgHeartRate().getBPM());
builder.append(" ")
.append(context.getString(R.string.average_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate);
builder.append(".");
}
builder.append(" ")
.append(context.getString(R.string.average_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate);
builder.append(".");
}
if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) {
int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM());
if (currentInterval != null && currentInterval.hasAverageHeartRate()) {
int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM());
builder.append(" ")
.append(context.getString(R.string.current_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate);
builder.append(".");
}
builder.append(" ")
.append(context.getString(R.string.current_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate);
builder.append(".");
}
return builder;
@@ -156,26 +158,34 @@ class VoiceAnnouncementUtils {
int seconds = (int) (duration.getSeconds() % 60);
if (hours > 0) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, null, "hour");
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, 0, "hour");
}
if (minutes > 0) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, null, "minute");
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, 0, "minute");
}
if (seconds > 0 || duration.isZero()) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, null, "second");
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, 0, "second");
}
}
/**
* Speaks as: 98.14 [UNIT] - ninety eight point one four [UNIT with correct plural form]
*
* @param number The number to speak
* @param precision The number of decimal places to announce
*/
private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, long integerPart, @Nullable String fractionalPart, @NonNull String unit) {
private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, double number, int precision, @NonNull String unit) {
TtsSpan.MeasureBuilder measureBuilder = new TtsSpan.MeasureBuilder()
.setUnit(unit);
if (fractionalPart == null) {
measureBuilder.setNumber(integerPart);
if (precision == 0) {
measureBuilder.setNumber((long)number);
} else {
// Round before extracting integral and decimal parts
double roundedNumber = Math.round(Math.pow(10, precision) * number) / Math.pow(10.0, precision);
long integerPart = (long) roundedNumber;
// Extract the decimal part
String fractionalPart = String.format("%." + precision + "f", (roundedNumber - integerPart)).substring(2);
measureBuilder.setIntegerPart(integerPart)
.setFractionalPart(fractionalPart);
}
@@ -19,7 +19,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
@@ -54,7 +54,7 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
//TODO upgrade to AGP7.0.0/API31 started complaining about removeUpdates.
public void onStop() {
if (locationManager != null && context != null) {
if (PermissionUtils.hasGPSPermission(context)) {
if (PermissionRequester.GPS.hasPermission(context)) {
locationManager.removeUpdates(this);
}
locationManager = null;
@@ -152,7 +152,7 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
Log.e(TAG, "Not started.");
return;
}
if (PermissionUtils.hasGPSPermission(context)) {
if (PermissionRequester.GPS.hasPermission(context)) {
try {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, gpsInterval.toMillis(), 0, this);
} catch (SecurityException e) {
@@ -23,7 +23,7 @@ class GpsStatus {
private static final String TAG = GpsStatus.class.getSimpleName();
// The duration that GpsStatus waits from minimal interval to consider GPS lost.
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(10);
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(30);
private Distance thresholdHorizontalAccuracy;
// Threshold for time without points.
@@ -58,7 +58,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
altitudeSumManager = new AltitudeSumManager();
remoteSensorManager.start();
altitudeSumManager.start(context);
altitudeSumManager.start(context, handler);
}
@@ -74,6 +74,7 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
entriesId = R.array.stats_rate_metric_options;
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
entriesId = R.array.stats_rate_imperial_options;
break;
default:
@@ -9,7 +9,7 @@ import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import java.util.Locale;
import java.util.Map;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -46,27 +46,15 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
}
private void setExportTrackFileFormatOptions() {
final TrackFileFormat[] trackFileFormats = {
Map<String, String> options = TrackFileFormat.toPreferenceIdLabelMap(getResources(),
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA,
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
TrackFileFormat.GPX,
TrackFileFormat.CSV
};
String[] entries = new String[trackFileFormats.length];
String[] entryValues = new String[trackFileFormats.length];
for (int i = 0; i < entries.length; i++) {
TrackFileFormat trackFileFormat = trackFileFormats[i];
String trackFileFormatUpperCase = trackFileFormat.getExtension().toUpperCase(Locale.US); //ASCII upper case
int photoMessageId = trackFileFormat.includesPhotos() ? R.string.export_with_photos : R.string.export_without_photos;
entries[i] = String.format("%s (%s)", trackFileFormatUpperCase, getString(photoMessageId));
entryValues[i] = trackFileFormat.getPreferenceId();
}
TrackFileFormat.CSV);
ListPreference listPreference = findPreference(getString(R.string.export_trackfileformat_key));
listPreference.setEntries(entries);
listPreference.setEntryValues(entryValues);
listPreference.setEntries(options.values().toArray(new String[0]));
listPreference.setEntryValues(options.keySet().toArray(new String[0]));
}
private void setExportDirectorySummary() {
@@ -2,31 +2,19 @@ package de.dennisguse.opentracks.settings;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
public class MainSettingsFragment extends PreferenceFragmentCompat {
private static final String TAG = MainSettingsFragment.class.getSimpleName();
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private final TrackRecordingServiceConnection.Callback bindServiceCallback =
(service, unused) -> service.getRecordingStatusObservable()
.observe(MainSettingsFragment.this, this::onRecordingStatusChanged);
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindServiceCallback);
findPreference(getString(R.string.settings_defaults_key)).setOnPreferenceClickListener(preference -> {
((SettingsActivity) getActivity()).openScreen(getString(R.string.settings_defaults_key));
return true;
@@ -69,20 +57,7 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
}
@Override
public void onResume() {
super.onResume();
trackRecordingServiceConnection.bind(getContext());
updatePrefsDependOnRecording();
}
@Override
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection.unbind(getContext());
}
@Override
public void onDisplayPreferenceDialog(Preference preference) {
public void onDisplayPreferenceDialog(@NonNull Preference preference) {
DialogFragment dialogFragment = null;
if (preference instanceof ResetDialogPreference) {
@@ -97,24 +72,4 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
super.onDisplayPreferenceDialog(preference);
}
private void onRecordingStatusChanged(RecordingStatus status) {
this.recordingStatus = status;
if (isAdded()) {
updatePrefsDependOnRecording();
}
}
private void updatePrefsDependOnRecording() {
Preference importExportPreference = findPreference(getString(R.string.settings_import_export_key));
Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
boolean isRecording = recordingStatus.isRecording();
importExportPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : getString(R.string.settings_import_export_summary));
importExportPreference.setEnabled(!isRecording);
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : getString(R.string.settings_reset_summary));
resetPreference.setEnabled(!isRecording);
}
}
@@ -47,6 +47,7 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
@@ -213,7 +214,6 @@ public class PreferencesUtils {
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
public static void applyDefaultUnit() {
if (getString(R.string.stats_units_key, "").equals("")) {
if (!Locale.US.equals(Locale.getDefault())) {
setUnit(UnitSystem.METRIC);
} else {
@@ -342,13 +342,58 @@ public class PreferencesUtils {
return getFloat(R.string.voice_speed_rate_key, DEFAULT);
}
public static boolean shouldVoiceAnnounceHeartRate() {
return getBoolean(R.string.voice_announce_heart_rate_key, false);
public static boolean shouldVoiceAnnounceTotalDistance() {
return getBoolean(R.string.voice_announce_total_distance_key, true);
}
@VisibleForTesting
public static void setVoiceAnnounceHeartRate(boolean value) {
setBoolean(R.string.voice_announce_heart_rate_key, value);
public static void setVoiceAnnounceTotalDistance(boolean value) {
setBoolean(R.string.voice_announce_total_distance_key, value);
}
public static boolean shouldVoiceAnnounceMovingTime() {
return getBoolean(R.string.voice_announce_moving_time_key, true);
}
@VisibleForTesting
public static void setVoiceAnnounceMovingTime(boolean value) {
setBoolean(R.string.voice_announce_moving_time_key, value);
}
public static boolean shouldVoiceAnnounceAverageSpeedPace() {
return getBoolean(R.string.voice_announce_average_speed_pace_key, true);
}
@VisibleForTesting
public static void setVoiceAnnounceAverageSpeedPace(boolean value) {
setBoolean(R.string.voice_announce_average_speed_pace_key, value);
}
public static boolean shouldVoiceAnnounceLapSpeedPace() {
return getBoolean(R.string.voice_announce_lap_speed_pace_key, true);
}
@VisibleForTesting
public static void setVoiceAnnounceLapSpeedPace(boolean value) {
setBoolean(R.string.voice_announce_lap_speed_pace_key, value);
}
public static boolean shouldVoiceAnnounceLapHeartRate() {
return getBoolean(R.string.voice_announce_lap_heart_rate_key, false);
}
@VisibleForTesting
public static void setVoiceAnnounceLapHeartRate(boolean value) {
setBoolean(R.string.voice_announce_lap_heart_rate_key, value);
}
public static boolean shouldVoiceAnnounceAverageHeartRate() {
return getBoolean(R.string.voice_announce_average_heart_rate_key, false);
}
@VisibleForTesting
public static void setVoiceAnnounceAverageHeartRate(boolean value) {
setBoolean(R.string.voice_announce_average_heart_rate_key, value);
}
public static Distance getRecordingDistanceInterval() {
@@ -373,6 +418,7 @@ public class PreferencesUtils {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setUnit(unitSystem)
.setDecimalCount(0)
.setThreshold(Double.MAX_VALUE)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
@@ -388,6 +434,7 @@ public class PreferencesUtils {
}
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
@@ -416,6 +463,7 @@ public class PreferencesUtils {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setThreshold(Double.MAX_VALUE)
.setUnit(unitSystem)
.build(resources);
for (int i = 0; i < entryValues.length; i++) {
@@ -432,6 +480,7 @@ public class PreferencesUtils {
}
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else {
@@ -488,6 +537,7 @@ public class PreferencesUtils {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(0)
.setThreshold(Double.MAX_VALUE)
.setUnit(unitSystem)
.build(resources);
@@ -509,6 +559,7 @@ public class PreferencesUtils {
}
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else if (value == recordingGPSAccuracyExcellent) {
@@ -560,6 +611,14 @@ public class PreferencesUtils {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
}
break;
case NAUTICAL_IMPERIAL:
double valueKnots = Speed.ofKMH(value).toKnots();
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_knots_recommended, valueKnots);
} else {
entries[i] = resources.getString(R.string.value_float_knots, valueKnots);
}
break;
default:
throw new RuntimeException("Not implemented");
}
@@ -761,4 +820,26 @@ public class PreferencesUtils {
public static void clear() {
sharedPreferences.edit().clear().commit();
}
public static void setShowOnMapFormat(final String showOnMapFormat) {
setString(R.string.show_on_map_format_key, showOnMapFormat);
}
public static String getShowOnMapFormat() {
return getString(R.string.show_on_map_format_key, IntentDashboardUtils.PREFERENCE_ID_ASK);
}
public static int getTotalRowsDeleted() {
return getInt(R.string.total_rows_deleted_key, 0);
}
public static void addTotalRowsDeleted(final int totalRowsDeletedToAdd) {
int newTotalRowsDeleted = getTotalRowsDeleted() + totalRowsDeletedToAdd;
setInt(R.string.total_rows_deleted_key, newTotalRowsDeleted);
}
public static void resetTotalRowsDeleted() {
setInt(R.string.total_rows_deleted_key, 0);
}
}
@@ -4,6 +4,7 @@ import android.os.Bundle;
import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R;
public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
@@ -17,5 +18,7 @@ public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
public void onStart() {
super.onStart();
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_api_title);
findPreference(getString(R.string.publicapi_package_key))
.setSummary(BuildConfig.APPLICATION_ID);
}
}
@@ -11,7 +11,6 @@ import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.databinding.SettingsBinding;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
public class SettingsActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
@@ -4,7 +4,8 @@ import de.dennisguse.opentracks.R;
public enum UnitSystem {
METRIC(R.string.stats_units_metric),
IMPERIAL(R.string.stats_units_imperial);
IMPERIAL(R.string.stats_units_imperial),
NAUTICAL_IMPERIAL(R.string.stats_units_nautical); // Nautical miles with feet
private final int preferenceIdentifier;
@@ -5,10 +5,15 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.fragment.app.DialogFragment;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import java.util.Map;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
public class UserInterfaceSettingsFragment extends PreferenceFragmentCompat {
@@ -28,6 +33,8 @@ public class UserInterfaceSettingsFragment extends PreferenceFragmentCompat {
startActivity(intent);
return true;
});
setShowOnMapFormatOptions();
}
@Override
@@ -64,4 +71,14 @@ public class UserInterfaceSettingsFragment extends PreferenceFragmentCompat {
super.onDisplayPreferenceDialog(preference);
}
private void setShowOnMapFormatOptions() {
Map<String, String> options = TrackFileFormat.toPreferenceIdLabelMap(getResources(), IntentDashboardUtils.SHOW_ON_MAP_TRACK_FILE_FORMATS);
options.put(IntentDashboardUtils.PREFERENCE_ID_DASHBOARD, getString(R.string.show_on_dashboard));
options.put(IntentDashboardUtils.PREFERENCE_ID_ASK, getString(R.string.show_on_map_format_ask));
ListPreference listPreference = findPreference(getString(R.string.show_on_map_format_key));
listPreference.setEntries(options.values().toArray(new String[0]));
listPreference.setEntryValues(options.keySet().toArray(new String[0]));
}
}
@@ -29,7 +29,7 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
/**
* Preference to select a discoverable Bluetooth LE device.
@@ -145,20 +145,18 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
bluetoothIcon = AnimatedVectorDrawableCompat.create(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
bluetoothIcon.start();
if (!PermissionUtils.hasBluetoothPermissions(getContext())) {
PermissionUtils.requestBluetoothPermission(this,
this::startBluetoothScan,
() -> {
if (PermissionUtils.shouldShowRequestPermissionRationaleBluetooth(this)) {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
}
dismiss();
});
} else {
startBluetoothScan();
}
PermissionRequester.BLUETOOTH.requestPermissionsIfNeeded(getContext(), this,
this::startBluetoothScan,
(requester) -> {
if (requester.shouldShowRequestPermissionRationale(this)) {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
}
dismiss();
});
startBluetoothScan();
}
private void startBluetoothScan() {

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