Compare commits

...

187 Commits

Author SHA1 Message Date
karol 63e175c76a chore: rename applicationId and app name to isolate fork from official app
CI Pipeline / Android Lint (push) Successful in 1m34s
CI Pipeline / Unit Tests (push) Successful in 1m10s
CI Pipeline / Build and Upload APK (push) Successful in 2m24s
2026-09-18 20:31:23 +02:00
karol 8142073abd chore: trigger Gitea Actions
CI Pipeline / Android Lint (push) Successful in 2m4s
CI Pipeline / Unit Tests (push) Successful in 1m8s
CI Pipeline / Build and Upload APK (push) Successful in 2m30s
2026-09-18 19:38:36 +02:00
karol 149c620525 ci: use PERSONAL_ACCESS_TOKEN for auto-update to trigger downstream pipelines 2026-09-18 18:57:04 +02:00
karol c96b0e3617 ci: add scheduled workflow to auto-sync upstream changes 2026-09-18 18:55:15 +02:00
karol 268c27dc4a build: output both signed release and debug APKs 2026-09-18 18:52:19 +02:00
karol 3146785498 ci: use dynamic run_number for release tags to avoid conflicts 2026-09-18 18:43:02 +02:00
karol 11589dfdd3 ci: use native gitea-release-action for uploading apks 2026-09-18 18:36:11 +02:00
karol 96828febdb ci: fix apk artifacts path for release action 2026-09-18 14:38:57 +02:00
karol 98ae6fe0d2 ci: merge workflows into a single pipeline with proper job dependencies 2026-09-11 16:30:13 +02:00
karol 2e27b06c01 fix(lint): add missing default translations to fix CI 2026-09-11 16:25:38 +02:00
karol 8db8b58020 trigger ci for ncipollo release 2026-09-11 16:09:08 +02:00
karol 9a9e1e6947 ci: swap release action to ncipollo for gitea compatibility 2026-09-11 16:06:43 +02:00
karol d9880f259d trigger ci for release test 2026-09-11 16:00:04 +02:00
karol 08b5ab381e ci: grant write permissions for release upload 2026-09-11 15:21:25 +02:00
karol a63298ef6e ci: use -node image variant to fix act runner checkout issue, update actions version 2026-09-11 15:08:38 +02:00
karol 1294e944a7 ci: use cimg/android:2024.04 image for runner compatibility 2026-09-11 15:00:00 +02:00
karol 7258335180 ci: fix runner labels to match homelab act_runner (ubuntu-latest) 2026-09-11 14:52:17 +02:00
karol 139d72affe ci: fix runner labels to match homelab act_runner (ubuntu-latest) 2026-09-11 14:50:37 +02:00
karol 3791ccf888 trigger ci 2026-09-11 14:43:34 +02:00
karol dc78615c83 ci: add auto-build and release workflow 2026-09-11 14:40:08 +02:00
karol 53dfa915fb Merge upstream updates into local fork 2026-09-11 14:10:42 +02:00
karol 7c6141f98c feat: prioritize Bluetooth sensor data over GPS
- Restructured sensor logic to give absolute priority to Bluetooth sensors, largely ignoring GPS data for distance and speed calculations.
- Implemented downstream UI expansions, custom recording layouts, and new settings to reflect and control the new sensor-first flow.
- Added MediaNotificationListenerService and integrated media playback tracking within statistics.
- Modified VoiceAnnouncementUtils to harmonize announcements with the updated architecture.
- Added 'About This Fork' section to the README.

Note: These modifications were co-created with Antigravity AI.
2026-09-11 14:10:42 +02:00
gallegonovato f8279c7095 Translated using Weblate (Spanish)
Currently translated at 100.0% (551 of 551 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/es/
2026-09-05 10:26:13 +00:00
Kito 465e3e9be8 Translated using Weblate (Indonesian)
Currently translated at 3.6% (7 of 191 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/id/
2026-09-05 10:26:13 +00:00
gallegonovato f38999a970 Translated using Weblate (Spanish)
Currently translated at 23.5% (45 of 191 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/es/
2026-09-05 10:26:13 +00:00
Dennis Guse 4dfd2485bf Bugfix: avoid crash in exporter. 2026-09-03 17:50:47 +02:00
Dennis Guse 10273cd772 TrackRecordingManager: rather crash than try to load invalid data. 2026-09-03 17:50:47 +02:00
Bone NI fbfe6f3ab8 Added translation using Weblate (Lao) 2026-09-02 09:54:56 +00:00
love80312 86726c5e0e Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (551 of 551 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hant/
2026-09-02 09:54:55 +00:00
love80312 b711c4fdc5 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 1.5% (3 of 190 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/zh_Hant/
2026-09-02 09:54:55 +00:00
CI/CD Bot by pstorch cefdb1280a chore(deps): update dependency com.android.tools.build:gradle to v9.4.0 2026-09-01 14:35:00 +00:00
Dennis Guse 91b379c328 Release: v4.28.3 2026-08-31 20:07:23 +02:00
Dennis Guse e28a84fc88 Allow to disable segment creation (max recording distance).
Fixes #2389.
2026-08-28 07:07:06 +02:00
Dennis Guse c2d8d06204 Merge remote-tracking branch 'weblate/main' 2026-08-28 06:47:06 +02:00
José Rebelo 7ca871c699 Bugfix: Respect declared namespace prefixes 2026-08-26 15:21:06 +01:00
Dennis Guse 337c50eb91 Cleanup: qName. 2026-08-26 07:34:22 +02:00
Dany Mestas 2ff8d21897 GPX: match activity types case-insensitively on import. 2026-08-25 22:23:57 +02:00
Dennis Guse 2d4e987812 GPX: export moving speed in summary. 2026-08-24 17:26:11 +02:00
CI/CD Bot by pstorch fb928ccc45 chore(deps): update dependency com.android.tools.build:gradle to v9.3.2 2026-08-24 03:34:20 +00:00
Dennis Guse ab8791a620 Release: v4.28.2 2026-08-22 19:20:38 +02:00
Dennis Guse 7c03085761 GPX: on import ignore TrackPoints without time.
Previous behavior: abort import.

Fixes #2384.
2026-08-19 20:45:23 +02:00
Dennis Guse 12a8686896 Cleanup. 2026-08-19 20:38:05 +02:00
Dennis Guse e12bb967e6 Cleanup. 2026-08-19 20:29:25 +02:00
Dennis Guse d56b018676 Bugfix: when stopping a recording, immediately stop the idle detection timer.
Fixes #2386.
2026-08-19 20:29:25 +02:00
CI/CD Bot by pstorch a6fd701004 chore(deps): update dependency gradle to v9.7.1 2026-08-19 15:34:29 +00:00
Dennis Guse ce26e20571 Sensor: on start register GPS only once.
Part of #2369.
2026-08-17 15:49:14 +02:00
CI/CD Bot by pstorch d3ff2061c6 chore(deps): update dependency androidx.appcompat:appcompat to v1.8.0 2026-08-12 17:34:26 +00:00
Dennis Guse aa63474114 Release: v4.28.1 2026-08-08 12:06:32 +02:00
CI/CD Bot by pstorch cf545dc17b chore(deps): update dependency gradle to v9.7.0 2026-08-06 14:36:45 +00:00
Dennis Guse a542f45661 Bugfix: HealthConnect doesn't provide granted permissions in time.
Fixes #2376.
2026-08-05 19:16:00 +02:00
Dennis Guse 1eeb706330 Bugfix: HealthConnect requires unique timestamps for locations.
Part of #2376.
2026-08-05 19:13:16 +02:00
Dennis Guse 01ea65742b HealthConnect: send total distance, elevation gain and speed. 2026-08-04 17:25:46 +02:00
Dennis Guse e9456919cb Bug: don't crash in settings if HealthConnect is not installed.
Part of #2376.
2026-08-03 20:27:47 +02:00
Priit Jõerüüt 2a8e1c99b6 Translated using Weblate (Estonian)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-07-29 20:34:20 +02:00
jimkats 245854581b Translated using Weblate (Greek)
Currently translated at 70.3% (390 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-07-29 20:34:20 +02:00
CI/CD Bot by pstorch e372a780f0 Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 2026-07-29 17:43:19 +00:00
Priit Jõerüüt 25a2f978c4 Translated using Weblate (Estonian)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-07-29 03:59:43 +00:00
jimkats 36c3885dce Translated using Weblate (Greek)
Currently translated at 70.3% (390 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-07-29 03:59:43 +00:00
Dennis Guse 00be0b565c Cleanup: removed unused methods and constants. 2026-07-26 15:50:01 +02:00
Dennis Guse 8b1e1cf294 Cleanup. 2026-07-26 15:34:34 +02:00
Dennis Guse ce313ba4fd Cleanup: add missing @NonNull annotation. 2026-07-26 15:33:29 +02:00
Dennis Guse df501bde0d Cleanup: removed empty localization (ia). 2026-07-26 15:30:55 +02:00
Dennis Guse 30ed8d8ba3 Cleanup: CustomContentProvider by inlining into UrlType. 2026-07-24 20:54:16 +02:00
Dennis Guse ea2127846d Cleanup: CustomContentProvider by inlining into UrlType. 2026-07-24 19:51:19 +02:00
Dennis Guse ac0da5e9d1 Cleanup: CustomContentProvider by inlining into UrlType. 2026-07-24 19:51:19 +02:00
Dennis Guse 47fea690c9 FOREGROUND_SERVICE is mandatory.
Revert a1fa95094f
2026-07-24 19:51:01 +02:00
Dennis Guse a1fa95094f Removed unused translations. 2026-07-23 22:39:52 +02:00
Dennis Guse c4a1fb0d98 Cleanup. 2026-07-23 22:39:17 +02:00
Dennis Guse 57cd4b3780 Bugfix: gravel was not selectable.
Introduced in fb25055f3e
2026-07-23 22:34:43 +02:00
Dennis Guse 5c645712ff Fix translations. 2026-07-23 22:31:09 +02:00
Dennis Guse e724baaafa Sensors: Polar Loop (Gen 2) can be used for heartrate. 2026-07-23 22:10:06 +02:00
CI/CD Bot by pstorch f55dcd7162 Update dependency com.android.tools.build:gradle to v9.3.1 2026-07-23 14:36:34 +00:00
Dennis Guse 74169cb819 Code cleanup using static code analysis (Android Studio). 2026-07-21 07:31:25 +02:00
Dennis Guse 1821f47cf1 Code analyze: only use String.replace() instead of String.replaceAll(). 2026-07-21 07:30:19 +02:00
Dennis Guse 925a253bbb Fix tests: PreferencesUtils wasn't visible enough (anymore). 2026-07-21 07:22:51 +02:00
Dennis Guse e89ae1c2df Translations: must use <a>. 2026-07-21 06:39:06 +02:00
Dennis Guse 75058118e0 Remove unused translation. 2026-07-21 06:39:06 +02:00
CI/CD Bot by pstorch 96559230f8 Update dependency com.android.tools.build:gradle to v9.3.0 2026-07-14 14:34:39 +00:00
ghose f16f2cfc64 Translated using Weblate (Galician)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2026-07-12 06:07:51 +00:00
Outbreak2096 7f353a5d13 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2026-07-08 18:08:05 +00:00
SomeTr 2453edaa65 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2026-07-08 18:08:04 +00:00
Priit Jõerüüt 1b1568511a Translated using Weblate (Estonian)
Currently translated at 98.7% (547 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-07-08 18:08:04 +00:00
miler 29a36c3ea7 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/es_419/
2026-07-08 18:08:04 +00:00
gallegonovato 71d0c7cc92 Translated using Weblate (Spanish)
Currently translated at 8.5% (16 of 187 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/es/
2026-07-08 18:08:04 +00:00
Dennis Guse b09d640844 Release: v4.28.0 2026-07-06 18:02:19 +02:00
Dennis Guse a039f1a00c Remove unused translation. 2026-07-06 17:57:23 +02:00
jimkats 3df5c06c20 Translated using Weblate (Greek)
Currently translated at 71.6% (391 of 546 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-07-05 17:08:03 +00:00
hipgnose 2904c65a01 Translated using Weblate (Portuguese (Brazil))
Currently translated at 3.2% (6 of 187 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/pt_BR/
2026-07-05 17:08:02 +00:00
Dennis Guse 4977789004 Implemented basic HealthConnect exporter
Fixes #1542

Co-authored-by: Manuel <manuel@mmarca.tech>
2026-07-03 08:24:42 +02:00
Dennis Guse 95e6448cba Health Connect: minimal implementation for writing route data. 2026-07-03 08:18:44 +02:00
Manuel a66c1b9407 HealthConnect: Rename PermissionsRationaleActivity to HealthConnectPermissionsRationaleActivity 2026-07-03 08:18:44 +02:00
Manuel 9d55ac261f HealthConnect: Wire settings screen 2026-07-03 08:18:44 +02:00
Manuel dbd3e2fc5c HealthConnect: Add settings screen resources 2026-07-03 08:18:44 +02:00
Manuel 713e454e44 HealthConnect: Queue exports in the background 2026-07-03 08:18:44 +02:00
Manuel f0616fc043 HealthConnect: Add exercise session exporter 2026-07-03 08:18:44 +02:00
Manuel 75ed3120bd HealthConnect: Add permissions rationale activity 2026-07-03 08:18:44 +02:00
Peter Storch b22e41cf66 fix #2349: dashboard API does not update while recording if more than one track is selected 2026-06-28 14:56:14 +02:00
Dennis Guse d1f003c0ce Bugfix: voice_announce_lap_power_key should not have been translatable.
For some localizations, users will experience that the setting got reverted.

Fixes #2359.
2026-06-28 13:02:28 +02:00
CI/CD Bot by pstorch 6ef80ade98 Update dependency gradle to v9.6.1 2026-06-28 12:29:00 +02:00
hipgnose e49d9fbd83 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (546 of 546 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/pt_BR/
2026-06-27 08:49:58 +00:00
Dennis Guse c7738b1cb2 Release: v4.27.2 2026-06-24 06:24:33 +02:00
Dennis Guse 713f5c7708 Cleanup. 2026-06-24 06:10:12 +02:00
Dennis Guse b9a36d1b3c Bugfix: storing idle and sensor-based trackpoints might happen in the wrong order (race condition).
Fixes #2356.
2026-06-21 18:50:26 +02:00
Dennis Guse 34e89ea3f9 TimeZoneOffsets now supports minutes.
Fixes #2357.
2026-06-21 13:54:59 +02:00
CI/CD Bot by pstorch 50a0e37086 Update actions/checkout action to v7 2026-06-19 05:44:07 +02:00
CI/CD Bot by pstorch 11ac6b7279 Update dependency gradle to v9.6.0 2026-06-19 00:34:17 +00:00
CI/CD Bot by pstorch aadb79bf7d Update dependency androidx.core:core-location-altitude to v1.0.0 2026-06-17 17:34:17 +00:00
ovl-005 da6b26644e Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.7% (539 of 546 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2026-06-12 21:41:16 +00:00
qwjyh b264655d75 Translated using Weblate (Japanese)
Currently translated at 52.1% (285 of 546 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/ja/
2026-06-09 14:16:51 +00:00
Outbreak2096 1ea9c0b216 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (546 of 546 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2026-06-09 14:16:51 +00:00
SomeTr 8040edac7a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (546 of 546 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2026-06-09 14:16:51 +00:00
ghose 037c79a1c5 Translated using Weblate (Galician)
Currently translated at 100.0% (546 of 546 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2026-06-09 14:16:51 +00:00
Priit Jõerüüt bc46f7447e Translated using Weblate (Estonian)
Currently translated at 100.0% (546 of 546 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-06-09 14:16:51 +00:00
Dennis Guse 6e88a448b1 Release: v4.27.1 2026-06-07 12:24:52 +02:00
jimkats 2dac8d3ea6 Translated using Weblate (Greek)
Currently translated at 71.7% (391 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-06-04 19:41:11 +02:00
CI/CD Bot by pstorch 0844c48d25 Update dependency androidx.core:core to v1.19.0 2026-06-03 21:25:51 +02:00
Dennis Guse 9607fda962 Android17: upgrade target sdk to 37.
Part of #2340.
2026-06-03 21:25:20 +02:00
Dennis Guse 77ed28a8e2 Revert "Remove StrictMode warnings."
This reverts commit f3e8d033cc.
2026-06-03 21:25:20 +02:00
Dennis Guse 4283e6d316 Bugfix: TrackRecordingService must handle restarts while recording.
We now store the Track.Id in a sharedPreferences.

Functionality was removed in e09b04308c

Fixes #2333
2026-05-29 16:36:29 +02:00
Dennis Guse ee030f32f5 Bugfix: resetting settings should restore data. 2026-05-29 16:36:29 +02:00
Dennis Guse fb25055f3e Activity: add gravel biking. 2026-05-27 12:23:32 +02:00
Dennis Guse 9cdf87b998 Test for announcement without speed/distance units.
Part of #2323
2026-05-27 11:29:18 +02:00
gallegonovato e9217b5327 Translated using Weblate (Spanish)
Currently translated at 7.5% (14 of 185 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/es/
2026-05-27 06:28:40 +00:00
claudio001 b4cb606499 Translated using Weblate (Italian)
Currently translated at 4.8% (9 of 184 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/it/
2026-05-24 22:06:57 +02:00
CI/CD Bot by pstorch d1975685bc Update dependency com.google.android.material:material to v1.14.0 2026-05-13 15:34:36 +00:00
CI/CD Bot by pstorch 8cd9dda1f5 Update dependency gradle to v9.5.1 2026-05-12 14:36:37 +00:00
Dennis Guse c072967dd1 PlayStore: data retention seems to be a thing now. 2026-05-10 11:32:02 +02:00
Dennis Guse 94e91d2063 PlayStore: add existing privacy policy. 2026-05-10 11:32:02 +02:00
Kilian Lackhove 796fa216f6 added bosch cadence sensor 2026-05-09 21:46:44 +02:00
Dennis Guse f143aabfbf Release: v4.27.0 2026-05-08 23:10:06 +02:00
Kilian Lackhove d58342233c fix onSharedPreferenceChanged for cyclingPowerHandler 2026-05-08 17:15:39 +02:00
CI/CD Bot by pstorch 91c254ba7f Update dependency com.android.tools.build:gradle to v9.2.1 2026-05-05 14:35:54 +00:00
CI/CD Bot by pstorch 19edb47554 Update dependency gradle to v9.5.0 2026-04-28 12:36:29 +00:00
Kilian Lackhove 1129c96562 initial support for Bosch ebike BLE power sensor 2026-04-27 21:04:33 +02:00
Dennis Guse 7224efa8a9 Make tests less flacky 2026-04-27 19:20:16 +02:00
Dennis Guse 149f8f809e Revert "Update dependency com.android.tools.build:gradle to v9.2.0"
This reverts commit a94f023a17.
2026-04-27 19:14:49 +02:00
Marcatili, Manuel c2f59c11bf removed the mockito from build.gradle 2026-04-26 09:33:03 +02:00
Marcatili, Manuel 8b7d4fa813 Added explicit assertions to tests 2026-04-26 09:33:03 +02:00
Marcatili, Manuel 81081b0ee5 reduce code duplication and creation of more test utils methods 2026-04-26 09:33:03 +02:00
Marcatili, Manuel def4f3a1b9 Cleanup unused utils 2026-04-26 09:33:03 +02:00
Marcatili, Manuel b679f00cc5 Improve clicking behaviour 2026-04-26 09:33:03 +02:00
Marcatili, Manuel 40494557d4 Implement dynamic waits, setup methods, isolate gps tests 2026-04-26 09:33:03 +02:00
CI/CD Bot by pstorch a94f023a17 Update dependency com.android.tools.build:gradle to v9.2.0 2026-04-21 14:34:38 +00:00
Kilian Lackhove ee105a1610 Fix file import/export on debug builds
Debug builds added .debug to the actual applicationId, but the settings XML
used a flavor-level applicationId resource without that suffix, causing an
"Unable to find explicit activity class" exception. Solved by generating the
applicationId resource from each resolved variant and keeping debug and
nightly branding limited to the launcher label. Also added an Android test that
verifies the settings targets resolve on-device.
2026-04-19 12:46:29 +02:00
Peter Storch 3bec73e074 fix #2317 dashboard API updates 2026-04-19 10:02:23 +02:00
Dennis Guse 1155a56e49 Merge branch 'stickflowerfightclub-main' 2026-04-13 22:25:37 +02:00
stickflowerfightclub 9b7e858d1d Sensors: Fix excessive altitude gain by enforcing SAMPLING_PERIOD on internal barometer.
On some modern devices like Pixel 8 Pro, the internal pressure sensor ignores the requested sampling period in registerListener and sends data at high frequency (~50Hz). This floods the AltitudeChangeHandler with noise, causing absurdly high altitude gain (e.g., 38km for a flat run).
This commit adds a manual throttle in BarometerInternal to strictly respect the SAMPLING_PERIOD (5s), allowing the exponential smoothing to work as intended.
2026-04-13 20:00:30 +02:00
CI/CD Bot by pstorch d1490fc343 Update dependency com.android.tools.build:gradle to v9.1.1 2026-04-13 14:34:27 +00:00
stickflowerfightclub 3f6a4975fa Fix: Allow selection of internal sensors when Bluetooth is disabled 2026-04-12 10:27:19 +02:00
Kilian Lackhove b5408eabcd Pre-populate BLE devices list with already-bonded devices 2026-04-11 21:46:20 +02:00
Kilian Lackhove e1e34452ae fix gradle 2026-04-11 16:03:11 +02:00
ghose df121b40d7 Translated using Weblate (Galician)
Currently translated at 17.3% (32 of 184 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/gl/
2026-03-28 05:57:25 +00:00
North-DaCoder 4e737616cb Translated using Weblate (German)
Currently translated at 99.8% (544 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/de/
2026-03-28 05:57:25 +00:00
CI/CD Bot by pstorch fb963781af Update dependency androidx.work:work-runtime to v2.11.2 2026-03-25 17:34:24 +00:00
CI/CD Bot by pstorch 96339b6a36 Update dependency gradle to v9.4.1 2026-03-19 09:34:15 +00:00
Dennis Guse d043842901 Release: v4.26.2 2026-03-14 16:13:43 +01:00
omeritzics cefb6b3abf Translated using Weblate (Hebrew)
Currently translated at 46.9% (256 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/he/
2026-03-14 12:47:58 +01:00
mikini 3ee6d43e3f Translated using Weblate (Danish)
Currently translated at 97.7% (533 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/da/
2026-03-14 12:47:58 +01:00
mikini 2edd9a1f05 Translated using Weblate (Danish)
Currently translated at 3.2% (6 of 183 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/da/
2026-03-14 12:47:58 +01:00
ntd f9a4fcfa0d Translated using Weblate (Italian)
Currently translated at 100.0% (545 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/it/
2026-03-14 12:47:58 +01:00
ntd 6bed9c5ddf Translated using Weblate (Italian)
Currently translated at 4.3% (8 of 182 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/it/
2026-03-14 12:47:58 +01:00
Dennis Guse 4e400c60f6 Bugfix: GPS via Bluetooth is not yet support and should therefore not be configurable.
Fixes #2294.
2026-03-14 12:16:20 +01:00
Dennis Guse ca6f973360 Cleanup. 2026-03-14 11:56:33 +01:00
CI/CD Bot by pstorch edb0e7f7bb Update dependency org.mockito:mockito-android to v5.23.0 2026-03-11 21:34:24 +00:00
CI/CD Bot by pstorch 876b013c2e Update dependency androidx.core:core to v1.18.0 2026-03-11 20:44:22 +01:00
CI/CD Bot by pstorch 7fb27cbe95 Update dependency androidx.core:core-location-altitude to v1.0.0-rc01 2026-03-11 18:34:04 +00:00
CI/CD Bot by pstorch 6d519e2fc1 Update dependency gradle to v9.4.0 2026-03-04 11:34:22 +00:00
CI/CD Bot by pstorch c2f3b50767 Update dependency com.android.tools.build:gradle to v9.1.0 2026-03-03 15:34:16 +00:00
CI/CD Bot by pstorch a95c6a1857 Update dependency org.mockito:mockito-android to v5.22.0 2026-02-28 08:47:10 +01:00
Dennis Guse f45dad167b Cleanup: ExportImportTest actually use implementation. 2026-02-27 22:08:10 +01:00
Dennis Guse 88c75d31a8 Bugfix: altitude type (EGM2008) should be shown in UI.
Fixes #2276.
2026-02-26 21:26:14 +01:00
Dennis Guse d49e7749f3 Cleanup: removed unnecessary interface from ContentProviderUtils. 2026-02-26 20:35:55 +01:00
Dennis Guse 8d3e953220 Cleanup: move URL to CustomContentProvider. 2026-02-26 20:33:45 +01:00
Dennis Guse 6566904906 Release: v4.26.1 2026-02-25 07:34:12 +01:00
qwjyh 1d0e8f59b7 Translated using Weblate (Japanese)
Currently translated at 48.8% (266 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/ja/
2026-02-25 06:29:29 +00:00
Dennis Guse be7987f88a Bugfix: crash when exporting zero tracks.
Fixes #2285
2026-02-21 21:45:00 +01:00
Dennis Guse a92a1e7838 Bugfix: layout's could not be deleted.
Introduced in e9deebea6c

Fixes #2283
2026-02-19 07:54:50 +01:00
Dennis Guse 8cba53cfe4 Cleanup. 2026-02-17 06:27:11 +01:00
Dennis Guse fcedb9c444 Bugfix: crash when heartrate sensor did not send data continuously.
Introduced in 1a14fed287

Fixes #2281.
2026-02-17 06:26:43 +01:00
Dennis Guse 2ec82af8f1 Release: v4.26.0 2026-02-15 18:39:39 +01:00
Dennis Guse 447d1bb7a3 Bugfix: speed and altitude were set to 0m repeatedly (UI only. Data is okay).
Fixes #2258.
2026-02-15 09:08:48 +01:00
Dennis Guse 2f6765e45f Bugfix: UI didn't show altitude in EGM2008 (but rather WGS84).
Introduced in 015352c989

Fixes #2279.
2026-02-15 09:08:48 +01:00
Dennis Guse 90bf2e6ab0 Cleanup: test is now working.
Part of #1279.
2026-02-15 09:08:48 +01:00
Dennis Guse d12423522e Cleanup. 2026-02-15 09:08:48 +01:00
Dennis Guse 24f052491b Cleanup: Move current time handling into SensorManager. 2026-02-15 09:08:48 +01:00
CI/CD Bot by pstorch 6b4e7d7d9b Update dependency com.android.tools.build:gradle to v9.0.1 2026-02-13 17:35:00 +00:00
Dennis Guse 00fcbb070a Cleanup. 2026-02-10 22:04:47 +01:00
264 changed files with 9043 additions and 1328 deletions
-36
View File
@@ -1,36 +0,0 @@
name: Android Test
on:
push:
branches:
- main
pull_request:
jobs:
lint:
name: Android Lint
runs-on: docker
container:
image: 'ubuntu:android-24.04'
steps:
- name: Checkout sources
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Lint
shell: bash
run: ./gradlew lint
android-check:
runs-on: docker
container:
image: 'ubuntu:android-24.04'
steps:
- name: Checkout sources
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Test
shell: bash
run: ./gradlew testNightlyDebugUnitTest --stacktrace
+56
View File
@@ -0,0 +1,56 @@
name: Auto-Sync Upstream
on:
schedule:
- cron: '0 3 * * *' # Uruchamia się codziennie o 3:00 w nocy
workflow_dispatch: # Pozwala uruchomić to ręcznie z poziomu Gitea
permissions:
contents: write
jobs:
sync:
name: Sync with Upstream
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Configure Git & Fetch
run: |
git config user.name "Gitea Actions Bot"
git config user.email "actions@gitea.local"
git remote add upstream https://gitea.local.labus.uk/upstream-mirrors/OpenTracks.git
git fetch upstream main
- name: Check for updates & Attempt Merge
id: merge
run: |
# Sprawdzamy czy są jakiekolwiek nowe zmiany
if git diff --quiet HEAD upstream/main; then
echo "Repozytorium jest aktualne. Brak zmian."
echo "has_changes=false" >> $GITHUB_ENV
exit 0
fi
echo "Znaleziono nowe zmiany. Próbuję scalić..."
echo "has_changes=true" >> $GITHUB_ENV
# Próba automatycznego scalenia (merge)
if git merge upstream/main --no-edit; then
echo "Scalenie przebiegło pomyślnie!"
else
echo "Wykryto konflikty! Przerywam scalanie."
git merge --abort
exit 1 # Błąd celowy - Gitea wyśle Ci powiadomienie o failu
fi
- name: Push changes
if: env.has_changes == 'true'
run: |
git push origin main
echo "Zmiany pomyślnie wypchnięte do repozytorium!"
+78
View File
@@ -0,0 +1,78 @@
name: CI Pipeline
on:
push:
branches:
- main
pull_request:
permissions:
contents: write
jobs:
lint:
name: Android Lint
runs-on: ubuntu-latest
container:
image: 'cimg/android:2024.04-node'
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Lint
shell: bash
run: ./gradlew lint
android-check:
name: Unit Tests
runs-on: ubuntu-latest
container:
image: 'cimg/android:2024.04-node'
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Test
shell: bash
run: ./gradlew testNightlyDebugUnitTest --stacktrace
release:
name: Build and Upload APK
needs: [lint, android-check]
if: github.event_name == 'push'
runs-on: ubuntu-latest
container:
image: 'cimg/android:2024.04-node'
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build APKs
shell: bash
run: |
./gradlew assembleDebug
./gradlew assembleRelease
- name: Prepare APKs
shell: bash
run: |
mkdir -p release-apks
find . -name "*.apk" -print -exec cp {} release-apks/ \;
ls -la release-apks/
- name: Upload to Gitea Releases
uses: https://gitea.com/actions/gitea-release-action@v1
with:
tag_name: build-${{ github.run_number }}
name: "Build #${{ github.run_number }}"
prerelease: true
files: |-
release-apks/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="OpenTracks">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="androidTest">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="unitTest">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="main">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
+12
View File
@@ -45,6 +45,18 @@ Translations are hosted on <a href="https://translate.codeberg.org/engage/opentr
<img src="https://translate.codeberg.org/widget/opentracks/opentracks-src-main-res-values-strings-xml-main/horizontal-auto.svg" alt="Translation status" />
</a>
## ⚠️ About This Fork
This repository is a customized fork of the official OpenTracks application.
**Core Purpose:**
The primary goal of this fork is to fundamentally change how Bluetooth sensors are prioritized over GPS data. In this modified version, Bluetooth sensor data is given absolute priority and actively ignores/overrides GPS signals in most scenarios. This ensures that cadence, speed, and distance measurements strictly rely on hardware sensors rather than falling back to or being averaged with GPS data.
**Consequential Changes:**
As a direct result of these sensor priority changes, several downstream modifications were implemented to support the new data flow:
* **UI & Settings:** Expanded settings menus and customized recording layouts to better expose the raw sensor data and give the user finer control.
* **Media & Voice Announcements:** Integrated media playback controls directly into the statistics view and overhauled voice announcements to handle audio focus and integrate with the new sensor-first approach.
## Screenshots
<div>
+14
View File
@@ -8,6 +8,7 @@
* Polar H10
* Polar OH1
Has updatable firmware that requires a user account at polar.com
* Polar Loop Gen 2
* Wahoo Tickr (Model: WFBTHR02)
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
* Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again)
@@ -112,3 +113,16 @@ Here is a list of the smartphones tested:
| Google | Nexus 4 | LineageOS 18.1 | Ok |
| Samsung | S4 | Android 11 | Ok |
| Sony | Xperia XZ1 Compact | LineageOS 16.1 | Failure |
## Bosch Smart System eBike
OpenTracks also supports Bosch Smart System eBikes via Bosch's custom BLE service `00000010-eaa2-11e9-81b4-2a2ae2dbcce4`.
Tested with:
* Bosch Perfomance Line CX (SW-Version 16.9.0) / Kiox 300 (SW-Version 16.4.0)
Currently supported measurements:
* Human power
* Cadence
+30 -23
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:9.0.0'
classpath 'com.android.tools.build:gradle:9.4.0'
}
}
@@ -18,11 +18,11 @@ allprojects {
}
def getVersionName = { ->
return 'git describe --tags'.execute().text.trim()
return 'git describe --tags'.execute(null, project.rootDir).text.trim()
}
def getVersionCodeNightly = { ->
return Integer.valueOf('git rev-list HEAD --count'.execute().text.trim())
return Integer.valueOf('git rev-list HEAD --count'.execute(null, project.rootDir).text.trim())
}
android {
@@ -33,7 +33,7 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
compileSdk = 36
compileSdk = 37
buildFeatures {
viewBinding = true
@@ -46,14 +46,14 @@ android {
}
defaultConfig {
applicationId 'de.dennisguse.opentracks'
versionCode 6564
versionName "v4.25.2"
applicationId 'de.dennisguse.opentracks.fork'
versionCode 6751
versionName "v4.28.3"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdk = 26
targetSdk = 36
targetSdk = 37
testInstrumentationRunner 'de.dennisguse.opentracks.TestRunner'
testInstrumentationRunnerArguments clearPackageData: 'true'
@@ -98,6 +98,7 @@ android {
release {
crunchPngs = false
minifyEnabled = false
signingConfig signingConfigs.debug
}
}
@@ -106,20 +107,18 @@ android {
productFlavors {
nightly {
dimension 'version'
applicationId 'de.dennisguse.opentracks.nightly'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.nightly'
applicationId 'de.dennisguse.opentracks.fork.nightly'
versionCode getVersionCodeNightly()
versionName getVersionName()
base.archivesName = "de.dennisguse.opentracks.nightly_${getVersionCodeNightly()}"
base.archivesName = "de.dennisguse.opentracks.fork.nightly_${getVersionCodeNightly()}"
signingConfig = signingConfigs.nightly
}
irreproducible {
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
dimension 'version'
applicationId 'de.dennisguse.opentracks'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks'
applicationId 'de.dennisguse.opentracks.fork'
versionNameSuffix 'irreproducible'
}
reproducible {
@@ -127,10 +126,9 @@ android {
// FDroid: https://f-droid.org/de/packages/de.dennisguse.opentracks.playstore
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
dimension 'version'
applicationId 'de.dennisguse.opentracks.playstore'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.playstore'
applicationId 'de.dennisguse.opentracks.fork.playstore'
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
base.archivesName = "de.dennisguse.opentracks.fork.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
signingConfig = signingConfigs.release
}
@@ -148,24 +146,33 @@ android {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.appcompat:appcompat:1.8.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.documentfile:documentfile:1.1.0'
implementation 'androidx.gridlayout:gridlayout:1.1.0'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.core:core:1.17.0'
implementation 'com.google.android.material:material:1.14.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.2'
implementation 'androidx.core:core:1.19.0'
implementation 'androidx.core:core-splashscreen:1.2.0'
implementation 'androidx.mediarouter:mediarouter:1.8.1'
implementation 'androidx.core:core-location-altitude:1.0.0-beta01'
implementation 'androidx.work:work-runtime:2.11.1'
implementation 'androidx.core:core-location-altitude:1.0.0'
implementation 'androidx.work:work-runtime:2.11.2'
implementation 'androidx.health.connect:connect-client:1.1.0'
androidTestImplementation 'androidx.test:core:1.7.0'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test:rules:1.7.0'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
androidTestImplementation 'org.mockito:mockito-android:5.21.0'
androidTestUtil 'androidx.test:orchestrator:1.6.1'
}
androidComponents {
onVariants(selector().all()) { variant ->
variant.resValues.put(
new com.android.build.api.variant.impl.ResValueKeyImpl("string", "applicationId"),
new com.android.build.api.variant.ResValue(variant.applicationId.get(), null)
)
}
}
View File
@@ -0,0 +1,10 @@
(versionName): OpenTracks
Ændringer:
-
Fejlrettelser:
-
Udvikler:
-
@@ -0,0 +1,12 @@
v4.26.0: OpenTracks
Changes:
- GPS is optional: configurable in the settings
- BLE sensor address/name is only shown after successful connection
Bugfix:
- Altitude is now shown again in EGM2008 (introduced in v4.25.0)
- Altitude and speed were shown as 0.0 briefly when data was stored
Developer:
- Refactored sensor subsystem
@@ -0,0 +1,6 @@
v4.26.1: OpenTracks
Bugfix:
- Crash when deleting a UI layout
- Crash when heartrate sensor data could not be decoded
- Crash when exporting zero tracks
@@ -0,0 +1,5 @@
v4.26.2: OpenTracks
Bugfix:
- GPS via Bluetooth is not (yet) supported
- Altitude type (EGM2008) wasn't shown
@@ -0,0 +1,14 @@
v4.27.0: OpenTracks
Changes:
- Sensor: power from Bosch eBike
- Sensors: include already bonded Bluetooth devices (like Bosch eBike)
- Barometer: enforce sampling frequency
Bugfix:
- Dashboard API: did not provide update
- GPS could not be enabled with Bluetooth disabled
Developer:
- Removed Mockito
- UI tests: less flacky
@@ -0,0 +1,11 @@
v4.27.1: OpenTracks
Changes:
- Support for Bosch Smart System eBikes (cadence, human power)
- Add activity "gravel biking"
Bugfix:
- While recording: TrackRecordingService would not restart correctly if killed by Android
Developer:
- TargetSDK 37
@@ -0,0 +1,5 @@
v4.27.2: OpenTracks
Bugfix:
- Not all valid timezone offsets could be set
- Fixed race condition for IDLE trackpoints
@@ -0,0 +1,10 @@
v4.28.0: OpenTracks
Changes:
- Support for exporting to Health Connect
Bugfix:
- Dashboard API: when sharing multiple tracks, the currently recorded one wasn't updated
- Configuration setting for announcement of lap power wasn't stored correctly (only some languages affected)
@@ -0,0 +1,10 @@
v4.28.1: OpenTracks
Changes:
- HealthConnect: sending total distance, speed and elevation gain
Bugfix:
- Gravel activity was not selectable
Developer:
- Cleanup ContentProvider URLs
@@ -0,0 +1,7 @@
v4.28.2: OpenTracks
Changes:
- Import: ignore GPX waypoints without time
Bugfix:
- Only start GPS once
@@ -0,0 +1,10 @@
v4.28.3: OpenTracks
Changes:
- Settings: allow to disable segment creation (max recording distance)
- Importer: match activity type case-insensitive
- GPX: export moving speed in summary
Bugfix:
- GPX: importer must respect namespace prefixes
@@ -1,15 +1,15 @@
v3.0.0: OpenTracks
Primer lanzamiento de OpenTracks (es decir, una bifurcación de MyTracks)
Primera versión de OpenTracks (es decir, una bifurcación dura de MyTracks)
Cambios:
- Cambio de nombre de MyTracks a OpenTracks
- Se agregó un sensor de frecuencia cardíaca Bluetooth LE
- Se eliminaron todas las funciones que requieren acceso a Internet.
- Se eliminó la integración de Google Maps
- Se eliminó el uso de los servicios de Google Play.
- Se eliminaron los sensores ANT+
- Se eliminaron los sensores clásicos de Bluetooth.
- Se eliminó Protobuf
- Se eliminaron la estimación de calorías y la estimación de actividad.
- Se eliminó la API del servicio MyTracks (es decir, no hay integración con otras aplicaciones)
- Se ha añadido el sensor de frecuencia cardíaca Bluetooth LE
- Se han eliminado todas las funciones que requerían acceso a Internet
- Se ha eliminado la integración con Google Maps
- Se ha eliminado el uso de los servicios de Google Play
- Se han eliminado los sensores ANT+
- Se han eliminado los sensores Bluetooth clásicos
- Se ha eliminado Protobuf
- Se han eliminado la estimación de calorías y la estimación de actividad
- Se ha eliminado la API del servicio MyTracks (es decir, no hay integración con otras aplicaciones)
@@ -0,0 +1,17 @@
v3.0.1: OpenTracks
Cambios:
- Compartir marcadores
- Configuración reestructurada
- Se ha eliminado el botón «Atrás» de la barra de acciones
- Exportación: se ha eliminado la opción de configuración del tamaño de las fotos
- Exportación: ahora se puede seleccionar el formato KML
Correcciones de errores:
- El directorio de exportación es ahora /OpenTracks/
- El modo horizontal ya no provoca fallos
- Se ha corregido la exportación a KML/KMZ
Desarrollador:
- Las pruebas se pueden ejecutar (aunque muchas fallan)
- Se ha habilitado la integración continua con TravisCI (sin pruebas)
@@ -0,0 +1,14 @@
v3.1.0: OpenTracks
Cambios:
- Se ha añadido la opción de mostrar el recorrido en aplicaciones de mapas (probado con OsmAnd y Maps.ME)
- Se ha añadido la opción de mostrar las estadísticas del recorrido sin desbloquear el dispositivo mientras se realiza el seguimiento
- Se ha eliminado el modo horizontal para la vista detallada del recorrido (presentaba errores)
- Se ha eliminado el historial de sugerencias de búsqueda
Corrección de errores:
- Se solicita permiso de almacenamiento para «Importar todo»/«Exportar todo»
Desarrollador:
- Actualización a Gradle 5.6
- No se minimizan las compilaciones de lanzamiento
@@ -0,0 +1,16 @@
v3.2.0: OpenTracks
Cambios:
- Se ha eliminado el widget
- Se han incorporado iconos de Material Design
- Se han eliminado el snowboard y el esquí (se necesitan nuevos iconos)
Corrección de errores:
- Se ha corregido la funcionalidad de búsqueda
Desarrollador:
- Se han corregido (algunos) casos de prueba
- Se ha eliminado la funcionalidad «parcelable» en desuso
- Limpieza y reformateo a fondo
- No se utiliza la difusión para la comunicación entre la aplicación y el widget
- El archivo stats_content.xml no incluye otros archivos
@@ -0,0 +1,7 @@
v3.2.1: OpenTracks
Cambios:
- Los ajustes ahora están en una sola página
Desarrollo:
-Los ajustes se migraron a Androidx compat (usa fragments)
@@ -0,0 +1,13 @@
v3.2.2: OpenTracks
Cambios:
- Se eliminaron las estadísticas agregadas
- Migrado a Material Design
Corrección de errores:
- Se corrigió la exportación de datos de sensores
- Se corrigió la importación de datos de sensores incompletos
Desarrollador:
- Se reemplazó la barra de acción (actionbar) por la barra de herramientas (toolbar)
- Migrado a androidx appcompat (incl. Material Design)
@@ -0,0 +1,14 @@
v3.2.3: OpenTracks
Cambios:
- Compartir: usar Android Sharesheet
- Compartir: compartir múltiples pistas como archivos individuales
- Usar Android ShareSheet
- Sensor de frecuencia cardíaca (Bluetooth LE): reconexión automática
Corrección de errores:
- Sensor de frecuencia cardíaca (Bluetooth LE): desconexión correcta
- La frecuencia cardíaca no siempre se mostraba durante la grabación
Desarrollador:
- Se habilitaron las pruebas de instrumentación en TravisCI
@@ -0,0 +1,5 @@
v3.2.4: OpenTracks
Corrección de errores:
- Caída/cierre inesperado (crash) en la actividad de ajustes
Se eliminó la posibilidad de cambiar la frecuencia de voz/fracción desde el menú contextual
@@ -0,0 +1,15 @@
v3.2.5: OpenTracks
Cambios:
- Deshabilitar el cambio de actividad
- Añadir permiso para Android 10: grabación de datos de ubicación
Corrección de errores:
- Corregir el restablecimiento de los ajustes
Desarrollador:
- Preferencias
- si no se pueden cargar los Ajustes, restablecer todas las preferencias
- se movió el acceso a las preferencias a PreferencesUtil
- se movieron los valores por defecto de las preferencias del código a XML
- usar SummaryProvider
@@ -0,0 +1,8 @@
v3.2.6: OpenTracks
Cambios:
- Monitor de frecuencia cardíaca: escanear dispositivos disponibles
- Eliminar acceso directo a los ajustes de Bluetooth
Desarrollador:
- Preferencias: se movieron los valores por defecto de las preferencias del código a XML
@@ -0,0 +1,10 @@
v3.2.7: OpenTracks
Cambios:
- La notificación muestra la precisión del GPS
- Mostrar notificación emergente (heads-up) si la precisión del GPS cae por debajo del umbral
- Mostrar siempre el gráfico tanto por tiempo como por distancia
- Se añadió ayuda (en Ajustes)
Corrección de errores:
- TrackDetailActivity: se deshabilitó el modo horizontal que no funcionaba (temporal)
@@ -0,0 +1,10 @@
v3.2.8: OpenTracks
Cambios:
- Gráfico: zoom mediante gestos
Corrección de errores:
- La exportación a través de KMZ no exportaba correctamente los nombres de archivo de las imágenes
- Compartir múltiples pistas no incluía los archivos KMZ
- No se mostraba la interfaz de usuario de MarkerDetailActivity
- Los ajustes se cerraban inesperadamente si la preferencia de frecuencia cardíaca no estaba inicializada
@@ -0,0 +1,10 @@
v3.2.9: OpenTracks
Cambios:
- Opción de mantener la pantalla encendida durante la grabación
- Marcadores: se eliminaron por completo los marcadores de división (split markers)
- Los anuncios de voz ahora funcionan sin el permiso READ_PHONE_STATE
Corrección de errores:
- Estadísticas: no se mostraba la frecuencia cardíaca
- Estadísticas: el tiempo se restablecía brevemente a 0s
@@ -0,0 +1,11 @@
v3.2.10: OpenTracks
Cambios:
- ChartView ya no es configurable
- Después de reiniciar: se eliminó el reinicio automático de una grabación incompleta
Corrección de errores:
- Los anuncios de voz usan MAY_DUCK (de nuevo)
- ChartView muestra marcadores (de nuevo)
- Se corrigió el diseño de grabación en horizontal (landscape recordingLayout) de TrackListActivity
- Se solicita el comportamiento heredado de la API de archivos (necesario para Android 10)
@@ -0,0 +1,12 @@
v3.2.11: OpenTracks
Error:
- La velocidad máxima es idéntica a la velocidad promedio
Corrección de errores:
- Cierre inesperado (crash) al seleccionar el sensor de frecuencia cardíaca
Desarrollador:
- Se reemplazó TabHost por TabsLayout
- Se eliminó el primer punto de ruta (waypoint) especial
- Se eliminaron las traducciones duplicadas
@@ -0,0 +1,13 @@
v3.3.0: OpenTracks
Cambios importantes:
- Fotos en almacenamiento privado (ocultas para otras apps)
NOTA: No se verán fotos de versiones anteriores
- Español (Latinoamérica) requiere Android 7.0 o superior
Cambios:
- Importar/exportar vía Storage Access Framework (sin permisos de almacenamiento)
Desarrollador:
- Limpieza de estructuras de datos internas
- Icono/nombre distintos para versión debug
@@ -0,0 +1,11 @@
v3.3.1: OpenTracks
Cambios:
- Android 10: permite conservar datos al desinstalar
- API Dashboard: icono de mapa para abrir la pista en app externa (ej. OSMDashboard)
Corrección de errores:
- Se corrigió el nombre de la app (roto en v3.3.0)
Desarrollador:
- Documentación de la API Dashboard en README_DEVELOPER.md
@@ -0,0 +1,11 @@
v3.3.2: OpenTracks
Cambios:
- Exportación: ahora sobrescribe los archivos
Corrección de errores:
- Se elimina siempre la notificación de grabación
Desarrollador:
- En API 26+ se usa java.time para analizar y escribir fechas
- Refactorización de TrackRecordingService
@@ -0,0 +1,12 @@
v3.4.0: OpenTracks
Cambios:
- API Dashboard: múltiples pistas
- Ayuda actualizada
Corrección de errores:
- Se corrigió el cierre inesperado al recrear fragmentos
- En pausa: evita que la app se detenga
Desarrollador:
- Pruebas habilitadas vía CI (Github Actions y TravisCI)
@@ -0,0 +1,14 @@
v3.5.0: OpenTracks
Cambios:
- Marcador: seleccionar imágenes de la galería
- Marcador: compartir imagen
- Utilizar el nombre de la ruta como nombre de archivo al compartir
- Se han eliminado los valores mínimos y máximos de altitud (eran imprecisos)
Corrección de errores:
- Los gráficos utilizan las unidades y la escala preferidas
Desarrollador:
- Base de datos: eliminar columnas en desuso
- Base de datos: añadir índices
@@ -0,0 +1,10 @@
v3.6.0: OpenTracks
Cambios:
- Ahora se pueden reanudar las rutas que se habían detenido
Corrección de errores:
- La importación de archivos KMZ no procesaba correctamente la URL de las fotos
Desarrollador:
- Se ha simplificado el procesamiento de datos
@@ -0,0 +1,4 @@
v3.6.1: OpenTracks
Corrección de errores:
- Importación de KMZ: guardar la ruta correcta de la imagen
@@ -0,0 +1,4 @@
v3.6.2: OpenTracks
Corrección de errores:
- Los gráficos no se mostraban correctamente (problema introducido en la v3.6.0)
@@ -0,0 +1,13 @@
v3.7.0: OpenTracks
Cambios:
- Informe de velocidad/ritmo por tipo de actividad
- Se ha añadido una solución provisional para el cambio de semana del GPS
- (Experimental) Compatibilidad con sensores de ciclismo Bluetooth LE (cadencia y velocidad)
- Se ha mejorado la importación de KMZ/KML
Corrección de errores:
- Importación de KMZ: solo se importan las imágenes utilizadas
Desarrollador:
- Se ha refactorizado TrackRecordingService
@@ -0,0 +1,7 @@
v3.7.1: OpenTracks
Solución provisional:
- Ocultar el botón de inicio del GPS
Corrección de errores:
- Se ha corregido un puntero nulo
@@ -0,0 +1,6 @@
v3.7.2: OpenTracks
Corrección de errores:
- Se ha vuelto a añadir el botón «Iniciar GPS»
- El nombre de la ruta ya no se sobrescribe al final de una grabación
- Marcadores: al eliminar una foto, también se borra el archivo
@@ -0,0 +1,7 @@
v3.7.3: OpenTracks
Corrección de errores:
- Se ha corregido el cálculo de la cadencia y la velocidad (mediante el sensor Bluetooth LE)
Desarrollador:
- Se han eliminado las traducciones que no se utilizaban
@@ -0,0 +1,12 @@
v3.7.4: OpenTracks
Cambios:
- Formato de compartir/exportar configurable
Corrección de errores:
- El diámetro de la rueda no se almacenaba correctamente (para el sensor de velocidad)
- Ayuda: los enlaces son clicables
Desarrollador:
- Se ha dividido TrackDetailActivity
- El sucesor de StatsFragment utiliza ConstraintLayout
@@ -0,0 +1,11 @@
v3.7.5: OpenTracks
Cambios:
- Se ha añadido un resumen de estadísticas agregadas (desde el menú de la pantalla principal)
- API del panel de control: se pasan los ajustes «mostrar en la pantalla de bloqueo» y «mantener la pantalla encendida»
- Se han actualizado las traducciones
- Importación: se ha flexibilizado el formato de fecha ISO 8601
- Mostrar ruta en el mapa: se puede seleccionar el formato de archivo y la apertura del panel de control funciona en Android R (API 30+)
Corrección de errores:
- El icono de la aplicación ahora se muestra correctamente
@@ -0,0 +1,11 @@
v3.7.6: OpenTracks
Cambios:
- Modo horizontal
- La interfaz de usuario para borrar rutas ahora es solo un indicador giratorio
- La velocidad se exporta e importa (GPX/KML/KMZ)
- El formato de exportación predeterminado es KMZ, incluyendo todos los datos y las imágenes
Desarrollador:
- Se han sustituido las AsyncTasks por hilos
- Sincronización: se genera un UUID por cada ruta (aún no se exporta)
@@ -0,0 +1,16 @@
v3.8.0: OpenTracks
Cambio importante:
- Importación/exportación de GPX de velocidad, cadencia y frecuencia cardíaca a través de TrackPointExtension/v2
Cambios:
- API del panel de control: permite acceder a los marcadores (incluida una llamada de retorno para mostrarlos en OpenTracks)
- Nuevo icono de marcador (chincheta naranja, SVG)
- Los archivos que se van a importar se pueden seleccionar a través del gestor de archivos
Correcciones de errores:
- La interfaz de usuario de estadísticas agregadas respeta la frecuencia preferida
- Fallaba la edición de un marcador al cambiar la orientación
Desarrollador:
- Se ha refactorizado TrackRecordingService
@@ -0,0 +1,8 @@
v3.8.1: OpenTracks
Cambios:
- Durante la grabación: se ha añadido la opción de pantalla completa
- El panel de control se inicia en la misma tarea
Correcciones de errores:
- Los filtros de intención para importar archivos eran demasiado optimistas
@@ -0,0 +1,14 @@
v3.9.0: OpenTracks
Cambios:
- Exportación de rutas con su UUID
- Se evita la importación de rutas ya existentes por su UUID (configurable; por defecto, activado)
- Ahora hay que mantener pulsado el botón de pausa/parada
- El preinicio del GPS muestra el estado de la señal
- Se han añadido nuevos iconos de actividad
- Sensor de velocidad: ahora se puede configurar el tamaño de la rueda en milímetros
Correcciones de errores:
- Se han corregido algunos fallos poco frecuentes (NPE)
- Se ha corregido el formato del ritmo
- Se ha cambiado la posición del icono de la aplicación en el lanzador
@@ -1,7 +1,7 @@
v4.14.1: OpenTracks
Cambios:
- Detección de inactividad se puede desactivar
- Se puede desactivar la detección de inactividad
Correcciones de errores:
- UI para añadir imágenes a los marcadores estaba oculto
Corrección de errores:
- La interfaz de usuario para añadir imágenes a los marcadores estaba oculta
@@ -0,0 +1,14 @@
v4.27.0: OpenTracks
Cambios:
- Sensor: potencia de la bicicleta eléctrica Bosch
- Sensores: se incluyen los dispositivos Bluetooth ya emparejados (como la bicicleta eléctrica Bosch)
- Barómetro: se aplica la frecuencia de muestreo
Corrección de errores:
- API del panel de control: no se proporcionaba la actualización
- No se podía activar el GPS con el Bluetooth desactivado
Desarrollador:
- Se ha eliminado Mockito
- Pruebas de la interfaz de usuario: menos inestables
@@ -1,8 +1,22 @@
OpenTracks es un tracker deportivo que respeta tu privacidad.
Un compañero de seguimiento deportivo que respeta tu privacidad.
Funciones:
* Tracker deportivo: haz un seguimiento de tus actividades al aire libre
* Marca lugares de interés, incluso agrega fotos a los marcadores
* Pulsómetro (compatible con dispositivos Bluetooth LE)
* Exporta tus rutas como KML o GPX
* Completamente offline, no necesitas internet
El deporte y las actividades al aire libre aportan felicidad, destreza y confianza.
Cuida tu salud llevando un registro de tu entrenamiento.
Registra tus sesiones de carrera o caminata, y te ofrece un ciclocomputador con una pantalla más grande para el ciclismo.
Marca lugares interesantes a lo largo de tu recorrido con fotos.
Guarda estadísticas detalladas para su análisis.
Comparte solo los datos que quieras que vean los demás.
* Avisos de voz.
* Compatible con sensores Bluetooth LE: frecuencia cardíaca, velocidad y distancia (ciclismo), cadencia (ciclismo) y medidor de potencia (ciclismo).
* Desnivel positivo y negativo: mediante sensor barométrico.
* Altitud mostrada en EGM2008 (por encima del nivel medio del mar); exportada como WGS84.
* Exporta los datos como rutas en formato KMZ (incluidas fotos), KML o GPX.
* No requiere acceso a Internet ni permisos adicionales.
* Temas oscuro y claro, respetando la configuración del sistema.
* Sin anuncios.
Software libre / Software gratuito / Código abierto,
lo que significa que puedes utilizar, estudiar, modificar y compartir el código fuente.
Licencia Apache 2.0
@@ -0,0 +1,5 @@
v4.26.2: OpenTracks
Arranxos:
- (aínda) non hai compatibilidade co GPS vía Bluetooth
- non se mostraba a altitude tipo (EGM2008)
@@ -0,0 +1,15 @@
v3.0.0: OpenTracks
Rilisan pertama OpenTracks (fork keras dari MyTracks)
Perubahan:
- Perubahan merek MyTracks ke OpenTracks
- Menambahkan sensor detak jantung Bluetooth LE
- Menghapus semua fitur yang memerlukan koneksi internet
- Menghapus integrasi Google Maps
- Menghapus penggunaan layanan Google Play
- Menghapus sensor ANT+
- Menghapus sensor klasik Bluetooth
- Menghapus Protobuf
- Menghapus estimasi kalori dan aktivitas
- Menghapus API MyTracks Service (dengan kata lain, tidak ada integrasi dengan aplikasi lain)
@@ -0,0 +1,17 @@
v3.0.1: OpenTracks
Perubahan:
- Bagikan penanda
- Perubahan struktur pengaturan
- Menghapus tombol kembali di bilah aksi
- Ekspor: menghapus pilihan pengaturan ukuran foto
- Ekspor: KML sebagai format sekarang dapat dipilih
Perbaikan Bug:
- direktori ekspor sekarang di /OpenTracks/
- mode lanskap tidak akan mogok
- memperbaiki ekspor KML/KMZ
Pengembang:
- Tes dapat dieksekusi (namun banyak yang gagal)
- Mengaktifkan integrasi terus menerus dengan TravisCI (tanpa tes)
@@ -0,0 +1,14 @@
v3.1.0: OpenTracks
Perubahan:
- Menambahkan opsi tampilan jalur salju di peta aplikasi (telah diuji dengan OsmAnd dan Maps.ME)
- Menambahkan ketika melacak, akan menampilkan statistik jalur tanpa perlu membuka kunci perangkat
- Menghapus mode landkap untuk tampilan detail jalur (tidak stabil)
- Menghapus riwayat saran penelusuran
Perbaikan Bug:
- Permintaan izin penyimpanan untuk impor/ekspor semua
Pengembang:
- Pembaruan ke Gradle 5.6
- Jangan mengecilkan (minify) rilisan build
@@ -1,4 +1,4 @@
Teman pelacakan olahraga yang menghormati privasi Anda.
Sebuah teman pelacak olahraga yang benar-benar menghormati privasi Anda.
Olahraga dan aktivitas luar ruangan memberikan kebahagiaan, keterampilan, dan kepercayaan diri.
@@ -11,12 +11,12 @@ Hanya membagikan data yang Anda ingin orang lain memilikinya.
* Notifikasi suara.
* Mendukung sensor Bluetooth LE: detak jantung, kecepatan dan jarak (bersepeda), irama (bersepeda), dan pengukur daya (bersepeda).
* Kenaikan dan penurunan ketinggian: melalui sensor barometrik.
* Ketinggian yang ditampilkan dalam EGM2008 (di atas permukaan laut); diekspor sebagai WGS84.
* Ketinggian ditampilkan dalam EGM2008 (di atas permukaan laut); diekspor sebagai WGS84.
* Mengekspor data sebagai lintasan, baik sebagai KMZ (termasuk foto), KML, atau GPX.
* Tidak ada akses Internet atau izin tambahan.
* Tema gelap dan terang, dengan memperhatikan pengaturan sistem.
* Tanpa iklan.
Perangkat lunak bebas / Perangkat Lunak Bebas / Sumber Terbuka
Perangkat Lunak Bebas / Sumber Terbuka
yang berarti Anda dapat menggunakan, mempelajari, mengubah, dan berbagi kode sumber.
Berlisensi Apache 2.0
@@ -1 +1 @@
Sebuah pelacak olahraga yang benar-benar menghormati privasi Anda.
Sebuah teman pelacak olahraga yang benar-benar menghormati privasi Anda.
@@ -0,0 +1,5 @@
v4.26.2: OpenTracks
Correzioni:
- Il GPS via Bluetooth non è (ancora) supportato
- Il tipo di altitudine (EGM2008) non viene mostrato
@@ -0,0 +1,22 @@
Un tracker sportivo che rispetta la tua privacy.
Sport e attività all'aria aperta portano a felicità, allenamento e fiducia
Valorizza la tua salute tenendo traccia dei tuoi progressi.
Registra quando stai correndo o passeggiando e regalati un monitor più grande quando fai ciclismo.
Segna con fotografie i posti interessanti incrociati durante il tragitto.
Tieni traccia delle statistiche in dettaglio per analizzarle.
Condividi solo i dati che vuoi.
* Annunci vocali.
* Supporto a sensori Bluetooth LE: battito cardiaco, velocità e distanza (ciclismo), cadenza (ciclismo), rilevamento potenza (ciclismo).
* Altitudine positiva o negativa tramite sensore barometrico.
* Altitudine mostrata in EGM2008 (sopra il livello del mare) ed esportata come WGS84.
* Esporta tracce come KMZ (con foto incluse), KML o GPX.
* Non richiede accesso a internet o altri permessi.
* Temi chiaro e scuro, o seguendo la impostazioni di sistema.
* Nessuna pubblicità.
Libre software / Free Software / Open Source
cioè puoi usare, studiare, cambiare e condividere il codice sorgente.
Licenza: Apache 2.0
@@ -1,11 +1,22 @@
OpenTracks é um aplicativo de rastreamento de esportes que respeita totalmente a sua privacidade.
Um companheiro de rastreamento esportivo que respeita sua privacidade.
Características:
* Rastreamento de esportes: monitore seu esporte e atividades ao ar livre
* Fotos e marcadores: marque locais interessantes durante o rastreamento
* Ganho e perda de altitude: via sensor barométrico
* Altitude mostrada no EGM2008 (acima do nível médio do mar); exportado como WGS84
* Anúncios de voz
* Suporta sensores Bluetooth LE: frequência cardíaca, velocidade e distância (ciclismo), cadência (ciclismo) e medidor de potência (ciclismo)
* Exportar dados: exporte trilhas como KMZ (incluindo fotos), KML, ou GPX
* Sem acesso à Internet: a Internet não é usada
Esportes e atividades ao ar livre garantem felicidade, habilidades e confiança.
Valoriza sua saúde acompanhando os seus treinos.
Grava enquanto você corre ou caminha e te entrega um computador de bicicleta com uma tela maior para o ciclismo.
Marca locais interessantes durante o percurso com fotos.
Deixa estatísticas registradas em grande detalhe para analises.
Compartilha apenas os dados que você quer que outros tenham.
* Anúncios por voz.
* Suporte para sensores Bluetooth LE: frequência cardíaca, velocidade e distância (ciclismo), cadência (ciclismo) e medição de potência (ciclismo).
* Ganhos e perdas de altitude: via sensor barométrico.
* Altitudes exibidas em EGM2008 (acima do nível médio do mar); exportado como WGS84.
* Exporta os dados de rastreio como KMZ (inclusive fotos), KML ou GPX.
* Sem acesso à internet ou permissões extra.
* Tema claro e escuro, respeitando as configurações do sistema.
* Sem propagandas.
Software Livre / Código Aberto
Significa que você pode usar, estudar, mudar e compartilhar o código.
Licença Apache 2.0
@@ -1 +1 @@
一個尊重您的隱私的運動蹤跡紀錄器。
一款尊重您隱私的運動蹤跡紀錄器。
+13
View File
@@ -0,0 +1,13 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
toolchainVendor=JETBRAINS
toolchainVersion=21
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
cat << 'EOC' > src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java.patch
--- src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java
+++ src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java
@@ -351,6 +351,11 @@
public static boolean shouldVoiceAnnouncementLongBreak() {
return getBoolean(R.string.settings_announcements_long_break_key, true);
}
+
+ public static long getLongBreakDuration() {
+ int minutes = getInt(R.string.settings_long_break_duration_key, 3);
+ return minutes * 60 * 1000L;
+ }
EOC
patch -p0 < src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java.patch
+13
View File
@@ -0,0 +1,13 @@
cat << 'EOC' > src/main/res/values/strings.xml.patch
--- src/main/res/values/strings.xml
+++ src/main/res/values/strings.xml
@@ -1071,6 +1071,8 @@
<string name="settings_announcements_long_break_key">settings_announcements_long_break_key</string>
<string name="settings_announcements_long_break_title">Long break</string>
+ <string name="settings_long_break_duration_key">settings_long_break_duration_key</string>
+ <string name="settings_long_break_duration_title">Long break duration (minutes)</string>
</resources>
EOC
patch -p0 < src/main/res/values/strings.xml.patch
+1
View File
@@ -0,0 +1 @@
cat src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java | grep -n "if (movingDistance != null) {" -B 2 -A 30
+14
View File
@@ -0,0 +1,14 @@
<html>
<body>
<h1>OpenTracks: Privacy Policy</h1>
<p>
OpenTracks does only store data on the local device that is relevant for tracking your sport exercise.
Stored data is not transmitted from the app itself to a third party.
Data is stored by OpenTracks until:
1. the user deletes the recorded track or
2. the user uninstalls OpenTracks
</p>
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'OpenTracks'
@@ -2,76 +2,117 @@ package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withParentIndex;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
import static de.dennisguse.opentracks.util.EspressoUtils.performClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilRecyclerViewItemCount;
import static de.dennisguse.opentracks.util.EspressoUtils.withRecyclerViewItemCount;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.renameStoppedTrack;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
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.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoDeleteTrackTest {
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
}
@Test
public void espressoDeleteTrackTest() {
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action))
.perform(click());
public void deleteTrackFromTrackStoppedActivity() {
recordTrackAndOpenTrackStoppedActivity();
// TrackRecordingActivity
onView(withId(R.id.track_recording_fab_action))
// wait; stay recording
.perform(waitFor(5000))
// stop;
.perform(longClick());
// TrackStoppedActivity: discard the just-recorded track directly
onView(withId(R.id.discard_button))
.perform(performClick());
onView(withText(android.R.string.ok))
.inRoot(isDialog())
.perform(performClick());
waitUntilDisplayed(withId(R.id.track_list), UI_TIMEOUT);
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0, UI_TIMEOUT);
// the recycler is empty again after discarding the only recorded track
onView(withId(R.id.track_list))
.check(matches(withRecyclerViewItemCount(0)));
}
@Test
public void deleteTrackFromSelectedTrackInTrackList() {
String uniqueTrackName = "DeleteFromTrackList-" + System.currentTimeMillis();
recordTrackAndOpenTrackStoppedActivity();
renameStoppedTrack(uniqueTrackName, UI_TIMEOUT);
// TrackStoppedActivity
onView(withId(R.id.finish_button))
.perform(click());
.perform(performClick());
waitUntilDisplayed(withId(R.id.track_list), UI_TIMEOUT);
waitUntilDisplayed(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)), UI_TIMEOUT);
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 1, UI_TIMEOUT);
// select track
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0)))
.perform(longClick());
onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)))
.perform(performLongClickOnAncestor());
// open menu and delete selected track
onView(
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(androidx.appcompat.R.id.action_mode_bar),
1),
2),
isDisplayed()))
.perform(click());
clickDeleteSelectedTrackAction();
onView(withText(android.R.string.ok))
.inRoot(isDialog())
.perform(performClick());
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0, UI_TIMEOUT);
onView(withText("Delete"))
.perform(click());
onView(withId(R.id.track_list))
.check(matches(withRecyclerViewItemCount(0)));
}
onView(withText("OK"))
.perform(click());
private void recordTrackAndOpenTrackStoppedActivity() {
startRecordingFromTrackList(UI_TIMEOUT);
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.discard_button, R.id.finish_button);
}
// tracklist is empty now
onView(allOf(withText("Start recording your next adventure here"), isDisplayed()));
private void clickDeleteSelectedTrackAction() {
try {
waitUntilDisplayed(withId(R.id.list_context_menu_delete), UI_TIMEOUT);
onView(withId(R.id.list_context_menu_delete))
.perform(performClick());
} catch (AssertionError ignored) {
onView(allOf(
withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description),
isDescendantOfA(withId(androidx.appcompat.R.id.action_mode_bar))))
.perform(performClick());
onView(withText(R.string.menu_delete))
.inRoot(isPlatformPopup())
.perform(performClickOnAncestor());
}
}
}
@@ -2,34 +2,43 @@ package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
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 de.dennisguse.opentracks.util.EspressoUtils.performClick;
import static de.dennisguse.opentracks.util.EspressoUtils.performClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
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.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.Locale;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoEditTrackRecordingTest {
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
@Rule
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
@@ -39,27 +48,27 @@ public class EspressoEditTrackRecordingTest {
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
}
@LargeTest
@Test
public void espressoEditTrackRecordingTest() {
{
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
startRecordingFromTrackList(UI_TIMEOUT);
{
// TrackRecordingActivity
// wait; stay recording
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(4000));
// open menu
openContextualActionModeOverflowMenu();
// Click the item.
onView(allOf(
withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description),
isDescendantOfA(withId(R.id.bottom_app_bar))))
.perform(performClick());
onView(withText(R.string.menu_edit))
.perform(click());
.inRoot(isPlatformPopup())
.perform(performClickOnAncestor());
waitUntilDisplayed(withId(R.id.track_edit_name), UI_TIMEOUT);
// change name for "New Name"
onView(withId(R.id.track_edit_name))
@@ -70,11 +79,11 @@ public class EspressoEditTrackRecordingTest {
// save edition
onView(withId(R.id.track_edit_save))
.perform(click());
.perform(performClick());
waitUntilDisplayed(withId(R.id.track_recording_fab_action), UI_TIMEOUT);
// stop;
onView(withId(R.id.track_recording_fab_action))
.perform(longClick());
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
// it's on track stopped activity and there are two buttons
onView(withId(R.id.resume_button))
@@ -1,100 +1,141 @@
package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.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 androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withParentIndex;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedViewCondition;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.EspressoUtils.withSelectedTab;
import static de.dennisguse.opentracks.util.EspressoUtils.withViewPagerCurrentItem;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.finishStoppedTrack;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.resumeStoppedTrack;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoUITest {
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
private final ActivityScenarioRule<TrackListActivity> activityRule = new ActivityScenarioRule<>(TrackListActivity.class);
private final GrantPermissionRule grantPermissionRule = TestUtil.createGrantPermissionRule();
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
public RuleChain rules = RuleChain.outerRule(grantPermissionRule)
.around(activityRule);
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
}
@LargeTest
@Test
public void record_stop_resume_stop_finish() {
startRecordingFromTrackList(UI_TIMEOUT);
{
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
{
// TrackRecordingActivity: wait to record some time and then stop
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(5000))
.perform(longClick());
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button);
resumeStoppedTrack(UI_TIMEOUT);
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.finish_button);
finishStoppedTrack(UI_TIMEOUT);
// TrackStoppedActivity: resume
onView(allOf(withId(R.id.resume_button), isClickable()))
.perform(click());
// TrackRecordingActivity: wait and then stop
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(5000))
.perform(longClick());
// TrackStoppedActivity
onView(withId(R.id.finish_button))
.perform(click());
onView(withId(R.id.track_list))
.check(matches(isDisplayed()));
}
}
@LargeTest
@Test
public void record_move_through_tabs() {
{
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
startRecordingFromTrackList(UI_TIMEOUT);
waitUntilDisplayed(withId(R.id.track_detail_activity_tablayout), UI_TIMEOUT);
waitUntilDisplayed(withId(R.id.track_detail_activity_view_pager), UI_TIMEOUT);
{
// TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
tabLayout.perform(selectTabAtIndex(1));
tabLayout.perform(waitFor(1000));
waitUntilTabSelectionSettlesAt(1);
tabLayout.perform(selectTabAtIndex(2));
tabLayout.perform(waitFor(1000));
waitUntilTabSelectionSettlesAt(2);
tabLayout.perform(selectTabAtIndex(3));
tabLayout.perform(waitFor(1000));
waitUntilTabSelectionSettlesAt(3);
tabLayout.perform(selectTabAtIndex(0));
tabLayout.perform(waitFor(1000));
waitUntilTabSelectionSettlesAt(0);
// stop
onView(withId(R.id.track_recording_fab_action)).
perform(longClick());
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
onView(withId(R.id.resume_button))
.check(matches(isDisplayed()));
onView(withId(R.id.finish_button))
.check(matches(isDisplayed()));
}
}
@LargeTest
@Test
public void selectAndDeleteTrack() {
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 5);
String uniqueTrackName = "SelectAndDeleteTrack-" + System.currentTimeMillis();
TrackBuilder trackBuilder = new TrackBuilder(pair.first);
trackBuilder.setName(uniqueTrackName);
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), trackBuilder.getTrack(), pair.second);
activityRule.getScenario().recreate();
waitUntilDisplayed(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)), UI_TIMEOUT);
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick());
onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)))
.perform(performLongClickOnAncestor());
onView(withId(androidx.appcompat.R.id.action_mode_bar))
.check(matches(isDisplayed()));
}
private void waitUntilTabSelectionSettlesAt(int index) {
waitForResumedViewCondition(
withId(R.id.track_detail_activity_tablayout),
view -> withSelectedTab(index).matches(view),
"selected tab to be " + index,
UI_TIMEOUT);
waitForResumedViewCondition(
withId(R.id.track_detail_activity_view_pager),
view -> withViewPagerCurrentItem(index).matches(view),
"ViewPager current item to be " + index,
UI_TIMEOUT);
}
}
@@ -20,22 +20,19 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;
@@ -78,7 +75,6 @@ import de.dennisguse.opentracks.util.FileUtils;
* @author Bartlomiej Niechwiej
* @author Youtao Liu
*/
@RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsTest {
private static final String NAME_PREFIX = "test name";
@@ -90,12 +86,6 @@ public class CustomContentProviderUtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@Mock
private Cursor cursorMock;
@Mock
private ContentResolver contentResolverMock;
@Before
public void setUp() {
contentProviderUtils = new ContentProviderUtils(context);
@@ -104,21 +94,21 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_noPoints() {
testIterator(new Track.Id(1), 0);
assertEquals(0, testIterator(new Track.Id(1), 0).size());
}
@Test
public void testLocationIterator_noAscending() {
testIterator(new Track.Id(1), 50);
testIterator(new Track.Id(2), 50);
assertEquals(50, testIterator(new Track.Id(1), 50).size());
assertEquals(50, testIterator(new Track.Id(2), 50).size());
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(new Track.Id(1), 20000 / 2);
assertEquals(20000 / 2, testIterator(new Track.Id(1), 20000 / 2).size());
}
private void testIterator(Track.Id trackId, int numPoints) {
private List<TrackPoint> testIterator(Track.Id trackId, int numPoints) {
TrackPoint.Id lastPointId = initializeTrack(trackId, numPoints);
List<TrackPoint> locations = new ArrayList<>(numPoints);
try (TrackPointIterator it = contentProviderUtils.getTrackPointIterator(trackId, null)) {
@@ -131,6 +121,7 @@ public class CustomContentProviderUtilsTest {
}
assertEquals(numPoints, locations.size());
}
return locations;
}
private TrackPoint.Id initializeTrack(Track.Id id, int numPoints) {
@@ -187,40 +178,62 @@ public class CustomContentProviderUtilsTest {
@Test
public void testCreateTrack() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
int columnIndex = 1;
// Id
when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getLong(columnIndex)).thenReturn(trackId.id());
//Uuid
columnIndex++;
when(cursorMock.getColumnIndexOrThrow(TracksColumns.UUID)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getBlob(columnIndex)).thenReturn(UUIDUtils.toBytes(UUID.randomUUID()));
// Name
columnIndex++;
String name = NAME_PREFIX + trackId.id();
when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getString(columnIndex)).thenReturn(name);
Track track = ContentProviderUtils.createTrack(cursorMock);
MatrixCursor cursor = new MatrixCursor(new String[]{
TracksColumns._ID,
TracksColumns.UUID,
TracksColumns.NAME,
TracksColumns.DESCRIPTION,
TracksColumns.ACTIVITY_TYPE,
TracksColumns.ACTIVITY_TYPE_LOCALIZED,
TracksColumns.STARTTIME,
TracksColumns.STARTTIME_OFFSET,
TracksColumns.STOPTIME,
TracksColumns.TOTALDISTANCE,
TracksColumns.TOTALTIME,
TracksColumns.MOVINGTIME,
TracksColumns.MAXSPEED,
TracksColumns.MIN_ALTITUDE,
TracksColumns.MAX_ALTITUDE,
TracksColumns.ALTITUDE_GAIN,
TracksColumns.ALTITUDE_LOSS,
});
cursor.addRow(new Object[]{
trackId.id(),
UUIDUtils.toBytes(UUID.randomUUID()),
name,
"",
ActivityType.UNKNOWN.getId(),
"",
1_000L,
ZoneOffset.UTC.getTotalSeconds(),
2_000L,
123.45f,
1_000L,
500L,
12.34f,
null,
null,
null,
null,
});
assertTrue(cursor.moveToFirst());
Track track = ContentProviderUtils.createTrack(cursor);
assertEquals(trackId, track.id());
assertEquals(name, track.name());
}
private void assertCount(int trackCount, int trackPointCount, int markerCount) {
ContentResolver contentResolver = context.getContentResolver();
try (Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID)) {
try (Cursor tracksCursor = contentResolver.query(CustomContentProvider.UrlType.TRACKS.getUri(), null, null, null, TracksColumns._ID)) {
assertEquals(trackCount, tracksCursor.getCount());
}
try (Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID)) {
try (Cursor tracksPointsCursor = contentResolver.query(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), null, null, null, TrackPointsColumns._ID)) {
assertEquals(trackPointCount, tracksPointsCursor.getCount());
}
try (Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID)) {
try (Cursor markerCursor = contentResolver.query(CustomContentProvider.UrlType.MARKERS.getUri(), null, null, null, MarkerColumns._ID)) {
assertEquals(markerCount, markerCursor.getCount());
}
}
@@ -449,8 +462,6 @@ public class CustomContentProviderUtilsTest {
marker.setDescription(TEST_DESC);
contentProviderUtils.insertMarker(marker.getMarker());
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
Marker.Id markerId = new Marker.Id(System.currentTimeMillis());
marker.setId(markerId);
ContentValues contentValues = contentProviderUtils.createContentValues(marker.getMarker(), trackId);
@@ -1184,7 +1195,7 @@ public class CustomContentProviderUtilsTest {
assertEquals(sensorStatistics.avgPower().getW(), stats.avgPower, 0f);
}
private void testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
private Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
// given
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
@@ -1207,22 +1218,27 @@ public class CustomContentProviderUtilsTest {
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.avgHeartRate().getBPM(), stats.avgHr, 0.01f);
assertEquals(sensorStatistics.maxHeartRate().getBPM(), stats.maxHr, 0.01f);
assertEquals(sensorStatistics.avgCadence().getRPM(), stats.avgCadence, 0.01f);
assertEquals(sensorStatistics.maxCadence().getRPM(), stats.maxCadence, 0.01f);
assertEquals(sensorStatistics.avgPower().getW(), stats.avgPower, 0.01f);
return Pair.create(sensorStatistics, stats);
}
@Test
public void testGetSensorStats_veryLongActivity12h() {
testGetSensorStats_randomData(43200 / 6, false);
Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> result = testGetSensorStats_randomData(43200 / 6, false);
assertEquals(result.first.avgHeartRate().getBPM(), result.second.avgHr, 0.01f);
assertEquals(result.first.maxHeartRate().getBPM(), result.second.maxHr, 0.01f);
assertEquals(result.first.avgCadence().getRPM(), result.second.avgCadence, 0.01f);
assertEquals(result.first.maxCadence().getRPM(), result.second.maxCadence, 0.01f);
assertEquals(result.first.avgPower().getW(), result.second.avgPower, 0.01f);
}
@Test
public void testGetSensorStats_withSeveralRandomStartSegments() {
testGetSensorStats_randomData(5000, true);
Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> result = testGetSensorStats_randomData(5000, true);
assertEquals(result.first.avgHeartRate().getBPM(), result.second.avgHr, 0.01f);
assertEquals(result.first.maxHeartRate().getBPM(), result.second.maxHr, 0.01f);
assertEquals(result.first.avgCadence().getRPM(), result.second.avgCadence, 0.01f);
assertEquals(result.first.maxCadence().getRPM(), result.second.maxCadence, 0.01f);
assertEquals(result.first.avgPower().getW(), result.second.avgPower, 0.01f);
}
private TrackPoint getLastValidTrackPoint(Track.Id trackId) {
@@ -0,0 +1,78 @@
package de.dennisguse.opentracks.data.models;
import static org.junit.Assert.assertEquals;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class ActivityTypeTest {
@Test
public void findBy_id() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("trail running"));
}
@Test
public void findBy_idOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
assertEquals(activityType, ActivityType.findBy(activityType.getId()));
}
}
@Test
public void findBy_synonymsOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
for (String synonym : activityType.synonyms) {
assertEquals(activityType, ActivityType.findBy(synonym));
assertEquals(activityType, ActivityType.findBy(synonym.toUpperCase(Locale.ROOT).replace(' ', '_')));
}
}
}
@Test
public void findBy_ignoresCase() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("RUNNING"));
assertEquals(ActivityType.ATV, ActivityType.findBy("atv"));
}
@Test
public void findBy_ignoresSeparators() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUNNING"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("mountain-biking"));
assertEquals(ActivityType.CROSS_COUNTRY_SKIING, ActivityType.findBy("cross_country_skiing"));
}
@Test
public void findBy_ignoresSurroundingAndRepeatedWhitespace() {
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy(" Mountain Biking "));
}
@Test
public void findBy_synonym() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("treadmill"));
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUN"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("MOUNTAIN_BIKE"));
assertEquals(ActivityType.SWIMMING_OPEN, ActivityType.findBy("SWIMMING_OPENWATER"));
assertEquals(ActivityType.BIKING, ActivityType.findBy("bike"));
}
@Test
public void findBy_unknown() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy("hovercraft"));
}
@Test
public void findBy_null() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(null));
}
@Test
public void findBy_blank() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(" "));
}
}
@@ -89,12 +89,12 @@ import de.dennisguse.opentracks.settings.PreferencesUtils;
public class ExportImportTest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(50, TimeUnit.SECONDS); //TODO 5s
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
//For csv_export_only() as we the timezone is hardcoded in the expectation.
//For csv_export_only() as the timezone is hardcoded in the expectation.
@Rule
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
@@ -199,7 +199,6 @@ public class ExportImportTest {
));
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.getSensorManager().clear();
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -214,7 +213,6 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3.1234567, 16.001, 10, 27, 15, 999.123, 0f);
trackPointCreator.getSensorManager().clear();
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
@@ -298,7 +296,22 @@ public class ExportImportTest {
Power.of(50),
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse("2020-02-02T02:02:18Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(5)),
Distance.of(0),
HeartRate.of(69),
null,
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
new TrackPoint(
null,
@@ -356,7 +369,16 @@ public class ExportImportTest {
null,
null,
new AltitudeGainLoss(0, 0)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse("2020-02-02T02:04:00Z")),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), actual);
assertEquals(new Statistics(
@@ -747,7 +769,7 @@ public class ExportImportTest {
sensorManager.getAltitudeChangeHandler().setAggregator(aggregatorBarometer);
}
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
public static void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
@@ -120,6 +120,184 @@ public class GPXTrackImporterTest {
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_missing_time() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_missing_time);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("20210907_213924.gpx", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackstatistics
Statistics trackStatistics = importedTrack.statistics();
assertEquals(0, trackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(0), trackStatistics.movingDuration());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(1, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null))
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_pwd() 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_with_power_pwr);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_gpxpx() 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_with_power_gpxpx);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_speed_no_namespace() throws IOException {
@@ -285,8 +463,8 @@ public class GPXTrackImporterTest {
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);
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone)) {
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_timezone);
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_timezone)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// when
@@ -26,6 +26,7 @@ public class TrackPointAssert {
Assert.assertEquals("has altitude", expected.position().hasAltitude(), actual.position().hasAltitude());
if (expected.position().hasAltitude()) {
Assert.assertEquals("altitude", expected.position().altitude().getClass(), actual.position().altitude().getClass());
Assert.assertEquals("altitude", expected.position().altitude().toM(), actual.position().altitude().toM(), delta);
}
@@ -14,7 +14,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.CustomContentProvider;
@RunWith(AndroidJUnit4.class)
public class DataProviderTest {
@@ -24,7 +24,7 @@ public class DataProviderTest {
@Test
public void supportsVersion2_tracks() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/1");
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = new String[]{
"_id",
"name",
@@ -64,7 +64,7 @@ public class DataProviderTest {
@Test
public void supportsVersion2_trackpoints() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
@@ -94,7 +94,7 @@ public class DataProviderTest {
@Test
public void supportsVersion2_markers() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/1");
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
@@ -126,7 +126,7 @@ public class DataProviderTest {
@Test
public void supportsVersion3_tracks() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/1");
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = {
"_id",
"name",
@@ -164,7 +164,7 @@ public class DataProviderTest {
@Test
public void supportsVersion3_trackpoints() {
//given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
@@ -194,7 +194,7 @@ public class DataProviderTest {
@Test
public void supportsVersion3_markers() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/1");
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
@@ -0,0 +1,137 @@
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Test;
public class BoschEbikeParserTest {
@Test
public void parse_multipleFrames_returnsCombinedMeasurements() {
byte[] raw = new byte[] {
// Bosch cadence frame
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76,
// Bosch human power frame
0x30, 0x05, (byte) 0x98, 0x5B, 0x08, (byte) 0x83, 0x01,
// Bosch speed frame
0x30, 0x07, (byte) 0x98, 0x2D, 0x08, (byte) 0x94, 0x0B, 0x10, 0x01
};
BoschEbikeParser.Data data = BoschEbikeParser.parse(raw);
assertNotNull(data);
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
assertEquals(131.0f, data.humanPower().getW(), 0.01f);
assertEquals(14.28 / 3.6, data.speed().toMPS(), 0.0001);
}
@Test
public void parse_multiByteVarint_returnsScaledSpeed() {
// Bosch speed frame using a multi-byte varint
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x07, (byte) 0x98, 0x2D, 0x08, (byte) 0xE1, 0x0E, 0x10, 0x01
});
assertNotNull(data);
assertEquals(18.89 / 3.6, data.speed().toMPS(), 0.0001);
}
@Test
public void parse_zeroValueFrame_returnsZeroCadence() {
// Bosch zero-value cadence frame
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x02, (byte) 0x98, 0x5A
});
assertNotNull(data);
assertEquals(0.0f, data.cadence().getRPM(), 0.01f);
assertNull(data.speed());
assertNull(data.humanPower());
}
@Test
public void parse_unsupportedMeasurementsOnly_returnsNull() {
// Bosch frame for message 0x9865, which the parser does not map to a measurement.
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x02, (byte) 0x98, 0x65
});
assertNull(data);
}
@Test
public void parse_unimplementedMeasurementsOnly_returnsNull() {
// Bosch motor power frame: message 0x985D with value 0x42, currently ignored.
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x04, (byte) 0x98, 0x5D, 0x08, 0x42
});
assertNull(data);
}
@Test
public void parse_malformedVarint_returnsNull() {
// Truncated Bosch speed frame: starts like a real 0x982D frame but the varint is incomplete.
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94
});
assertNull(data);
}
@Test
public void parse_varintLargerThan32Bit_returnsNull() {
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x08, (byte) 0x98, 0x2D, 0x08,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01
});
assertNull(data);
}
@Test
public void parse_validMeasurementBeforeMalformedFrame_returnsValidMeasurement() {
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
// Bosch cadence frame: message 0x985A with value 0x76
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76,
// Truncated Bosch speed frame: malformed trailing frame should not discard the valid cadence above.
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94
});
assertNotNull(data);
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
assertNull(data.speed());
}
@Test
public void parse_malformedVarintDoesNotConsumeNextFrameStartByte() {
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
// Truncated Bosch speed frame: the varint continuation bit is set, but the frame ends
// before the next varint byte. The following frame start byte must not be consumed.
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94,
// Bosch cadence frame: message 0x985A with value 0x76
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76
});
assertNotNull(data);
assertNull(data.speed());
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
}
@Test
public void parse_leadingNoise_ignored() {
byte[] raw = new byte[] {
// Noise bytes to verify the parser resynchronizes on the Bosch frame start byte 0x30.
0x01, 0x02, 0x03,
// Bosch cadence frame: message 0x985A with value 0x76
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76
};
BoschEbikeParser.Data data = BoschEbikeParser.parse(raw);
assertNotNull(data);
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
}
}
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -35,4 +36,27 @@ public class CyclingPowerBluetoothTest {
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
}
@Test
public void parsePayload_boschPower() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BoschEbikeParser.BOSCH_EBIKE.measurementUUID(), 0, 0);
// Bosch human power frame: message 0x985B with value 0x7B => 123 W.
characteristic.setValue(new byte[] { 0x30, 0x04, (byte) 0x98, 0x5B, 0x08, 0x7B });
CyclingPowerBluetooth.Data power = new CyclingPowerBluetooth().parsePayload(BoschEbikeParser.BOSCH_EBIKE, null, characteristic);
assertEquals(123, power.power().getW(), 0.01);
assertNull(power.crank());
}
@Test
public void parsePayload_boschCadenceOnly_returnsNull() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BoschEbikeParser.BOSCH_EBIKE.measurementUUID(), 0, 0);
// Bosch cadence-only frame: message 0x985A with value 0x01B8 => 220 / 2 = 110 rpm, so power parsing should ignore it.
characteristic.setValue(new byte[] { 0x30, 0x05, (byte) 0x98, 0x5A, 0x08, (byte) 0xB8, 0x01 });
CyclingPowerBluetooth.Data power = new CyclingPowerBluetooth().parsePayload(BoschEbikeParser.BOSCH_EBIKE, null, characteristic);
assertNull(power);
}
}
@@ -96,6 +96,25 @@ public class SensorDataCyclingTest {
assertNull(current.getAggregatedValue(Instant.MIN));
}
@Test
public void compute_cadence_directCadence_withoutPrevious() {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
current.add(Instant.MIN, new CyclingCadenceBluetooth.DirectCadenceData(Cadence.of(92)));
assertEquals(92, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
}
@Test
public void compute_cadence_directCadence_winsOverCrankData() {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1L, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.DirectCadenceData(Cadence.of(92)));
assertEquals(92, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
}
@Test
public void compute_speed() {
AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", "");
@@ -1,24 +1,24 @@
package de.dennisguse.opentracks.services;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.junit.Assert.assertEquals;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
@@ -29,32 +29,24 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
@RunWith(MockitoJUnitRunner.class)
public class TrackRecordingServiceNotificationManagerTest {
private static final String CHANNEL_ID = "TrackRecordingServiceNotificationManagerTest";
private final Context context = ApplicationProvider.getApplicationContext();
private final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
@Mock
private NotificationCompat.Builder notificationCompatBuilder;
@Mock
private NotificationManager notificationManager;
@After
public void tearDown() {
notificationManager.cancelAll();
}
@Test
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
TrackPoint trackPointMock = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
null,
null, null, Distance.of(999f),
Altitude.WGS84.of(10), null,
null,
Speed.ZERO)
);
notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean()))
.thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
TrackingNotificationBuilder notificationBuilder = new TrackingNotificationBuilder(context, CHANNEL_ID);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationBuilder);
subject.setUnitSystem(UnitSystem.METRIC);
Track track = new Track(
@@ -68,16 +60,43 @@ public class TrackRecordingServiceNotificationManagerTest {
Statistics.DEFAULT
);
RecordingData data = new RecordingData(track, trackPointMock, null, null);
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(1000));
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
assertEquals(java.util.Arrays.asList(false, true, true, true, true, true, false, true), notificationBuilder.onlyAlertOnceValues);
}
// when
subject.updateTrackPoint(context, data, Distance.of(100));
subject.updateTrackPoint(context, data, Distance.of(100));
subject.updateTrackPoint(context, data, Distance.of(1000));
subject.updateTrackPoint(context, data, Distance.of(100));
private RecordingData createRecordingData(Track track, Distance horizontalAccuracy) {
TrackPoint trackPoint = new TrackPoint(
TrackPoint.Type.TRACKPOINT,
new Position(
null,
null,
null,
horizontalAccuracy,
Altitude.WGS84.of(10),
null,
null,
Speed.ZERO
)
);
return new RecordingData(track, trackPoint, null, null);
}
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
verify(notificationCompatBuilder, times(2)).setOnlyAlertOnce(false);
private static final class TrackingNotificationBuilder extends NotificationCompat.Builder {
private final List<Boolean> onlyAlertOnceValues = new ArrayList<>();
private TrackingNotificationBuilder(Context context, String channelId) {
super(context, channelId);
setSmallIcon(R.drawable.ic_logo_color_24dp);
}
@NonNull
@Override
public NotificationCompat.Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
onlyAlertOnceValues.add(onlyAlertOnce);
return super.setOnlyAlertOnce(onlyAlertOnce);
}
}
}
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.Intent;
@@ -93,6 +94,41 @@ public class TrackRecordingServiceRecordingTest {
service = startService();
}
@MediumTest
@Test
public void reCreate_not_recording() {
// given
TrackIdStorage.unset(context);
assertFalse(service.isRecording());
// when
service.onStartCommand(null, 0, 1);
// then
assertFalse(service.isRecording());
}
@MediumTest
@Test
public void reCreate_recording() {
// given
assertFalse(service.isRecording());
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
// when (crash and restart)
service.onDestroy();
service.onCreate();
service.onStartCommand(null, 0, 1);
// then
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingStatusObservable().getValue().trackId());
service.endCurrentTrack();
}
@MediumTest
@Test
public void recording_startStop() {
@@ -15,6 +15,7 @@
*/
package de.dennisguse.opentracks.services;
import static de.dennisguse.opentracks.util.LiveDataTestUtils.waitForValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -41,6 +42,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -56,6 +58,8 @@ import de.dennisguse.opentracks.sensors.GpsStatusValue;
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceStateMachineTest {
private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(2);
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@@ -123,6 +127,7 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.tryStartSensors();
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -131,11 +136,12 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.stopSensors();
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
// assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); TODO BUG: GPS can only be stopped if a service is recording.
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
}
@MediumTest
@@ -146,6 +152,9 @@ public class TrackRecordingServiceStateMachineTest {
// when
Track.Id trackId = service.startNewTrack();
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilRecordingDataIsNotDefault();
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
@@ -154,9 +163,13 @@ public class TrackRecordingServiceStateMachineTest {
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
service.endCurrentTrack();
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
// when
service.resumeTrack(trackId);
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
@@ -167,6 +180,8 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.endCurrentTrack();
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
// then
assertFalse(service.isRecording());
@@ -177,6 +192,8 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.resumeTrack(trackId);
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
@@ -254,4 +271,25 @@ public class TrackRecordingServiceStateMachineTest {
assertNotNull(trackId);
assertNull(newTrackId);
}
private void waitUntilRecordingDataIsNotDefault() {
waitForValue(service.getRecordingDataObservable(),
recordingData -> !RecordingData.NOT_RECORDING.equals(recordingData),
WAIT_TIMEOUT,
"recording data to leave NOT_RECORDING");
}
private void waitUntilGpsStatusIs(GpsStatusValue expected) {
waitForValue(service.getGpsStatusObservable(),
expected::equals,
WAIT_TIMEOUT,
"gps status to become " + expected);
}
private void waitUntilRecordingStatusIs(RecordingStatus expected) {
waitForValue(service.getRecordingStatusObservable(),
expected::equals,
WAIT_TIMEOUT,
"recording status to become " + expected);
}
}
@@ -0,0 +1,121 @@
package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static de.dennisguse.opentracks.services.TrackRecordingService.RECORDING_DATA_UPDATE_INTERVAL;
import static de.dennisguse.opentracks.util.LiveDataTestUtils.waitForValue;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.ExportImportTest;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.GpsHandler;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceUITest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
private static final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@Test
public void getDataForUI_gps() throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
RecordingData recordingData;
// given
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock("2020-02-02T02:02:02Z");
// when
service.startNewTrack();
disconnectRealGps(trackPointCreator.getSensorManager().getGpsHandler());
trackPointCreator.getSensorManager().reset();
RecordingData initialRecordingData = waitUntilLatestTrackPointMatches(service, Instant.parse("2020-02-02T02:02:02Z"), false);
// then
TrackPoint initialTrackPoint = initialRecordingData.latestTrackPoint();
assertEquals(TrackPoint.Type.TRACKPOINT, initialTrackPoint.type());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), initialTrackPoint.getTime());
// when
// In this step, the data is saved (and Aggregator.resetAggregated called), but data should still be shown in the UI.
trackPointCreator.setClock("2020-02-02T02:02:03Z");
ExportImportTest.sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
recordingData = waitUntilLatestTrackPointMatches(service, Instant.parse("2020-02-02T02:02:03Z"), true);
// then
new TrackPointAssert().assertEquals(new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.1234567, 14.0014567, null,
Altitude.EGM2008.of(1013.05), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
recordingData.latestTrackPoint());
}
private void disconnectRealGps(GpsHandler gpsHandler) {
if (gpsHandler.driver != null) {
gpsHandler.driver.disconnect();
}
gpsHandler.onConnected(null, GpsInternal.LOCATION_PROVIDER);
}
private RecordingData waitUntilLatestTrackPointMatches(TrackRecordingService service, Instant expectedTime, boolean requiresLocation) {
return waitForValue(service.getRecordingDataObservable(),
recordingData -> {
TrackPoint latestTrackPoint = recordingData != null ? recordingData.latestTrackPoint() : null;
return latestTrackPoint != null
&& expectedTime.equals(latestTrackPoint.getTime())
&& (!requiresLocation || latestTrackPoint.position().hasLocation());
},
RECORDING_DATA_UPDATE_INTERVAL.multipliedBy(3),
"latest track point to reach " + expectedTime + (requiresLocation ? " with location" : ""));
}
}
@@ -52,6 +52,7 @@ public class VoiceAnnouncementUtilsTest {
@Before
public void setUp() {
PreferencesUtils.setVoiceAnnounceUnit(true);
PreferencesUtils.setVoiceAnnounceHeartRateCurrent(false);
PreferencesUtils.setVoiceAnnounceLapHeartRate(false);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
@@ -91,6 +92,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -131,6 +135,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -171,6 +178,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -211,6 +221,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -221,6 +234,51 @@ public class VoiceAnnouncementUtilsTest {
assertEquals("12:00 AM. Total distance 19.9 kilometers. 1 hour. Average moving speed 19.9 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_without_unit() {
PreferencesUtils.setVoiceAnnounceUnit(false);
Track track = new Track(
null,
null,
"",
"",
"",
ActivityType.AIRPLANE,
ZoneOffset.UTC,
new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1),
Distance.of(19900),
Speed.of(100),
null,
new AltitudeGainLoss(6000, 6000),
null,
null
));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("12:00 AM. Total distance 19.9. 1 hour. Average moving speed 19.9.", announcement);
}
@Test
public void getAnnouncement_withInterval_metric_speed() {
// given
@@ -233,6 +291,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -273,6 +334,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -295,6 +359,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -335,6 +402,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -375,6 +445,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -415,6 +488,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -455,6 +531,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -477,6 +556,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -517,6 +599,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -539,6 +624,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -556,6 +644,7 @@ public class VoiceAnnouncementUtilsTest {
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
int lapHeartRate = Math.round(data.second.avgHeartRate().getBPM());
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
@@ -566,6 +655,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -573,7 +665,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
// then
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate " + lapHeartRate + " bpm.", announcement);
}
@Test
@@ -589,6 +681,7 @@ public class VoiceAnnouncementUtilsTest {
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
int lapHeartRate = Math.round(data.second.avgHeartRate().getBPM());
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
@@ -599,6 +692,9 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
@@ -606,7 +702,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
// then
assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate " + lapHeartRate + " bpm.", announcement);
}
@Test
@@ -0,0 +1,51 @@
package de.dennisguse.opentracks.settings;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R;
@RunWith(AndroidJUnit4.class)
public class ImportExportSettingsIntentTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void applicationIdResource_matchesInstalledPackage() {
assertEquals(BuildConfig.APPLICATION_ID, context.getString(R.string.applicationId));
assertEquals(context.getPackageName(), context.getString(R.string.applicationId));
}
@Test
public void importExportPreferenceTargets_areResolvableInCurrentPackage() throws PackageManager.NameNotFoundException {
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity");
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity");
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserOneFileActivity");
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity");
}
private void assertResolvable(String activityClassName) throws PackageManager.NameNotFoundException {
String applicationId = context.getString(R.string.applicationId);
ComponentName componentName = new ComponentName(applicationId, activityClassName);
ActivityInfo activityInfo = context.getPackageManager().getActivityInfo(componentName, 0);
assertEquals(applicationId, activityInfo.packageName);
ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(new Intent().setComponent(componentName), 0);
assertNotNull(resolveInfo);
}
}
@@ -1,36 +1,41 @@
package de.dennisguse.opentracks.ui.aggregatedStatistics;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilRecyclerViewItemCount;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.stopTrackRecordingService;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.util.Pair;
import android.view.View;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.core.IsInstanceOf;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
@@ -41,53 +46,69 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(AndroidJUnit4.class)
public class EspressoAggregatedFilterTest {
private final String ACTIVITY_TYPE_LOCALIZED = "activityTypeLocalized";
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
private final String ACTIVITY_TYPE_LOCALIZED = "activityTypeLocalized";
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
private ActivityScenario<AggregatedStatisticsActivity> scenario;
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(androidx.test.core.app.ApplicationProvider.getApplicationContext());
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 20);
TrackBuilder trackBuilder = new TrackBuilder(pair.first);
trackBuilder.setActivityTypeLocalized(ACTIVITY_TYPE_LOCALIZED);
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), trackBuilder.getTrack(), pair.second);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, trackBuilder.getTrack(), pair.second);
scenario = ActivityScenario.launch(AggregatedStatisticsActivity.class);
}
@After
public void tearDown() {
stopTrackRecordingService();
if (scenario != null) {
scenario.close();
}
}
@Test
public void espressoAggregatedFilterTest() {
// open AggregatedStatisticsActivity through toolbar's menu item
onView(ViewMatchers.withId(R.id.bottom_app_bar))
.check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar))
.check(matches(hasDescendant(withId(R.id.aggregated_stats_button))));
onView(withId(R.id.aggregated_stats_button))
.perform(click());
waitUntilDisplayed(withId(R.id.bottom_app_bar), UI_TIMEOUT);
waitUntilDisplayed(withId(R.id.aggregated_statistics_filter), UI_TIMEOUT);
waitUntilRecyclerViewItemCount(withId(R.id.aggregated_stats_list), 1, UI_TIMEOUT);
waitUntilDisplayed(allOf(withId(R.id.aggregated_stats_type_label), withText(ACTIVITY_TYPE_LOCALIZED)), UI_TIMEOUT);
// open FilterDialogFragment through toolbar's menu item
// open FilterDialogFragment through the current bottom app bar action item
onView(withId(R.id.bottom_app_bar))
.check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar))
.check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
onView(withId(R.id.aggregated_statistics_filter))
.perform(click());
.perform(performClick());
// check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text
onView(allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
Matcher<View> activityTypeFilter = allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
withParent(allOf(withId(R.id.filter_items),
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
isDisplayed()))
.perform(waitFor(2000));
isDisplayed());
onView(activityTypeFilter)
.inRoot(isDialog())
.check(matches(isDisplayed()));
// check there's an edit text for "from date"
onView(withId(R.id.filter_date_edit_text_from))
.inRoot(isDialog())
.check(matches(isDisplayed()));
// check there's an edit text for "to date"
onView(withId(R.id.filter_date_edit_text_to))
.inRoot(isDialog())
.check(matches(isDisplayed()));
}
}
@@ -63,8 +63,8 @@ public class RecordingLayoutTest extends TestCase {
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals("Default Layout", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@@ -78,8 +78,8 @@ public class RecordingLayoutTest extends TestCase {
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals("Default Layout", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@@ -144,6 +144,6 @@ public class RecordingLayoutTest extends TestCase {
String csv = recordingLayout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
assertEquals("Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;", csv);
}
}
@@ -35,7 +35,10 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 1000f;
// when and then
whenAndThen(50, distanceInterval);
IntervalStatisticsComputation computation = computeIntervalStatistics(50, distanceInterval);
assertIntervalStatisticsComputation(computation, 50, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
}
/**
@@ -49,7 +52,10 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 1000f;
// when and then
whenAndThen(200, distanceInterval);
IntervalStatisticsComputation computation = computeIntervalStatistics(200, distanceInterval);
assertIntervalStatisticsComputation(computation, 200, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
}
/**
@@ -63,7 +69,10 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 3000f;
// when and then
whenAndThen(3000, distanceInterval);
IntervalStatisticsComputation computation = computeIntervalStatistics(3000, distanceInterval);
assertIntervalStatisticsComputation(computation, 3000, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
}
/**
@@ -77,7 +86,10 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 3000f;
// when and then
whenAndThen(1000, distanceInterval);
IntervalStatisticsComputation computation = computeIntervalStatistics(1000, distanceInterval);
assertIntervalStatisticsComputation(computation, 1000, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
}
/**
@@ -91,7 +103,10 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 1000f;
// when and then
whenAndThen(10000, distanceInterval);
IntervalStatisticsComputation computation = computeIntervalStatistics(10000, distanceInterval);
assertIntervalStatisticsComputation(computation, 10000, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
}
@Test
@@ -122,15 +137,18 @@ public class IntervalStatisticsUpdaterTest {
Pair<Track, List<TrackPoint>> trackWithStats = new Pair<>(dummyTrack, trackPoints);
// when and then
whenAndThen(trackWithStats.first, trackWithStats.second, numberOfPoints, distanceInterval);
IntervalStatisticsComputation computation = computeIntervalStatistics(trackWithStats.first, trackWithStats.second, distanceInterval);
assertIntervalStatisticsComputation(computation, numberOfPoints, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
}
private void whenAndThen(int numberOfPoints, float distanceInterval) {
private IntervalStatisticsComputation computeIntervalStatistics(int numberOfPoints, float distanceInterval) {
Pair<Track, List<TrackPoint>> trackWithStats = buildTrackWithTrackPoints(numberOfPoints);
whenAndThen(trackWithStats.first, trackWithStats.second, numberOfPoints, distanceInterval);
return computeIntervalStatistics(trackWithStats.first, trackWithStats.second, distanceInterval);
}
private void whenAndThen(Track track, List<TrackPoint> trackPoints, int numberOfPoints, float distanceInterval) {
private IntervalStatisticsComputation computeIntervalStatistics(Track track, List<TrackPoint> trackPoints, float distanceInterval) {
IntervalStatisticsUpdater intervalStatistics = new IntervalStatisticsUpdater(Distance.of(distanceInterval));
intervalStatistics.addTrackPoints(trackPoints.iterator());
@@ -157,23 +175,27 @@ public class IntervalStatisticsUpdaterTest {
}
}
// then
assertEquals(track.statistics().totalDuration().toSeconds(), totalTime, 0.01);
assertEquals(track.statistics().totalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(intervalList.size(), (int) Math.ceil(track.statistics().totalDistance().toM() / distanceInterval));
if (totalGain != null && totalLoss != null) {
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
assertEquals(totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
return new IntervalStatisticsComputation(track, intervalList, totalDistance, totalTime, totalGain, totalLoss);
}
private void assertIntervalStatisticsComputation(IntervalStatisticsComputation computation, int numberOfPoints, float distanceInterval) {
assertEquals(computation.track.statistics().totalDuration().toSeconds(), computation.totalTime, 0.01);
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
assertEquals(computation.intervalList.size(), (int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval));
if (computation.totalGain != null && computation.totalLoss != null) {
assertEquals(computation.totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
assertEquals(computation.totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
} else {
assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(i -> i.altitudeGainLoss() != null));
assertTrue(computation.intervalList.stream().noneMatch(i -> i.altitudeGainLoss() != null));
}
for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).totalDistance().toM(), distanceInterval, 0.001);
totalDistance = totalDistance.minus(intervalList.get(i).totalDistance());
Distance remainingDistance = computation.totalDistance;
for (int i = 0; i < computation.intervalList.size() - 1; i++) {
assertEquals(computation.intervalList.get(i).totalDistance().toM(), distanceInterval, 0.001);
remainingDistance = remainingDistance.minus(computation.intervalList.get(i).totalDistance());
}
assertEquals(intervalList.get(intervalList.size() - 1).totalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(computation.intervalList.get(computation.intervalList.size() - 1).totalDistance().toM(), remainingDistance.toM(), 0.01);
}
private static Pair<Track, List<TrackPoint>> buildTrackWithTrackPoints(int numberOfPoints) {
@@ -195,4 +217,27 @@ public class IntervalStatisticsUpdaterTest {
return new Pair<>(track, trackPoints);
}
private static final class IntervalStatisticsComputation {
private final Track track;
private final List<Statistics> intervalList;
private final Distance totalDistance;
private final float totalTime;
private final Float totalGain;
private final Float totalLoss;
private IntervalStatisticsComputation(Track track,
List<Statistics> intervalList,
Distance totalDistance,
float totalTime,
Float totalGain,
Float totalLoss) {
this.track = track;
this.intervalList = intervalList;
this.totalDistance = totalDistance;
this.totalTime = totalTime;
this.totalGain = totalGain;
this.totalLoss = totalLoss;
}
}
}
@@ -0,0 +1,107 @@
package de.dennisguse.opentracks.util;
import static org.junit.Assert.fail;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public final class ActivityTestUtils {
private ActivityTestUtils() {
}
public static <T extends Activity> T waitForResumedActivity(Class<T> activityClass, Duration timeout) {
T resumedActivity = getResumedActivity(activityClass);
if (resumedActivity != null) {
return resumedActivity;
}
Application application = ApplicationProvider.getApplicationContext();
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<T> resumedActivityRef = new AtomicReference<>();
Application.ActivityLifecycleCallbacks callbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityResumed(@NonNull Activity activity) {
if (activityClass.isInstance(activity)) {
resumedActivityRef.set(activityClass.cast(activity));
latch.countDown();
}
}
@Override
public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
};
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
application.registerActivityLifecycleCallbacks(callbacks);
T currentResumedActivity = findResumedActivity(activityClass);
if (currentResumedActivity != null) {
resumedActivityRef.set(currentResumedActivity);
latch.countDown();
}
});
try {
if (!latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
fail("Timed out waiting for resumed activity " + activityClass.getSimpleName());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("Interrupted while waiting for resumed activity " + activityClass.getSimpleName());
} finally {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> application.unregisterActivityLifecycleCallbacks(callbacks));
}
return resumedActivityRef.get();
}
private static <T extends Activity> T getResumedActivity(Class<T> activityClass) {
AtomicReference<T> resumedActivity = new AtomicReference<>();
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> resumedActivity.set(findResumedActivity(activityClass)));
return resumedActivity.get();
}
private static <T extends Activity> T findResumedActivity(Class<T> activityClass) {
for (Activity activity : ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)) {
if (activityClass.isInstance(activity)) {
return activityClass.cast(activity);
}
}
return null;
}
}
@@ -1,14 +1,25 @@
package de.dennisguse.opentracks.util;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static org.hamcrest.Matchers.allOf;
import android.app.Activity;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ListView;
import android.widget.AdapterView;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
@@ -16,61 +27,112 @@ import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
public class EspressoUtils {
public static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
public static Matcher<View> withRecyclerViewItemCount(final int count) {
return new TypeSafeMatcher<>() {
@Override
protected boolean matchesSafely(View view) {
if (!(view instanceof RecyclerView recyclerView)) {
return false;
}
RecyclerView.Adapter<?> adapter = recyclerView.getAdapter();
return adapter != null && adapter.getItemCount() == count;
}
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
description.appendText("RecyclerView should have " + count + " items");
}
};
}
public static Matcher<View> withListSize (final int size) {
public static Matcher<View> withSelectedTab(final int index) {
return new TypeSafeMatcher<>() {
@Override
public boolean matchesSafely(final View view) {
return ((ListView) view).getCount() == size;
protected boolean matchesSafely(View view) {
return view instanceof TabLayout tabLayout
&& tabLayout.getSelectedTabPosition() == index;
}
@Override
public void describeTo(final Description description) {
description.appendText("ListView should have " + size + " items");
public void describeTo(Description description) {
description.appendText("TabLayout should have selected tab " + index);
}
};
}
public static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
public static Matcher<View> withViewPagerCurrentItem(final int index) {
return new TypeSafeMatcher<>() {
@Override
public String getDescription() {
return "Wait for milliseconds.";
protected boolean matchesSafely(View view) {
return view instanceof ViewPager2 viewPager2
&& viewPager2.getCurrentItem() == index
&& viewPager2.getScrollState() == ViewPager2.SCROLL_STATE_IDLE;
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
public void describeTo(Description description) {
description.appendText("ViewPager2 should be idle on item " + index);
}
};
}
public static void waitForResumedView(final Matcher<View> viewMatcher, final Duration timeout) {
waitForResumedViewCondition(viewMatcher, ignored -> true, "view to match " + viewMatcher, timeout);
}
public static void waitUntilDisplayed(final Matcher<View> viewMatcher, final Duration timeout) {
waitForResumedView(allOf(viewMatcher, isDisplayed()), timeout);
}
public static void waitUntilRecyclerViewItemCount(final Matcher<View> viewMatcher, final int expectedCount, final Duration timeout) {
waitForResumedViewCondition(
allOf(viewMatcher, isDisplayed()),
view -> withRecyclerViewItemCount(expectedCount).matches(view),
"RecyclerView item count to be " + expectedCount,
timeout);
}
public static void waitForResumedViewCondition(final Matcher<View> viewMatcher,
final Predicate<View> condition,
final String conditionDescription,
final Duration timeout) {
long deadline = SystemClock.uptimeMillis() + timeout.toMillis();
do {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
AtomicBoolean matched = new AtomicBoolean(false);
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
for (Activity activity : ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)) {
View decorView = activity.getWindow() != null ? activity.getWindow().getDecorView() : null;
if (decorView == null) {
continue;
}
for (View child : TreeIterables.breadthFirstViewTraversal(decorView)) {
if (viewMatcher.matches(child) && condition.test(child)) {
matched.set(true);
return;
}
}
}
});
if (matched.get()) {
return;
}
} while (SystemClock.uptimeMillis() < deadline);
throw new AssertionError("Timed out waiting for " + conditionDescription);
}
public static ViewAction selectTabAtIndex(final int index) {
return new ViewAction() {
@Override
@@ -91,4 +153,135 @@ public class EspressoUtils {
};
}
public static ViewAction performClick() {
return new ViewAction() {
@Override
public String getDescription() {
return "invoke View.performClick()";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, View view) {
if (!view.performClick()) {
throw new PerformException.Builder()
.withActionDescription(getDescription())
.withViewDescription(HumanReadables.describe(view))
.build();
}
uiController.loopMainThreadUntilIdle();
}
};
}
public static ViewAction performClickOnAncestor() {
return new ViewAction() {
@Override
public String getDescription() {
return "invoke performClick() on the matched view or its ancestor";
}
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(View.class);
}
@Override
public void perform(UiController uiController, View view) {
View current = view;
while (current != null) {
if (current.performClick()) {
uiController.loopMainThreadUntilIdle();
return;
}
ViewParent parent = current.getParent();
current = parent instanceof View ? (View) parent : null;
}
current = view;
while (current != null) {
ViewParent parent = current.getParent();
if (parent instanceof AdapterView<?> adapterView) {
int position = adapterView.getPositionForView(current);
if (position != AdapterView.INVALID_POSITION
&& adapterView.performItemClick(current, position, adapterView.getAdapter().getItemId(position))) {
uiController.loopMainThreadUntilIdle();
return;
}
}
current = parent instanceof View ? (View) parent : null;
}
throw new PerformException.Builder()
.withActionDescription(getDescription())
.withViewDescription(HumanReadables.describe(view))
.build();
}
};
}
public static ViewAction performLongClick() {
return new ViewAction() {
@Override
public String getDescription() {
return "invoke View.performLongClick()";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, View view) {
if (!view.performLongClick()) {
throw new PerformException.Builder()
.withActionDescription(getDescription())
.withViewDescription(HumanReadables.describe(view))
.build();
}
uiController.loopMainThreadUntilIdle();
}
};
}
public static ViewAction performLongClickOnAncestor() {
return new ViewAction() {
@Override
public String getDescription() {
return "invoke performLongClick() on the matched view or its ancestor";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, View view) {
View current = view;
while (current != null) {
if (current.performLongClick()) {
uiController.loopMainThreadUntilIdle();
return;
}
ViewParent parent = current.getParent();
current = parent instanceof View ? (View) parent : null;
}
throw new PerformException.Builder()
.withActionDescription(getDescription())
.withViewDescription(HumanReadables.describe(view))
.build();
}
};
}
}

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