Compare commits

...

243 Commits

Author SHA1 Message Date
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
Dennis Guse 708cbf0b2a Refactored Sensor subsystem 2026-02-10 18:30:15 +01:00
Dennis Guse 60e2fe57f9 Bugfix: SensorHandler may receive a connectionLost from BluetoothDriver without being connected first. 2026-02-10 18:13:13 +01:00
Dennis Guse 14cebf3fec Aggregator: there are no default values anymore. 2026-02-10 11:13:57 +01:00
Dennis Guse b651a8ca6a GPS is now optional: data source (internal, none) be selected in settings.
Migrated to GPSHandler.

Fixes #1279.
2026-02-10 10:07:41 +01:00
Dennis Guse d75472ef11 BluetoothLeSensorPreference was not adhering to configured default value. 2026-02-09 18:15:37 +01:00
Dennis Guse 0b849c3d55 CyclingDistanceSpeedHandler: uses wheel size (again).
Skipped while to SensorHandler: a5dcbec3961957dfc651cf4ffd62ec075324c0ba
2026-02-09 18:15:37 +01:00
Dennis Guse 1c767c41a7 DriverObserver: renamed methods and all drivers properly use onConnected, onConnectionLost and onDisconnected. 2026-02-09 18:15:37 +01:00
Dennis Guse dfa80d61d7 Restored functionality: sensor name/address is shown AFTER connecting. 2026-02-09 18:15:37 +01:00
Dennis Guse fa558c9ab7 Cleanup: SensorManager avoids instanceof for testing. 2026-02-09 18:15:37 +01:00
Dennis Guse a1a363698f SensorHandler now interacts with Aggregator. 2026-02-09 18:15:37 +01:00
Dennis Guse b79acfb942 Cleanup: SensorManager provides context and handler. 2026-02-09 18:15:37 +01:00
Dennis Guse 24ea718ff2 Cleanup: renamed GainManager to AltitudeChangeHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 6d187fa66d Cleanup: removed DriverObserver0 and moved sensor drivers to separate package. 2026-02-09 18:15:37 +01:00
Dennis Guse ea27db13fa Created RunningSpeedCadenceDistanceHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 29d2e7c78c Created CyclingPowerHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 4b7badbecc Created CyclingDistanceSpeedHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 0dcb8010e2 Created CyclingCadenceHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse 0a9d4b207c Created TemperatureHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse c6cb388ce4 Created HeartRateHandler. 2026-02-09 18:15:37 +01:00
Dennis Guse e6e009ba14 GainManager should only re-connect if sensor should be changed. 2026-02-09 18:15:37 +01:00
Dennis Guse f8aa456549 Extract SensorHandler from GainManager for re-use. 2026-02-09 18:15:37 +01:00
Dennis Guse 167d90332e Extract Driver for internal GPS to GpsInternal. 2026-02-09 18:15:37 +01:00
Dennis Guse 719a052f34 Cleanup: merged BluetoothRemoteSensorManager into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 244ca26210 Cleanup: created new DriverObserver interface. 2026-02-09 18:15:37 +01:00
Dennis Guse 476f3a517b Cleanup: GainManager now processes AggregatorBarometer directly (instead of SensorManager). 2026-02-09 18:15:37 +01:00
Dennis Guse 289e93eb5d Cleanup: moved AggregatorGPS to GPSManager. 2026-02-09 18:15:37 +01:00
Dennis Guse 18ff96733e Cleanup: moved Raw into Aggregator.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 0f2c5adf26 Cleanup: SensorManager.SensorDataChangedObserver inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 74954df600 Cleanup: SensorManager.SensorDataChangedObserver inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 1a14fed287 Cleanup: SensorDataAggregator inlined into SensorManager.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 744853ee91 Cleanup: removed SensorConnector interface.
Preparation for refactoring.
2026-02-09 18:15:37 +01:00
Dennis Guse 597e1b2c7f Release: v4.25.2 2026-02-09 07:51:06 +01:00
jimkats 8156993921 Translated using Weblate (Greek)
Currently translated at 51.0% (278 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-02-08 22:49:35 +01:00
Dennis Guse 9890dc28bd Dashboard API: provide version 2 again (required by OSMDashboard until v6.4.1).
Part of #2265.
2026-02-08 22:48:43 +01:00
Dennis Guse b65d83010d Bugfix: ShareContentProvider should report contentType for all Uris (not only TrackFileFormat).
Part of #2265.
2026-02-08 18:11:52 +01:00
Dennis Guse 1a5d06f9c6 Cleanup: renamed IntentDashboardUtils.java to DataProvider. 2026-02-07 19:20:48 +01:00
Dennis Guse 723c21d7ec Dashboard API: add tests for column names.
Part of #2265.
2026-02-07 09:39:23 +01:00
Dennis Guse 8fe0c3be7d Bugfix: update data for UI immediately after starting recording. 2026-02-06 18:54:32 +01:00
Dennis Guse e64792bf3f Resetting preferences requires clearing and naming each preference file explicitly. 2026-02-06 18:54:18 +01:00
Dennis Guse 21bafff761 TrackRecordingService should start UI timer at the end of startRecording. 2026-02-06 18:54:06 +01:00
Dennis Guse c33c2de51d Cleanup: EspressoUI should always lookup UI elements. 2026-02-06 18:53:55 +01:00
Dennis Guse a6b11c1c37 Release: v4.25.1 2026-02-01 22:05:05 +01:00
Dennis Guse 315788308b Bumping DashboardAPI to version 3.
Fixes #2255.
2026-02-01 20:15:24 +01:00
Dennis Guse 547c1c5c65 Rename MarkerColumns.
Fixes #2255.
2026-02-01 20:15:24 +01:00
tace16 69efd6fb82 Translated using Weblate (Tamil)
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/ta/
2026-01-31 18:08:56 +01:00
ghose 3a0524f96d Translated using Weblate (Galician)
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/gl/
2026-01-31 18:08:56 +01:00
Priit Jõerüüt 05456b44ef Translated using Weblate (Estonian)
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/et/
2026-01-31 18:08:56 +01:00
tace16 b449dad619 Translated using Weblate (Tamil)
Currently translated at 100.0% (179 of 179 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/ta/
2026-01-31 18:08:56 +01:00
SomeTr ccefe36489 Translated using Weblate (Ukrainian)
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/uk/
2026-01-31 18:08:56 +01:00
ovl-005 90e9ab6d2a Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.1% (535 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/nb_NO/
2026-01-31 18:08:56 +01:00
Outbreak2096 d1061fa6cd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 88.8% (159 of 179 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/zh_Hans/
2026-01-31 18:08:56 +01:00
Outbreak2096 c0c813001f Translated using Weblate (Chinese (Simplified Han script))
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/zh_Hans/
2026-01-31 18:08:56 +01:00
Outbreak2096 51498bbcf9 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (542 of 542 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-01-31 18:08:56 +01:00
CI/CD Bot by pstorch 4ef1fcefbc Update dependency gradle to v9.3.1 2026-01-29 15:35:43 +00:00
CI/CD Bot by pstorch 9a572130a7 Update dependency androidx.work:work-runtime to v2.11.1 2026-01-29 00:34:18 +00:00
Dennis Guse e931f5ab7c Reproducible build: for naming the APK use the actual versionCode.
Fixes #2252.
2026-01-23 15:11:37 +01:00
Dennis Guse 9affe28fad Release: v4.25.0 2026-01-21 18:33:40 +01:00
Dennis Guse 4905a37951 Build scripts: add some error handling. 2026-01-21 18:33:20 +01:00
Dennis Guse 439671d0a4 Build scripts: add some error handling. 2026-01-18 15:49:41 +01:00
Dennis Guse 825e8dda6e Revert "Translated using Weblate (English (en_001))"
This reverts commit 99a9248b27.
2026-01-18 10:13:00 +01:00
Dennis Guse 661c839c78 Update README_TESTED_SENSORS.md
Assioma Pro MX-2

Fixes #2246.
2026-01-18 10:08:42 +01:00
Dennis Guse 8eb42a7ecb Upgrade to Android Gradle Plugin 9.0: removing applicationVariants 2026-01-17 22:58:17 +01:00
CI/CD Bot by pstorch 660a55ff5a Update dependency gradle to v9.3.0 2026-01-16 12:45:31 +00:00
CI/CD Bot by pstorch 958697bcb3 Update dependency com.android.tools.build:gradle to v9 2026-01-15 17:42:07 +00:00
ntd 6029eb0317 Translated using Weblate (Italian)
Currently translated at 100.0% (542 of 542 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-01-10 21:48:12 +01:00
ntd 8b56d53a6a Translated using Weblate (Italian)
Currently translated at 3.9% (7 of 178 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/it/
2026-01-10 21:48:12 +01:00
Dennis Guse e9deebea6c Refactor: RecordingLayout. 2026-01-10 21:43:54 +01:00
Dennis Guse 54844b9a9a Sensor: add temperature.
Fixes #2241.
2026-01-10 21:43:54 +01:00
Dennis Guse 738201c1c2 Cleanup. 2026-01-04 09:57:05 +01:00
discollizard 48b6be0bd1 record button moved to the middle of the screen
Part of #2199
2026-01-03 18:40:17 -03:00
Priit Jõerüüt 6b924cd936 Translated using Weblate (Estonian)
Currently translated at 100.0% (542 of 542 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-01-02 19:56:27 +01:00
discollizard 5f083de530 adjusted distance time and markers to be horizontally consistent
formatter

initial date and time are always visible in the list while recording
2026-01-02 15:31:42 -03:00
Matt Votava 278f6323f1 track import: get extension from file name, not URI
When importing tracks from nextcloud, the URI for a file may be something like

content://org.nextcloud.documents/tree/7keh3ot7zmiaxecoktv6bfn6gdygt2lg%2F707/document/7keh3ot7zmiaxecoktv6bfn6gdygt2lg%2F745

The URI contains no ".", so getExtension() would return the whole URI as the extension, resulting in an "Unsupported file format" error.

To fix this, also pass DocumentFile.getName() to the ImportWorker.
2025-12-28 01:30:10 -08:00
Dennis Guse 5baa8fa21e Cleanup: KMLTrackImporter doesn't need Position at first. 2025-12-28 08:59:19 +01:00
Dennis Guse 707f4add71 Cleanup: GpsStatusManager only needs Position. 2025-12-28 08:59:19 +01:00
Dennis Guse b583f8f23d Sticker: added printable PDF 2025-12-27 08:38:01 +01:00
Priit Jõerüüt c847f622e4 Translated using Weblate (Estonian)
Currently translated at 91.3% (495 of 542 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/
2025-12-27 07:35:12 +01:00
Priit Jõerüüt 70dd58f484 Translated using Weblate (Estonian)
Currently translated at 78.9% (428 of 542 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/
2025-12-27 07:35:12 +01:00
Dennis Guse 015352c989 TrackPoint is now a record. 2025-12-24 17:15:19 +01:00
Dennis Guse 42ae582cd8 Release: v4.24.2 2025-12-19 22:14:30 +01:00
frazou 6e0ad50397 Translated using Weblate (French)
Currently translated at 100.0% (544 of 544 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/fr/
2025-12-13 12:35:09 +01:00
Dennis Guse d43efad52a Bugfix: crash on select all tracks or all markers.
Fixes #2231.
2025-12-12 21:59:45 +01:00
Dennis Guse 693904529d Release: v4.24.1 2025-12-11 22:46:23 +01:00
Dennis Guse 08143a546a Bugfix: marker.icon column is still used by OSMDashboard v6.4.1
Fixes https://codeberg.org/OpenTracksApp/OSMDashboard/issues/552
2025-12-11 22:44:44 +01:00
Dennis Guse eb49156ee7 Bugfix: crash when using number as default track name.
Fixes #2229.
2025-12-11 18:24:47 +01:00
CI/CD Bot by pstorch 51c03d8d75 Update dependency com.android.tools.build:gradle to v8.13.2 2025-12-11 05:33:57 +00:00
CI/CD Bot by pstorch 666cbbad68 Update dependency org.mockito:mockito-android to v5.21.0 2025-12-09 19:34:11 +00:00
282 changed files with 8661 additions and 4178 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
image: 'ubuntu:android-24.04'
steps:
- name: Checkout sources
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
@@ -27,7 +27,7 @@ jobs:
image: 'ubuntu:android-24.04'
steps:
- name: Checkout sources
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
+18 -1
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)
@@ -44,7 +45,10 @@ However, often only one value is provided.
We only support _Instantaneous Power_.
For cycling (incl. cadence):
* Assioma Duo Powermeter
* Assioma
* Duo Powermeter
* Pro MX-2
* Elite drivo II
* QUARQ Red DZero Powermeter
* Tacx Satori Smart
@@ -109,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
+2 -5
View File
@@ -11,9 +11,6 @@ CHANGELOG_FILE="$CHANGELOG_DIR/$VERSIONID.txt"
git diff --exit-code || echo -e "\nWARNING WARNING WARNING WARNING WARNING: this branch contains uncommit changes."
#TODO Execute all tests: we are destroying data, running on a real device.
# ./gradlew connectedCheck
# Changelog
echo -e "\nPrevious changelog\n------------"
PREVIOUS_CHANGELOG=$(find "$CHANGELOG_DIR" -iname "[[:digit:]]**.txt" | sort -n | tail -1)
@@ -34,8 +31,8 @@ gedit -w "$CHANGELOG_FILE"
git add "$CHANGELOG_FILE"
echo "Updating build.gradle"
sed -i s/versionCode\ .*/versionCode\ "$VERSIONID"/g build.gradle
sed -i s/versionName\ .*/versionName\ "\"$VERSIONNAME\""/g build.gradle
sed -i -e "s/versionCode\ [0-9]\{1,\}/versionCode\ $VERSIONID/g" build.gradle
sed -i -e "s/versionName\ \"v[0-9\.]\{1,\}\"/versionName\ \"$VERSIONNAME\"/g" build.gradle
git diff
+7 -2
View File
@@ -6,8 +6,13 @@ export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64/
JAVA_VERSION=`java --version`
if [[ $JAVA_VERSION != "openjdk 21."* ]]; then
echo "OpenJDK version should be 21.X"
exit -1;
echo -e "OpenJDK version should be 21.X"
fi
git diff --exit-code || echo -e "\nWARNING WARNING WARNING WARNING WARNING: this branch contains uncommit changes."
if [[ $(git log "$(git describe --tags --abbrev=0)..HEAD" --no-merges --oneline | wc -l) -gt 0 ]]; then
echo -e "No version tag for this commit; please use RELEASE.sh";
fi
RELEASE_STORE_PASSWORD=
+33 -32
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.1'
classpath 'com.android.tools.build:gradle:9.3.2'
}
}
@@ -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 getVersionCode = { ->
return Integer.valueOf('git rev-list HEAD --count'.execute().text.trim())
def getVersionCodeNightly = { ->
return Integer.valueOf('git rev-list HEAD --count'.execute(null, project.rootDir).text.trim())
}
android {
@@ -33,11 +33,12 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
compileSdk = 36
compileSdk = 37
buildFeatures {
viewBinding = true
buildConfig = true
resValues = true
}
androidResources {
@@ -46,13 +47,13 @@ android {
defaultConfig {
applicationId 'de.dennisguse.opentracks'
versionCode 6508
versionName "v4.24.0"
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'
@@ -106,6 +107,11 @@ android {
nightly {
dimension 'version'
applicationId 'de.dennisguse.opentracks.nightly'
versionCode getVersionCodeNightly()
versionName getVersionName()
base.archivesName = "de.dennisguse.opentracks.nightly_${getVersionCodeNightly()}"
signingConfig = signingConfigs.nightly
}
irreproducible {
@@ -120,27 +126,13 @@ android {
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
dimension 'version'
applicationId 'de.dennisguse.opentracks.playstore'
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
signingConfig = signingConfigs.release
}
}
applicationVariants.configureEach { variant ->
variant.resValue 'string', 'applicationId', variant.applicationId
variant.outputs.configureEach {
if (variant.flavorName == 'nightly') {
setVersionCodeOverride(getVersionCode())
setVersionNameOverride(getVersionName())
}
if (variant.flavorName == 'reproducible') {
outputFileName = "${applicationId}_${variant.versionName}.apk"
} else {
outputFileName = "${applicationId}_${variant.versionCode}.apk"
}
}
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
@@ -153,24 +145,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.0'
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.20.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)
)
}
}
@@ -0,0 +1,10 @@
(versionName): OpenTracks
Ændringer:
-
Fejlrettelser:
-
Udvikler:
-
@@ -0,0 +1,6 @@
v4.24.1: OpenTracks
Bugfix:
- Restore compatibility with OSM Dashboard v6.4.1 and earlier (introduced in v4.24.0)
- Crash when using default track name by number (introduced in v4.24.0)
@@ -0,0 +1,4 @@
v4.24.2: OpenTracks
Bugfix:
- Crash when select all (introduced in v4.24.0)
@@ -0,0 +1,13 @@
v4.25.0: OpenTracks
Changes:
- Record button is centered
- Support for BLE thermometer (Environmental Sensing)
Bugfix:
- On import: get type from filename (not URI)
Developer:
- TrackPoint is a record
- Update to Android Gradle Plugin 9.0
@@ -0,0 +1,7 @@
v4.25.1: OpenTracks
Bugfix:
- Since v4.24.0, the API for transferring data to OSM Dashboard was broken.
Sadly, the changes in OpenTracks could not be reverted.
Thus, the protocol version was bumped to 3.
This requires updating OSM Dashboard to a version AFTER v6.5.7.
@@ -0,0 +1,8 @@
v4.25.2: OpenTracks
Changes:
- Restored compatibility with OSMDashboard (Dashboard API protocol version 2)
Bugfix:
- Update UI immediately after resuming a track
@@ -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
@@ -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 son ejecutables (pero muchas fallan)
- Se ha habilitado la integración continua con TravisCI (sin pruebas)
@@ -0,0 +1,14 @@
v3.1.0: OpenTracks
Cambios:
- Se agregó la capacidad de mostrar rutas en la aplicación de mapas (probado con OsmAnd y Maps.ME).
- Se ha añadido la posibilidad de visualizar las estadísticas de seguimiento sin desbloquear el dispositivo durante el mismo.
- Se ha eliminado el modo horizontal de la vista de detalles de la pista (debido a un error).
- El historial de sugerencias de búsqueda ha sido eliminado.
Corrección de errores:
- Solicitar permisos de almacenamiento para todas las importaciones/todas las exportaciones
Desarrolladores:
- Actualizar a Gradle 5.6
- No minimice la versión 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,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
@@ -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)
@@ -4,7 +4,7 @@ Modifiche:
- Velocità degli annunci vocali configurabile
- API <26: L'icona del lanciatore omette lo sfondo
Bugfix:
Correzioni:
- UI di registrazione: mostra i valori corretti
- Registrazione UI: mostra (di nuovo) l'altitudine in EGM2008
@@ -1,4 +1,4 @@
v4.22.1: OpenTracks
Modiciche:
Modifiche:
- Primo rilascio da Codeberg
@@ -1,4 +1,4 @@
v4.22.2: OpenTracks
Correzione di errori:
Correzioni:
- L'impostazione degli annunci vocali per l'ora e l'unità di misura è stata collegata
@@ -0,0 +1,5 @@
v4.26.2: OpenTracks
Correzioni:
- Il GPS via Bluetooth non è (ancora) supportato
- Il tipo di altitudine (EGM2008) non viene mostrato
@@ -3,7 +3,7 @@
Modifiche:
-
Correzioni di errori:
Correzioni:
-
Sviluppatore:
@@ -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 +1 @@
Un tracker sportivo che rispetta completamente la tua privacy.
Un tracker sportivo che rispetta la tua privacy.
@@ -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
@@ -0,0 +1,15 @@
v4.24.0: OpenTracks
மாற்றங்கள்:
- PublicAPI: தரவு இருந்தால் ஸ்டார்ட் டிராக் மட்டுமே புலத்தை அமைக்கும்
- மார்க்கர் புகைப்படங்களை மீண்டும் பகிரலாம்
- புகைப்படங்கள் இல்லாமல் KMZ அகற்றப்பட்டது
- இதயத் துடிப்பு 0.0 தவறானதாகக் கருதப்படுகிறது (சென்சார் ஸ்டாடிஸ்டிக்ஸ் தவிர)
- சிறப்பு ஒருங்கிணைப்புகளுக்கான இறக்குமதி தீர்வு நீக்கப்பட்டது (100/200)
(சிறப்பு டிராக் பாயிண்ட்களை ஏற்றுமதி/இறக்குமதி செய்ய MyTracks பயன்படுத்தியது)
டெவலப்பர்:
- ட்ராக் மற்றும் மார்க்கர் பதிவுகள்
- திரட்டப்பட்ட புள்ளிவிவரங்கள் தரவுத்தளத்தில் கணக்கிடப்படுகின்றன
- TrackStatisticsUpdater மற்றும் SegmentStatisticUpdater எனப் பிரிக்கவும்
- அறிமுகப்படுத்தப்பட்ட இட்டரேட்டர் வகுப்புகள் (கர்சர் அல்லது பட்டியலைக் கடந்து செல்வதற்குப் பதிலாக)
@@ -0,0 +1,5 @@
v4.24.1: OpenTracks
பிழைத்திருத்தம்:
- OSM டாஷ்போர்டு v6.4.1 மற்றும் அதற்கு முந்தையவற்றுடன் இணக்கத்தன்மையை மீட்டெடுக்கவும் (v4.24.0 இல் அறிமுகப்படுத்தப்பட்டது)
- எண்ணின்படி இயல்புநிலை ட்ராக் பெயரைப் பயன்படுத்தும்போது செயலிழக்கிறது (v4.24.0 இல் அறிமுகப்படுத்தப்பட்டது)
@@ -0,0 +1,4 @@
v4.24.2: OpenTracks
பிழைத்திருத்தம்:
- அனைத்தையும் தேர்ந்தெடுக்கும்போது செயலிழக்க (v4.24.0 இல் அறிமுகப்படுத்தப்பட்டது)
@@ -0,0 +1,12 @@
v4.25.0: OpenTracks
மாற்றங்கள்:
- பதிவுப் பொத்தான் மையமாக உள்ளது
- BLE தெர்மோமீட்டருக்கான ஆதரவு (சுற்றுச்சூழல் உணர்தல்)
பிழைத்திருத்தம்:
- இறக்குமதியில்: கோப்பு பெயரிலிருந்து வகையைப் பெறுங்கள் (URI அல்ல)
டெவலப்பர்:
- TrackPoint ஒரு பதிவு
- Android Gradle செருகுநிரல் 9.0 க்கு புதுப்பிக்கவும்
@@ -17,6 +17,6 @@
* 深色和浅色主题,跟随系统设置。
* 无广告。
‌自由软件/开放源代码
自由软件/开放源代码
意味着您可以使用、研究、修改和分享源代码。
采用 Apache 2.0 许可
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+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>
Binary file not shown.
@@ -2,71 +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();
@Test
public void espressoDeleteTrackTest() {
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action)).perform(click());
// TrackRecordingActivity
onView(withId(R.id.track_recording_fab_action))
// wait; stay recording
.perform(waitFor(5000))
// stop;
.perform(longClick());
// TrackStoppedActivity
onView(withId(R.id.finish_button)).perform(click());
// select track
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick());
// 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());
onView(withText("Delete")).perform(click());
onView(withText("OK")).perform(click());
// tracklist is empty now
onView(allOf(withText("Start recording your next adventure here"), isDisplayed()));
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
}
@Test
public void deleteTrackFromTrackStoppedActivity() {
recordTrackAndOpenTrackStoppedActivity();
// 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);
onView(withId(R.id.finish_button))
.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);
onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)))
.perform(performLongClickOnAncestor());
clickDeleteSelectedTrackAction();
onView(withText(android.R.string.ok))
.inRoot(isDialog())
.perform(performClick());
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0, UI_TIMEOUT);
onView(withId(R.id.track_list))
.check(matches(withRecyclerViewItemCount(0)));
}
private void recordTrackAndOpenTrackStoppedActivity() {
startRecordingFromTrackList(UI_TIMEOUT);
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.discard_button, R.id.finish_button);
}
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,35 +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.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.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);
@@ -40,46 +48,48 @@ public class EspressoEditTrackRecordingTest {
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
}
@LargeTest
@Test
public void espressoEditTrackRecordingTest() {
{
// TrackListActivity: start recording
ViewInteraction fabRecordButton = onView(withId(R.id.track_list_fab_action));
fabRecordButton.perform(click());
}
startRecordingFromTrackList(UI_TIMEOUT);
{
// TrackRecordingActivity
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
// wait; stay recording
trackControllerStopButton.perform(waitFor(4000));
// open menu
openContextualActionModeOverflowMenu();
// Click the item.
onView(withText(R.string.menu_edit)).perform(click());
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))
.inRoot(isPlatformPopup())
.perform(performClickOnAncestor());
waitUntilDisplayed(withId(R.id.track_edit_name), UI_TIMEOUT);
// change name for "New Name"
ViewInteraction textInputEditText = onView(withId(R.id.track_edit_name));
textInputEditText.perform(scrollTo(), replaceText("New Name"));
onView(withId(R.id.track_edit_name))
.perform(scrollTo(), replaceText("New Name"));
ViewInteraction textInputEditText2 = onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()));
textInputEditText2.perform(closeSoftKeyboard());
onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()))
.perform(closeSoftKeyboard());
// save edition
ViewInteraction appCompatButton = onView(withId(R.id.track_edit_save));
appCompatButton.perform(click());
onView(withId(R.id.track_edit_save))
.perform(performClick());
waitUntilDisplayed(withId(R.id.track_recording_fab_action), UI_TIMEOUT);
// stop;
trackControllerStopButton.perform(longClick());
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
// it's on track stopped activity and there are two buttons
ViewInteraction resumeButton = onView(withId(R.id.resume_button));
resumeButton.check(matches(isDisplayed()));
ViewInteraction finishButton = onView(withId(R.id.finish_button));
finishButton.check(matches(isDisplayed()));
onView(withId(R.id.resume_button))
.check(matches(isDisplayed()));
onView(withId(R.id.finish_button))
.check(matches(isDisplayed()));
}
}
}
@@ -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
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
trackControllerRecordButton.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
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
trackControllerRecordButton.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));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
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
trackControllerStopButton.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);
}
}
@@ -17,9 +17,13 @@ import de.dennisguse.opentracks.data.MarkerIterator;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
@@ -92,9 +96,9 @@ public class TestDataUtil {
);
List<Marker> markers = List.of(
new Marker(null, trackId, "Marker 1", "Marker description 1", "Marker typeLocalized 3", trackPoints.get(1).getPosition(), null),
new Marker(null, trackId, "Marker 2", "Marker description 2", "Marker typeLocalized 3", trackPoints.get(4).getPosition(), null),
new Marker(null, trackId, "Marker 3", "Marker description 3", "Marker typeLocalized 3", trackPoints.get(5).getPosition(), null)
new Marker(null, trackId, "Marker 1", "Marker description 1", "Marker typeLocalized 3", trackPoints.get(1).position(), null),
new Marker(null, trackId, "Marker 2", "Marker description 2", "Marker typeLocalized 3", trackPoints.get(4).position(), null),
new Marker(null, trackId, "Marker 3", "Marker description 3", "Marker typeLocalized 3", trackPoints.get(5).position(), null)
);
return new TrackData(track, trackPoints, markers);
@@ -112,8 +116,10 @@ public class TestDataUtil {
return pair.first;
}
public static TrackPoint createTrackPoint(int i) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT,
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type, AltitudeGainLoss altitudeGainLoss) {
return new TrackPoint(
null,
type,
new Position(
Instant.ofEpochSecond(i),
INITIAL_LATITUDE + (double) i / 10000.0,
@@ -123,21 +129,25 @@ public class TestDataUtil {
null,
null,
Speed.of(5f + (i / 10f))
));
trackPoint.setHeartRate(100f + i % 80);
trackPoint.setCadence(300f + i);
trackPoint.setPower(400f + i);
trackPoint.setAltitudeGainLoss(ALTITUDE_GAIN, ALTITUDE_LOSS);
return trackPoint;
),
null,
HeartRate.of(100f + i % 80),
null,
Cadence.of(300f + i),
Power.of(400f + i),
altitudeGainLoss
);
}
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type) {
TrackPoint trackPoint = createTrackPoint(i);
trackPoint.setType(type);
return trackPoint;
return createTrackPoint(i, TrackPoint.Type.TRACKPOINT, new AltitudeGainLoss(ALTITUDE_GAIN, ALTITUDE_LOSS));
}
public static TrackPoint createTrackPoint(int i) {
return createTrackPoint(i, TrackPoint.Type.TRACKPOINT);
}
/**
* Inserts a track with locations into the database.
*
@@ -164,7 +174,7 @@ public class TestDataUtil {
"Marker name",
description,
"Marker typeLocalized",
trackPoint.getPosition(),
trackPoint.position(),
photoUri);
}
@@ -6,8 +6,12 @@ import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
@@ -20,7 +24,9 @@ public class TestSensorDataUtil {
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
int i = trackPointList.size() + 1;
TrackPoint tp = new TrackPoint(type,
TrackPoint tp = new TrackPoint(
null,
type,
new Position(
time,
TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0,
@@ -30,18 +36,15 @@ public class TestSensorDataUtil {
null,
null,
Speed.of(5f + (i / 10f))
)
),
null,
HeartRate.ofOrNull(hr),
null,
cadence != null ? Cadence.of(cadence) : null,
power != null ? Power.of(power) : null,
new AltitudeGainLoss(3, 3)
);
if (hr != null) {
tp.setHeartRate(hr);
}
if (cadence != null) {
tp.setCadence(cadence);
}
if (power != null) {
tp.setPower(power);
}
tp.setAltitudeGainLoss(3, 3);
trackPointList.add(tp);
}
@@ -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;
@@ -54,19 +51,22 @@ import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.MarkerBuilder;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -75,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";
@@ -87,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);
@@ -101,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)) {
@@ -124,10 +117,11 @@ public class CustomContentProviderUtilsTest {
assertNotNull(trackPoint);
locations.add(trackPoint);
// Make sure the IDs are returned in the right order.
assertEquals(lastPointId.id() - numPoints + locations.size(), trackPoint.getId().id());
assertEquals(lastPointId.id() - numPoints + locations.size(), trackPoint.id().id());
}
assertEquals(numPoints, locations.size());
}
return locations;
}
private TrackPoint.Id initializeTrack(Track.Id id, int numPoints) {
@@ -167,7 +161,7 @@ public class CustomContentProviderUtilsTest {
try (TrackPointIterator it = contentProviderUtils.getTrackPointIterator(id, null)) {
while (it.hasNext()) {
TrackPoint trackPoint = it.next();
lastPointId = trackPoint.getId();
lastPointId = trackPoint.id();
counter++;
}
}
@@ -184,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());
}
}
@@ -237,7 +253,7 @@ public class CustomContentProviderUtilsTest {
"",
"",
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -300,7 +316,7 @@ public class CustomContentProviderUtilsTest {
"",
"",
"",
getLastValidTrackPoint(trackId1).getPosition(),
getLastValidTrackPoint(trackId1).position(),
null
));
@@ -446,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);
@@ -473,7 +487,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -553,7 +567,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
Marker.Id marker2Id = contentProviderUtils.insertMarker(new Marker(
@@ -562,7 +576,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -589,7 +603,7 @@ public class CustomContentProviderUtilsTest {
"",
MOCK_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
}
@@ -613,7 +627,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -636,7 +650,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -647,7 +661,7 @@ public class CustomContentProviderUtilsTest {
"",
TEST_DESC_NEW,
"",
getLastValidTrackPoint(trackId).getPosition(),
getLastValidTrackPoint(trackId).position(),
null
));
@@ -803,20 +817,27 @@ public class CustomContentProviderUtilsTest {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackPoint trackPoint = TestDataUtil.createTrackPoint(5);
trackPoint.setHeartRate(1F);
trackPoint.setCadence(2F);
trackPoint.setPower(3F);
TrackPoint trackPoint = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
TestDataUtil.createTrackPoint(5).position(),
null,
HeartRate.of(1),
null,
Cadence.of(2),
Power.of(3),
null
);
// when
contentProviderUtils.insertTrackPoint(trackPoint, trackId);
// then
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertTrue(trackPoints.get(10).hasHeartRate());
assertEquals(trackPoint.getHeartRate(), trackPoints.get(10).getHeartRate());
assertEquals(trackPoint.getCadence(), trackPoints.get(10).getCadence());
assertEquals(trackPoint.getPower(), trackPoints.get(10).getPower());
assertNotNull(trackPoints.get(10).heartRate());
assertEquals(trackPoint.heartRate(), trackPoints.get(10).heartRate());
assertEquals(trackPoint.cadence(), trackPoints.get(10).cadence());
assertEquals(trackPoint.power(), trackPoints.get(10).power());
}
@Test
@@ -857,9 +878,9 @@ public class CustomContentProviderUtilsTest {
for (int i = 0; i < trackpointIds.size(); i++) {
assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
assertEquals(startTrackPointId.id() + i, trackPoint.getId().id());
assertEquals(startTrackPointId.id() + i, trackPoint.id().id());
Position position = trackPoint.getPosition();
Position position = trackPoint.position();
assertEquals(TestDataUtil.INITIAL_LATITUDE + i / 10000.0, position.latitude(), 0.01);
assertEquals(TestDataUtil.INITIAL_LONGITUDE - i / 10000.0, position.longitude(), 0.01);
assertEquals(i / 100.0, position.horizontalAccuracy().distance_m(), 0.01);
@@ -879,11 +900,18 @@ public class CustomContentProviderUtilsTest {
public void testGetSensorStats_noSensorData() {
// given
List<TrackPoint> trackPointList = new ArrayList<>();
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
trackPoint.setPower(null);
trackPoint.setCadence(null);
trackPoint.setHeartRate(null);
TrackPoint trackPoint = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
TestDataUtil.createTrackPoint(1).position(),
null,
null,
null,
null,
null,
null
);
trackPointList.add(trackPoint);
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Track track = TestDataUtil.createTrack(trackId);
@@ -1167,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();
@@ -1190,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) {
@@ -1217,7 +1250,7 @@ public class CustomContentProviderUtilsTest {
return trackpoints.reversed()
.stream()
.filter(it -> List.of(TrackPoint.Type.TRACKPOINT, TrackPoint.Type.SEGMENT_START_AUTOMATIC).contains(it.getType()))
.filter(it -> List.of(TrackPoint.Type.TRACKPOINT, TrackPoint.Type.SEGMENT_START_AUTOMATIC).contains(it.type()))
.findFirst().orElseThrow();
}
@@ -13,7 +13,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
public class TrackPointTest {
@Test
public void distanceToPrevious() {
public void distanceToPrevious_gps() {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
@@ -38,20 +38,123 @@ public class TrackPointTest {
// without sensor distance
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_first() {
TrackPoint tp1 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null
);
TrackPoint tp2 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null));
// tp1 has sensor distance
tp1.setSensorDistance(Distance.of(5));
tp1.setSensorDistance(null);
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_second() {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null));
TrackPoint tp2 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null);
// tp2 has sensor distance
tp1.setSensorDistance(null);
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
@Test
public void distanceToPrevious_sensor_distance_both() {
TrackPoint tp1 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(0),
0d,
0.0001,
null,
null,
null,
null,
null),
Distance.of(10),
null,
null,
null,
null,
null
);
TrackPoint tp2 = new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.ofEpochMilli(1),
0d,
0.0002,
null,
null,
null,
null,
null),
Distance.of(5),
null,
null,
null,
null,
null);
// tp1 and tp2 have sensor distance
tp1.setSensorDistance(Distance.of(10));
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
}
@@ -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(" "));
}
}
@@ -26,7 +26,6 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.io.BufferedReader;
import java.io.File;
@@ -61,22 +60,24 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
@@ -93,17 +94,19 @@ public class ExportImportTest {
@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"));
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();
}
private final Context context = ApplicationProvider.getApplicationContext();
PreferencesUtils.resetPreferences(context, true);
}
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -123,11 +126,13 @@ public class ExportImportTest {
private TrackImporter trackImporter;
@Before
public void fileSetup() throws IOException {
public void fileSetup() throws IOException, TimeoutException {
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
tmpFileUri = Uri.fromFile(tmpFile);
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(200), true);
setUp();
}
@After
@@ -158,20 +163,28 @@ public class ExportImportTest {
"Marker 1",
"Marker 1 desc",
"Marker 1 typeLocalized",
service.getLastStoredTrackPointWithLocation().getPosition(),
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.1234567, 14.0014567,
Distance.of(10),
Altitude.WGS84.of(1020.25),
Distance.of(13),
null,
Speed.of(15)
),
null
));
// A sensor-only TrackPoint
trackPointCreator.setClock("2020-02-02T02:02:04Z");
mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f);
mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f, 27);
trackPointCreator.setClock("2020-02-02T02:02:14Z");
mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null);
trackPointCreator.setClock("2020-02-02T02:02:14Z"); //ignored
mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null, 28);
trackPointCreator.setClock("2020-02-02T02:02:15Z");
mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null);
mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null, 29);
trackPointCreator.setClock("2020-02-02T02:02:16Z");
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null); // Distance will be added to next TrackPoint
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null, 30); //Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 0f);
contentProviderUtils.insertMarker(
@@ -181,12 +194,11 @@ public class ExportImportTest {
"Marker 2",
"Marker 2 desc",
"Marker 2 typeLocalized",
service.getLastStoredTrackPointWithLocation().getPosition(),
service.getLastStoredTrackPointWithLocation().position(),
null
));
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet(trackPointCreator);
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -201,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().sensorDataSet = new SensorDataSet(trackPointCreator);
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
@@ -218,13 +229,158 @@ public class ExportImportTest {
@LargeTest
@Test
public void track() throws TimeoutException {
setUp();
Track track = contentProviderUtils.getTrack(trackId);
Statistics trackStatistics = track.statistics();
assertEquals(ZoneOffset.of("+01:00"), track.zoneOffset());
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:04Z"),
null, null, null,
null, null,
null,
Speed.of(15)),
Distance.of(10),
HeartRate.of(66),
Temperature.of(27),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.parse("2020-02-02T02:02:15Z")),
null,
HeartRate.of(68),
Temperature.of(29),
Cadence.of(3),
Power.of(50),
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(5)),
Distance.of(2),
HeartRate.of(69),
Temperature.of(30),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(0, 0)
),
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,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.IDLE,
Position.of(Instant.parse("2020-02-02T02:03:30Z")),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null, Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)),
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(
Instant.parse("2020-02-02T02:02:02Z"),
Instant.parse("2020-02-02T02:04:00Z"),
@@ -238,77 +394,12 @@ public class ExportImportTest {
null
),
trackStatistics);
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Instant.parse("2020-02-02T02:02:04Z"))
.setSensorDistance(Distance.of(10))
.setSpeed(Speed.of(15))
.setHeartRate(HeartRate.of(66))
.setCadence(3)
.setPower(50).setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:15Z"))
.setHeartRate(HeartRate.of(68))
.setCadence(3)
.setPower(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(5)))
.setSensorDistance(Distance.of(2))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(69))
.setCadence(3)
.setPower(50),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z"))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.1229858398438), null,
null, Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
}
//TODO Does not test marker images
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp();
public void kmz_with_trackdetail_and_sensordata() throws IOException {
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -360,9 +451,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException, IOException {
setUp();
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws IOException {
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
@@ -387,8 +476,6 @@ public class ExportImportTest {
@LargeTest
@Test
public void gpx() throws TimeoutException, IOException {
setUp();
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -421,51 +508,86 @@ public class ExportImportTest {
.setDelta(0.05); // speed is not fully
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.123456, 14.001456d, Distance.of(10),
Altitude.WGS84.of(1020.2), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.2), null,
null,
Speed.of(5)))
.setAltitudeGainLoss(1, 1)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
.setCadence(3f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(5)),
Distance.of(12),
HeartRate.of(69),
Temperature.of(30),
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3.123456, 14.002456, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
3.123456, 16d, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3.123456, 16.001, Distance.of(10),
Altitude.WGS84.of(999.0999755859375), null,
null,
Speed.of(10)))
.setAltitudeGainLoss(0, 0)
.setSpeed(Speed.of(15))
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), actual);
// 3. trackstatistics
@@ -492,9 +614,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
setUp();
public void gpx_duplicate_trackUUID() throws IOException {
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
@@ -520,9 +640,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void csv_export_only() throws TimeoutException, IOException {
setUp();
public void csv_export_only() throws IOException {
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -566,17 +684,17 @@ public class ExportImportTest {
}
}
private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain) {
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
private static void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain, float temperature) {
SensorManager sensorManager = trackPointCreator.getSensorManager();
AggregatorCyclingPower cyclingPower = new AggregatorCyclingPower("", "");
cyclingPower.add(new Raw<>(trackPointCreator.createNow(), new BluetoothHandlerManagerCyclingPower.Data(Power.of(power), null)));
sensorDataSet.add(cyclingPower);
cyclingPower.add(trackPointCreator.getNow(), new CyclingPowerBluetooth.Data(Power.of(power), null));
sensorManager.setAggregator(cyclingPower);
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
avgHeartRate.add(new Raw<>(trackPointCreator.createNow(), HeartRate.of(heartRate)));
sensorDataSet.add(avgHeartRate);
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(heartRate));
sensorManager.setAggregator(avgHeartRate);
AggregatorCyclingCadence cyclingCadence = new AggregatorCyclingCadence("", "") {
@NonNull
@@ -590,36 +708,68 @@ public class ExportImportTest {
return true;
}
};
sensorDataSet.add(cyclingCadence);
sensorManager.setAggregator(cyclingCadence);
if (distance != null && speed != null) {
AggregatorCyclingDistanceSpeed distanceSpeed = Mockito.mock(AggregatorCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasReceivedData()).thenReturn(true);
Mockito.when(distanceSpeed.getAggregatedValue(Mockito.any())).thenReturn(new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed)));
sensorDataSet.add(distanceSpeed);
AggregatorCyclingDistanceSpeed aggregatorCyclingDistanceSpeed = new AggregatorCyclingDistanceSpeed("", "") {
@NonNull
@Override
public Data getAggregatedValue(Instant now) {
return new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed));
}
@Override
public boolean hasReceivedData() {
return true;
}
};
sensorManager.setAggregator(aggregatorCyclingDistanceSpeed);
} else {
sensorDataSet.add(new AggregatorCyclingDistanceSpeed("", ""));
sensorManager.setAggregator(new AggregatorCyclingDistanceSpeed("", ""));
}
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.onChange(sensorDataSet);
sensorManager.setAggregator(new AggregatorTemperature("", "'") {
@NonNull
@Override
public Temperature getAggregatedValue(Instant now) {
return Temperature.of(temperature);
}
@Override
public boolean hasReceivedData() {
return true;
}
});
trackPointCreator.onChange();
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
private static void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
SensorManager sensorManager = trackPointCreator.getSensorManager();
if (altitudeGain != null) {
AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class);
Mockito.when(barometer.hasReceivedData()).thenReturn(true);
Mockito.when(barometer.getAggregatedValue(Mockito.any())).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
sensorDataSet.add(barometer);
} else {
sensorDataSet.add(new AggregatorBarometer("test", null));
if (altitudeGain == null) {
sensorManager.getAltitudeChangeHandler().setAggregator(new AggregatorBarometer("test", null));
return;
}
AggregatorBarometer aggregatorBarometer = new AggregatorBarometer("", "") {
@NonNull
@Override
public AltitudeGainLoss getAggregatedValue(Instant now) {
return new AltitudeGainLoss(altitudeGain, altitudeGain);
}
@Override
public boolean hasReceivedData() {
return true;
}
};
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);
@@ -631,6 +781,6 @@ public class ExportImportTest {
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
}
}
@@ -27,8 +27,12 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
@@ -116,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 {
@@ -193,51 +375,86 @@ public class GPXTrackImporterTest {
TrackPointAssert a = new TrackPointAssert()
.setDelta(0.05); // speed is not fully
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3d, 14d, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(1, 1),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3d, 14.001, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(5)))
.setAltitudeGainLoss(1, 1)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
.setCadence(3f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(5)),
Distance.of(12),
HeartRate.of(69),
null,
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(1, 1)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3d, 14.002, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
3d, 16d, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3d, 16.001, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(10)))
.setAltitudeGainLoss(0, 0)
.setSpeed(Speed.of(15))
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), actual);
}
@@ -246,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
@@ -16,57 +16,48 @@ public class TrackPointAssert {
public void assertEquals(TrackPoint expected, TrackPoint actual) {
Assert.assertEquals("time", expected.getTime(), actual.getTime());
Assert.assertEquals("type", expected.getType(), actual.getType());
Assert.assertEquals("type", expected.type(), actual.type());
Assert.assertEquals("has location,", expected.hasLocation(), actual.hasLocation());
if (expected.hasLocation()) {
Assert.assertEquals("latitude", expected.getPosition().latitude(), actual.getPosition().latitude(), 0.001);
Assert.assertEquals("longitude", expected.getPosition().longitude(), actual.getPosition().longitude(), 0.001);
Assert.assertEquals("has location,", expected.position().hasLocation(), actual.position().hasLocation());
if (expected.position().hasLocation()) {
Assert.assertEquals("latitude", expected.position().latitude(), actual.position().latitude(), 0.001);
Assert.assertEquals("longitude", expected.position().longitude(), actual.position().longitude(), 0.001);
}
Assert.assertEquals("has altitude", expected.hasAltitude(), actual.hasAltitude());
if (expected.hasAltitude()) {
Assert.assertEquals("altitude", expected.getAltitude().toM(), actual.getAltitude().toM(), delta);
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);
}
Assert.assertEquals("has altitudeGainLoss", expected.hasAltitudeGainLoss(), actual.hasAltitudeGainLoss());
if (expected.hasAltitudeGainLoss()) {
Assert.assertEquals("altitudeGainLoss", expected.getAltitudeGainLoss(), actual.getAltitudeGainLoss());
Assert.assertEquals("altitudeGainLoss", expected.altitudeGainLoss(), actual.altitudeGainLoss());
Assert.assertEquals("has speed", expected.position().hasSpeed(), actual.position().hasSpeed());
if (expected.position().hasSpeed()) {
Assert.assertEquals("speed", expected.position().speed().toMPS(), actual.position().speed().toMPS(), delta);
}
Assert.assertEquals("has speed", expected.hasSpeed(), actual.hasSpeed());
if (expected.hasSpeed()) {
Assert.assertEquals("speed", expected.getSpeed().toMPS(), actual.getSpeed().toMPS(), delta);
Assert.assertEquals("has horizontalAccuracy", expected.position().hasHorizontalAccuracy(), actual.position().hasHorizontalAccuracy());
if (expected.position().hasHorizontalAccuracy()) {
Assert.assertEquals("horizontalAccuracy", expected.position().horizontalAccuracy().toM(), actual.position().horizontalAccuracy().toM(), delta);
}
Assert.assertEquals("has verticalAccuracy", expected.position().hasVerticalAccuracy(), actual.position().hasVerticalAccuracy());
if (expected.position().hasVerticalAccuracy()) {
Assert.assertEquals("verticalAccuracy", expected.position().verticalAccuracy().toM(), actual.position().verticalAccuracy().toM(), delta);
}
Assert.assertEquals("has horizontalAccuracy", expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
if (expected.hasHorizontalAccuracy()) {
Assert.assertEquals("horizontalAccuracy", expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
}
Assert.assertEquals("has verticalAccuracy", expected.hasVerticalAccuracy(), actual.hasVerticalAccuracy());
if (expected.hasVerticalAccuracy()) {
Assert.assertEquals("verticalAccuracy", expected.getVerticalAccuracy().toM(), actual.getVerticalAccuracy().toM(), delta);
Assert.assertEquals("has sensorDistance", expected.sensorDistance() != null, actual.sensorDistance() != null);
if (expected.sensorDistance() != null) {
Assert.assertEquals("sensorDistance", expected.sensorDistance().toM(), actual.sensorDistance().toM(), delta);
}
Assert.assertEquals("has sensorDistance", expected.hasSensorDistance(), actual.hasSensorDistance());
if (expected.hasSensorDistance()) {
Assert.assertEquals("sensorDistance", expected.getSensorDistance().toM(), actual.getSensorDistance().toM(), delta);
}
Assert.assertEquals("heartRate", expected.heartRate(), actual.heartRate());
Assert.assertEquals("has heartrate", expected.hasHeartRate(), actual.hasHeartRate());
if (expected.hasHeartRate()) {
Assert.assertEquals("heartrate", expected.getHeartRate(), actual.getHeartRate());
}
Assert.assertEquals("temperature", expected.temperature(), actual.temperature());
Assert.assertEquals("has power", expected.hasPower(), actual.hasPower());
if (expected.hasPower()) {
Assert.assertEquals("power", expected.getPower(), actual.getPower());
}
Assert.assertEquals("power", expected.power(), actual.power());
Assert.assertEquals("has cadence", expected.hasCadence(), actual.hasCadence());
if (expected.hasCadence()) {
Assert.assertEquals("cadence", expected.getCadence(), actual.getCadence());
}
Assert.assertEquals("cadence", expected.cadence(), actual.cadence());
}
public void assertEquals(List<TrackPoint> expected, List<TrackPoint> actual) {
@@ -0,0 +1,224 @@
package de.dennisguse.opentracks.publicapi;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
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.data.CustomContentProvider;
@RunWith(AndroidJUnit4.class)
public class DataProviderTest {
private final ContentResolver resolver = ApplicationProvider.getApplicationContext().getContentResolver();
@Test
public void supportsVersion2_tracks() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = new String[]{
"_id",
"name",
"description",
"category",
"activity_type_localized",
"starttime",
"stoptime",
"totaltime",
"movingtime",
"totaldistance",
"maxspeed",
"avgspeed",
"avgmovingspeed",
"minelevation",
"maxelevation",
"elevationgain",
"elevationloss"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTracksColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.track", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTracksColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion2_trackpoints() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
"latitude",
"longitude",
"time",
"type",
"speed"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTrackpointsColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTrackpointsColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion2_markers() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
"description",
"category",
"trackid",
"longitude",
"latitude",
"photoUrl",
"icon"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedMarkersColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.waypoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedMarkersColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_tracks() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = {
"_id",
"name",
"description",
"activity_type",
"activity_type_localized",
"time_start",
"time_stop",
"duration_total",
"duration_moving",
"distance",
"speed_max",
"altitude_min",
"altitude_max",
"altitude_gain",
"altitude_loss"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTracksColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.track", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTracksColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_trackpoints() {
//given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
"latitude",
"longitude",
"time",
"type",
"speed"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTrackpointsColumns,
null,
null,
null
)) {
//then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTrackpointsColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_markers() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
"description",
"marker_type_localized",
"trackid",
"latitude",
"longitude",
"photoUrl"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedMarkersColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.waypoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedMarkersColumns, cursor.getColumnNames());
}
}
}
@@ -1,38 +0,0 @@
package de.dennisguse.opentracks.sensors;
import static org.junit.Assert.assertEquals;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
public class BluetoothHandlerCyclingPowerTest {
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
// then
assertEquals(40, powerCadence.power().getW(), 0.01);
}
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
// then
assertEquals(0, powerCadence.power().getW(), 0.01);
assertEquals(12, powerCadence.crank().crankRevolutionsCount());
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
}
}
@@ -28,7 +28,6 @@ import java.util.List;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(AndroidJUnit4.class)
public class GpsStatusTest {
@@ -82,16 +81,16 @@ public class GpsStatusTest {
// when / then
subject.start();
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX, GPS_SIGNAL_BAD), statusList);
}
@@ -106,13 +105,13 @@ public class GpsStatusTest {
// when / then
subject.start();
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now().minusMillis(1000))));
subject.onNewTrackPoint(Position.of(ok, Instant.now().minusMillis(1000)));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX), statusList);
subject.determineGpsStatusByTime(Instant.now());
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_FIX), statusList);
}
@@ -133,13 +132,13 @@ public class GpsStatusTest {
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(ok, Instant.now())));
subject.onNewTrackPoint(Position.of(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX), statusList);
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST), statusList);
subject.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(badFix, Instant.now())));
subject.onNewTrackPoint(Position.of(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_BAD), statusList);
Thread.sleep(100);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -13,11 +13,11 @@ public class BluetoothHandlerBarometricPressureTest {
@Test
public void parseEnvironmentalSensing_Pa() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BarometerBluetooth.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
// when
AtmosphericPressure pressure = BluetoothHandlerBarometricPressure.parseEnvironmentalSensing(characteristic);
AtmosphericPressure pressure = BarometerBluetooth.parseEnvironmentalSensing(characteristic);
// then
assertEquals(AtmosphericPressure.ofHPA(1001.65f), pressure);
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -8,16 +8,16 @@ import org.junit.Test;
import de.dennisguse.opentracks.data.models.HeartRate;
public class BluetoothHandlerManagerHeartRateTest {
public class BluetoothHandlerHeartRateTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(HeartRateBluetooth.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
HeartRate heartRate = HeartRateBluetooth.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(60), heartRate);
@@ -26,11 +26,11 @@ public class BluetoothHandlerManagerHeartRateTest {
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(HeartRateBluetooth.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
HeartRate heartRate = HeartRateBluetooth.parseHeartRate(characteristic);
// then
assertEquals(HeartRate.of(257), heartRate);
@@ -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,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -8,14 +8,14 @@ import android.util.Pair;
import org.junit.Test;
public class BluetoothHandlerCyclingDistanceSpeedTest {
public class CyclingDistanceSpeedBluetoothTest {
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertNull(sensor.first);
@@ -24,11 +24,11 @@ public class BluetoothHandlerCyclingDistanceSpeedTest {
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertEquals(65535 + 16777216, sensor.first.wheelRevolutionsCount());
@@ -37,11 +37,11 @@ public class BluetoothHandlerCyclingDistanceSpeedTest {
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
// then
assertEquals(200 + 16777216, sensor.first.wheelRevolutionsCount());
@@ -0,0 +1,62 @@
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
public class CyclingPowerBluetoothTest {
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingPowerBluetooth.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
CyclingPowerBluetooth.Data powerCadence = CyclingPowerBluetooth.parseCyclingPower(characteristic);
// then
assertEquals(40, powerCadence.power().getW(), 0.01);
}
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingPowerBluetooth.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
CyclingPowerBluetooth.Data powerCadence = CyclingPowerBluetooth.parseCyclingPower(characteristic);
// then
assertEquals(0, powerCadence.power().getW(), 0.01);
assertEquals(12, powerCadence.crank().crankRevolutionsCount());
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);
}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.sensors;
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
@@ -10,15 +10,15 @@ import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
public class BluetoothHandlerRunningSpeedAndCadenceTest {
public class RunningSpeedAndCadenceBluetoothTest {
@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(RunningSpeedAndCadenceBluetooth.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
// when
BluetoothHandlerRunningSpeedAndCadence.Data sensor = BluetoothHandlerRunningSpeedAndCadence.parseRunningSpeedAndCadence("sensorName", characteristic);
RunningSpeedAndCadenceBluetooth.Data sensor = RunningSpeedAndCadenceBluetooth.parseRunningSpeedAndCadence("sensorName", characteristic);
// then
assertEquals(Speed.of(5), sensor.speed());
@@ -11,7 +11,7 @@ public class AggregatorBarometerTest {
private static void addSensorValue(AggregatorBarometer aggregatorBarometer, float[] values) {
for (float f : values) {
aggregatorBarometer.add(new Raw<>(Instant.MIN, AtmosphericPressure.ofHPA(f)));
aggregatorBarometer.add(Instant.MIN, AtmosphericPressure.ofHPA(f));
}
}
@@ -24,7 +24,7 @@ public class AggregatorBarometerTest {
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
// then
Assert.assertEquals(0f, subject.aggregatedValue.gain_m(), 0.01);
Assert.assertEquals(15f, subject.aggregatedValue.loss_m(), 0.01);
Assert.assertEquals(0f, subject.output.gain_m(), 0.01);
Assert.assertEquals(15f, subject.output.loss_m(), 0.01);
}
}
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.sensors.sensorData;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -12,10 +13,9 @@ import java.time.Instant;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.UintUtils;
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
@RunWith(AndroidJUnit4.class)
public class SensorDataCyclingTest {
@@ -25,8 +25,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 2048));
// then
assertEquals(60, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -37,8 +37,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 6184)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 8016)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 6184));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 8016));
// then
assertEquals(33.53, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -49,8 +49,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 2048));
// then
assertEquals(Cadence.of(0), current.getAggregatedValue(Instant.MIN));
@@ -62,8 +62,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 1024)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 1024));
// then
assertFalse(current.hasReceivedData()); //TODO Cadence should be 0?
@@ -74,8 +74,8 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 0)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, UintUtils.UINT16_MAX - 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 0));
// then
assertEquals(60, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
@@ -87,13 +87,32 @@ public class SensorDataCyclingTest {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(0, 2048)));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(UintUtils.UINT32_MAX - 1, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(0, 2048));
// then
// TODO See #953
// assertEquals(60, current.getValue().getRPM(), 0.01);
assertEquals(Cadence.of(0), current.getAggregatedValue(Instant.MIN));
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
@@ -102,8 +121,8 @@ public class SensorDataCyclingTest {
current.setWheelCircumference(Distance.ofMM(2150));
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016)));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(1, 6184));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(2, 8016));
// then
assertEquals(2.15, current.getAggregatedValue(Instant.MIN).distance().toM(), 0.01);
@@ -117,14 +136,13 @@ public class SensorDataCyclingTest {
current.setWheelCircumference(Distance.ofMM(2000));
// when
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024)));
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(0, 2048)));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(UintUtils.UINT32_MAX - 1, 1024));
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(0, 2048));
// then
// TODO See #953
// assertEquals(2, current.getValue().getDistance().toM(), 0.01);
// assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
assertEquals(new AggregatorCyclingDistanceSpeed.Data(Distance.ZERO, Distance.ZERO, Speed.ZERO), current.getAggregatedValue(Instant.MIN));
assertNull(current.getAggregatedValue(Instant.MIN));
}
}
@@ -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);
// 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));
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
verify(notificationCompatBuilder, times(2)).setOnlyAlertOnce(false);
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);
}
}
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);
}
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,11 +2,11 @@ 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;
import android.location.Location;
import android.location.LocationManager;
import android.os.Looper;
import androidx.annotation.NonNull;
@@ -35,6 +35,7 @@ import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
@@ -43,12 +44,12 @@ import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -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() {
@@ -126,8 +162,16 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -145,7 +189,8 @@ public class TrackRecordingServiceRecordingTest {
Track.Id trackId = service.startNewTrack();
//We do not want the real GPS to interfere.
LocationManagerCompat.removeUpdates((LocationManager) context.getSystemService(Context.LOCATION_SERVICE), trackPointCreator.getSensorManager().getGpsManager());
GpsInternal gpsInternal = (GpsInternal)trackPointCreator.getSensorManager().getGpsHandler().driver;
LocationManagerCompat.removeUpdates(gpsInternal.locationManager, gpsInternal.locationListenerCompat);
String gps1 = "2020-02-02T02:02:03Z";
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
@@ -201,8 +246,16 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
//when
@@ -216,8 +269,17 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -249,8 +311,16 @@ public class TrackRecordingServiceRecordingTest {
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(starTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(pauseTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), trackPoints);
}
@@ -278,8 +348,17 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -296,16 +375,21 @@ public class TrackRecordingServiceRecordingTest {
mockAltitudeChange(trackPointCreator, 0);
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataSet.add(new AggregatorHeartRate("", ""));
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(sensor1, HeartRate.of(5))); //Should be ignored
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
sensorManager.setAggregator(avgHeartRate);
//Should be ignored
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(5));
sensorManager.onChange();
String sensor3 = "2020-02-02T02:02:13Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(sensor3, HeartRate.of(7)));
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(7));
sensorManager.onChange();
String stopTime = "2020-02-02T02:02:15Z";
trackPointCreator.setClock(stopTime);
@@ -314,12 +398,28 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor3))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(7)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
.setHeartRate(HeartRate.of(7))
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.parse(sensor3)),
null,
HeartRate.of(7),
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
HeartRate.of(7),
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -369,32 +469,65 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps2),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -441,30 +574,54 @@ public class TrackRecordingServiceRecordingTest {
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.00002, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse(stopTime),
45.00002, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -499,24 +656,47 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -559,8 +739,16 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -603,24 +791,48 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse(gps2),
45.1, 35d, Distance.of(1),
null, null,
null,
Speed.of(15)))
.setAltitudeGainLoss(0, 0),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGainLoss(0, 0)
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse(stopTime)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@@ -637,19 +849,22 @@ public class TrackRecordingServiceRecordingTest {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
AggregatorRunning aggregatorRunning = new AggregatorRunning("", "");
SensorManager sensorManager = trackPointCreator.getSensorManager();
sensorManager.sensorDataSet.add(new AggregatorRunning("", ""));
sensorManager.sensorDataSet.barometer = null;
sensorManager.setAggregator(aggregatorRunning);
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
sensorManager.onChanged(new Raw<>(sensor1, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.ZERO))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(1), Distance.ZERO));
sensorManager.onChange();
// when
String sensor2 = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(sensor2);
sensorManager.onChanged(new Raw<>(sensor2, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(2))));
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(2), Distance.of(2)));
sensorManager.onChange();
// when
String gps1 = "2020-02-02T02:02:05Z";
@@ -658,12 +873,15 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor3 = "2020-02-02T02:02:06Z";
trackPointCreator.setClock(sensor3);
sensorManager.onChanged(new Raw<>(sensor3, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(12))));
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(3), Distance.of(12)));
sensorManager.onChange();
// when
String sensor4 = "2020-02-02T02:02:07Z";
trackPointCreator.setClock(sensor4);
sensorManager.onChanged(new Raw<>(sensor4, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(14)))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(4), Distance.of(14)));
sensorManager.onChange();
// when
String gps2 = "2020-02-02T02:02:08Z";
@@ -672,7 +890,9 @@ public class TrackRecordingServiceRecordingTest {
// when
String sensor5 = "2020-02-02T02:02:10Z";
trackPointCreator.setClock(sensor5);
sensorManager.onChanged(new Raw<>(sensor5, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(16)))); //Should be ignored
//Should be ignored
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(5), Distance.of(16)));
sensorManager.onChange();
// when
String gps3 = "2020-02-02T02:02:12Z";
@@ -691,49 +911,107 @@ public class TrackRecordingServiceRecordingTest {
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor2)) //First moving TrackPoint: store as the time might be interesting.
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new TrackPoint( //First moving TrackPoint: stored as the time might be interesting.
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(sensor2),
null, null, null,
null, null,
null,
Speed.of(5)),
Distance.of(2),
null,
null,
Cadence.of(2),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps1),
45d, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.ZERO),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(sensor3))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(5)),
Distance.ZERO,
null,
null,
Cadence.of(2),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(sensor3),
null, null, null,
null, null,
null,
Speed.of(5)),
Distance.of(10),
null,
null,
Cadence.of(3),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps3),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.of(4.0)),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
Speed.of(5)),
Distance.of(4.0),
null,
null,
Cadence.of(5),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse(gps4),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.of(5)))
.setSensorDistance(Distance.ZERO),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL,
Speed.of(5)),
Distance.ZERO,
null,
null,
Cadence.of(5),
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse(stopTime),
45.001, 35d, Distance.of(1),
null, null,
null,
Speed.ZERO)) //Sensor data is now outdated, but we do not fall back to GPS.
.setSensorDistance(Distance.ZERO)
Speed.ZERO), //Sensor data is now outdated, but we do not fall back to GPS.
Distance.ZERO,
null,
null,
Cadence.of(0), //TODO This could be null, right?
null,
null
)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
trackPointCreator.getSensorManager().sensorDataSet.barometer = new AggregatorBarometer("", "") {
trackPointCreator.getSensorManager().getAltitudeChangeHandler().setAggregator(new AggregatorBarometer("", "") {
@Override
public boolean hasReceivedData() {
return true;
@@ -744,7 +1022,7 @@ public class TrackRecordingServiceRecordingTest {
public AltitudeGainLoss getAggregatedValue(Instant now) {
return new AltitudeGainLoss(altitudeGain, altitudeGain);
}
};
});
}
private static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
@@ -756,6 +1034,6 @@ public class TrackRecordingServiceRecordingTest {
location.setSpeed(speed);
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
}
}
@@ -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,26 +42,29 @@ 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;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
/**
* Testing the states of TrackRecordingService.
* As states are checked against MutableLiveData that are updated via `postValue()` (is asynchronous to be inform other Threads), Thread.sleep() is required.
*/
@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);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
public GrantPermissionRule mRuntimePermissionRule = TestUtil.createGrantPermissionRule();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@@ -117,13 +121,13 @@ public class TrackRecordingServiceStateMachineTest {
@MediumTest
@Test
public void gps_startStop() throws InterruptedException {
public void gps_startStop() {
// given
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when
service.tryStartSensors();
Thread.sleep(1000);
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -132,23 +136,25 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.stopSensors();
Thread.sleep(1000);
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
@Test
public void recording_startStopResume_no_data() throws InterruptedException {
public void recording_startStopResume_no_data() {
// given
assertFalse(service.isRecording());
// when
Track.Id trackId = service.startNewTrack();
Thread.sleep(1000);
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilRecordingDataIsNotDefault();
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
@@ -157,10 +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);
Thread.sleep(1000);
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
@@ -171,7 +180,8 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.endCurrentTrack();
Thread.sleep(1000);
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
// then
assertFalse(service.isRecording());
@@ -182,12 +192,12 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.resumeTrack(trackId);
Thread.sleep(1000);
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
}
@@ -213,7 +223,7 @@ public class TrackRecordingServiceStateMachineTest {
@MediumTest
@Test
public void cannotEnd_without_starting() throws InterruptedException {
public void cannotEnd_without_starting() {
// given
assertFalse(service.isRecording());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -222,7 +232,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.endCurrentTrack();
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -233,36 +242,54 @@ public class TrackRecordingServiceStateMachineTest {
@Ignore("TODO Bug: GPS can be stopped although the current track is recording")
@MediumTest
@Test
public void recording_stopGPS_noop() throws InterruptedException {
public void recording_stopGPS_noop() {
// given
Track.Id trackId = service.startNewTrack();
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
// when
service.stopSensors(); //TODO Should be ignored as service is recording
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
}
@MediumTest
@Test
public void recording_startRecording_alreadyRecording() throws InterruptedException {
public void recording_startRecording_alreadyRecording() {
// given
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
// when
Track.Id newTrackId = service.startNewTrack();
Thread.sleep(1000);
// then
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" : ""));
}
}
@@ -15,7 +15,6 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.time.Duration;
import java.time.Instant;
@@ -34,10 +33,11 @@ import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatisticsUpdater;
//Due to DateTimeFormatter using NBSP, these tests only work in SDK34+
@@ -52,6 +52,7 @@ public class VoiceAnnouncementUtilsTest {
@Before
public void setUp() {
PreferencesUtils.setVoiceAnnounceUnit(true);
PreferencesUtils.setVoiceAnnounceHeartRateCurrent(false);
PreferencesUtils.setVoiceAnnounceLapHeartRate(false);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
@@ -84,8 +85,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -117,8 +125,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -150,8 +165,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -183,8 +205,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -193,13 +222,62 @@ 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
);
// 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
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, null).toString();
@@ -231,8 +309,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, false, null, null).toString();
@@ -246,8 +331,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, false, data.second, null).toString();
@@ -279,8 +371,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
@@ -312,8 +411,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
@@ -345,8 +451,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_METER, true, null, null).toString();
@@ -378,8 +491,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
@@ -393,8 +513,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.IMPERIAL_FEET, true, data.second, null).toString();
@@ -426,8 +553,15 @@ public class VoiceAnnouncementUtilsTest {
null
));
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, false, null, null).toString();
@@ -441,8 +575,15 @@ public class VoiceAnnouncementUtilsTest {
// given
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.IMPERIAL_FEET, false, data.second, null).toString();
@@ -458,17 +599,25 @@ 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);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
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
@@ -484,17 +633,25 @@ 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);
SensorDataSet dataSet = Mockito.mock(SensorDataSet.class);
Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused"));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null
);
// when
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);
}
}
@@ -60,8 +60,7 @@ public class PreferencesOpenHelperTest {
// when update to version 2
PreferencesOpenHelper.newInstance(2).check();
String updatedOldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";"
+ PreferencesUtils.getLayoutColumnsByDefault() + ";distance,1,1,0;speed,1,1,0;";
String updatedOldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";2;distance,1,1,0;speed,1,1,0;";
// then there should be one layout with old custom layout that has the new CSV value.
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
@@ -110,7 +109,7 @@ public class PreferencesOpenHelperTest {
PreferencesOpenHelper.newInstance(1).check();
// then custom layout should be equals to default layout.
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsv(PreferencesUtils.buildDefaultLayout(), resources);
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsvLine(PreferencesUtils.buildDefaultLayout(), resources);
List<RecordingLayout> customRecordingLayout = PreferencesUtils.getAllCustomLayouts();
assertEquals(1, customRecordingLayout.size());
@@ -210,7 +210,7 @@ public class PreferencesUtilsTest {
String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layouts_key), null);
assertNotNull(csv);
assertEquals(csv,
"road cycling;" + PreferencesUtils.getLayoutColumnsByDefault() + ";"
"road cycling;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1,0;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1,0;"
@@ -258,8 +258,8 @@ public class PreferencesUtilsTest {
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
List<RecordingLayout> layoutsToBeUpdated = new ArrayList<>();
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsv(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsv(runningProfile, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsvLine(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsvLine(runningProfile, resources));
PreferencesUtils.updateCustomLayouts(layoutsToBeUpdated);
@@ -303,7 +303,7 @@ public class PreferencesUtilsTest {
+ context.getString(R.string.stats_custom_layout_distance_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
RecordingLayout recordingLayoutToBeUpdated = RecordingLayoutIO.fromCsv(cyclingProfileUpdated, resources);
RecordingLayout recordingLayoutToBeUpdated = RecordingLayoutIO.fromCsvLine(cyclingProfileUpdated, resources);
PreferencesUtils.updateCustomLayout(recordingLayoutToBeUpdated);
// then only updated profile is modified in the custom layouts
@@ -89,8 +89,7 @@ public class TrackStatisticsUpdaterTest {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3));
@@ -103,15 +102,18 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_distance_from_GPS_moving_and_sensor_moving() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(Distance.of(10f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
TrackPoint tp4 = createTrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), Distance.of(5));
TrackPoint tp5 = new TrackPoint(null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.ofEpochMilli(5000)),
Distance.of(10f),
null,
null,
null,
null,
null);
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3));
@@ -131,13 +133,10 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSensorDistance(Distance.of(5f));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), Distance.of(5));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), null);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
// when
@@ -158,10 +157,8 @@ public class TrackStatisticsUpdaterTest {
public void addTrackPoint_maxSpeed_multiple_segments() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), Speed.of(2), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), Speed.of(2), null),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(4))
));
assertEquals(Speed.of(2), subject.getTrackStatistics().maxSpeed());
@@ -169,10 +166,8 @@ public class TrackStatisticsUpdaterTest {
// when
List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(5)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6))
.setSpeed(Speed.of(1f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7))
.setSpeed(Speed.of(1f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6), Speed.of(1), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7), Speed.of(1), null),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(8))
).forEach(subject::addTrackPoint);
@@ -185,16 +180,32 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(2f)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), Speed.of(2), null),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), Speed.of(2), null),
new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setHeartRate(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setHeartRate(50),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
createTrackPoint(0, 1, Altitude.WGS84.of(0), Instant.ofEpochSecond(50)),
createTrackPoint(0, 2, Altitude.WGS84.of(0), Instant.ofEpochSecond(55)),
@@ -210,21 +221,43 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30))
.setSensorDistance(Distance.ofKilometer(1)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setHeartRate(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setHeartRate(50),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(50))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(55))
.setSensorDistance(Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1), null, Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2), null, Distance.of(10)),
new TrackPoint(
null,
TrackPoint.Type.IDLE,
Position.of(Instant.ofEpochSecond(30)),
Distance.ofKilometer(1),
null,
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
null,
HeartRate.of(50),
null,
null,
null,
null
),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(50), null, Distance.of(10)),
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(55), null, Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(60))
));
@@ -240,8 +273,8 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
subject.addTrackPoint(
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(10))
.setSensorDistance(Distance.of(10)));
createTrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(10), null, Distance.of(10))
);
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)));
// then
@@ -252,8 +285,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setSensorDistance(Distance.ZERO));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(40)),
Distance.ZERO,
null,
null,
null,
null,
null
)
);
// then
assertTrue(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -262,8 +305,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setSensorDistance(Distance.of(1)));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(45)),
Distance.of(1),
null,
null,
null,
null,
null
)
);
// then
assertTrue(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -272,8 +325,18 @@ public class TrackStatisticsUpdaterTest {
// when
subject.addTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(50))
.setSensorDistance(Distance.of(10)));
new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
Position.of(Instant.ofEpochSecond(50)),
Distance.of(10),
null,
null,
null,
null,
null
)
);
// then
assertFalse(subject.isIdle());
assertEquals(Duration.ofSeconds(30), subject.getTrackStatistics().movingDuration());
@@ -294,15 +357,12 @@ public class TrackStatisticsUpdaterTest {
public void copy_constructor() {
// given
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
TrackPoint tp2 = createTrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000), Speed.of(5f), null);
TrackPoint tp3 = createTrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000), Speed.of(5f), null);
TrackPoint tp4 = createTrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000), Speed.of(5f), null);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
TrackStatisticsUpdater initial = new TrackStatisticsUpdater(List.of(tp1, tp2,tp3, tp4));
TrackStatisticsUpdater initial = new TrackStatisticsUpdater(List.of(tp1, tp2, tp3, tp4));
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(initial, tp5);
@@ -335,7 +395,13 @@ public class TrackStatisticsUpdaterTest {
}
public TrackPoint createTrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
return new TrackPoint(TrackPoint.Type.TRACKPOINT,
return createTrackPoint(latitude, longitude, altitude, time, null, null);
}
public TrackPoint createTrackPoint(double latitude, double longitude, Altitude altitude, Instant time, Speed speed, Distance sensorDistance) {
return new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
time,
latitude,
@@ -344,7 +410,14 @@ public class TrackStatisticsUpdaterTest {
altitude,
null,
null,
null
));
speed
),
sensorDistance,
null,
null,
null,
null,
null
);
}
}
@@ -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.ViewInteraction;
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.TrackListActivity;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
@@ -41,47 +46,69 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(AndroidJUnit4.class)
public class EspressoAggregatedFilterTest {
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
private final String ACTIVITY_TYPE_LOCALIZED = "activityTypeLocalized";
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
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
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());
// 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(performClick());
// check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text
ViewInteraction checkBox = onView(
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()));
checkBox.perform(waitFor(2000));
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());
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)).check(matches(isDisplayed()));
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)).check(matches(isDisplayed()));
onView(withId(R.id.filter_date_edit_text_to))
.inRoot(isDialog())
.check(matches(isDisplayed()));
}
}
@@ -28,7 +28,7 @@ public class RecordingLayoutTest extends TestCase {
String csv = "running;2;" + context.getString(R.string.stats_custom_layout_speed_key) + ",1,1,0;" + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;" + context.getString(R.string.stats_custom_layout_altitude_key) + ",0,1,0;" + context.getString(R.string.stats_custom_layout_gain_key) + ",0,0,0;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -60,13 +60,12 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;speed,1,1;distance,0,0;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(0, recordingLayout.getFields().size());
assertEquals("Default Layout", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@Test
@@ -75,13 +74,13 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(0, recordingLayout.getFields().size());
assertEquals("Default Layout", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@Test
@@ -90,7 +89,7 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;speed,distance,total time;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -105,7 +104,7 @@ public class RecordingLayoutTest extends TestCase {
String csv1 = "Layout Name;2;speed,1,0;distance;";
// when create a layout from CSV line
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
@@ -145,6 +144,6 @@ public class RecordingLayoutTest extends TestCase {
String csv = recordingLayout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;" + PreferencesUtils.getLayoutColumnsByDefault() + ";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
@@ -104,8 +119,7 @@ public class IntervalStatisticsUpdaterTest {
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
for (int i = 0; i < numberOfPoints; i++) {
trackPoints.add(TestDataUtil.createTrackPoint(i)
.setAltitudeGainLoss(null));
trackPoints.add(TestDataUtil.createTrackPoint(i, TrackPoint.Type.TRACKPOINT, null));
}
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoints);
@@ -123,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());
@@ -158,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) {
@@ -196,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();
}
};
}
}
@@ -0,0 +1,57 @@
package de.dennisguse.opentracks.util;
import static org.junit.Assert.fail;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.test.platform.app.InstrumentationRegistry;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
public final class LiveDataTestUtils {
private LiveDataTestUtils() {
}
public static <T> T waitForValue(LiveData<T> liveData,
Predicate<T> predicate,
Duration timeout,
String description) {
AtomicReference<T> lastValue = new AtomicReference<>(liveData.getValue());
CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = value -> {
lastValue.set(value);
if (predicate.test(value)) {
latch.countDown();
}
};
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
liveData.observeForever(observer);
T currentValue = liveData.getValue();
lastValue.set(currentValue);
if (predicate.test(currentValue)) {
latch.countDown();
}
});
try {
if (!latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
fail("Timed out waiting for " + description + ". Last value: " + lastValue.get());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("Interrupted while waiting for " + description);
} finally {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> liveData.removeObserver(observer));
}
return lastValue.get();
}
}
@@ -0,0 +1,30 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.content.Intent;
import androidx.test.core.app.ApplicationProvider;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.services.TrackRecordingService;
public final class TestEnvironmentUtils {
private TestEnvironmentUtils() {
}
public static void stopTrackRecordingService() {
Context context = ApplicationProvider.getApplicationContext();
context.stopService(new Intent(context, TrackRecordingService.class));
}
public static void deleteAllTracks() {
Context context = ApplicationProvider.getApplicationContext();
new ContentProviderUtils(context).deleteAllTracks(context);
}
public static void resetTrackRecordingServiceAndDeleteTracks() {
stopTrackRecordingService();
deleteAllTracks();
}
}
@@ -0,0 +1,50 @@
package de.dennisguse.opentracks.util;
import static org.junit.Assert.fail;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
public final class TrackRecordingServiceTestUtils {
private TrackRecordingServiceTestUtils() {
}
public static void waitForRecordingState(boolean isRecording, Duration timeout) {
Context context = ApplicationProvider.getApplicationContext();
CountDownLatch connectedLatch = new CountDownLatch(1);
AtomicReference<TrackRecordingService> serviceRef = new AtomicReference<>();
TrackRecordingServiceConnection connection = new TrackRecordingServiceConnection((service, self) -> {
serviceRef.set(service);
connectedLatch.countDown();
});
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> connection.bind(context));
try {
if (!connectedLatch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
fail("Timed out waiting to bind TrackRecordingService");
}
LiveDataTestUtils.waitForValue(serviceRef.get().getRecordingStatusObservable(),
status -> status != null && status.isRecording() == isRecording,
timeout,
"recording state to become " + isRecording);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("Interrupted while waiting for recording state " + isRecording);
} finally {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> connection.unbind(context));
}
}
}
@@ -0,0 +1,70 @@
package de.dennisguse.opentracks.util;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static de.dennisguse.opentracks.util.ActivityTestUtils.waitForResumedActivity;
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClick;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.TrackRecordingServiceTestUtils.waitForRecordingState;
import static org.hamcrest.Matchers.allOf;
import androidx.annotation.IdRes;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.TrackStoppedActivity;
public final class TrackUiTestUtils {
private TrackUiTestUtils() {
}
public static void startRecordingFromTrackList(Duration timeout) {
onView(withId(R.id.track_list_fab_action))
.perform(performClick());
waitForResumedActivity(TrackRecordingActivity.class, timeout);
waitForRecordingState(true, timeout);
waitUntilDisplayed(withId(R.id.track_recording_fab_action), timeout);
}
public static void stopRecordingToTrackStopped(Duration timeout, @IdRes int... expectedVisibleViewIds) {
onView(withId(R.id.track_recording_fab_action))
.perform(performLongClick());
waitForResumedActivity(TrackStoppedActivity.class, timeout);
waitUntilDisplayed(withId(R.id.track_edit_name), timeout);
for (int viewId : expectedVisibleViewIds) {
waitUntilDisplayed(withId(viewId), timeout);
}
onView(withId(R.id.track_edit_name))
.perform(closeSoftKeyboard());
}
public static void resumeStoppedTrack(Duration timeout) {
onView(allOf(withId(R.id.resume_button), isClickable()))
.perform(performClick());
waitForResumedActivity(TrackRecordingActivity.class, timeout);
waitForRecordingState(true, timeout);
waitUntilDisplayed(withId(R.id.track_recording_fab_action), timeout);
}
public static void finishStoppedTrack(Duration timeout) {
onView(withId(R.id.finish_button))
.perform(performClick());
waitForResumedActivity(TrackListActivity.class, timeout);
waitUntilDisplayed(withId(R.id.track_list), timeout);
}
public static void renameStoppedTrack(String name, Duration timeout) {
waitUntilDisplayed(withId(R.id.track_edit_name), timeout);
onView(withId(R.id.track_edit_name))
.perform(scrollTo(), replaceText(name), closeSoftKeyboard());
}
}
+2 -2
View File
@@ -4,10 +4,10 @@
"2020-02-02T03:02:04+01:00","TRACKPOINT",,,,,,54,1,1,10,66,3,50
"2020-02-02T03:02:15+01:00","TRACKPOINT",,,,,,,,,,68,3,50
"2020-02-02T03:02:17+01:00","TRACKPOINT",3.123456,14.001456,1020.2,10,,18,0,0,2,69,3,50
"2020-02-02T03:02:18+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
"2020-02-02T03:02:18+01:00","SEGMENT_END_MANUAL",3.123456,14.001456,1020.2,10,,18,0,0,0,69,3,50
"2020-02-02T03:03:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
"2020-02-02T03:03:21+01:00","TRACKPOINT",3.123456,14.002456,999.1,10,,54,0,0,,,,
"2020-02-02T03:03:22+01:00","SEGMENT_START_AUTOMATIC",3.123456,16,999.1,10,,54,0,0,,,,
"2020-02-02T03:03:30+01:00","IDLE",,,,,,,0,0,,,,
"2020-02-02T03:03:50+01:00","TRACKPOINT",3.123456,16.001,999.1,10,,54,0,0,,,,
"2020-02-02T03:04:00+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
"2020-02-02T03:04:00+01:00","SEGMENT_END_MANUAL",,,,,,,0,0,,,,
1 #time trackpoint_type latitude longitude altitude accuracy_horizontal accuracy_vertical speed altitude_gain altitude_loss sensor_distance heartrate cadence power
4 2020-02-02T03:02:04+01:00 TRACKPOINT 54 1 1 10 66 3 50
5 2020-02-02T03:02:15+01:00 TRACKPOINT 68 3 50
6 2020-02-02T03:02:17+01:00 TRACKPOINT 3.123456 14.001456 1020.2 10 18 0 0 2 69 3 50
7 2020-02-02T03:02:18+01:00 SEGMENT_END_MANUAL 3.123456 14.001456 1020.2 10 18 0 0 0 69 3 50
8 2020-02-02T03:03:20+01:00 SEGMENT_START_MANUAL
9 2020-02-02T03:03:21+01:00 TRACKPOINT 3.123456 14.002456 999.1 10 54 0 0
10 2020-02-02T03:03:22+01:00 SEGMENT_START_AUTOMATIC 3.123456 16 999.1 10 54 0 0
11 2020-02-02T03:03:30+01:00 IDLE 0 0
12 2020-02-02T03:03:50+01:00 TRACKPOINT 3.123456 16.001 999.1 10 54 0 0
13 2020-02-02T03:04:00+01:00 SEGMENT_END_MANUAL 0 0
@@ -0,0 +1,22 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>20210907_213924.gpx</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<hdop>4</hdop>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time/>
<hdop>4</hdop>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -28,10 +28,11 @@ xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/
<opentracks:typeTranslated><![CDATA[unknown]]></opentracks:typeTranslated>
<gpxtrkx:TrackStatsExtension>
<gpxtrkx:Distance>0.0</gpxtrkx:Distance>
<gpxtrkx:TimerTime>0</gpxtrkx:TimerTime>
<gpxtrkx:TotalElapsedTime>0</gpxtrkx:TotalElapsedTime>
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
<gpxtrkx:MovingSpeed>0.0</gpxtrkx:MovingSpeed>
</gpxtrkx:TrackStatsExtension>
</extensions>
<trkseg>
@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>gpx with power</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>100</gpxpx:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>110</gpxpx:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>150</gpxpx:PowerInWatts>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>gpx with power</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>100</pwr:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>110</pwr:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>150</pwr:PowerInWatts>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
+1 -1
View File
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_name" translatable="false">OpenTracks (Debug)</string>
<string name="launcher_name" translatable="false">OpenTracks (Debug)</string>
</resources>
+33 -3
View File
@@ -69,6 +69,12 @@ limitations under the License.
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"
android:minSdkVersion="33" />
<uses-permission android:name="android.permission.health.WRITE_EXERCISE" />
<uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE" />
<uses-permission android:name="android.permission.health.WRITE_DISTANCE" />
<uses-permission android:name="android.permission.health.WRITE_ELEVATION_GAINED" />
<uses-permission android:name="android.permission.health.WRITE_SPEED" />
<application
android:name=".Startup"
android:allowBackup="true"
@@ -78,7 +84,7 @@ limitations under the License.
android:hardwareAccelerated="true"
android:hasFragileUserData="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:label="@string/launcher_name"
android:supportsRtl="true"
android:theme="@style/DayNightDynamicTheme"> <!-- Default for AppCompatActivities -->
@@ -126,9 +132,32 @@ limitations under the License.
<activity android:name=".AboutActivity" />
<!-- Permissions -->
<!-- until Android 13 -->
<activity
android:name=".HelpActivity"
android:label="@string/menu_help" />
android:name=".HealthConnectPermissionsRationaleActivity"
android:exported="true">
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
</activity>
<!-- for Android 14 -->
<activity-alias
android:name="ViewPermissionUsageActivity"
android:exported="true"
android:permission="android.permission.START_VIEW_PERMISSION_USAGE"
android:targetActivity=".HealthConnectPermissionsRationaleActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
</intent-filter>
</activity-alias>
<activity android:name=".settings.HealthConnect$Settings" />
<activity android:name=".settings.HealthConnect$ConfigurePermissions" />
<activity android:name=".settings.HealthConnect$ExportAll" />
<activity android:name=".HelpActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity" />
@@ -410,5 +439,6 @@ limitations under the License.
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
<package android:name="com.google.android.apps.healthdata" />
</queries>
</manifest>
@@ -0,0 +1,28 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import de.dennisguse.opentracks.databinding.ActivityHealthConnectPermissionsRationaleBinding;
public class HealthConnectPermissionsRationaleActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityHealthConnectPermissionsRationaleBinding viewBinding = ActivityHealthConnectPermissionsRationaleBinding.inflate(getLayoutInflater());
setSupportActionBar(viewBinding.healthConnectActivityToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(viewBinding.getRoot());
}
@Override
public boolean onSupportNavigateUp() {
getOnBackPressedDispatcher().onBackPressed();
return true;
}
}
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.StrictMode;
import android.util.Log;
import java.lang.reflect.Method;
@@ -27,6 +28,11 @@ public class Startup extends Application {
// Include version information into stack traces.
Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE);
// In debug builds: show thread and VM warnings.
if (BuildConfig.DEBUG) {
Log.d(TAG, "Enabling strict mode");
StrictMode.enableDefaults();
}
PreferencesUtils.initPreferences(this);
// Set default values of preferences on first start.
@@ -47,6 +47,7 @@ import java.util.Objects;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.services.MissingPermissionException;
import de.dennisguse.opentracks.services.RecordingStatus;
@@ -62,7 +63,6 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.RecyclerViewSwipeDeleteCallback;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@@ -367,7 +367,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
if (itemId == R.id.list_context_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, false, trackIds);
DataProvider.showTrackOnMap(this, recordingStatus.isRecording(), trackIds);
return true;
}
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
@@ -45,7 +46,6 @@ import de.dennisguse.opentracks.share.ShareUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
/**
@@ -167,7 +167,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, false, trackId);
DataProvider.showTrackOnMap(this, false, trackId);
return true;
}
@@ -38,17 +38,18 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
/**
@@ -266,7 +267,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, true, trackId);
DataProvider.showTrackOnMap(this, true, trackId);
return true;
}
@@ -278,15 +279,17 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
Intent intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId)
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.getPosition().toLocation());
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.position().toLocation());
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_menu_select_layout) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
List<String> layoutNames = PreferencesUtils.getAllCustomLayoutNames();
builder.setTitle(getString(R.string.custom_layout_select_layout)).setItems(layoutNames.toArray(new String[0]), (dialog, which) -> PreferencesUtils.setDefaultLayout(layoutNames.get(which)));
List<String> layoutNames = PreferencesUtils.getAllCustomLayouts().stream()
.map(RecordingLayout::getName)
.toList();
builder.setTitle(getString(R.string.custom_layout_select_layout)).setItems(layoutNames.toArray(new String[0]), (dialog, which) -> PreferencesUtils.setSelectedLayoutName(layoutNames.get(which)));
builder.create().show();
return true;
}
@@ -18,6 +18,7 @@ import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.databinding.TrackStoppedBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.exporter.ExportUtils;
import de.dennisguse.opentracks.io.healthconnect.exporter.HealthConnectUtils;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
@@ -93,6 +94,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
viewBinding.finishButton.setOnClickListener(v -> {
storeTrackMetaData(contentProviderUtils, track);
ExportUtils.postWorkoutExport(this, trackId);
HealthConnectUtils.postExportTrack(this, trackId);
finish();
});
@@ -35,11 +35,11 @@ import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
/**
* A fragment to display track chart to the user.
@@ -204,7 +204,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull Statistics trackStatistics) {
if (isResumed()) {
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.getSpeed(), chartByDistance, viewBinding.chartView.getUnitSystem());
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.position().speed(), chartByDistance, viewBinding.chartView.getUnitSystem());
pendingPoints.add(point);
}
}
@@ -27,8 +27,8 @@ public record ChartPoint(
chartByDistance
? trackStatistics.totalDistance().toKM_Miles(unitSystem)
: trackStatistics.totalDuration().toMillis(),
trackPoint.hasAltitude()
? Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem)
trackPoint.position().hasAltitude()
? Distance.of(trackPoint.position().altitude().toM()).toM_FT(unitSystem)
: null,
smoothedSpeed != null
? smoothedSpeed.to(unitSystem)
@@ -36,14 +36,14 @@ public record ChartPoint(
smoothedSpeed != null
? smoothedSpeed.toPace(unitSystem).toSeconds() / 60d
: null,
trackPoint.hasHeartRate()
? (double) trackPoint.getHeartRate().getBPM()
trackPoint.heartRate() != null
? (double) trackPoint.heartRate().getBPM()
: null,
trackPoint.hasCadence()
? (double) trackPoint.getCadence().getRPM()
trackPoint.cadence() != null
? (double) trackPoint.cadence().getRPM()
: null,
trackPoint.hasPower()
? (double) trackPoint.getPower().getW()
trackPoint.power() != null
? (double) trackPoint.power().getW()
: null
);
}
@@ -22,6 +22,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -36,7 +37,6 @@ import java.util.Collections;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
@@ -49,12 +49,13 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.util.FileUtils;
@@ -68,21 +69,10 @@ public class ContentProviderUtils {
private static final String TAG = ContentProviderUtils.class.getSimpleName();
// The authority (the first part of the URI) for the app's content provider.
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
// The base URI for the app's content provider.
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
private static final String ID_SEPARATOR = ",";
private final ContentResolver contentResolver;
//TODO Check if this can be used in more than one place. If not remove.
public interface ContentProviderSelectionInterface {
SelectionData buildSelection();
}
public ContentProviderUtils(Context context) {
contentResolver = context.getContentResolver();
}
@@ -142,7 +132,7 @@ public class ContentProviderUtils {
@VisibleForTesting
public void deleteAllTracks(Context context) {
// Delete tracks last since it triggers a database vacuum call
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
contentResolver.delete(CustomContentProvider.UrlType.TRACKS.getUri(), null, null);
File dir = FileUtils.getPhotoDir(context);
FileUtils.deleteDirectoryRecurse(dir);
@@ -155,13 +145,13 @@ public class ContentProviderUtils {
}
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(trackId -> Long.toString(trackId.id())).toArray(String[]::new));
contentResolver.delete(CustomContentProvider.UrlType.TRACKS.getUri(), whereClause, trackIds.stream().map(trackId -> Long.toString(trackId.id())).toArray(String[]::new));
}
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
// Delete track folder resources.
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
contentResolver.delete(CustomContentProvider.UrlType.TRACKS.getUri(), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
}
//TODO Only use for tests; also move to tests.
@@ -205,7 +195,7 @@ public class ContentProviderUtils {
selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
}
return new TrackListIterator(contentResolver.query(TracksColumns.CONTENT_URI, PROJECTION, selection, selectionArgs, sortOrder));
return new TrackListIterator(contentResolver.query(CustomContentProvider.UrlType.TRACKS.getUri(), PROJECTION, selection, selectionArgs, sortOrder));
}
public Track getTrack(@NonNull Track.Id trackId) {
@@ -236,7 +226,7 @@ public class ContentProviderUtils {
* @param sortOrder the sort order. Can be null
*/
Cursor getTrackCursor(String selection, String[] selectionArgs, String sortOrder) {
return contentResolver.query(TracksColumns.CONTENT_URI, null, selection, selectionArgs, sortOrder);
return contentResolver.query(CustomContentProvider.UrlType.TRACKS.getUri(), null, selection, selectionArgs, sortOrder);
}
/**
@@ -248,7 +238,7 @@ public class ContentProviderUtils {
*/
public Track.Id insertTrack(Track track) {
//TODO Unset trackId
Uri uri = contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track));
Uri uri = contentResolver.insert(CustomContentProvider.UrlType.TRACKS.getUri(), createContentValues(track));
return new Track.Id(ContentUris.parseId(uri));
}
@@ -259,7 +249,7 @@ public class ContentProviderUtils {
* @param track the track
*/
public void updateTrack(Track track) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), TracksColumns._ID + "=?", new String[]{Long.toString(track.id().id())});
contentResolver.update(CustomContentProvider.UrlType.TRACKS.getUri(), createContentValues(track), TracksColumns._ID + "=?", new String[]{Long.toString(track.id().id())});
}
private ContentValues createContentValues(Track track) {
@@ -281,7 +271,7 @@ public class ContentProviderUtils {
}
public void updateTrackStatistics(@NonNull Track.Id trackId, @NonNull Statistics trackStatistics) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
contentResolver.update(CustomContentProvider.UrlType.TRACKS.getUri(), createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
}
private ContentValues createContentValues(Statistics trackStatistics) {
@@ -373,7 +363,7 @@ public class ContentProviderUtils {
public void deleteMarker(Context context, Marker.Id markerId) {
final Marker marker = getMarker(markerId);
deleteMarkerPhoto(context, marker);
contentResolver.delete(MarkerColumns.CONTENT_URI, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.id())});
contentResolver.delete(CustomContentProvider.UrlType.MARKERS.getUri(), MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.id())});
}
/**
@@ -433,7 +423,7 @@ public class ContentProviderUtils {
if (updateMarker.photoUrl() == null) {
deleteMarkerPhoto(context, savedMarker);
}
int rows = contentResolver.update(MarkerColumns.CONTENT_URI, createContentValues(updateMarker, updateMarker.trackId()), MarkerColumns._ID + "=?", new String[]{Long.toString(updateMarker.id().id())});
int rows = contentResolver.update(CustomContentProvider.UrlType.MARKERS.getUri(), createContentValues(updateMarker, updateMarker.trackId()), MarkerColumns._ID + "=?", new String[]{Long.toString(updateMarker.id().id())});
return rows == 1;
}
@@ -441,7 +431,7 @@ public class ContentProviderUtils {
assert marker.id() == null;
assert marker.trackId() != null;
Uri uri = contentResolver.insert(MarkerColumns.CONTENT_URI, createContentValues(marker, marker.trackId()));
Uri uri = contentResolver.insert(CustomContentProvider.UrlType.MARKERS.getUri(), createContentValues(marker, marker.trackId()));
return new Marker.Id(ContentUris.parseId(uri));
}
@@ -502,7 +492,7 @@ public class ContentProviderUtils {
if (maxCount >= 0) {
sortOrder += " LIMIT " + maxCount;
}
return contentResolver.query(MarkerColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
return contentResolver.query(CustomContentProvider.UrlType.MARKERS.getUri(), projection, selection, selectionArgs, sortOrder);
}
public MarkerIterator searchMarkers(Track.Id trackId, String query) {
@@ -520,7 +510,7 @@ public class ContentProviderUtils {
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
MarkerColumns.TYPE_LOCALIZED + " LIKE ?";
selectionArgs = new String[]{"%" + query + "%", "%" + query + "%", "%" + query + "%"};
sortOrder = MarkerColumns.DEFAULT_SORT_ORDER + " DESC";
sortOrder = BaseColumns._ID + " DESC";
}
return new MarkerIterator(this, getMarkerCursor(null, selection, selectionArgs, sortOrder, -1));
@@ -538,13 +528,19 @@ public class ContentProviderUtils {
int speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
int bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
int sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
int temperatureIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_TEMPERATURE);
int sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
int sensorDistanceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_DISTANCE);
int sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
int altitudeGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_GAIN);
int altitudeLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_LOSS);
TrackPoint trackPoint = new TrackPoint(
AltitudeGainLoss altitudeGainLoss = null;
if (!cursor.isNull(altitudeGainIndex) && !cursor.isNull(altitudeLossIndex)) {
altitudeGainLoss = new AltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex));
}
return new TrackPoint(
new TrackPoint.Id(cursor.getInt(idIndex)),
TrackPoint.Type.getById(cursor.getInt(typeIndex)),
new Position(
@@ -556,26 +552,14 @@ public class ContentProviderUtils {
!cursor.isNull(accuracyVerticalIndex) ? Distance.of(cursor.getFloat(accuracyVerticalIndex)) : null,
!cursor.isNull(bearingIndex) ? cursor.getFloat(bearingIndex) : null,
!cursor.isNull(speedIndex) ? Speed.of(cursor.getFloat(speedIndex)) : null
));
if (!cursor.isNull(sensorHeartRateIndex)) {
trackPoint.setHeartRate(cursor.getFloat(sensorHeartRateIndex));
}
if (!cursor.isNull(sensorCadenceIndex)) {
trackPoint.setCadence(cursor.getFloat(sensorCadenceIndex));
}
if (!cursor.isNull(sensorDistanceIndex)) {
trackPoint.setSensorDistance(Distance.of(cursor.getFloat(sensorDistanceIndex)));
}
if (!cursor.isNull(sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(sensorPowerIndex));
}
if (!cursor.isNull(altitudeGainIndex) && !cursor.isNull(altitudeLossIndex)) {
trackPoint.setAltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex));
}
return trackPoint;
),
!cursor.isNull(sensorDistanceIndex) ? Distance.of(cursor.getFloat(sensorDistanceIndex)) : null,
!cursor.isNull(sensorHeartRateIndex) ? HeartRate.of(cursor.getFloat(sensorHeartRateIndex)) : null,
!cursor.isNull(temperatureIndex) ? Temperature.of(cursor.getFloat(temperatureIndex)) : null,
!cursor.isNull(sensorCadenceIndex) ? Cadence.of(cursor.getFloat(sensorCadenceIndex)) : null,
!cursor.isNull(sensorPowerIndex) ? Power.of(cursor.getFloat(sensorPowerIndex)) : null,
altitudeGainLoss
);
}
//TODO Only used for file import; might be better to replace it.
@@ -584,7 +568,7 @@ public class ContentProviderUtils {
for (int i = 0; i < trackPoints.size(); i++) {
values[i] = createContentValues(trackPoints.get(i), trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
return contentResolver.bulkInsert(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), values);
}
//TODO Set trackId in this method.
@@ -593,7 +577,7 @@ public class ContentProviderUtils {
for (int i = 0; i < markers.size(); i++) {
values[i] = createContentValues(markers.get(i), trackId);
}
return contentResolver.bulkInsert(MarkerColumns.CONTENT_URI, values);
return contentResolver.bulkInsert(CustomContentProvider.UrlType.MARKERS.getUri(), values);
}
/**
@@ -614,7 +598,7 @@ public class ContentProviderUtils {
selectionArgs = new String[]{Long.toString(trackId.id())};
}
return getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns.DEFAULT_SORT_ORDER);
return getTrackPointCursor(null, selection, selectionArgs, BaseColumns._ID);
}
/**
@@ -625,7 +609,7 @@ public class ContentProviderUtils {
* @return the content provider URI of the inserted trackPoint
*/
public Uri insertTrackPoint(TrackPoint trackPoint, Track.Id trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(trackPoint, trackId));
return contentResolver.insert(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), createContentValues(trackPoint, trackId));
}
/**
@@ -637,42 +621,45 @@ public class ContentProviderUtils {
private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId.id());
values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
values.put(TrackPointsColumns.TYPE, trackPoint.type().type_db);
if (trackPoint.hasLocation()) {
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getPosition().latitude() * 1E6));
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getPosition().longitude() * 1E6));
if (trackPoint.position().hasLocation()) {
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.position().latitude() * 1E6));
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.position().longitude() * 1E6));
}
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude().toM());
if (trackPoint.position().hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.position().altitude().toM());
}
if (trackPoint.hasHorizontalAccuracy()) {
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.getHorizontalAccuracy().toM());
if (trackPoint.position().hasHorizontalAccuracy()) {
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.position().horizontalAccuracy().toM());
}
if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed().toMPS());
if (trackPoint.position().hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.position().speed().toMPS());
}
if (trackPoint.hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
if (trackPoint.position().hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.position().bearing());
}
if (trackPoint.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate().getBPM());
if (trackPoint.heartRate() != null) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.heartRate().getBPM());
}
if (trackPoint.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCadence().getRPM());
if (trackPoint.temperature() != null) {
values.put(TrackPointsColumns.SENSOR_TEMPERATURE, trackPoint.temperature().getCelsius());
}
if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM());
if (trackPoint.cadence() != null) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.cadence().getRPM());
}
if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower().getW());
if (trackPoint.sensorDistance() != null) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.sensorDistance().toM());
}
if (trackPoint.power() != null) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.power().getW());
}
if (trackPoint.hasAltitudeGainLoss()) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.getAltitudeGainLoss().gain_m());
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.getAltitudeGainLoss().loss_m());
if (trackPoint.altitudeGainLoss() != null) {
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.altitudeGainLoss().gain_m());
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.altitudeGainLoss().loss_m());
}
return values;
}
@@ -697,7 +684,7 @@ public class ContentProviderUtils {
* @param sortOrder the sort order
*/
private Cursor getTrackPointCursor(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, projection, selection, selectionArgs, sortOrder);
return contentResolver.query(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), projection, selection, selectionArgs, sortOrder);
}
public static String formatIdListForUri(Track.Id... trackIds) {
@@ -732,7 +719,7 @@ public class ContentProviderUtils {
public SensorStatistics getSensorStats(@NonNull Track.Id trackId) {
SensorStatistics sensorStatistics = null;
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.id()), null, null, null, null)) {
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(CustomContentProvider.UrlType.TRACKS_SENSOR_STATS.getUri(), trackId.id()), null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
final int MAX_HR_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_HR);
final int AVG_HR_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_HR);
@@ -755,9 +742,9 @@ public class ContentProviderUtils {
}
@NonNull
public List<AggregatedStatistic> getAggregatedStatisticsForTracks(ContentProviderSelectionInterface selection) {
public List<AggregatedStatistic> getAggregatedStatisticsForTracks(TrackSelection selection) {
SelectionData data = selection.buildSelection();
try (Cursor cursor = contentResolver.query(TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS, null, data.selection(), data.getSelectionArgs(), null)) {
try (Cursor cursor = contentResolver.query(CustomContentProvider.UrlType.TRACKS_AGGREGATED_STATISTICS.getUri(), null, data.selection(), data.getSelectionArgs(), null)) {
ArrayList<AggregatedStatistic> aggregatedStatistics = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
aggregatedStatistics.add(new AggregatedStatistic(
@@ -27,6 +27,7 @@ import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Build;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
@@ -34,10 +35,12 @@ import androidx.annotation.NonNull;
import java.util.Arrays;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
@@ -49,6 +52,11 @@ import de.dennisguse.opentracks.settings.PreferencesUtils;
*/
public class CustomContentProvider extends ContentProvider {
// The authority (the first part of the URI) for the app's content provider.
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
// The base URI for the app's content provider.
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
private static final String TAG = CustomContentProvider.class.getSimpleName();
private static final String SQL_LIST_DELIMITER = ",";
@@ -66,48 +74,39 @@ public class CustomContentProvider extends ContentProvider {
*/
private final String SENSOR_STATS_QUERY =
"WITH time_select AS " +
"(SELECT t1." + TrackPointsColumns.TIME + " * (t1." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")) time_value " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t1 " +
"WHERE t1." + TrackPointsColumns._ID + " > t." + TrackPointsColumns._ID + " AND t1." + TrackPointsColumns.TRACKID + " = ? ORDER BY _id LIMIT 1) " +
"(SELECT t1." + TrackPointsColumns.TIME + " * (t1." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")) time_value " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t1 " +
"WHERE t1." + TrackPointsColumns._ID + " > t." + TrackPointsColumns._ID + " AND t1." + TrackPointsColumns.TRACKID + " = ? ORDER BY _id LIMIT 1) " +
"SELECT " +
"SUM(t." + TrackPointsColumns.SENSOR_HEARTRATE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_HR + ", " +
"SELECT " +
"SUM(t." + TrackPointsColumns.SENSOR_HEARTRATE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_HR + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_HEARTRATE + ") " + TrackPointsColumns.ALIAS_MAX_HR + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_HEARTRATE + ") " + TrackPointsColumns.ALIAS_MAX_HR + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_CADENCE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_CADENCE + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_CADENCE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_CADENCE + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_CADENCE + ") " + TrackPointsColumns.ALIAS_MAX_CADENCE + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_CADENCE + ") " + TrackPointsColumns.ALIAS_MAX_CADENCE + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_POWER + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_POWER + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_POWER + ") " + TrackPointsColumns.ALIAS_MAX_POWER + " " +
"MAX(t." + TrackPointsColumns.SENSOR_POWER + ") " + TrackPointsColumns.ALIAS_MAX_POWER + " " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
public CustomContentProvider() {
//TODO Add tests for used URIs (incl. Dashboard API)
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath() + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS.getPath(), UrlType.TRACKS_AGGREGATED_STATISTICS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal());
UrlType[] urlTypes = UrlType.values();
for (int i = 0; i < urlTypes.length; i++) {
uriMatcher.addURI(AUTHORITY_PACKAGE, urlTypes[i].getPathWithFilter(), i);
}
}
@Override
@@ -143,7 +142,7 @@ public class CustomContentProvider extends ContentProvider {
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
notifyChanges(url);
int totalChanges = getTotalChanges() - totalChangesBefore;
Log.i(TAG, "Deleted " + totalChanges + " total rows from database");
@@ -170,14 +169,7 @@ public class CustomContentProvider extends ContentProvider {
@Override
public String getType(@NonNull Uri url) {
return switch (getUrlType(url)) {
case TRACKPOINTS -> TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_BY_TRACKID -> TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS -> TracksColumns.CONTENT_TYPE;
case MARKERS -> MarkerColumns.CONTENT_TYPE;
case MARKERS_BY_TRACKID -> MarkerColumns.CONTENT_ITEMTYPE;
default -> throw new IllegalArgumentException("Unknown URL " + url);
};
return getUrlType(url).contentType;
}
@Override
@@ -193,7 +185,8 @@ public class CustomContentProvider extends ContentProvider {
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
notifyChanges(url);
return result;
}
@@ -216,7 +209,8 @@ public class CustomContentProvider extends ContentProvider {
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
notifyChanges(url);
return numInserted;
}
@@ -225,11 +219,12 @@ public class CustomContentProvider extends ContentProvider {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String sortOrder = null;
String groupBy = null;
//TODO queryBuilder.setStrict(true);
//TODO queryBuilder.appendWhere() should use selectionArgs?
switch (getUrlType(url)) {
case TRACKPOINTS -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
sortOrder = sort != null ? sort : BaseColumns._ID;
}
case TRACKPOINTS_BY_ID -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
@@ -239,18 +234,28 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
}
case DASHBOARD_TRACKPOINTS_BY_TRACKIDS -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKPOINTS);
}
case TRACKS -> {
if (projection != null && Arrays.asList(projection).contains(TracksColumns.MARKER_COUNT)) {
queryBuilder.setTables(TracksColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT " + MarkerColumns.TRACKID + " AS markerTrackId, COUNT(*) AS " + TracksColumns.MARKER_COUNT + " FROM " + MarkerColumns.TABLE_NAME + " GROUP BY " + MarkerColumns.TRACKID + ") ON (" + TracksColumns.TABLE_NAME + "." + TracksColumns._ID + "= markerTrackId)");
} else {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
}
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
sortOrder = sort != null ? sort : BaseColumns._ID;
}
case TRACKS_BY_ID -> {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
}
case DASHBOARD_TRACKS_BY_IDS -> {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKS);
}
case TRACKS_SENSOR_STATS -> {
//TODO Replace with SQLQueryBuilder?
long trackId = ContentUris.parseId(url);
@@ -271,7 +276,7 @@ public class CustomContentProvider extends ContentProvider {
}
case MARKERS -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
sortOrder = sort != null ? sort : BaseColumns._ID;
}
case MARKERS_BY_ID -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
@@ -281,6 +286,11 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
}
case DASHBOARD_MARKERS_BY_TRACKIDS -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_MARKERS);
}
default -> throw new IllegalArgumentException("Unknown url " + url);
}
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder);
@@ -313,10 +323,36 @@ public class CustomContentProvider extends ContentProvider {
}
}
getContext().getContentResolver().notifyChange(url, null, false);
notifyChanges(url);
return count;
}
private void notifyChanges(@NonNull Uri uri) {
getContext().getContentResolver().notifyChange(uri, null, false);
// Also notify dashboard URIs for compatibility
Uri dashboardUri = getDashboardUri(uri);
if (dashboardUri != null) {
getContext().getContentResolver().notifyChange(dashboardUri, null, false);
}
}
private Uri getDashboardUri(Uri uri) {
String path = uri.getPath();
if (path == null) return null;
String dashboardPath;
if (path.startsWith("/tracks")) {
dashboardPath = path.replaceFirst("/tracks", "/dashboard/tracks");
} else if (path.startsWith("/markers")) {
dashboardPath = path.replaceFirst("/markers", "/dashboard/markers");
} else if (path.startsWith("/trackpoints")) {
dashboardPath = path.replaceFirst("/trackpoints", "/dashboard/trackpoints");
} else {
return null;
}
return Uri.parse(CONTENT_BASE_URI + dashboardPath);
}
@NonNull
private UrlType getUrlType(Uri url) {
UrlType[] urlTypes = UrlType.values();
@@ -351,7 +387,7 @@ public class CustomContentProvider extends ContentProvider {
}
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
if (rowId >= 0) {
return ContentUris.appendId(TrackPointsColumns.CONTENT_URI_BY_ID.buildUpon(), rowId).build();
return ContentUris.appendId(UrlType.TRACKPOINTS_BY_ID.getUri().buildUpon(), rowId).build();
}
throw new SQLiteException("Failed to insert a track point " + url);
}
@@ -359,7 +395,7 @@ public class CustomContentProvider extends ContentProvider {
private Uri insertTrack(Uri url, ContentValues contentValues) {
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
return ContentUris.appendId(UrlType.TRACKS.getUri().buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a track " + url);
}
@@ -367,21 +403,49 @@ public class CustomContentProvider extends ContentProvider {
private Uri insertMarker(Uri url, ContentValues contentValues) {
long rowId = db.insert(MarkerColumns.TABLE_NAME, MarkerColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(MarkerColumns.CONTENT_URI.buildUpon(), rowId).build();
return ContentUris.appendId(UrlType.MARKERS.getUri().buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a marker " + url);
}
private enum UrlType {
TRACKPOINTS,
TRACKPOINTS_BY_ID,
TRACKPOINTS_BY_TRACKID,
TRACKS,
TRACKS_BY_ID,
TRACKS_SENSOR_STATS,
TRACKS_AGGREGATED_STATISTICS,
MARKERS,
MARKERS_BY_ID,
MARKERS_BY_TRACKID
public enum UrlType {
TRACKPOINTS("/trackpoints", "", "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint"),
TRACKPOINTS_BY_ID("/trackpoints", "#", "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint"),
//TODO Check if still used
TRACKPOINTS_BY_TRACKID("/trackpoints/trackid", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint"),
TRACKS("/tracks", "", "vnd.android.cursor.dir/vnd.de.dennisguse.track"),
TRACKS_BY_ID("/tracks", "#", "vnd.android.cursor.dir/vnd.de.dennisguse.track"),
TRACKS_SENSOR_STATS("/tracks/sensorstats", "#", "vnd.android.cursor.dir/vnd.de.dennisguse.sensorstats"),
TRACKS_AGGREGATED_STATISTICS("/tracks/aggregated/", "", "vnd.android.cursor.dir/vnd.de.dennisguse.aggregatedstatistics"),
MARKERS("/markers", "", "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint"),
MARKERS_BY_ID("/markers", "#", "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint"),
@Deprecated //TODO Should not be used anymore
MARKERS_BY_TRACKID("/markers/trackid", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint"),
DASHBOARD_TRACKS_BY_IDS("/dashboard/tracks", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.track"),
DASHBOARD_TRACKPOINTS_BY_TRACKIDS("/dashboard/trackpoints", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint"),
DASHBOARD_MARKERS_BY_TRACKIDS("/dashboard/markers", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint");
private final String path;
private final String filter;
private final String contentType;
UrlType(@NonNull String path, String filter, @NonNull String contentType) {
this.path = path;
this.filter = filter;
this.contentType = contentType;
}
public Uri getUri() {
return Uri.parse(CONTENT_BASE_URI + path);
}
private String getPathWithFilter() {
if (TextUtils.isEmpty(filter))
return path;
return path + "/" + filter;
}
}
}

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