Compare commits

...

102 Commits

Author SHA1 Message Date
Dennis Guse 220c7b1443 Release: v4.2.0 2023-01-03 23:10:05 +01:00
Dennis Guse 33bb63976d Merge branch 'translations' 2023-01-03 23:04:44 +01:00
Dennis Guse 8db8f68d69 Fix pt translations. 2023-01-03 23:04:37 +01:00
Dennis Guse 99b417217e Bugfix: StatisticsRecordingFragment should only clear viewHolder when the views get destroyed.
Introduced in d70e7b8788
2023-01-02 23:37:38 +01:00
Dennis Guse ac80386b8a Refactor tests: use mocks for BluetoothRemoteSensorManager. 2022-12-30 16:22:46 +01:00
Dennis Guse b96441a964 Refactor: BluetoothUtils return HeartRate. 2022-12-30 16:11:22 +01:00
dependabot[bot] 207f9563df Bump mockito-android from 4.10.0 to 4.11.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.10.0 to 4.11.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.10.0...v4.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-29 12:55:30 +01:00
ssantos 7b84bf448b Translated using Weblate (Portuguese)
Currently translated at 100.0% (503 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/pt/
2022-12-29 11:49:03 +01:00
Dennis Guse 41a62fd412 Merge remote-tracking branch 'weblate2/main' 2022-12-27 22:26:30 +01:00
Dan 1087dbf387 Translated using Weblate (Ukrainian)
Currently translated at 88.8% (447 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2022-12-18 00:55:30 +01:00
dependabot[bot] e16d1d53c6 Bump mockito-android from 4.9.0 to 4.10.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.9.0 to 4.10.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.9.0...v4.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-15 07:31:29 +01:00
Dennis Guse 00c9ba6595 Merge remote-tracking branch 'weblate2/main' 2022-12-08 20:11:00 +01:00
Dennis Guse 7c1995c67f Bugfix: NPE; introduced in b53ef455a6
Fixes #1431.
2022-12-08 20:07:26 +01:00
ovl-1 d2e0ae8b11 Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.8% (492 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2022-11-28 16:47:53 +01:00
Allan Nordhøy f0aa49537d Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.8% (492 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2022-11-26 06:46:57 +01:00
Dennis Guse 14ace294f3 Make Public API summary translatable.
Fixes #1430.
2022-11-25 21:17:18 +01:00
Dennis Guse b53ef455a6 Merge pull request #1427 from OpenTracksApp/heartrate#1118
Heartrate zones
2022-11-25 19:03:56 +01:00
Dennis Guse 3dc6c6607d Removed obsolete test.
Introduced in d70e7b8788
2022-11-25 07:50:25 +01:00
Dennis Guse dd91e05fb6 Bugfix: sensor name/address must be visible.
Introduced in d70e7b8788
2022-11-25 07:42:18 +01:00
Dennis Guse 19fe5275b9 Heartrate zones: adding preference for max heartrate.
Fixes #1118.
2022-11-25 07:41:04 +01:00
Dennis Guse 1aeb9ca332 Heartrate zones: adding preference for max heartrate.
Part of #1118.
2022-11-25 07:05:55 +01:00
Allan Nordhøy 520c240a19 Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.6% (491 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2022-11-25 00:13:22 +01:00
Dennis Guse 48aaca13d0 Migrated to hosted.weblate.org
Reason: weblate.bubu1.eu shutdown end of 2022.
Alternative: translate.codeberg.org
Selection method: flipped a coin

Fixes #1396.
2022-11-24 20:57:47 +01:00
Hosted Weblate 88a31f4f39 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/
2022-11-24 20:49:19 +01:00
Dennis Guse 5ce9e6606b Merge pull request #1386 from OpenTracksApp/refactor#1118
Simplify RecordingLayout so we can create custom UIs #1118
2022-11-22 07:12:12 +01:00
Dennis Guse 4ea70b15e7 Renamed Layout to RecordingLayout.
Part of #1118.
2022-11-22 07:06:56 +01:00
Dennis Guse d3cf2957cc StatisticsCustomLayoutEditAdapter: use StatisticsViewHolder (with demo data).
Part of #1118.
2022-11-22 07:06:47 +01:00
Dennis Guse dbac595754 StatisticsCustomLayoutEditAdapter: use viewBinding.
Part of #1118.
2022-11-22 07:06:47 +01:00
Dennis Guse d70e7b8788 StatisticsRecordingFragment: render Views manually (instead of RecyclerView).
Part of #1118.
2022-11-22 07:06:45 +01:00
dependabot[bot] f6835852b6 Bump mockito-android from 4.8.1 to 4.9.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.8.1 to 4.9.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.8.1...v4.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-15 06:37:13 +01:00
Dennis Guse d5864726f6 StatisticsData: do not expose DataField.
Part of #1118.
2022-11-10 21:45:28 +01:00
Dennis Guse 3cc3271726 Cleanup: move Layout/DataField persistence into one class.
Part of #1118.
2022-11-10 21:45:28 +01:00
Dennis Guse 8aef9c8d28 Refactor StatisticsAdapter to use ViewBindings.
Part of #1118.
2022-11-10 21:45:28 +01:00
dependabot[bot] bcca00d110 Bump core from 1.4.0 to 1.5.0
Bumps core from 1.4.0 to 1.5.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-10 07:41:28 +01:00
dependabot[bot] aa929161b4 Bump orchestrator from 1.4.1 to 1.4.2
Bumps orchestrator from 1.4.1 to 1.4.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-10 07:19:57 +01:00
dependabot[bot] 70cb9cbf51 Bump junit from 1.1.3 to 1.1.4
Bumps junit from 1.1.3 to 1.1.4.

---
updated-dependencies:
- dependency-name: androidx.test.ext:junit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-10 07:19:49 +01:00
dependabot[bot] fef3b97661 Bump runner from 1.4.0 to 1.5.1
Bumps runner from 1.4.0 to 1.5.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-09 22:39:56 +01:00
dependabot[bot] 945f366195 Bump espresso-core from 3.4.0 to 3.5.0
Bumps espresso-core from 3.4.0 to 3.5.0.

---
updated-dependencies:
- dependency-name: androidx.test.espresso:espresso-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-09 22:36:09 +01:00
dependabot[bot] 8c0a851d87 Bump rules from 1.4.0 to 1.5.0
Bumps rules from 1.4.0 to 1.5.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-09 22:35:54 +01:00
Dennis Guse 3eb89d7899 Release: v4.1.2 2022-11-04 22:02:07 +01:00
Nicolas Palix f12ff4a625 Translated using Weblate (French)
Currently translated at 100.0% (503 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-11-02 18:15:03 +01:00
Sam 71dac06ce8 README: add FitTrackee to format compatibility matrix 2022-11-02 18:14:59 +01:00
Markus Dittmann 07090cd72c Fix incorrect German translation of "Open Track"
Change the German translation of the "Open Track" button label from
"Offene Strecke" (an "open track") to "Strecke öffnen" ("open this track").

Without context, both variants of the original english phrase are correct
translations, but as a button label in the context of importing a track it is
clear that "open" is a verb and the phrase is an action prompt, and not an
adjective to describe the track.
2022-11-02 18:13:02 +01:00
Dennis Guse 26a20fe32d Voice announcements: distance only with one decimal unit.
Follow-up to: 8a9cd40bd5
2022-10-29 20:35:37 +02:00
FTno d74ef256d7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (503 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-10-28 22:55:44 +02:00
Lorin-lab 0392ba5128 Translated using Weblate (French)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-10-28 22:55:44 +02:00
Lorin-lab 3d12ebdaa3 Translated using Weblate (French (Switzerland))
Currently translated at 53.1% (267 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr_CH/
2022-10-28 22:55:44 +02:00
Dennis Guse b3cb5e8091 Merge pull request #1406 from OpenTracksApp/raceCondition#1328
Handle LocationManager callbacks with TrackRecordingService's handler.
2022-10-28 22:55:40 +02:00
Dennis Guse 8f9cbcc077 Github Actions: disable test execution as results are flaky due to performance limitations.
Fixes #1409.
2022-10-28 22:39:45 +02:00
Dennis Guse 14b6746dff Handle LocationManager callbacks with TrackRecordingService's handler.
Fixes #1328.
2022-10-28 22:35:36 +02:00
Dennis Guse a5eca8fa64 Test: do not request GPS permissions for test that use fake locations anyhow. 2022-10-28 18:41:54 +02:00
Dennis Guse 498ee20e2a Test: update and re-enable ExportImportTest for csv_export. 2022-10-28 18:41:54 +02:00
Dennis Guse 2207865a70 Cleanup using AndroidStudio analyze. 2022-10-28 17:49:59 +02:00
Dennis Guse 8073f12947 Bugfix: prevent crash while stopping recording (race condition).
Part of #1328
2022-10-27 20:30:00 +02:00
dependabot[bot] cacfcbd4d6 Bump material from 1.6.1 to 1.7.0
Bumps [material](https://github.com/material-components/material-components-android) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.6.1...1.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-23 12:39:59 +02:00
Lorin-lab 08673de331 Fixed bad string for an answer in the FAQ 2022-10-19 21:16:00 +02:00
Dennis Guse aecd1504f3 Unpin indirect dependency versions again (upstream fixed it).
Introduced in 594f69320a
2022-10-19 20:00:52 +02:00
Dennis Guse f3bf42083a Cleanup. 2022-10-18 20:21:20 +02:00
Dennis Guse 8a9cd40bd5 Voice announcements: distance only with one decimal unit. 2022-10-18 20:21:20 +02:00
dependabot[bot] fee1d1a871 Bump mockito-android from 4.8.0 to 4.8.1
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.8.0...v4.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-18 07:04:18 +02:00
dependabot[bot] 0787ae6686 Bump gradle from 7.3.0 to 7.3.1
Bumps gradle from 7.3.0 to 7.3.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-17 17:38:05 +02:00
Dennis Guse 747ef9fc56 pt/BR: fix plurals. 2022-10-17 17:37:07 +02:00
mondstern 5911d60818 Translated using Weblate (Irish)
Currently translated at 98.0% (492 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ga/
2022-10-13 00:11:00 +02:00
gbpu b89dbf5819 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (501 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2022-10-13 00:10:58 +02:00
mondstern 99e2c1f38d Translated using Weblate (Irish)
Currently translated at 91.7% (89 of 97 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/ga/
2022-10-11 10:11:03 +02:00
mondstern faa1ffe53b Translated using Weblate (Irish)
Currently translated at 72.9% (366 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ga/
2022-10-11 10:11:01 +02:00
mondstern 8461122df0 Added translation using Weblate (Irish) 2022-10-09 17:56:21 +02:00
FTno 2ca7aa8f1d Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-10-05 15:10:54 +02:00
Dennis Guse 9db2aff631 Release: v4.1.1 2022-09-29 20:23:21 +02:00
Xosé M b7cedab1fb Translated using Weblate (Galician)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2022-09-28 21:39:42 +02:00
metezd 0a193c8b5c Translated using Weblate (Turkish)
Currently translated at 97.8% (490 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-09-28 21:39:42 +02:00
Mingu113 7049fab204 Translated using Weblate (Vietnamese)
Currently translated at 73.6% (70 of 95 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/vi/
2022-09-28 21:39:42 +02:00
Mingu113 80fa49bcf0 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (501 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2022-09-28 21:39:42 +02:00
Leon Marz a1eb5fb15f Check if the device has GPS
Fixes #1380
2022-09-28 21:39:33 +02:00
dependabot[bot] 9160b9b104 Bump reactivecircus/android-emulator-runner from 2.25.0 to 2.26.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.25.0 to 2.26.0.
- [Release notes](https://github.com/reactivecircus/android-emulator-runner/releases)
- [Changelog](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/CHANGELOG.md)
- [Commits](https://github.com/reactivecircus/android-emulator-runner/compare/v2.25.0...v2.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-26 20:19:51 +02:00
Dennis Guse 85a73c3c85 Implement introduction screen.
Fixes #1316.
2022-09-18 15:44:49 +02:00
Dennis Guse 8389f9150a Update sensor documentation. 2022-09-17 17:08:50 +02:00
Dennis Guse 3075f4010f Release: v4.1.0 2022-09-15 23:14:53 +02:00
Dennis Guse 82e305e6a7 Upgrade to Android Gradle Plugin 7.3.0 2022-09-15 22:14:55 +02:00
Dennis Guse 6351a335c0 Merge pull request #1217 from OpenTracksApp/kml23#339
Upgrade to KML2.3
2022-09-13 23:51:10 +02:00
Dennis Guse 6538daedef README: add export format compatibility matrix.
Part of #339.
2022-09-13 23:50:43 +02:00
Dennis Guse 82fe859c84 KML: Export KML2.3 - KML2.2 and KML2.3 can be imported.
Fixes #339.
2022-09-13 23:20:06 +02:00
Dennis Guse f567b03be5 Bugfix: import all crashed with ClassCastException. 2022-09-11 14:37:35 +02:00
monyxie 7dc4e8dbc8 Translated using Weblate (Chinese (Simplified))
Currently translated at 72.6% (69 of 95 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/zh_Hans/
2022-09-10 23:11:09 +02:00
monyxie 2a272a887e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (501 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/zh_Hans/
2022-09-10 23:11:09 +02:00
Xosé M 868509e00b Translated using Weblate (Galician)
Currently translated at 100.0% (501 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2022-09-10 23:11:08 +02:00
FTno 477bdd5214 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (501 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-09-10 23:11:08 +02:00
Xosé M 286cec8d77 Translated using Weblate (Galician)
Currently translated at 100.0% (501 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2022-09-10 23:11:08 +02:00
Dennis Guse 58d4ee7d7d Merge pull request #1372 from OpenTracksApp/cadence#1352
Cycling cadence from powermeter
2022-09-10 23:10:58 +02:00
dependabot[bot] 0e93a304f0 Bump appcompat from 1.5.0 to 1.5.1
Bumps appcompat from 1.5.0 to 1.5.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-07 22:24:38 +02:00
dependabot[bot] 0adc4c7986 Bump mockito-android from 4.7.0 to 4.8.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.7.0 to 4.8.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v4.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-07 22:20:51 +02:00
Dennis Guse a93424629d BLE: process cycling cadence from powermeter.
Fixes #1352.
2022-09-04 19:55:32 +02:00
Dennis Guse da29766fd0 BLE: BluetoothConnectionManager supports multiple BLE services per sensor type (e.g., cadence).
Part of #1352.
2022-09-04 19:11:31 +02:00
Dennis Guse dc7afbd07f BLE: extract SensorDataCycling.
Part of #1352.
2022-09-04 12:11:09 +02:00
Dennis Guse 67b174feac BLE: do not try to connect twice.
Might had some interesting side effects.
Part of #1352.
2022-09-04 12:11:09 +02:00
Dennis Guse b6b81b5d3d BLE: extract sensor managers into separate files.
Part of #1352.
2022-09-04 12:11:09 +02:00
Dennis Guse 7d804b603d BLE: decode cadence from cycling powermeter.
Part of #1352.
2022-09-04 12:11:09 +02:00
Dennis Guse 8696a15ce7 Cleanup: introduced ServiceMeasurementUUID.
Part of #1352.
2022-09-04 11:54:26 +02:00
Dennis Guse 989fe39849 Cleanup: BLE preferences.
Part of #1352.
2022-09-04 11:31:42 +02:00
Dennis Guse ed1e432691 Merge remote-tracking branch 'weblate/main' 2022-09-03 23:58:15 +02:00
Dennis Guse 4f612e32d6 Voice announcements: "speed" -> "average moving speed" (en only).
Fixes #1346.
2022-09-03 23:26:09 +02:00
Surfoo 08ab8cc61d Translated using Weblate (French)
Currently translated at 100.0% (501 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-08-28 17:01:16 +02:00
170 changed files with 3998 additions and 2297 deletions
@@ -1,3 +1,6 @@
# We have flaky test results due to performance limitations in Github Actions.
# Happens too often nowadays; See https://github.com/OpenTracksApp/OpenTracks/issues/1409
#https://github.com/marketplace/actions/android-emulator-runner
name: Test
on:
@@ -23,7 +26,7 @@ jobs:
java-version: '11'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2.25.0
uses: reactivecircus/android-emulator-runner@v2.26.0
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
+22 -14
View File
@@ -40,8 +40,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</tr>
</table>
<a href="https://weblate.bubu1.eu/engage/OpenTracks/">
<img src="https://weblate.bubu1.eu/widgets/OpenTracks/-/opentracks/horizontal-auto.svg" alt="Translation status" />
<a href="https://hosted.weblate.org/engage/opentracks/">
<img src="https://hosted.weblate.org/widgets/opentracks/-/horizontal-auto.svg" alt="Translation status" />
</a>
## Screenshots
@@ -64,19 +64,19 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* __Voice announcements__
* __Photos and Markers:__ mark interesting locations while tracking
* __Export:__
* export tracks either as KMZ (incl. photos), KML, or GPX
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
* export tracks either as [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) (incl. photos), [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html), or [GPX 1.1](https://www.topografix.com/GPX/1/1/)
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
* __Altitude:__
* gain/loss via barometric sensor (if present)
* shown in EGM2008 (above mean sea level); exported as WGS84
* gain/loss via barometric sensor (if present)
* shown in EGM2008 (above mean sea level); exported as WGS84
* __Bluetooth LE sensors:__
* heart rate
* cycling: speed and distance
* cycling: cadence
* cycling: power meter
* running: speed and cadence
* support for BLE sensor training only (i.e., without GPS) for indoor training
* heart rate
* cycling: speed and distance
* cycling: cadence
* cycling: power meter
* running: speed and cadence
* support for BLE sensor training only (i.e., without GPS) for indoor training
An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md)
@@ -121,7 +121,15 @@ For testing via adb: `adb shell am start -e someParameter someValue -n "package/
The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
__IMPORTANT__: triggering `StartRecording` does not check if Android permissions (location or Bluetooth) were granted. If they are not granted, nothing will be recorded.
## File formats compatibility with open-source software
| | [GPX 1.1](https://www.topografix.com/GPX/1/1/) | [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) | [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) |
|------------------------------------------------------|------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------|
| [OpenLayers 7.1.0](https://openlayers.org/) | ? | [no](https://github.com/openlayers/openlayers/issues/14104) | [no](https://github.com/openlayers/openlayers/issues/14104) |
| [Golden Cheetah 3.5](https://www.goldencheetah.org/) | ? | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) |
| [GpxPod](https://apps.nextcloud.com/apps/gpxpod) | ? | ? | ? |
| [OsmAnd](https://github.com/osmandapp/OsmAnd) | ? | [no](https://github.com/osmandapp/OsmAnd/issues/15271) | [no](https://github.com/osmandapp/OsmAnd/issues/15271) |
| [FitTrackee](https://github.com/SamR1/FitTrackee) | yes | n/a | n/a |
## Dashboard API (incl. map)
+4 -2
View File
@@ -20,7 +20,7 @@ Usually, it takes two days before the update is published (build, signed, and pu
The recent build activity can be found [here](https://f-droid.org/wiki/index.php?title=Special:RecentChanges&days=7&from=&hidebots=0&hideanons=1&hideliu=1&limit=500)
## Google Play Store
Application id: de.dennisguse.opentracks.playstore
Application id: `de.dennisguse.opentracks.playstore`
Requirement: `gem install fastlane`
@@ -35,4 +35,6 @@ Supported languages: https://support.google.com/googleplay/android-developer/tab
## Translations (localization)
Translations of the OpenTracks are handled in Weblate: [https://weblate.bubu1.eu/projects/OpenTracks/](https://weblate.bubu1.eu/projects/OpenTracks/).
Translations of the OpenTracks are handled in
Weblate: [https://hosted.weblate.org/projects/opentracks/](https://hosted.weblate.org/projects/opentracks)
.
+12 -12
View File
@@ -2,7 +2,7 @@
## 0x180D: Heartrate Service
* Decathlon|Geonaute|Kalenji Dual HRM Belt
* Decathlon/Geonaute/Kalenji Dual HRM Belt
* moofit heart rate monitor
* Polar H7
* Polar H10
@@ -38,13 +38,13 @@ However, often only one value is provided.
We only support _Instantaneous Power_.
For cycling:
For cycling (incl. cadence):
* Assioma Duo Powermeter
* Elite drivo II
* QUARQ Red DZero Powermeter
* Tacx Satori Smart
* Wahoo Kickr v4.0
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is propietary @ firmware v1.061)
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is proprietary @ firmware v1.061)
For running:
* Stryd
@@ -63,12 +63,12 @@ There are smartphones that have a barometric sensor that OpenTracks uses to comp
In our tests there are smartphones whose barometric sensor works well and others don't. Here is a list of the smartphones tested:
|Brand|Model|Android Version|Tests|
|-----|-----|---------------|-----------|
|Google|Pixel|LineageOS 17.1|Ok|
|Google|Pixel 2 XL|LineageOS 17.1|Ok on the bike handlebars but it fails when I carry it in a pocket or backpack|
|Google|Pixel 3|Android 11|Failure|
|Google|Pixel 3a|Android 11|Ok|
|Google|Pixel 3a|Lineage 18.1|Ok|
|Google|Pixel 4a|Android 11|Ok|
|Sony|Xperia XZ1 Compact|LineageOS 16.1|Failure|
| Brand | Model | Android Version | Tests |
|--------|--------------------|-----------------|--------------------------------------------------------------------------------|
| Google | Pixel | LineageOS 17.1 | Ok |
| Google | Pixel 2 XL | LineageOS 17.1 | Ok on the bike handlebars but it fails when I carry it in a pocket or backpack |
| Google | Pixel 3 | Android 11 | Failure |
| Google | Pixel 3a | Android 11 | Ok |
| Google | Pixel 3a | Lineage 18.1 | Ok |
| Google | Pixel 4a | Android 11 | Ok |
| Sony | Xperia XZ1 Compact | LineageOS 16.1 | Failure |
+13 -14
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0-rc01'
classpath 'com.android.tools.build:gradle:7.3.1'
}
}
@@ -59,8 +59,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 5017
versionName "v4.0.8"
versionCode 5119
versionName "v4.2.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -122,21 +122,20 @@ android {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-splashscreen:1.0.0'
androidTestImplementation 'androidx.test:core:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'org.mockito:mockito-android:4.7.0'
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'org.mockito:mockito-android:4.11.0'
androidTestUtil 'androidx.test:orchestrator:1.4.1'
androidTestUtil 'androidx.test:orchestrator:1.4.2'
}
@@ -7,5 +7,5 @@ Changes:
Bugfix:
- Speech announcements use MAY_DUCK (again)
- ChartView shows markers (again)
- Fixed landscape layout of TrackListActivity
- Fixed landscape recordingLayout of TrackListActivity
- Request legacy file API behavior (necessary for Android 10)
@@ -0,0 +1,8 @@
v4.1.0: OpenTracks
Changes:
- Export KML/KMZ version 2.3 only (was 2.2)
- For crank-based powermeters: fetch cadence
Bugfix:
- Workaround for crash on "import all"
@@ -0,0 +1,7 @@
v4.1.1: OpenTracks
Changes:
- Add introduction screen (show once)
Bugfix:
- Do not crash if no GPS device is present
@@ -0,0 +1,11 @@
v4.1.2: OpenTracks
Changes:
- Voice announcements only use on decimal for distance
Bugfix:
- Crash when stopping (race condition)
Developer:
- Github actions: disabled instrumentation testing (too slow)
- Tests are done with only fake GPS data
@@ -0,0 +1,8 @@
v4.2.0: OpenTracks
Changes:
- Show heartrate zone information
Developer:
- Migrated to hosted.weblate.org
- Recording UI uses Views directly
@@ -0,0 +1,16 @@
v3.18.6: OpenTracks
Athruithe:
- "Taispeáin ar léarscáil" - feidhmiúlacht trí KML in ionad KMZ
- Ní chuimsíonn fógra gutha ainm na haipe
- Agus é ag taifeadadh: déantar gnóthachan/caillteanas ingearchló a nuashonrú gach soicind
- Tuairiscí an tsiopa aistrithe
Maoláin:
- Aistriúcháin fógartha gutha seasta neamhbhailí
Forbróir:
- ceadanna Android12 (BLUETOOTH_SCAN)
- ceadanna Android12 (ACCESS_COARSE_LOCATION)
- Ag dearbhú do Trackpoints (tástáil)
- Aistriúcháin neamhúsáidte bainte
@@ -0,0 +1,8 @@
v3.19.1: OpenTracks
Athruithe:
- Aistriúcháin nuashonraithe.
Ceartúchán Bug:
- Easpórtáil Gach Rianta fabht seasta.
- Lean bug Track socraithe.
@@ -0,0 +1,7 @@
v3.20.0: OpenTracks
Athruithe:
- Agus tú ag taifeadadh: tá UI staitisticí inchumraithe anois
Ceartúchán Bug:
- Níor tugadh aird ar athrú ainm/catagóir an traic le linn an taifeadta (a tugadh isteach i v3.19.0)
@@ -0,0 +1,6 @@
v3.20.1: OpenTracks
Athruithe:
- Is féidir fógraí a úsáid anois de réir achair agus ama
- Bíp fógraí mura bhfuil aon bhogearraí Téacs-go-hurlabhra suiteáilte
- Baineadh minicíocht oiriúnaitheach suíomh GPS
@@ -0,0 +1,11 @@
v3.22.0: OpenTracks
Athruithe:
- Ní stórálfar láithreacha ach amháin má thaistilítear > an t-eatramh faid a thaifeadtar
(feidhmiúlacht fágtha bainte den bhrath díomhaoin/neamhdhíomhaoin)
- Is féidir Chomhéadain a chumrú chun clog a thaispeáint
- Is féidir luas díomhaoin a chumrú anois
Ceartúchán Bug:
- Eatraimh: tuairteála nuair a stóráiltear gnóthachan ach ní caillteanas
- Agus tú ag taifeadadh: seachain tuairteáil agus fógraí gutha á ndíchumasú
@@ -0,0 +1,10 @@
v3.23.0: OpenTracks
Athruithe:
- Cuireadh próifílí Chomhéadain leis
- Staitisticí comhiomlánaithe le haghaidh rianta roghnaithe
- TrackPoints díomhaoin/neamhdhomhaoin athfhorfheidhmithe de réir mar a stop an t-am bogtha ag obair (tugtha isteach in v3.22.0)
Ceartúcháin fhabht:
- Braiteoirí BLE: sonraí soiléire maidir le dínascadh
- Ní mór an t-easpórtáil uathoibríoch a cheadú go fóill
@@ -0,0 +1,11 @@
v3.25.0: OpenTracks
Athruithe:
- Tá GPS roghnach anois lena n-áirítear. Rabhadh GPS díchumasaithe
- PublicAPI: is féidir le feidhmchláir eile sonraí taifeadta a fháil anois má thosaíonn siad ar thaifeadadh (curtha i bhfeidhm do Gadgetbridge)
- Gníomhaíochtaí nua: snámh, snámh oscailte, agus workout
- Baineadh feidhmiúlacht pic sciar
Ceartúchán Bug:
- Chomhéadain: luas / airde nuair a léim idir 0.0 agus an luach iarbhír
- Is féidir rianta le emojis a onnmhairiú
@@ -0,0 +1,5 @@
v3.25.1: OpenTracks
Ceartúchán Bug:
- Easpórtáil uathoibríoch: tá súil againn teipeanna ciúine a chosc
- Easpórtáil uathoibríoch: cosc tuairteála
@@ -0,0 +1,8 @@
v3.26.0: OpenTracks
Athruithe:
- Is féidir fógraí gutha a chumrú
- Fógraí gutha faoi ráta croí
Ceartúchán Bug:
- Tuairteála mar gheall ar riocht cine
@@ -0,0 +1,4 @@
v3.26.1: OpenTracks
Ceartúchán Bug:
- Níor sháraigh barra gníomhaíochta comhthéacsúla an barra uirlisí i dtéama na hoíche.
@@ -0,0 +1,13 @@
v4.0.1: OpenTracks
Athruithe:
- Is féidir fógraí gutha a chumrú anois
- Folaíonn mód lánscáileáin anois barra nascleanúna Android freisin
- Ba cheart go mbeadh nasc le braiteoirí BLE níos tapúla anois
- Ceadaigh socruithe a athshocrú agus tú ag taifeadadh
- Ceadaigh socruithe iompórtála/easpórtála a athrú agus tú ag taifeadadh
- KML/KMZ: ná húsáid deilbhíní le luchtú ó Google
Ceartúchán Bug:
- Oibríonn TrackStoppedActivity ar scáileáin níos lú anois
- Iarratas ar cheadanna ar an rianliosta
@@ -0,0 +1,10 @@
v4. 0. 2: OpenTracks
Athruithe:
- - Agus muirmhíle
- Is féidir socruithe a athshocrú agus taifeadadh á dhéanamh
Ceartúcháin:
- Fógra gutha slánú
- Cairt: bhí ingearchló smoothed weirdly
- Ní raibh easpórtáil ag seiceáil an bhfuil an comhad ann
@@ -0,0 +1,8 @@
v4.0. 3: OpenTracks
Athruithe:
Sonraíocht: Max. luas mar a thuairiscigh braiteoir (GPS nó BLE)
Ceartúcháin:
- Cosc ar thuairteáil mura bhfuil Painéal Na Nionstraimí OSM suiteáilte
- Easpórtáil rian GPX: ná heaspórtáil deighleáin an amhráin folamh
@@ -0,0 +1,13 @@
v4. 0. 4: OpenTracks
Athruithe:
- Stóráil sonraí braiteoir BLE fiú gan achar (gach 10s)
- PUBLIC API: ceadaíonn leagan ainm, catagóir, agus cur síos
Ceartúcháin:
- Tuairteála nuair a ghlacadh pictiúr agus rothlach an fón
-
Forbróir:
Sprioc-SDK 33
- Baraiméadar úsáideann eatramh de 5s
@@ -0,0 +1,7 @@
v4. 0. 5: OpenTracks
Athruithe:
- - PlayStore release faigh lógó eile
Ceartúcháin:
- Taispeánann liosta eatramh " - " mura bhfuil gnóthachan/caillteanas i láthair
@@ -0,0 +1,5 @@
v4.0. 6: OpenTracks
Ceartúcháin:
- Egm2008 fhritháireamh mícheart ríomh le haghaidh domhanfhad diúltach
- - Crashed API poiblí má Bhí Rianta Oscailte sa chúlra (26 AIBREÁN+)
@@ -0,0 +1,10 @@
v4. 0. 7: OpenTracks
Athruithe:
- Gps comhartha caillte teorainn ama: 30s
- - Android 13: cead a iarraidh chun fógraí a thaispeáint
- Scrios rianta níos tapúla (bunachar sonraí folúis amháin más gá)
Ceartúcháin:
- PublicAPI stop ordú mar thoradh ar tuairteála (a tugadh isteach i v4. 0. 6)
- Ceadanna: ceadanna a iarraidh le linn tosaithe
@@ -0,0 +1,11 @@
v4. 0. 8: OpenTracks
Athruithe:
- Airde a thaispeántar i gcónaí i m/ft
- Taispeáin crios ama an riain mura bhfuil sé éagsúil le crios ama na feiste
- PublicAPI: tacaíocht deilbhín an riain a shocrú go sainráite
Forbróir:
- A Tugadh Isteach Cianfhormáideoir
- Uasghrádú GO AGP 7.3/desugar Ar Feadh Ré.aicme
- Android12+: déan splancscáileán trí Androidx
@@ -0,0 +1,8 @@
v4. 1. 0: OpenTracks
Athruithe:
- Easpórtáil leagan KML/KMZ 2. 3 amháin (2. 2)
- Le haghaidh crank-bhunaithe powermeters: faigh cadence
Ceartúcháin:
- Workaround do tuairteála ar "iompórtáil gach rud"
@@ -0,0 +1,7 @@
v4.1. 1: OpenTracks
Athruithe:
- Cuir scáileán réamhrá leis (taispeáin uair amháin)
Ceartúcháin:
- Ná tuairteála mura bhfuil gléas GPS i láthair
@@ -0,0 +1,22 @@
Cairde rianaithe spóirt a bhfuil meas aige ar do phríobháideachas.
Tugann gníomhaíochtaí spóirt agus lasmuigh sonas, scil agus muinín.
Cuir luach ar do shláinte trí súil a choinneáil ar d'oiliúint.
Taifeadann tú de réir mar a théann tú ag rith nó ag siúl, agus tugann sé ríomhaire rothar duit le scáileán níos mó le haghaidh rothaíochta.
Marcáil láithreacha suimiúla ar do bhealach le pictiúir.
Coinnigh staitisticí taifeadta go mion go leor le haghaidh anailíse.
Comhroinn ach na sonraí a theastaíonn uait a bheith ag daoine eile.
* Fógraí gutha.
* Tacaíonn braiteoirí Bluetooth LE: ráta croí, luas agus fad (rothaíocht), deireadh (rothaíocht), agus méadar cumhachta (rothaíocht).
* Gnóthachan agus caillteanas airde: trí bhraiteoir barometric.
* Airde léirithe in EGM2008 (os cionn meánleibhéal na farraige); onnmhairiú mar WGS84.
* Easpórtáil sonraí mar rianta mar KMZ (grianghraif san áireamh), KML, nó GPX.
* Gan rochtain ar an Idirlíon nó ceadanna breise.
* Téama dorcha agus éadrom, ag urramú socruithe an chórais.
* Gan fógraí.
Bogearraí Libre / Bogearraí Saor in Aisce / Foinse Oscailte
rud a chiallaíonn gur féidir leat an cód foinse a úsáid, a staidéar, a athrú agus a roinnt.
Ceadúnaithe Apache 2.0
@@ -0,0 +1 @@
Cairde rianaithe spóirt a bhfuil meas aige ar do phríobháideachas.
+1
View File
@@ -0,0 +1 @@
OpenTracks
@@ -0,0 +1,16 @@
v3.18.6: OpenTracks
Thay đổi:
- Tính năng "Hiện trên bản đồ" thông qua KML thay vì KMZ
- Thông báo bằng giọng nói không bao gồm tên ứng dụng
- Trong lúc ghi: Tăng/ giảm độ cao được cập nhật mỗi giây
- Thông tin cửa hàng được cập nhật
Sửa lỗi:
- Sửa lỗi các bản dịch Thông báo giọng nói không hợp lệ
Phát triển:
- Quyền Android 12 (BLUETOOTH_SCAN)
- Quyền Android 12 (ACCESS_COARSE_LOCATION)
- Khẳng định cho TrackPoints (thử nghiệm)
- Loại bỏ các bản dịch thừa
@@ -1,11 +1,22 @@
OpenTracks là một môn thể thao theo dõi ứng dụng hoàn toàn tôn trọng sự riêng tư.
OpenTracks là một ứng dụng theo dõi thể theo tôn trọng sự riêng tư của bạn.
Tính năng:
* Thể theo dõi theo dõi thể thao và hoạt động ngoài trời
* Ảnh và đánh dấu: đánh dấu địa điểm thú vị trong khi theo dõi
* Độ cao được và mất: thông qua khí quyển cảm biến
* Độ cao thể hiện trong EGM2008 (trên mực nước biển); xuất khẩu như WGS84
* Tiếng nói thông báo
* Hỗ trợ Bluetooth LE cảm biến: tim, tốc độ và khoảng cách (đi xe đạp), nhịp (đi xe đạp), và đồng hồ điện (đi xe đạp)
* Xuất dữ liệu: xuất bài hát hoặc là BIỂU (bao gồm. ảnh) DÕI hay C
* Không có Internet: Internet là không được sử dụng
Thể thao và các hoạt động ngoài trời cho bạn niềm vui, kĩ năng và sự tự tin.
Quý trọng sức khỏe của bạn bằng cách theo dõi tập luyện của bạn.
Ghi lại quá trình khi bạn chạy bộ hoặc đi bộ, và cho bạn một màn hình xe đạp khi đạp xe.
Đánh dấu những địa điểm thú vị trong lúc bạn di chuyển bằng hình ảnh.
Giữ các thống kê chi tiết để phân tích kĩ càng.
Chỉ chia sẻ những dữ liệu bạn muốn chia sẻ.
* Thông báo bằng giọng nói.
* Hỗ trợ Bluetooth LE cảm biến: nhịp tim, tốc độ và khoảng cách (đi xe đạp), nhịp (đi xe đạp), và đồng hồ sức mạnh (đi xe đạp)
* Tăng hoặc giảm độ cao: thông qua cảm biến khí áp.
* Độ cao được hiển thị dưới EGM2009 (trên nghĩa là trên mực nước biển), KML hoặc GPX.
* Xuất dữ liệu được lại dưới KMZ (gồm hình ảnh), KML hoặc GPX.
* Không cần Internet hoặc các quyền không cần thiết.
* Chế độ tối hoặc sáng, theo cài đặt hệ thống.
* Không quảng cáo.
Phần mềm Libre / Free Software / Mã nguồn mở
có nghĩa là bạn có thể dùng, học tập, thay đổi và chia sẻ mã nguồn.
Licensed Apache 2.0
@@ -1 +1 @@
Một môn thể thao theo dõi mà hoàn toàn tôn trọng sự riêng tư.
Một người bạn theo dõi mà tôn trọng sự riêng tư của bạn.
@@ -1,11 +1,22 @@
OpenTracks是一个完全尊重你的隐私的运动跟踪应用程序。
一款尊重您的隐私的运动追踪应用。
健康幸福的生活总少不了各种运动和户外活动。
使用本应用,您可以保持对您的训练数据的有效追踪,为您的健康助力。
本应用可以在您跑步或散步时记录数据,也可以在您骑车时作为一个大屏幕自行车电脑。
您可以为路上的有趣位置创建标记,还可以用图片作为标记。
您的数据会被详尽地记录下来,供您查阅分析。
您的数据只会分享给您想要分享给的人。
特点。
* 运动追踪:追踪你的运动和户外活动
* 照片和标记:在跟踪时标记有趣的地点
* 海拔增减:通过气压传感器
* 高度显示为EGM2008(平均海平面以上);导出为WGS84
* 语音播报
* 支持蓝牙LE传感器:心率、速度和距离(骑车)、节奏(骑车)和功率计(骑车)。
* 导出数据:以KMZ(包括照片)、KML或GPX形式导出轨迹
* 没有互联网接入。不使用互联网
* 支持蓝牙LE传感器:心率、速度和距离(骑车)、踏频(骑车)和功率计(骑车)。
* 海拔增减:通过气压传感器。
* EGM2008 海拔显示(平均海平面以上);导出为 WGS84。
* 导出路线数据,支持 KMZ(包括照片)、KML 和 GPX 格式。
* 不需要联网,不需要额外权限。
* 深色和浅色主题,跟随系统设置。
* 没有广告。
本应用是开源软件
意味着您可以使用、学习、修改和分发本应用的源代码。
本应用使用 Apache 2.0 协议发布。
@@ -1 +1 @@
一个完全尊重你隐私的运动追踪器。
一款尊重您的隐私的运动追踪应用。
+2 -1
View File
@@ -1 +1,2 @@
android.useAndroidX=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=256m
@@ -28,14 +28,14 @@ public class KmlTrackExporterTest {
@Test
public void writeCloseSegment_only_write_sensordata_if_present() {
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
"<gx:coord/>\n" +
"<coord/>\n" +
"<when>1970-01-01T01:00:00+01:00</when>\n" +
"<gx:coord/>\n" +
"<coord/>\n" +
"<ExtendedData>\n" +
"<SchemaData schemaUrl=\"#schema\">\n" +
"</SchemaData>\n" +
"</ExtendedData>\n" +
"</gx:Track>\n";
"</Track>\n";
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
@@ -12,19 +12,16 @@ import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -61,7 +58,8 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
@@ -72,6 +70,9 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p>
* Note: those tests are affected by {@link de.dennisguse.opentracks.sensors.sensorData.SensorData}.isRecent().
* If the test device is too slow (like in a CI) these are likely to fail as the sensor data will be omitted from actual.
*/
@RunWith(AndroidJUnit4.class)
public class ExportImportTest {
@@ -81,9 +82,6 @@ public class ExportImportTest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
//For csv_export_only() as we the timezone is hardcoded in the expectation.
@Rule
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
@@ -156,18 +154,19 @@ public class ExportImportTest {
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:14Z");
mockBLESensorData(trackPointCreator, 15f, null, 66f, 3f, 50f);
mockBLESensorData(trackPointCreator, 15f, null, 67f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:15Z");
mockBLESensorData(trackPointCreator, null, null, 66f, 3f, 50f);
mockBLESensorData(trackPointCreator, null, null, 68f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:16Z");
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f); // Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
BluetoothRemoteSensorManager mockRemoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class);
trackPointCreator.setRemoteSensorManager(mockRemoteSensorManager);
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -180,7 +179,7 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:03:24Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
trackPointCreator.setRemoteSensorManager(mockRemoteSensorManager);
service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId);
@@ -358,7 +357,7 @@ public class ExportImportTest {
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setSensorDistance(Distance.of(14))
.setHeartRate(66f)
.setHeartRate(69)
.setPower(50f)
.setCadence(3f)
.setHorizontalAccuracy(Distance.of(10)),
@@ -446,7 +445,6 @@ public class ExportImportTest {
assertNull(trackImported);
}
@Ignore(value = "TODO Fails on CI; works on API24 and API30 locally")
@LargeTest
@Test
public void csv_export_only() throws TimeoutException, IOException {
@@ -497,32 +495,33 @@ public class ExportImportTest {
}
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) {
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator) {
@Override
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate)));
BluetoothRemoteSensorManager remoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class);
Mockito.when(remoteSensorManager.fill(Mockito.any())).thenAnswer(invocation -> {
TrackPoint trackPoint = invocation.getArgument(0);
SensorDataCycling.CyclingCadence cyclingCadence = Mockito.mock(SensorDataCycling.CyclingCadence.class);
Mockito.when(cyclingCadence.hasValue()).thenReturn(true);
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
sensorDataSet.set(cyclingCadence);
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate)));
if (distance != null && speed != null) {
SensorDataCycling.DistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCycling.DistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCycling.DistanceSpeed distanceSpeed = Mockito.mock(SensorDataCycling.DistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed);
}
SensorDataCyclingCadence cyclingCadence = Mockito.mock(SensorDataCyclingCadence.class);
Mockito.when(cyclingCadence.hasValue()).thenReturn(true);
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
sensorDataSet.set(cyclingCadence);
sensorDataSet.fillTrackPoint(trackPoint);
return sensorDataSet;
if (distance != null && speed != null) {
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed);
}
sensorDataSet.fillTrackPoint(trackPoint);
return sensorDataSet;
});
trackPointCreator.setRemoteSensorManager(remoteSensorManager);
trackPointCreator.onChange(new SensorDataSet());
}
@@ -9,8 +9,10 @@ import org.junit.Test;
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.Speed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
public class BluetoothUtilsTest {
@@ -18,36 +20,36 @@ public class BluetoothUtilsTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.getServiceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
// then
assertEquals(60, heartRate);
assertEquals(HeartRate.of(60), heartRate);
}
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.getServiceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
// then
assertEquals(257, heartRate);
assertEquals(HeartRate.of(257), heartRate);
}
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertNull(sensor.getDistanceSpeed());
@@ -56,11 +58,11 @@ public class BluetoothUtilsTest {
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(65535 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
@@ -69,11 +71,11 @@ public class BluetoothUtilsTest {
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(200 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
@@ -82,19 +84,34 @@ public class BluetoothUtilsTest {
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.getServiceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
int power_w = BluetoothUtils.parseCyclingPower(characteristic);
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
// then
assertEquals(40, power_w);
assertEquals(40, powerCadence.getPower().getValue().getW(), 0.01);
}
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.getServiceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
// then
assertEquals(0, powerCadence.getPower().getValue().getW(), 0.01);
assertEquals(12, powerCadence.getCadence().getCrankRevolutionsCount());
assertEquals(17125, powerCadence.getCadence().getCrankRevolutionsTime());
}
@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID, 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_SPEED_CADENCE.getServiceUUID(), 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
// when
@@ -21,8 +21,8 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_1() {
// given
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s
// when
current.compute(previous);
@@ -34,8 +34,8 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_2() {
// given
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 6184);
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 8016);
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 6184);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 8016);
// when
current.compute(previous);
@@ -47,8 +47,8 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_sameCount() {
// given
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024);
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 2048);
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 2048);
// when
current.compute(previous);
@@ -61,8 +61,8 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_sameTime() {
// given
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024);
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 1024);
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 1024);
// when
current.compute(previous);
@@ -74,8 +74,8 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_rollOverTime() {
// given
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 0);
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 0);
// when
current.compute(previous);
@@ -89,8 +89,8 @@ public class SensorDataCyclingTest {
@Deprecated
public void compute_cadence_rollOverCount() {
// given
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous);
@@ -102,8 +102,8 @@ public class SensorDataCyclingTest {
@Test
public void compute_cadence_overflow() {
// given
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous);
@@ -115,8 +115,8 @@ public class SensorDataCyclingTest {
@Test
public void compute_speed() {
// given
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 1, 6184);
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 1, 6184);
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 2, 8016);
// when
current.compute(previous, Distance.ofMM(2150));
@@ -131,8 +131,8 @@ public class SensorDataCyclingTest {
@Deprecated
public void compute_speed_rollOverCount() {
// given
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous, Distance.ofMM(2000));
@@ -145,8 +145,8 @@ public class SensorDataCyclingTest {
@Test
public void compute_speed_overflow() {
// given
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous, Distance.ofMM(2000));
@@ -158,8 +158,8 @@ public class SensorDataCyclingTest {
@Test
public void equals_speed_with_no_data() {
// given
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress");
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress");
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
@@ -172,8 +172,8 @@ public class SensorDataCyclingTest {
@Test
public void equals_cadence_with_no_data() {
// given
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress");
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress");
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
@@ -29,7 +29,6 @@ import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
@@ -57,9 +56,6 @@ public class TrackRecordingServiceTestMarker {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@@ -128,7 +124,6 @@ public class TrackRecordingServiceTestMarker {
public void recording_GPSfix_createsMarker() {
// given
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.stopGPS();
trackPointCreator.setClock("2020-02-02T02:02:02Z");
Track.Id trackId = service.startNewTrack();
@@ -11,7 +11,6 @@ import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
@@ -53,9 +52,6 @@ public class TrackRecordingServiceTestRecording {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@@ -95,7 +91,6 @@ public class TrackRecordingServiceTestRecording {
PreferencesUtils.setString(R.string.idle_speed_key, R.string.idle_speed_default);
service = startService();
service.getTrackPointCreator().stopGPS();
}
@After
@@ -117,7 +112,6 @@ public class TrackRecordingServiceTestRecording {
String startTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// then
@@ -155,7 +149,6 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
@@ -202,7 +195,6 @@ public class TrackRecordingServiceTestRecording {
String starTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(starTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
String pauseTime = "2020-02-02T02:02:03Z";
@@ -235,7 +227,6 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
String stopTime = "2020-02-02T02:02:03Z";
@@ -246,7 +237,6 @@ public class TrackRecordingServiceTestRecording {
String resumeTime = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(resumeTime);
service.resumeTrack(trackId);
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// then
@@ -268,7 +258,6 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager();
@@ -195,7 +195,7 @@ public class TrackRecordingServiceTestStateMachine {
@MediumTest
@Test
public void cannotResume_non_existing_track() throws InterruptedException {
public void cannotResume_non_existing_track() {
// given
assertFalse(service.isRecording());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -6,6 +6,8 @@ import android.location.Location;
import androidx.test.rule.ServiceTestRule;
import org.mockito.Mockito;
import java.time.Clock;
import java.util.concurrent.TimeoutException;
@@ -27,7 +29,8 @@ public class TrackRecordingServiceTestUtils {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, service.getTrackPointCreator()));
BluetoothRemoteSensorManager remoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class);
service.getTrackPointCreator().setRemoteSensorManager(remoteSensorManager);
service.getTrackPointCreator().setClock(Clock.systemUTC());
service.endCurrentTrack();
}
@@ -8,7 +8,6 @@ import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.AfterClass;
@@ -31,9 +30,6 @@ public class VoiceAnnouncementManagerTest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
@@ -64,7 +64,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
assertEquals("Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Average moving speed 18.4 kilometers per hour.", announcement);
}
@Test
@@ -80,7 +80,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 1 second. Speed 20.0 kilometers per hour.", announcement);
assertEquals("Total distance 20.0 kilometers. 1 hour 1 second. Average moving speed 20.0 kilometers per hour.", announcement);
}
@Test
@@ -96,13 +96,13 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
assertEquals("Total distance 20.0 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_rounding_check_two() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(19990));
stats.setTotalDistance(Distance.of(19900));
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
stats.setMaxSpeed(Speed.of(100));
@@ -112,7 +112,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 19.99 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
assertEquals("Total distance 19.9 kilometers. 1 hour. Average moving speed 19.9 kilometers per hour.", announcement);
}
@Test
@@ -134,7 +134,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
assertEquals("Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
}
@Test
@@ -150,7 +150,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, null, null).toString();
// then
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement);
assertEquals("Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement);
}
@Test
@@ -172,7 +172,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, lastInterval, null).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
assertEquals("Total distance 14.2 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
}
@Test
@@ -188,7 +188,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, null, null).toString();
// then
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Speed 11.4 miles per hour.", announcement);
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
}
@Test
@@ -210,7 +210,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, lastInterval, null).toString();
// then
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
assertEquals("Total distance 8.8 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
}
@Test
@@ -226,7 +226,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, null, null).toString();
// then
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
}
@Test
@@ -248,7 +248,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, lastInterval, null).toString();
// then
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
assertEquals("Total distance 8.8 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
}
@Test
@@ -274,7 +274,7 @@ public class VoiceAnnouncementUtilsTest {
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
// then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
assertEquals("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. Current heart rate 133 bpm.", announcement);
}
@Test
@@ -6,6 +6,7 @@ import static org.mockito.Mockito.verify;
import android.content.Context;
import android.location.Location;
import android.os.Handler;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
@@ -49,7 +50,7 @@ public class GPSHandlerTest {
Mockito.when(trackPointCreator.createNow())
.thenReturn(Instant.now());
locationHandler.onStart(context);
locationHandler.onStart(context, new Handler());
}
/**
@@ -17,7 +17,8 @@ import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayoutIO;
@RunWith(AndroidJUnit4.class)
public class PreferencesOpenHelperTest {
@@ -38,10 +39,10 @@ public class PreferencesOpenHelperTest {
PreferencesOpenHelper.newInstance(1).check();
// then there should be one layout with old custom layout that has the new CSV value.
List<Layout> layouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(layouts);
assertEquals(layouts.size(), 1);
assertEquals(layouts.get(0).toCsv(), PreferencesUtils.getCustomLayout().toCsv());
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(recordingLayouts);
assertEquals(recordingLayouts.size(), 1);
assertEquals(recordingLayouts.get(0).toCsv(), PreferencesUtils.getCustomLayout().toCsv());
}
@Test
@@ -63,10 +64,10 @@ public class PreferencesOpenHelperTest {
+ PreferencesUtils.getLayoutColumnsByDefault() + ";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<Layout> layouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(layouts);
assertEquals(layouts.size(), 1);
assertEquals(layouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(recordingLayouts);
assertEquals(recordingLayouts.size(), 1);
assertEquals(recordingLayouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
}
@Test
@@ -87,10 +88,10 @@ public class PreferencesOpenHelperTest {
String updatedOldCustomLayoutCsv = "whatever;3;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<Layout> layouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(layouts);
assertEquals(layouts.size(), 1);
assertEquals(layouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(recordingLayouts);
assertEquals(recordingLayouts.size(), 1);
assertEquals(recordingLayouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
}
@Test
@@ -109,10 +110,10 @@ public class PreferencesOpenHelperTest {
PreferencesOpenHelper.newInstance(1).check();
// then custom layout should be equals to default layout.
Layout defaultLayout = Layout.fromCsv(PreferencesUtils.buildDefaultLayout(), resources);
List<Layout> customLayout = PreferencesUtils.getAllCustomLayouts();
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsv(PreferencesUtils.buildDefaultLayout(), resources);
List<RecordingLayout> customRecordingLayout = PreferencesUtils.getAllCustomLayouts();
assertEquals(customLayout.size(), 1);
assertEquals(defaultLayout.toCsv(), customLayout.get(0).toCsv());
assertEquals(customRecordingLayout.size(), 1);
assertEquals(defaultRecordingLayout.toCsv(), customRecordingLayout.get(0).toCsv());
}
}
@@ -22,7 +22,8 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayoutIO;
@RunWith(AndroidJUnit4.class)
public class PreferencesUtilsTest {
@@ -85,13 +86,13 @@ public class PreferencesUtilsTest {
editor.commit();
// when
List<Layout> layouts = PreferencesUtils.getAllCustomLayouts();
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
// then
assertEquals(layouts.size(), 1);
assertTrue(layouts.get(0).getFields().size() > 0);
assertEquals(layouts.get(0).getName(), context.getString(R.string.stats_custom_layout_default_layout));
assertTrue(layouts.get(0).getFields().stream().anyMatch(DataField::isVisible));
assertEquals(recordingLayouts.size(), 1);
assertTrue(recordingLayouts.get(0).getFields().size() > 0);
assertEquals(recordingLayouts.get(0).getName(), context.getString(R.string.stats_custom_layout_default_layout));
assertTrue(recordingLayouts.get(0).getFields().stream().anyMatch(DataField::isVisible));
}
@Test
@@ -103,12 +104,12 @@ public class PreferencesUtilsTest {
editor.commit();
// when
Layout layout = PreferencesUtils.getCustomLayout();
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
// then
assertTrue(layout.getFields().size() > 0);
assertEquals(layout.getName(), context.getString(R.string.stats_custom_layout_default_layout));
assertTrue(layout.getFields().stream().anyMatch(DataField::isVisible));
assertTrue(recordingLayout.getFields().size() > 0);
assertEquals(recordingLayout.getName(), context.getString(R.string.stats_custom_layout_default_layout));
assertTrue(recordingLayout.getFields().stream().anyMatch(DataField::isVisible));
}
@Test
@@ -126,28 +127,28 @@ public class PreferencesUtilsTest {
editor.apply();
// when
Layout layout = PreferencesUtils.getCustomLayout();
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
// then
assertEquals(layout.getFields().size(), 4);
assertEquals(layout.getName(), "run");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(recordingLayout.getFields().size(), 4);
assertEquals(recordingLayout.getName(), "run");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(layout.getFields().get(0).isVisible());
assertTrue(layout.getFields().get(0).isPrimary());
assertEquals(recordingLayout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(recordingLayout.getFields().get(0).isVisible());
assertTrue(recordingLayout.getFields().get(0).isPrimary());
assertEquals(layout.getFields().get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertTrue(layout.getFields().get(1).isVisible());
assertFalse(layout.getFields().get(1).isPrimary());
assertEquals(recordingLayout.getFields().get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertTrue(recordingLayout.getFields().get(1).isVisible());
assertFalse(recordingLayout.getFields().get(1).isPrimary());
assertEquals(layout.getFields().get(2).getKey(), context.getString(R.string.stats_custom_layout_average_moving_speed_key));
assertFalse(layout.getFields().get(2).isVisible());
assertTrue(layout.getFields().get(2).isPrimary());
assertEquals(recordingLayout.getFields().get(2).getKey(), context.getString(R.string.stats_custom_layout_average_moving_speed_key));
assertFalse(recordingLayout.getFields().get(2).isVisible());
assertTrue(recordingLayout.getFields().get(2).isPrimary());
assertEquals(layout.getFields().get(3).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertFalse(layout.getFields().get(3).isVisible());
assertFalse(layout.getFields().get(3).isPrimary());
assertEquals(recordingLayout.getFields().get(3).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertFalse(recordingLayout.getFields().get(3).isVisible());
assertFalse(recordingLayout.getFields().get(3).isPrimary());
}
@Test
@@ -165,44 +166,44 @@ public class PreferencesUtilsTest {
editor.apply();
// when
Layout layout = PreferencesUtils.getCustomLayout();
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
// then
assertEquals(layout.getFields().size(), 4);
assertEquals(layout.getName(), "walking");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(recordingLayout.getFields().size(), 4);
assertEquals(recordingLayout.getName(), "walking");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(layout.getFields().get(0).isVisible());
assertTrue(layout.getFields().get(0).isPrimary());
assertEquals(recordingLayout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(recordingLayout.getFields().get(0).isVisible());
assertTrue(recordingLayout.getFields().get(0).isPrimary());
assertEquals(layout.getFields().get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertTrue(layout.getFields().get(1).isVisible());
assertFalse(layout.getFields().get(1).isPrimary());
assertEquals(recordingLayout.getFields().get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertTrue(recordingLayout.getFields().get(1).isVisible());
assertFalse(recordingLayout.getFields().get(1).isPrimary());
assertEquals(layout.getFields().get(2).getKey(), context.getString(R.string.stats_custom_layout_coordinates_key));
assertFalse(layout.getFields().get(2).isVisible());
assertTrue(layout.getFields().get(2).isPrimary());
assertTrue(layout.getFields().get(2).isWide());
assertEquals(recordingLayout.getFields().get(2).getKey(), context.getString(R.string.stats_custom_layout_coordinates_key));
assertFalse(recordingLayout.getFields().get(2).isVisible());
assertTrue(recordingLayout.getFields().get(2).isPrimary());
assertTrue(recordingLayout.getFields().get(2).isWide());
assertEquals(layout.getFields().get(3).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertFalse(layout.getFields().get(3).isVisible());
assertFalse(layout.getFields().get(3).isPrimary());
assertEquals(recordingLayout.getFields().get(3).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertFalse(recordingLayout.getFields().get(3).isVisible());
assertFalse(recordingLayout.getFields().get(3).isPrimary());
}
@Test
public void testSetCustomLayout() {
// given
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
Layout layoutSrc = new Layout("road cycling");
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false));
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, false, false));
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_average_moving_speed_key), context.getString(R.string.stats_average_moving_speed), false, true, false));
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), false, false, false));
RecordingLayout recordingLayoutSrc = new RecordingLayout("road cycling");
recordingLayoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_moving_time_key), true, true, false));
recordingLayoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_distance_key), true, false, false));
recordingLayoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_average_moving_speed_key), false, true, false));
recordingLayoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_speed_key), false, false, false));
// when
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.stats_custom_layouts_key), layoutSrc.toCsv());
editor.putString(context.getString(R.string.stats_custom_layouts_key), recordingLayoutSrc.toCsv());
editor.commit();
// then
@@ -215,13 +216,13 @@ public class PreferencesUtilsTest {
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1,0;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0,0;");
Layout layoutDst = PreferencesUtils.getCustomLayout();
assertEquals(layoutSrc.getName(), layoutDst.getName());
assertEquals(layoutSrc.getFields().size(), layoutDst.getFields().size());
for (int i = 0; i < layoutSrc.getFields().size(); i++) {
assertEquals(layoutSrc.getFields().get(i).getKey(), layoutDst.getFields().get(i).getKey());
assertEquals(layoutSrc.getFields().get(i).isVisible(), layoutDst.getFields().get(i).isVisible());
assertEquals(layoutSrc.getFields().get(i).isPrimary(), layoutDst.getFields().get(i).isPrimary());
RecordingLayout recordingLayoutDst = PreferencesUtils.getCustomLayout();
assertEquals(recordingLayoutSrc.getName(), recordingLayoutDst.getName());
assertEquals(recordingLayoutSrc.getFields().size(), recordingLayoutDst.getFields().size());
for (int i = 0; i < recordingLayoutSrc.getFields().size(); i++) {
assertEquals(recordingLayoutSrc.getFields().get(i).getKey(), recordingLayoutDst.getFields().get(i).getKey());
assertEquals(recordingLayoutSrc.getFields().get(i).isVisible(), recordingLayoutDst.getFields().get(i).isVisible());
assertEquals(recordingLayoutSrc.getFields().get(i).isPrimary(), recordingLayoutDst.getFields().get(i).isPrimary());
}
}
@@ -247,7 +248,7 @@ public class PreferencesUtilsTest {
editor.putString(context.getString(R.string.stats_custom_layouts_key), cyclingProfile + "\n" + runningProfile);
editor.apply();
List<Layout> layoutsBefore = PreferencesUtils.getAllCustomLayouts();
List<RecordingLayout> layoutsBefore = PreferencesUtils.getAllCustomLayouts();
// when cyling profile is updated
String cyclingProfileUpdated = "cycling;2;"
@@ -256,14 +257,14 @@ public class PreferencesUtilsTest {
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
List<Layout> layoutsToBeUpdated = new ArrayList<>();
layoutsToBeUpdated.add(Layout.fromCsv(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(Layout.fromCsv(runningProfile, resources));
List<RecordingLayout> layoutsToBeUpdated = new ArrayList<>();
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsv(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(RecordingLayoutIO.fromCsv(runningProfile, resources));
PreferencesUtils.updateCustomLayouts(layoutsToBeUpdated);
// then only updated profile is modified in the custom layouts
List<Layout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
List<RecordingLayout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
assertEquals(layoutsBefore.size(), 2);
assertEquals(layoutsAfter.size(), 2);
@@ -294,7 +295,7 @@ public class PreferencesUtilsTest {
editor.putString(context.getString(R.string.stats_custom_layouts_key), cyclingProfile + "\n" + runningProfile);
editor.apply();
List<Layout> layoutsBefore = PreferencesUtils.getAllCustomLayouts();
List<RecordingLayout> layoutsBefore = PreferencesUtils.getAllCustomLayouts();
// when cyling profile is updated
String cyclingProfileUpdated = "cycling;2;"
@@ -302,11 +303,11 @@ 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;";
Layout layoutToBeUpdated = Layout.fromCsv(cyclingProfileUpdated, resources);
PreferencesUtils.updateCustomLayout(layoutToBeUpdated);
RecordingLayout recordingLayoutToBeUpdated = RecordingLayoutIO.fromCsv(cyclingProfileUpdated, resources);
PreferencesUtils.updateCustomLayout(recordingLayoutToBeUpdated);
// then only updated profile is modified in the custom layouts
List<Layout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
List<RecordingLayout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
assertEquals(layoutsBefore.size(), 2);
assertEquals(layoutsAfter.size(), 2);
@@ -337,9 +338,9 @@ public class PreferencesUtilsTest {
editor.apply();
// when it gets the custom layout
Layout layout = PreferencesUtils.getCustomLayout();
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
// then the first one was returned
assertEquals(layout.getName(), "cycling");
assertEquals(recordingLayout.getName(), "cycling");
}
}
@@ -78,7 +78,7 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
data.trackPoints.forEach(it -> subject.addTrackPoint(it));
data.trackPoints.forEach(subject::addTrackPoint);
// then
TrackStatistics statistics = subject.getTrackStatistics();
@@ -12,7 +12,6 @@ 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;
@@ -72,7 +71,7 @@ public class EspressoAggregatedFilterTest {
ViewInteraction checkBox = onView(
allOf(withId(R.id.filter_dialog_check_button), withText(CATEGORY),
withParent(allOf(withId(R.id.filter_items),
withParent(IsInstanceOf.<View>instanceOf(android.view.ViewGroup.class)))),
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
isDisplayed()));
checkBox.perform(waitFor(2000));
@@ -10,7 +10,7 @@ import org.junit.runner.RunWith;
import java.util.List;
@RunWith(AndroidJUnit4.class)
public class CsvLayoutUtilsTest extends TestCase {
public class CsvRecordingLayoutUtilsTest extends TestCase {
@Test
public void testGetCsvLineParts() {
@@ -1,76 +0,0 @@
package de.dennisguse.opentracks.ui.customRecordingLayout;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.CoreMatchers.is;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.ViewAssertion;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.matcher.ViewMatchers;
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.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoCustomLayoutTest {
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@Test
public void customLayoutTest() {
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(ViewMatchers.withId(R.id.track_list_fab_action));
trackControllerRecordButton.perform(click());
// Get custom layout preferences and check all data fields are showed.
Layout layout = PreferencesUtils.getCustomLayout();
onView(withId(R.id.stats_recycler_view)).check(new RecyclerViewItemCountAssertion((int) layout.getFields().stream().filter(DataField::isVisible).count()));
// stop recording
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
trackControllerStopButton.perform(longClick());
}
private static class RecyclerViewItemCountAssertion implements ViewAssertion {
private final int expectedCount;
public RecyclerViewItemCountAssertion(int expectedCount) {
this.expectedCount = expectedCount;
}
@Override
public void check(View view, NoMatchingViewException noViewFoundException) {
if (noViewFoundException != null) {
throw noViewFoundException;
}
RecyclerView recyclerView = (RecyclerView) view;
RecyclerView.Adapter<?> adapter = recyclerView.getAdapter();
assertThat(adapter.getItemCount(), is(expectedCount));
}
}
}
@@ -17,7 +17,7 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(AndroidJUnit4.class)
public class LayoutTest extends TestCase {
public class RecordingLayoutTest extends TestCase {
private final Context context = ApplicationProvider.getApplicationContext();
private final Resources resources = ApplicationProvider.getApplicationContext().getResources();
@@ -28,21 +28,17 @@ public class LayoutTest 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
Layout layout = Layout.fromCsv(csv, resources);
List<DataField> dataFieldList = layout.getFields();
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), "running");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(recordingLayout.getName(), "running");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(dataFieldList.size(), 4);
assertEquals(dataFieldList.get(0).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertEquals(dataFieldList.get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertEquals(dataFieldList.get(2).getKey(), context.getString(R.string.stats_custom_layout_altitude_key));
assertEquals(dataFieldList.get(3).getKey(), context.getString(R.string.stats_custom_layout_gain_key));
assertEquals(dataFieldList.get(0).getTitle(), context.getString(R.string.stats_speed));
assertEquals(dataFieldList.get(1).getTitle(), context.getString(R.string.stats_distance));
assertEquals(dataFieldList.get(2).getTitle(), context.getString(R.string.stats_altitude));
assertEquals(dataFieldList.get(3).getTitle(), context.getString(R.string.stats_gain));
assertTrue(dataFieldList.get(0).isVisible());
assertTrue(dataFieldList.get(0).isPrimary());
assertFalse(dataFieldList.get(0).isWide());
@@ -64,13 +60,13 @@ public class LayoutTest extends TestCase {
String csv1 = "Layout Name;speed,1,1;distance,0,0;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv1, resources);
List<DataField> dataFieldList = layout.getFields();
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(layout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(layout.getFields().size(), 0);
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(recordingLayout.getFields().size(), 0);
}
@Test
@@ -79,13 +75,13 @@ public class LayoutTest extends TestCase {
String csv1 = "Layout Name;2;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv1, resources);
List<DataField> dataFieldList = layout.getFields();
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(layout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(layout.getFields().size(), 0);
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(recordingLayout.getFields().size(), 0);
}
@Test
@@ -94,13 +90,13 @@ public class LayoutTest extends TestCase {
String csv1 = "Layout Name;2;speed,distance,total time;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv1, resources);
List<DataField> dataFieldList = layout.getFields();
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), "Layout Name");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(layout.getFields().size(), 0);
assertEquals(recordingLayout.getName(), "Layout Name");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(recordingLayout.getFields().size(), 0);
}
@Test
@@ -109,27 +105,27 @@ public class LayoutTest extends TestCase {
String csv1 = "Layout Name;2;speed,1,0;distance;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv1, resources);
List<DataField> dataFieldList = layout.getFields();
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(csv1, resources);
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), "Layout Name");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(layout.getFields().size(), 1);
assertEquals(recordingLayout.getName(), "Layout Name");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(recordingLayout.getFields().size(), 1);
}
@Test
public void testToCsv() {
// given a layout's object
Layout layout = new Layout("Test Layout", 2);
layout.addField("key1", "Title 1", false, false, false);
layout.addField("key2", "Title 2", false, true, false);
layout.addField("key3", "Title 3", true, false, false);
layout.addField("key4", "Title 4", true, true, false);
layout.addField("key5", "Title 5", true, true, true);
RecordingLayout recordingLayout = new RecordingLayout("Test Layout", 2);
recordingLayout.addField(new DataField("key1", false, false, false));
recordingLayout.addField(new DataField("key2", false, true, false));
recordingLayout.addField(new DataField("key3", true, false, false));
recordingLayout.addField(new DataField("key4", true, true, false));
recordingLayout.addField(new DataField("key5", true, true, true));
// when converts it to CSV
String csv = layout.toCsv();
String csv = recordingLayout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
@@ -138,15 +134,15 @@ public class LayoutTest extends TestCase {
@Test
public void testToCsv_columnsByDefault() {
// given a layout's object
Layout layout = new Layout("Test Layout");
layout.addField("key1", "Title 1", false, false, false);
layout.addField("key2", "Title 2", false, true, false);
layout.addField("key3", "Title 3", true, false, false);
layout.addField("key4", "Title 4", true, true, false);
layout.addField("key5", "Title 5", true, true, true);
RecordingLayout recordingLayout = new RecordingLayout("Test Layout");
recordingLayout.addField(new DataField("key1", false, false, false));
recordingLayout.addField(new DataField("key2", false, true, false));
recordingLayout.addField(new DataField("key3", true, false, false));
recordingLayout.addField(new DataField("key4", true, true, false));
recordingLayout.addField(new DataField("key5", true, true, true));
// when converts it to CSV
String csv = layout.toCsv();
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;");
@@ -12,7 +12,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
@RunWith(AndroidJUnit4.class)
public class StatisticsUtilsTest extends TestCase {
@@ -44,16 +44,16 @@ public class StatisticsUtilsTest extends TestCase {
@Test
public void testFilterVisible() {
// given
Layout layout = new Layout("profile");
layout.addField(new DataField("key1", "title1", true, true, false));
layout.addField(new DataField("key2", "title2", false, true, false));
layout.addField(new DataField("key3", "title3", true, true, false));
layout.addField(new DataField("key4", "title4", false, true, false));
layout.addField(new DataField("key5", "title5", true, true, false));
RecordingLayout recordingLayout = new RecordingLayout("profile");
recordingLayout.addField(new DataField("key1", true, true, false));
recordingLayout.addField(new DataField("key2", false, true, false));
recordingLayout.addField(new DataField("key3", true, true, false));
recordingLayout.addField(new DataField("key4", false, true, false));
recordingLayout.addField(new DataField("key5", true, true, false));
// when
Layout resultTrue = StatisticsUtils.filterVisible(layout, true);
Layout resultFalse = StatisticsUtils.filterVisible(layout, false);
RecordingLayout resultTrue = StatisticsUtils.filterVisible(recordingLayout, true);
RecordingLayout resultFalse = StatisticsUtils.filterVisible(recordingLayout, false);
// then
assertEquals(resultTrue.getFields().size(), 3);
@@ -1,199 +0,0 @@
package de.dennisguse.opentracks.viewmodels;
import static org.mockito.Mockito.when;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.R;
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.Power;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
@RunWith(MockitoJUnitRunner.class)
public class StatisticDataBuilderTest extends TestCase {
private final Context context = ApplicationProvider.getApplicationContext();
@Mock
private Track trackMock;
@Mock
private TrackStatistics trackStatisticsMock;
@Mock
private RecordingData recordingDataMock;
@Mock
private SensorStatistics sensorStatisticsMock;
@Test
public void testFromRecordingData() {
when(trackMock.getTrackStatistics()).thenReturn(trackStatisticsMock);
when(trackStatisticsMock.getTotalTime()).thenReturn(Duration.ofMillis(0));
when(trackStatisticsMock.getMovingTime()).thenReturn(Duration.ofMillis(0));
when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
when(recordingDataMock.getTrackStatistics()).thenReturn(trackStatisticsMock);
// given
Layout layout = new Layout(context.getString(R.string.default_activity_default));
layout.addField(context.getString(R.string.stats_custom_layout_total_time_key), context.getString(R.string.stats_total_time), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), false, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), false, true, false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
// then
assertEquals(statisticDataList.size(), 3);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_total_time))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_moving_time))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_distance))));
}
@Test
public void testFromRecordingData_allFields() {
when(trackMock.getTrackStatistics()).thenReturn(trackStatisticsMock);
when(trackStatisticsMock.getTotalTime()).thenReturn(Duration.ofMillis(0));
when(trackStatisticsMock.getMovingTime()).thenReturn(Duration.ofMillis(0));
when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
when(recordingDataMock.getTrackStatistics()).thenReturn(trackStatisticsMock);
// given
Layout layout = new Layout(context.getString(R.string.default_activity_default));
layout.addField(context.getString(R.string.stats_custom_layout_total_time_key), context.getString(R.string.stats_total_time), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_average_moving_speed_key), context.getString(R.string.stats_average_moving_speed), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_average_speed_key), context.getString(R.string.stats_average_speed), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_pace_key), context.getString(R.string.stats_pace), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_average_moving_pace_key), context.getString(R.string.stats_average_moving_pace), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_average_pace_key), context.getString(R.string.stats_average_pace), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_fastest_pace_key), context.getString(R.string.stats_fastest_pace), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_altitude_key), context.getString(R.string.stats_altitude), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_gain_key), context.getString(R.string.stats_gain), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_loss_key), context.getString(R.string.stats_loss), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_coordinates_key), context.getString(R.string.stats_coordinates), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_heart_rate_key), context.getString(R.string.stats_sensors_heart_rate), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_cadence_key), context.getString(R.string.stats_sensors_cadence), true, true, false);
layout.addField(context.getString(R.string.stats_custom_layout_power_key), context.getString(R.string.stats_sensors_power), true, true, false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
// then
assertEquals(statisticDataList.size(), 18);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_total_time))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_moving_time))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_distance))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_speed))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_moving_speed))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_speed))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_max_speed))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_pace))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_moving_pace))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_pace))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_fastest_pace))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_altitude))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_gain))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_loss))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_coordinates))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_heart_rate))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_cadence))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_power))));
}
@Test
public void testFromSensorStatistics_onlyHeartRate() {
when(sensorStatisticsMock.hasHeartRate()).thenReturn(true);
when(sensorStatisticsMock.getMaxHeartRate()).thenReturn(HeartRate.of(200f));
when(sensorStatisticsMock.getAvgHeartRate()).thenReturn(HeartRate.of(150f));
when(sensorStatisticsMock.hasCadence()).thenReturn(false);
when(sensorStatisticsMock.hasPower()).thenReturn(false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock);
// then
assertEquals(statisticDataList.size(), 2);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_max))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_avg))));
}
@Test
public void testFromSensorStatistics_onlyCadence() {
when(sensorStatisticsMock.hasHeartRate()).thenReturn(false);
when(sensorStatisticsMock.hasCadence()).thenReturn(true);
when(sensorStatisticsMock.getAvgCadence()).thenReturn(Cadence.of(90f));
when(sensorStatisticsMock.getMaxCadence()).thenReturn(Cadence.of(110f));
when(sensorStatisticsMock.hasPower()).thenReturn(false);
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock);
// then
assertEquals(statisticDataList.size(), 2);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_max))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_avg))));
}
@Test
public void testFromSensorStatistics_onlyPower() {
when(sensorStatisticsMock.hasHeartRate()).thenReturn(false);
when(sensorStatisticsMock.hasCadence()).thenReturn(false);
when(sensorStatisticsMock.hasPower()).thenReturn(true);
when(sensorStatisticsMock.getAvgPower()).thenReturn(Power.of(300f));
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_power_avg))));
// then
assertEquals(statisticDataList.size(), 1);
}
@Test
public void testFromSensorStatistics() {
when(sensorStatisticsMock.hasHeartRate()).thenReturn(true);
when(sensorStatisticsMock.getMaxHeartRate()).thenReturn(HeartRate.of(200f));
when(sensorStatisticsMock.getAvgHeartRate()).thenReturn(HeartRate.of(150f));
when(sensorStatisticsMock.hasCadence()).thenReturn(true);
when(sensorStatisticsMock.getAvgCadence()).thenReturn(Cadence.of(90f));
when(sensorStatisticsMock.getMaxCadence()).thenReturn(Cadence.of(110f));
when(sensorStatisticsMock.hasPower()).thenReturn(true);
when(sensorStatisticsMock.getAvgPower()).thenReturn(Power.of(300f));
// when
List<StatisticData> statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock);
// then
assertEquals(statisticDataList.size(), 5);
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_max))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_avg))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_max))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_avg))));
assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_power_avg))));
}
}
+10 -8
View File
@@ -1,11 +1,13 @@
#time,trackpoint_type,latitude,longitude,altitude,accuracy_horizontal,accuracy_vertical,speed,altitude_gain,altitude_loss,sensor_distance,heartrate,cadence,power
"2020-02-02T03:02:02+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
"2020-02-02T03:02:03+01:00","TRACKPOINT",3,3,10,10,,54,1,1,,,,
"2020-02-02T03:02:03+01:00","TRACKPOINT",3,14,10,10,,54,1,1,,,,
"2020-02-02T03:02:04+01:00","SENSORPOINT",,,,,,54,1,1,10,66,3,50
"2020-02-02T03:02:05+01:00","TRACKPOINT",3,3,10,10,,18,0,0,2,66,3,50
"2020-02-02T03:02:06+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
"2020-02-02T03:02:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
"2020-02-02T03:02:21+01:00","TRACKPOINT",3,3,10,10,,54,0,0,,,,
"2020-02-02T03:02:22+01:00","SEGMENT_START_AUTOMATIC",3,3,10,10,,54,0,0,,,,
"2020-02-02T03:02:23+01:00","TRACKPOINT",3,3,10,10,,54,0,0,,,,
"2020-02-02T03:02:24+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
"2020-02-02T03:02:15+01:00","SENSORPOINT",,,,,,,,,,68,3,50
"2020-02-02T03:02:16+01:00","SENSORPOINT",,,,,,18,,,2,69,3,50
"2020-02-02T03:02:17+01:00","TRACKPOINT",3,14.001,10,10,,18,0,0,2,69,3,50
"2020-02-02T03:02:18+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
"2020-02-02T03:03:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
"2020-02-02T03:03:21+01:00","TRACKPOINT",3,14.002,10,10,,54,0,0,,,,
"2020-02-02T03:03:22+01:00","SEGMENT_START_AUTOMATIC",3,16,10,10,,54,0,0,,,,
"2020-02-02T03:03:23+01:00","TRACKPOINT",3,16.001,10,10,,54,0,0,,,,
"2020-02-02T03:03:24+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
1 #time trackpoint_type latitude longitude altitude accuracy_horizontal accuracy_vertical speed altitude_gain altitude_loss sensor_distance heartrate cadence power
2 2020-02-02T03:02:02+01:00 SEGMENT_START_MANUAL
3 2020-02-02T03:02:03+01:00 TRACKPOINT 3 3 14 10 10 54 1 1
4 2020-02-02T03:02:04+01:00 SENSORPOINT 54 1 1 10 66 3 50
5 2020-02-02T03:02:05+01:00 2020-02-02T03:02:15+01:00 TRACKPOINT SENSORPOINT 3 3 10 10 18 0 0 2 66 68 3 50
6 2020-02-02T03:02:06+01:00 2020-02-02T03:02:16+01:00 SEGMENT_END_MANUAL SENSORPOINT 18 2 69 3 50
7 2020-02-02T03:02:20+01:00 2020-02-02T03:02:17+01:00 SEGMENT_START_MANUAL TRACKPOINT 3 14.001 10 10 18 0 0 2 69 3 50
8 2020-02-02T03:02:21+01:00 2020-02-02T03:02:18+01:00 TRACKPOINT SEGMENT_END_MANUAL 3 3 10 10 54 0 0
9 2020-02-02T03:02:22+01:00 2020-02-02T03:03:20+01:00 SEGMENT_START_AUTOMATIC SEGMENT_START_MANUAL 3 3 10 10 54 0 0
10 2020-02-02T03:02:23+01:00 2020-02-02T03:03:21+01:00 TRACKPOINT 3 3 14.002 10 10 54 0 0
11 2020-02-02T03:02:24+01:00 2020-02-02T03:03:22+01:00 SEGMENT_END_MANUAL SEGMENT_START_AUTOMATIC 3 16 10 10 54 0 0
12 2020-02-02T03:03:23+01:00 TRACKPOINT 3 16.001 10 10 54 0 0
13 2020-02-02T03:03:24+01:00 SEGMENT_END_MANUAL
+13 -7
View File
@@ -87,6 +87,17 @@ limitations under the License.
</intent-filter>
</activity>
<activity
android:name=".introduction.IntroductionActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".AboutActivity" />
<activity
@@ -310,13 +321,8 @@ limitations under the License.
<activity
android:name=".TrackListActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/SplashTheme">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
@@ -354,7 +360,7 @@ limitations under the License.
android:exported="false"
android:finishOnTaskLaunch="true"
android:launchMode="singleInstance"
android:process=":crash"/>
android:process=":crash" />
<provider
android:name=".data.ShareContentProvider"
@@ -33,4 +33,10 @@ public class AboutActivity extends AbstractActivity {
viewBinding = AboutBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
}
@@ -38,7 +38,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.core.splashscreen.SplashScreen;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
@@ -159,8 +158,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
@@ -146,9 +146,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
finish();
return true;
});
viewBinding.trackRecordingFabAction.setOnClickListener((view) -> {
Toast.makeText(TrackRecordingActivity.this, getString(R.string.hold_to_stop), Toast.LENGTH_LONG).show();
});
viewBinding.trackRecordingFabAction.setOnClickListener((view) -> Toast.makeText(TrackRecordingActivity.this, getString(R.string.hold_to_stop), Toast.LENGTH_LONG).show());
viewBinding.bottomAppBar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24);
setSupportActionBar(viewBinding.bottomAppBar);
@@ -1,176 +0,0 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextClock;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.ui.customRecordingLayout.CustomLayoutFieldType;
import de.dennisguse.opentracks.viewmodels.StatisticData;
public class StatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<StatisticData> statisticDataList;
private final Context context;
public StatisticsAdapter(Context context) {
this.context = context;
this.statisticDataList = new ArrayList<>();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view;
if (viewType == CustomLayoutFieldType.CLOCK.value()) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_clock_item, parent, false);
return new StatisticsAdapter.ViewClockHolder(view);
} else {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_generic_item, parent, false);
return new StatisticsAdapter.ViewHolder(view);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
StatisticData statisticData = statisticDataList.get(position);
if (getItemViewType(position) == CustomLayoutFieldType.CLOCK.value()) {
StatisticsAdapter.ViewClockHolder viewHolder = (StatisticsAdapter.ViewClockHolder) holder;
viewHolder.setData(statisticData);
} else {
StatisticsAdapter.ViewHolder viewHolder = (StatisticsAdapter.ViewHolder) holder;
viewHolder.setData(statisticData);
}
}
@Override
public int getItemCount() {
if (statisticDataList == null) {
return 0;
} else {
return statisticDataList.size();
}
}
@Override
public int getItemViewType(int position) {
if (statisticDataList.get(position).getField().getType(context) == CustomLayoutFieldType.CLOCK) {
return CustomLayoutFieldType.CLOCK.value();
} else {
return CustomLayoutFieldType.GENERIC.value();
}
}
public boolean isItemWide(int position) {
return statisticDataList.get(position).getField().isWide();
}
public List<StatisticData> swapData(List<StatisticData> data) {
if (statisticDataList == data) {
return null;
}
statisticDataList = data;
if (data != null) {
this.notifyDataSetChanged();
}
return data;
}
public static class WithRecordedLayout extends StatisticsAdapter {
public WithRecordedLayout(Context context) {
super(context);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_recorded_item, parent, false);
return new StatisticsAdapter.ViewRecordedHolder(view);
}
}
private class ViewHolder extends RecyclerView.ViewHolder {
final TextView value;
final TextView unit;
final TextView descMain;
final TextView descSecondary;
public ViewHolder(@NonNull View itemView) {
super(itemView);
value = itemView.findViewById(R.id.stats_value);
unit = itemView.findViewById(R.id.stats_unit);
descMain = itemView.findViewById(R.id.stats_description_main);
descSecondary = itemView.findViewById(R.id.stats_description_secondary);
}
public void setData(StatisticData statisticData) {
if (statisticData == null) {
return;
}
this.value.setText(statisticData.hasValue() ? statisticData.getValue() : context.getString(R.string.value_unknown));
this.value.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.TextAppearance_OpenTracks_PrimaryValue : R.style.TextAppearance_OpenTracks_SecondaryValue);
this.unit.setText(statisticData.getUnit());
this.descMain.setText(statisticData.getField().getTitle());
this.descMain.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.TextAppearance_OpenTracks_PrimaryHeader : R.style.TextAppearance_OpenTracks_SecondaryHeader);
if (statisticData.hasDescription()) {
this.descSecondary.setVisibility(View.VISIBLE);
this.descSecondary.setText(statisticData.getDescription());
} else {
this.descSecondary.setVisibility(View.GONE);
}
}
}
private class ViewClockHolder extends RecyclerView.ViewHolder {
final TextClock value;
final TextView descMain;
public ViewClockHolder(@NonNull View itemView) {
super(itemView);
value = itemView.findViewById(R.id.stats_clock);
descMain = itemView.findViewById(R.id.stats_description_main);
}
public void setData(StatisticData statisticData) {
if (statisticData == null) {
return;
}
this.value.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.TextAppearance_OpenTracks_PrimaryValue : R.style.TextAppearance_OpenTracks_SecondaryValue);
this.descMain.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.TextAppearance_OpenTracks_PrimaryHeader : R.style.TextAppearance_OpenTracks_SecondaryHeader);
}
}
private class ViewRecordedHolder extends ViewHolder {
public ViewRecordedHolder(@NonNull View itemView) {
super(itemView);
}
public void setData(StatisticData statisticData) {
if (statisticData == null) {
return;
}
this.value.setText(statisticData.hasValue() ? statisticData.getValue() : context.getString(R.string.value_unknown));
this.unit.setText(statisticData.getUnit());
this.descMain.setText(statisticData.getField().getTitle());
if (statisticData.hasDescription()) {
this.descSecondary.setVisibility(View.VISIBLE);
this.descSecondary.setText(statisticData.getDescription());
} else {
this.descSecondary.setVisibility(View.GONE);
}
}
}
}
@@ -27,7 +27,6 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@@ -0,0 +1,33 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
public class HeartRateZones {
private final HeartRate max;
public HeartRateZones(@NonNull HeartRate max) {
this.max = max;
}
public int getColorForZone(HeartRate current) {
if (current != null) {
if (current.getBPM() >= max.getBPM() * 0.9) {
return R.color.heart_rate_zone_textcolor_5;
}
if (current.getBPM() >= max.getBPM() * 0.8) {
return R.color.heart_rate_zone_textcolor_4;
}
if (current.getBPM() >= max.getBPM() * 0.7) {
return R.color.heart_rate_zone_textcolor_3;
}
if (current.getBPM() >= max.getBPM() * 0.6) {
return R.color.heart_rate_zone_textcolor_2;
}
}
return R.color.heart_rate_zone_textcolor_1;
}
}
@@ -385,10 +385,6 @@ public class TrackPoint {
return this;
}
public boolean hasSensorData() {
return hasHeartRate() || hasCadence() || hasPower();
}
public boolean hasHeartRate() {
return heartRate != null;
}
@@ -31,11 +31,8 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.adapters.StatisticsAdapter;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
@@ -47,8 +44,6 @@ import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.viewmodels.StatisticData;
import de.dennisguse.opentracks.viewmodels.StatisticDataBuilder;
/**
* A fragment to display track statistics to the user for a recorded {@link Track}.
@@ -72,7 +67,6 @@ public class StatisticsRecordedFragment extends Fragment {
}
private SensorStatistics sensorStatistics;
private StatisticsAdapter.WithRecordedLayout sensorsAdapter;
private Track.Id trackId;
@Nullable // Lazily loaded.
@@ -113,8 +107,6 @@ public class StatisticsRecordedFragment extends Fragment {
trackId = getArguments().getParcelable(TRACK_ID_KEY);
contentProviderUtils = new ContentProviderUtils(getContext());
sensorsAdapter = new StatisticsAdapter.WithRecordedLayout(getContext());
}
@Override
@@ -123,7 +115,7 @@ public class StatisticsRecordedFragment extends Fragment {
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
// sensorsRecyclerView.setAdapter(sensorsAdapter);
return viewBinding.getRoot();
}
@@ -261,10 +253,6 @@ public class StatisticsRecordedFragment extends Fragment {
if (sensorStatistics == null) {
return;
}
List<StatisticData> sensorDataList = StatisticDataBuilder.fromSensorStatistics(getContext(), sensorStatistics);
if (sensorDataList.size() > 0) {
sensorsAdapter.swapData(sensorDataList);
}
//TODO
}
}
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -9,16 +10,14 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.gridlayout.widget.GridLayout;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.adapters.StatisticsAdapter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.RecordingData;
@@ -26,9 +25,10 @@ import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.viewmodels.StatisticData;
import de.dennisguse.opentracks.viewmodels.StatisticsDataModel;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.viewmodels.Mapping;
import de.dennisguse.opentracks.viewmodels.StatisticViewHolder;
/**
* A fragment to display track statistics to the user for a currently recording {@link Track}.
@@ -48,68 +48,37 @@ public class StatisticsRecordingFragment extends Fragment {
private RecordingData recordingData = TrackRecordingService.NOT_RECORDING;
private Layout layout;
private final List<StatisticViewHolder<?>> viewHolders = new LinkedList<>();
private RecordingLayout recordingLayout;
private StatisticsRecordingBinding viewBinding;
private StatisticsAdapter statisticsAdapter;
private GridLayoutManager gridLayoutManager;
private StatisticsDataModel viewModel;
private LiveData<List<StatisticData>> statisticsLiveData;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true;
unitSystem = PreferencesUtils.getUnitSystem();
updateDataOnUI();
}
if (PreferencesUtils.isKey(R.string.stats_custom_layouts_key, key) || PreferencesUtils.isKey(R.string.stats_custom_layout_selected_layout_key, key)) {
updateUInecessary = true;
layout = PreferencesUtils.getCustomLayout();
gridLayoutManager.setSpanCount(layout.getColumnsPerRow());
}
if (key != null && updateUInecessary && isResumed()) {
getActivity().runOnUiThread(this::updateUI);
onLayoutChanged(PreferencesUtils.getCustomLayout());
}
};
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> {
service.getRecordingDataObservable()
.observe(StatisticsRecordingFragment.this, this::onRecordingDataChanged);
};
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> service.getRecordingDataObservable()
.observe(StatisticsRecordingFragment.this, this::onRecordingDataChanged);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
statisticsAdapter = new StatisticsAdapter(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
RecyclerView recyclerView = viewBinding.statsRecyclerView;
recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), RecyclerView.VERTICAL));
layout = PreferencesUtils.getCustomLayout();
gridLayoutManager = new GridLayoutManager(getContext(), layout.getColumnsPerRow());
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (statisticsAdapter.isItemWide(position)) {
return layout.getColumnsPerRow();
}
return 1;
}
});
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.setAdapter(statisticsAdapter);
return viewBinding.getRoot();
}
@@ -119,10 +88,6 @@ public class StatisticsRecordingFragment extends Fragment {
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
viewModel = new ViewModelProvider(getActivity()).get(StatisticsDataModel.class);
statisticsLiveData = viewModel.getStatsData();
statisticsLiveData.observe(getActivity(), statsDataList -> statisticsAdapter.swapData(statsDataList));
trackRecordingServiceConnection.startConnection(getContext());
}
@@ -141,6 +106,7 @@ public class StatisticsRecordingFragment extends Fragment {
@Override
public void onDestroyView() {
super.onDestroyView();
viewHolders.clear();
viewBinding = null;
}
@@ -148,17 +114,56 @@ public class StatisticsRecordingFragment extends Fragment {
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection = null;
viewModel = null;
if (statisticsLiveData != null) {
statisticsLiveData.removeObservers(getActivity());
}
statisticsLiveData = null;
}
private void updateUI() {
if (isResumed()) {
viewModel.update(recordingData, layout, unitSystem);
private void onLayoutChanged(@NonNull RecordingLayout newRecordingLayout) {
if (newRecordingLayout.equals(recordingLayout)) {
return;
}
recordingLayout = newRecordingLayout;
viewBinding.statsLayout.setColumnCount(recordingLayout.getColumnsPerRow());
viewBinding.statsLayout.removeAllViews(); //Let's start from scratch
viewHolders.clear();
Map<String, Callable<StatisticViewHolder<?>>> m = Mapping.create(getContext());
int rowIndex = 0;
int columnIndex = 0;
for (int i = 0; i < recordingLayout.getFields().size(); i++) {
DataField dataField = recordingLayout.getFields().get(i);
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.setGravity(Gravity.FILL_HORIZONTAL);
param.width = 0;
if (dataField.isWide()) {
rowIndex++;
param.columnSpec = GridLayout.spec(0, recordingLayout.getColumnsPerRow(), 1);
param.rowSpec = GridLayout.spec(rowIndex, 1, 1);
columnIndex = 0;
rowIndex++;
} else {
if (columnIndex >= recordingLayout.getColumnsPerRow()) {
columnIndex = 0;
rowIndex++;
}
param.columnSpec = GridLayout.spec(columnIndex, 1, 1);
param.rowSpec = GridLayout.spec(rowIndex, 1, 1);
columnIndex++;
}
try {
StatisticViewHolder<?> viewHolder = m.get(dataField.getKey()).call();
viewHolder.initialize(getContext(), getLayoutInflater());
viewHolder.configureUI(dataField);
viewHolders.add(viewHolder);
viewBinding.statsLayout.addView(viewHolder.getView(), param);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -171,6 +176,12 @@ public class StatisticsRecordingFragment extends Fragment {
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, getString(R.string.stats_rate_key));
}
updateUI();
updateDataOnUI();
}
private void updateDataOnUI() {
if (isResumed()) {
viewHolders.forEach(i -> i.onChanged(unitSystem, recordingData));
}
}
}
@@ -0,0 +1,93 @@
package de.dennisguse.opentracks.introduction;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.splashscreen.SplashScreen;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.List;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.databinding.IntroductionBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.IntentUtils;
public class IntroductionActivity extends AbstractActivity {
private IntroductionBinding viewBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
if (!PreferencesUtils.shouldShowIntroduction()) {
startActivity(new Intent(this, TrackListActivity.class));
finish();
}
viewBinding.nextButton.setOnClickListener(v -> {
int nextItemId = viewBinding.introductionViewPager.getCurrentItem() + 1;
if (nextItemId < viewBinding.introductionViewPager.getAdapter().getItemCount()) {
viewBinding.introductionViewPager.setCurrentItem(nextItemId);
} else {
PreferencesUtils.setShowIntroduction(false);
startActivity(IntentUtils.newIntent(this, TrackListActivity.class));
finish();
}
});
viewBinding.introductionViewPager.setAdapter(new CustomFragmentPagerAdapter(this));
viewBinding.introductionViewPager.setUserInputEnabled(false);
}
@Override
protected View getRootView() {
viewBinding = IntroductionBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
private static class CustomFragmentPagerAdapter extends FragmentStateAdapter {
private static final List<FragmentCreator> fragmentCreators = List.of(
WelcomeFragment::newInstance,
OSMDashboardFragment::newInstance
);
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
super(fa);
}
@NonNull
@Override
public Fragment createFragment(int position) {
FragmentCreator fc = fragmentCreators.get(position);
if (fc != null) {
return fc.newInstance();
}
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
@Override
public int getItemCount() {
return fragmentCreators.size();
}
}
private interface FragmentCreator {
Fragment newInstance();
}
}
@@ -0,0 +1,32 @@
package de.dennisguse.opentracks.introduction;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import de.dennisguse.opentracks.databinding.OsmDashboardBinding;
public class OSMDashboardFragment extends Fragment {
public static OSMDashboardFragment newInstance() {
return new OSMDashboardFragment();
}
private OsmDashboardBinding viewBinding;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = OsmDashboardBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
@Override
public void onDestroy() {
super.onDestroy();
viewBinding = null;
}
}
@@ -0,0 +1,37 @@
package de.dennisguse.opentracks.introduction;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import de.dennisguse.opentracks.databinding.WelcomeBinding;
public class WelcomeFragment extends Fragment {
public static WelcomeFragment newInstance() {
return new WelcomeFragment();
}
private WelcomeBinding viewBinding;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = WelcomeBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
@Override
public void onDestroy() {
super.onDestroy();
viewBinding = null;
}
}
@@ -44,7 +44,8 @@ import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML.
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML version 2.3.
* https://docs.opengeospatial.org/is/12-007r2/12-007r2.html
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
@@ -58,6 +59,8 @@ public class KMLTrackExporter implements TrackExporter {
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
public static final String EXTENDED_DATA_TYPE_CATEGORY = "type";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
@@ -224,11 +227,13 @@ public class KMLTrackExporter implements TrackExporter {
private void writeHeader(Track[] tracks) {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\"");
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.3\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\">");
//TODO ADD xsi:schemaLocation here!
//TODO ADD xsi:schemaLocation for atom
printWriter.println("xsi:schemaLocation=" +
"\"http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd"
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
@@ -309,16 +314,16 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
writeCategory(track.getCategory());
printWriter.println("<gx:MultiTrack>");
printWriter.println("<MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<gx:interpolate>1</gx:interpolate>");
printWriter.println("<interpolate>1</interpolate>");
}
}
private void writeEndTrack() {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</MultiTrack>");
printWriter.println("</Placemark>");
}
}
@@ -326,7 +331,7 @@ public class KMLTrackExporter implements TrackExporter {
@VisibleForTesting
void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<gx:Track>");
printWriter.println("<Track>");
speedList.clear();
distanceList.clear();
powerList.clear();
@@ -373,7 +378,7 @@ public class KMLTrackExporter implements TrackExporter {
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>");
printWriter.println("</Track>");
}
}
@@ -383,9 +388,9 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>");
if (trackPoint.hasLocation()) {
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
printWriter.println("<coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</coord>");
} else {
printWriter.println("<gx:coord/>");
printWriter.println("<coord/>");
}
speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null);
@@ -408,16 +413,16 @@ public class KMLTrackExporter implements TrackExporter {
* @param name the name of the simple array data
*/
private void writeSimpleArrayData(List<Float> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
printWriter.println("<SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) {
Float value = list.get(i);
if (value == null) {
printWriter.println("<gx:value />");
printWriter.println("<value />");
} else {
printWriter.println("<gx:value>" + SENSOR_DATA_FORMAT.format(value) + "</gx:value>");
printWriter.println("<value>" + SENSOR_DATA_FORMAT.format(value) + "</value>");
}
}
printWriter.println("</gx:SimpleArrayData>");
printWriter.println("</SimpleArrayData>");
}
/**
@@ -521,7 +526,7 @@ public class KMLTrackExporter implements TrackExporter {
return;
}
printWriter.println("<ExtendedData>");
printWriter.println("<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("<Data name=\"" + EXTENDED_DATA_TYPE_CATEGORY + "\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("</ExtendedData>");
}
@@ -554,8 +559,8 @@ public class KMLTrackExporter implements TrackExporter {
* @param extendedDataType the extended data display name
*/
private void writeSimpleArrayStyle(String name, String extendedDataType) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"float\">");
printWriter.println("<SimpleArrayField name=\"" + name + "\" type=\"float\">");
printWriter.println("<displayName>" + StringUtils.formatCData(extendedDataType) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>");
printWriter.println("</SimpleArrayField>");
}
}
@@ -11,7 +11,6 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -48,7 +47,14 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
}
private void loadData(List<DocumentFile> documentFiles) {
List<DocumentFile> fileList = documentFiles.stream().map(FileUtils::getFiles).flatMap(Collection::stream).collect(Collectors.toList());
List<ArrayList<DocumentFile>> nestedFileList = documentFiles.stream()
.map(FileUtils::getFiles)
// TODO flatMap(Collection::stream) fails with ClassCastException; try in the future again
.collect(Collectors.toList());
List<DocumentFile> fileList = new ArrayList<>();
nestedFileList.forEach(fileList::addAll);
summary.totalCount = fileList.size();
filesToImport.addAll(fileList);
importNextFile();
@@ -41,7 +41,7 @@ import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Imports a KML file.
* Imports a KML file; preferred version: KML2.3, but also supports KML2.2.
*
* @author Jimmy Shih
*/
@@ -54,18 +54,31 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
private static final String TAG_COORDINATES = "coordinates";
private static final String TAG_DESCRIPTION = "description";
private static final String TAG_ICON = "icon";
private static final String TAG_GX_COORD = "gx:coord";
private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack";
private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
private static final String TAG_GX_TRACK = "gx:Track";
private static final String TAG_GX_VALUE = "gx:value";
private static final String TAG_COORD = "coord";
private static final String TAG_KML22_COORD = "gx:coord";
private static final String TAG_MULTI_TRACK = "MultiTrack";
private static final String TAG_KML22_MULTI_TRACK = "gx:MultiTrack";
private static final String TAG_DATA_CATEGORY = "Data"; //used for Track.category
private static final String TAG_SIMPLE_ARRAY_DATA = "SimpleArrayData";
private static final String TAG_KML22_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
private static final String TAG_TRACK = "Track";
private static final String TAG_KML22_TRACK = "gx:Track";
private static final String TAG_VALUE = "value";
private static final String TAG_KML22_VALUE = "gx:value";
private static final String TAG_HREF = "href";
private static final String TAG_KML = "kml";
private static final String TAG_NAME = "name";
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
private static final String TAG_PLACEMARK = "Placemark";
private static final String TAG_STYLE_URL = "styleUrl";
private static final String TAG_VALUE = "value";
// private static final String TAG_VALUE = "value"; TODO
private static final String TAG_WHEN = "when";
private static final String TAG_UUID = "opentracks:trackid";
@@ -81,7 +94,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
private final ArrayList<Instant> whenList = new ArrayList<>();
private final ArrayList<Location> locationList = new ArrayList<>();
private String extendedDataType;
private String dataType;
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
@@ -128,17 +141,21 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
// Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, category, and description.
onMarkerStart();
break;
case TAG_GX_MULTI_TRACK:
case TAG_MULTI_TRACK:
case TAG_KML22_MULTI_TRACK:
trackImporter.newTrack();
break;
case TAG_GX_TRACK:
case TAG_TRACK:
case TAG_KML22_TRACK:
if (trackImporter == null) {
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
throw new SAXException("Missing " + TAG_MULTI_TRACK);
}
onTrackSegmentStart();
break;
case TAG_GX_SIMPLE_ARRAY_DATA:
onExtendedDataStart(attributes);
case TAG_DATA_CATEGORY:
case TAG_SIMPLE_ARRAY_DATA:
case TAG_KML22_SIMPLE_ARRAY_DATA:
dataType = attributes.getValue(ATTRIBUTE_NAME);
break;
}
}
@@ -162,18 +179,28 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
case TAG_COORDINATES:
onMarkerLocationEnd();
break;
case TAG_GX_MULTI_TRACK:
case TAG_MULTI_TRACK:
case TAG_KML22_MULTI_TRACK:
trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset);
zoneOffset = null;
break;
case TAG_GX_TRACK:
case TAG_TRACK:
case TAG_KML22_TRACK:
onTrackSegmentEnd();
break;
case TAG_GX_COORD:
case TAG_COORD:
case TAG_KML22_COORD:
onCoordEnded();
break;
case TAG_GX_VALUE:
onExtendedDataValueEnd();
case TAG_VALUE:
case TAG_KML22_VALUE:
if (KMLTrackExporter.EXTENDED_DATA_TYPE_CATEGORY.equals(dataType)) {
if (content != null) {
category = content.trim();
}
} else {
onExtendedDataValueEnd();
}
break;
case TAG_NAME:
if (content != null) {
@@ -195,11 +222,6 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
icon = content.trim();
}
break;
case TAG_VALUE:
if (content != null) {
category = content.trim();
}
break;
case TAG_WHEN:
if (content != null) {
try {
@@ -401,11 +423,6 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
return location;
}
private void onExtendedDataStart(Attributes attributes) {
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
}
private void onExtendedDataValueEnd() throws SAXException {
Float value = null;
if (content != null) {
@@ -414,11 +431,11 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
try {
value = Float.parseFloat(content);
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
throw new SAXException(createErrorMessage("Unable to parse value:" + content), e);
}
}
}
switch (extendedDataType) {
switch (dataType) {
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
sensorSpeedList.add(value);
break;
@@ -447,7 +464,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
accuracyVertical.add(value);
break;
default:
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
}
}
@@ -25,7 +25,6 @@ import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
@@ -30,16 +30,11 @@ import android.util.Log;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
/**
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
@@ -51,8 +46,7 @@ public abstract class BluetoothConnectionManager<DataType> {
private final SensorDataObserver observer;
private final UUID serviceUUUID;
private final UUID measurementUUID;
private final List<ServiceMeasurementUUID> serviceMeasurementUUIDs;
private BluetoothGatt bluetoothGatt;
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@@ -81,15 +75,24 @@ public abstract class BluetoothConnectionManager<DataType> {
@Override
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothGattService service = gatt.getService(serviceUUUID);
if (service == null) {
Log.e(TAG, "Could not get service for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID);
BluetoothGattService gattService = null;
ServiceMeasurementUUID serviceMeasurement = null;
for (ServiceMeasurementUUID s : serviceMeasurementUUIDs) {
gattService = gatt.getService(s.getServiceUUID());
if (gattService != null) {
serviceMeasurement = s;
break;
}
}
if (gattService == null) {
Log.e(TAG, "Could not get gattService for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceMeasurement);
return;
}
BluetoothGattCharacteristic characteristic = service.getCharacteristic(measurementUUID);
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(serviceMeasurement.getMeasurementUUID());
if (characteristic == null) {
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID + " characteristicUUID=" + measurementUUID);
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceMeasurement.getServiceUUID() + " characteristicUUID=" + serviceMeasurement.getMeasurementUUID());
return;
}
gatt.setCharacteristicNotification(characteristic, true);
@@ -108,8 +111,16 @@ public abstract class BluetoothConnectionManager<DataType> {
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress());
SensorData<DataType> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
UUID serviceUUID = characteristic.getService().getUuid();
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress() + " with service " + serviceUUID + " and characteristics " + characteristic.getUuid());
Optional<ServiceMeasurementUUID> serviceMeasurementUUID = serviceMeasurementUUIDs.stream()
.filter(s -> s.getServiceUUID().equals(characteristic.getService().getUuid())).findFirst();
if (serviceMeasurementUUID.isEmpty()) {
Log.e(TAG, "Unknown service UUID; not supported?");
return;
}
SensorData<DataType> sensorData = parsePayload(serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -122,15 +133,20 @@ public abstract class BluetoothConnectionManager<DataType> {
}
};
BluetoothConnectionManager(UUID serviceUUUID, UUID measurementUUID, SensorDataObserver observer) {
this.serviceUUUID = serviceUUUID;
this.measurementUUID = measurementUUID;
BluetoothConnectionManager(ServiceMeasurementUUID serviceUUUID, SensorDataObserver observer) {
this.serviceMeasurementUUIDs = List.of(serviceUUUID);
this.observer = observer;
}
BluetoothConnectionManager(List<ServiceMeasurementUUID> serviceUUUID, SensorDataObserver observer) {
this.serviceMeasurementUUIDs = serviceUUUID;
this.observer = observer;
}
synchronized void connect(Context context, @NonNull BluetoothDevice device) {
if (bluetoothGatt != null) {
Log.w(TAG, "Already connected; ignoring.");
return;
}
Log.d(TAG, "Connecting to: " + device);
@@ -172,114 +188,7 @@ public abstract class BluetoothConnectionManager<DataType> {
/**
* @return null if data could not be parsed.
*/
protected abstract SensorData<DataType> parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
public static class HeartRateConnectionManager extends BluetoothConnectionManager<HeartRate> {
HeartRateConnectionManager(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataHeartRate createEmptySensorData(String address) {
return new SensorDataHeartRate(address);
}
@Override
protected SensorDataHeartRate parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Integer heartRate = BluetoothUtils.parseHeartRate(characteristic);
return heartRate != null ? new SensorDataHeartRate(address, sensorName, HeartRate.of(heartRate)) : null;
}
}
public static class CyclingCadence extends BluetoothConnectionManager<Cadence> {
CyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataCycling.CyclingCadence createEmptySensorData(String address) {
return new SensorDataCycling.CyclingCadence(address);
}
@Override
protected SensorDataCycling.CyclingCadence parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getCadence() != null) {
return cadenceAndSpeed.getCadence();
}
return null;
}
}
public static class CyclingDistanceSpeed extends BluetoothConnectionManager<SensorDataCycling.DistanceSpeed.Data> {
CyclingDistanceSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataCycling.DistanceSpeed createEmptySensorData(String address) {
return new SensorDataCycling.DistanceSpeed(address);
}
@Override
protected SensorDataCycling.DistanceSpeed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getDistanceSpeed() != null) {
return cadenceAndSpeed.getDistanceSpeed();
}
return null;
}
}
public static class CyclingPower extends BluetoothConnectionManager<Power> {
CyclingPower(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_POWER_UUID, BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataCyclingPower createEmptySensorData(String address) {
return new SensorDataCyclingPower(address);
}
@Override
protected SensorDataCyclingPower parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Integer cyclingPower = BluetoothUtils.parseCyclingPower(characteristic);
return cyclingPower != null ? new SensorDataCyclingPower(address, sensorName, Power.of(cyclingPower)) : null;
}
}
public static class RunningSpeedAndCadence extends BluetoothConnectionManager<SensorDataRunning.Data> {
RunningSpeedAndCadence(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_UUID, BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID, observer);
}
@Override
protected SensorDataRunning createEmptySensorData(String address) {
return new SensorDataRunning(address);
}
@Override
protected SensorDataRunning parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
return BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic);
}
}
protected abstract SensorData<DataType> parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, @NonNull BluetoothGattCharacteristic characteristic);
interface SensorDataObserver {
@@ -31,7 +31,8 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -55,7 +56,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
public static final Duration MAX_SENSOR_DATE_SET_AGE_MS = Duration.ofSeconds(5);
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
private final BluetoothAdapter bluetoothAdapter;
private final Context context;
@@ -64,11 +65,11 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private Distance preferenceWheelCircumference;
private final BluetoothConnectionManager.HeartRateConnectionManager heartRate = new BluetoothConnectionManager.HeartRateConnectionManager(this);
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
private final BluetoothConnectionManager.CyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingDistanceSpeed(this);
private final BluetoothConnectionManager.CyclingPower cyclingPower = new BluetoothConnectionManager.CyclingPower(this);
private final BluetoothConnectionManager.RunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionManager.RunningSpeedAndCadence(this);
private final BluetoothRemoteSensorManagerHeartRate heartRate = new BluetoothRemoteSensorManagerHeartRate(this);
private final BluetoothRemoteSensorManagerCyclingCadence cyclingCadence = new BluetoothRemoteSensorManagerCyclingCadence(this);
private final BluetoothRemoteSensorManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothRemoteSensorManagerCyclingDistanceSpeed(this);
private final BluetoothRemoteSensorManagerCyclingPower cyclingPower = new BluetoothRemoteSensorManagerCyclingPower(this);
private final BluetoothRemoteSensorManagerRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothRemoteSensorManagerRunningSpeedAndCadence(this);
private final SensorDataSet sensorDataSet = new SensorDataSet();
@@ -186,24 +187,24 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
@Override
public synchronized void onChanged(SensorData<?> sensorData) {
if (sensorData instanceof SensorDataCycling.CyclingCadence) {
SensorDataCycling.CyclingCadence previous = sensorDataSet.getCyclingCadence();
if (sensorData instanceof SensorDataCyclingCadence) {
SensorDataCyclingCadence previous = sensorDataSet.getCyclingCadence();
Log.d(TAG, "Previous: " + previous + "; current: " + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cadence data repeated.");
return;
}
((SensorDataCycling.CyclingCadence) sensorData).compute(previous);
((SensorDataCyclingCadence) sensorData).compute(previous);
}
if (sensorData instanceof SensorDataCycling.DistanceSpeed) {
SensorDataCycling.DistanceSpeed previous = sensorDataSet.getCyclingDistanceSpeed();
if (sensorData instanceof SensorDataCyclingDistanceSpeed) {
SensorDataCyclingDistanceSpeed previous = sensorDataSet.getCyclingDistanceSpeed();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cycling speed data repeated.");
return;
}
((SensorDataCycling.DistanceSpeed) sensorData).compute(previous, preferenceWheelCircumference);
((SensorDataCyclingDistanceSpeed) sensorData).compute(previous, preferenceWheelCircumference);
}
if (sensorData instanceof SensorDataRunning) {
SensorDataRunning previous = sensorDataSet.getRunningDistanceSpeedCadence();
@@ -0,0 +1,47 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
public class BluetoothRemoteSensorManagerCyclingCadence extends BluetoothConnectionManager<Cadence> {
private static final String TAG = BluetoothRemoteSensorManagerCyclingCadence.class.getSimpleName();
BluetoothRemoteSensorManagerCyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_CADENCE, observer);
}
@Override
protected SensorDataCyclingCadence createEmptySensorData(String address) {
return new SensorDataCyclingCadence(address);
}
@Override
protected SensorDataCyclingCadence parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
//TODO Implement to ServiceMeasurement.parse()?
if (serviceMeasurementUUID.equals(BluetoothUtils.CYCLING_POWER)) {
SensorDataCyclingPower.Data data = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic);
if (data!= null) {
return data.getCadence();
}
} else if (serviceMeasurementUUID.equals(BluetoothUtils.CYCLING_SPEED_CADENCE)) {
SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getCadence() != null) {
return cadenceAndSpeed.getCadence();
}
}
Log.e(TAG, "Don't know how to decode this payload.");
return null;
}
}
@@ -0,0 +1,32 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
public class BluetoothRemoteSensorManagerCyclingDistanceSpeed extends BluetoothConnectionManager<SensorDataCyclingDistanceSpeed.Data> {
BluetoothRemoteSensorManagerCyclingDistanceSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE, observer);
}
@Override
protected SensorDataCyclingDistanceSpeed createEmptySensorData(String address) {
return new SensorDataCyclingDistanceSpeed(address);
}
@Override
protected SensorDataCyclingDistanceSpeed parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getDistanceSpeed() != null) {
return cadenceAndSpeed.getDistanceSpeed();
}
return null;
}
}
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
public class BluetoothRemoteSensorManagerCyclingPower extends BluetoothConnectionManager<Power> {
BluetoothRemoteSensorManagerCyclingPower(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_POWER, observer);
}
@Override
protected SensorDataCyclingPower createEmptySensorData(String address) {
return new SensorDataCyclingPower(address);
}
@Override
protected SensorDataCyclingPower parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCyclingPower.Data cyclingPower = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic);
return cyclingPower != null ? cyclingPower.getPower() : null;
}
}
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
public class BluetoothRemoteSensorManagerHeartRate extends BluetoothConnectionManager<HeartRate> {
BluetoothRemoteSensorManagerHeartRate(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.HEARTRATE, observer);
}
@Override
protected SensorDataHeartRate createEmptySensorData(String address) {
return new SensorDataHeartRate(address);
}
@Override
protected SensorDataHeartRate parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
return heartRate != null ? new SensorDataHeartRate(address, sensorName, heartRate) : null;
}
}
@@ -0,0 +1,24 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
public class BluetoothRemoteSensorManagerRunningSpeedAndCadence extends BluetoothConnectionManager<SensorDataRunning.Data> {
BluetoothRemoteSensorManagerRunningSpeedAndCadence(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.RUNNING_SPEED_CADENCE, observer);
}
@Override
protected SensorDataRunning createEmptySensorData(String address) {
return new SensorDataRunning(address);
}
@Override
protected SensorDataRunning parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
return BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic);
}
}
@@ -30,8 +30,13 @@ import java.util.UUID;
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.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
/**
@@ -43,23 +48,40 @@ public class BluetoothUtils {
public static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
public static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
new UUID(0x2A3700001000L, 0x800000805f9b34fbL)
);
public static final List<UUID> HEART_RATE_SUPPORTING_DEVICES = Collections.unmodifiableList(Arrays.asList(
BluetoothUtils.HEART_RATE_SERVICE_UUID,
// Used for device discovery in preferences
public static final List<ServiceMeasurementUUID> HEART_RATE_SUPPORTING_DEVICES = Collections.unmodifiableList(Arrays.asList(
HEARTRATE,
//Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery).
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb") //Miband3
));
new ServiceMeasurementUUID(
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"), //Miband3
HEARTRATE.getMeasurementUUID()
))
);
public static final UUID CYCLING_POWER_UUID = new UUID(0x181800001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_POWER_MEASUREMENT_CHAR_UUID = new UUID(0x2A6300001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
new UUID(0x181800001000L, 0x800000805f9b34fbL),
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
);
public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
new UUID(0x181600001000L, 0x800000805f9b34fbL),
new UUID(0x2A5B00001000L, 0x800000805f9b34fbL)
);
public static final UUID RUNNING_RUNNING_SPEED_CADENCE_UUID = new UUID(0x181400001000L, 0x800000805f9b34fbL);
public static final UUID RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID = new UUID(0x2A5300001000L, 0x800000805f9b34fbL);
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
CYCLING_POWER,
CYCLING_SPEED_CADENCE
);
public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID(
new UUID(0x181400001000L, 0x800000805f9b34fbL),
new UUID(0x2A5300001000L, 0x800000805f9b34fbL)
);
private static final String TAG = BluetoothUtils.class.getSimpleName();
@@ -80,7 +102,7 @@ public class BluetoothUtils {
return BluetoothUtils.getAdapter(context) != null;
}
public static Integer parseHeartRate(BluetoothGattCharacteristic characteristic) {
public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
byte[] raw = characteristic.getValue();
if (raw.length == 0) {
@@ -89,26 +111,58 @@ public class BluetoothUtils {
boolean formatUINT16 = ((raw[0] & 0x1) == 1);
if (formatUINT16 && raw.length >= 3) {
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1);
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1));
}
if (!formatUINT16 && raw.length >= 2) {
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1);
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1));
}
return null;
}
public static Integer parseCyclingPower(BluetoothGattCharacteristic characteristic) {
public static SensorDataCyclingPower.Data parseCyclingPower(String address, String sensorName, BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength < 4) {
if (valueLength == 0) {
return null;
}
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 2);
int index = 0;
int flags1 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
int flags2 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
boolean hasPedalPowerBalance = (flags1 & 0x01) > 0;
boolean hasAccumulatedTorque = (flags1 & 0x04) > 0;
boolean hasWheel = (flags1 & 16) > 0;
boolean hasCrank = (flags1 & 32) > 0;
Integer instantaneousPower = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, index);
index += 2;
if (hasPedalPowerBalance) {
index += 1;
}
if (hasAccumulatedTorque) {
index += 2;
}
if (hasWheel) {
index += 2 + 2;
}
SensorDataCyclingCadence cadence = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime);
}
return new SensorDataCyclingPower.Data(new SensorDataCyclingPower(sensorName, address, Power.of(instantaneousPower)), cadence);
}
public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
public static SensorDataCyclingCadenceAndDistanceSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
@@ -120,25 +174,25 @@ public class BluetoothUtils {
boolean hasCrank = (flags & 0x02) > 0;
int index = 1;
SensorDataCycling.DistanceSpeed speed = null;
SensorDataCyclingDistanceSpeed speed = null;
if (hasWheel && valueLength - index >= 6) {
int wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
index += 4;
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
speed = new SensorDataCycling.DistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime);
speed = new SensorDataCyclingDistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime);
index += 2;
}
SensorDataCycling.CyclingCadence cadence = null;
SensorDataCyclingCadence cadence = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
cadence = new SensorDataCycling.CyclingCadence(address, sensorName, crankCount, crankTime);
cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime);
}
return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed);
return new SensorDataCyclingCadenceAndDistanceSpeed(address, sensorName, cadence, speed);
}
public static SensorDataRunning parseRunningSpeedAndCadence(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
@@ -0,0 +1,47 @@
package de.dennisguse.opentracks.sensors;
import androidx.annotation.NonNull;
import java.util.Objects;
import java.util.UUID;
public class ServiceMeasurementUUID {
private final UUID serviceUUID;
private final UUID measurementUUID;
public ServiceMeasurementUUID(UUID serviceUUID, UUID measurementUUID) {
this.serviceUUID = serviceUUID;
this.measurementUUID = measurementUUID;
}
public UUID getServiceUUID() {
return serviceUUID;
}
public UUID getMeasurementUUID() {
return measurementUUID;
}
@NonNull
@Override
public String toString() {
return "ServiceMeasurementUUID{" +
"serviceUUID=" + serviceUUID +
", measurementUUID=" + measurementUUID +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ServiceMeasurementUUID that = (ServiceMeasurementUUID) o;
return Objects.equals(serviceUUID, that.serviceUUID) && Objects.equals(measurementUUID, that.measurementUUID);
}
@Override
public int hashCode() {
return Objects.hash(serviceUUID, measurementUUID);
}
}
@@ -74,7 +74,7 @@ public abstract class SensorData<T> {
*/
private boolean isRecent() {
return Instant.now()
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE));
}
@NonNull
@@ -1,255 +0,0 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.UintUtils;
/**
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Cycling Cadence and Speed sensors.
*/
public final class SensorDataCycling {
private static final String TAG = SensorDataCycling.class.getSimpleName();
private SensorDataCycling() {
}
public static class CyclingCadence extends SensorData<Cadence> {
private final Long crankRevolutionsCount; // UINT32
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
public CyclingCadence(String sensorAddress) {
super(sensorAddress);
this.crankRevolutionsCount = null;
this.crankRevolutionsTime = null;
}
public CyclingCadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
super(sensorAddress, sensorName);
this.crankRevolutionsCount = crankRevolutionsCount;
this.crankRevolutionsTime = crankRevolutionsTime;
}
/**
* Workaround for Wahoo CADENCE: provides speed instead of cadence
*/
public CyclingCadence(@NonNull DistanceSpeed speed) {
this(speed.getSensorAddress(), speed.getSensorName(), speed.wheelRevolutionsCount, speed.wheelRevolutionsTime);
}
public boolean hasData() {
return crankRevolutionsCount != null && crankRevolutionsTime != null;
}
public long getCrankRevolutionsCount() {
return crankRevolutionsCount;
}
public int getCrankRevolutionsTime() {
return crankRevolutionsTime;
}
@NonNull
@Override
protected Cadence getNoneValue() {
return Cadence.of(0);
}
public void compute(CyclingCadence previous) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null;
return;
}
// TODO We have to treat with overflow according to the documentation: read https://github.com/OpenTracksApp/OpenTracks/pull/953#discussion_r711625268
if (crankRevolutionsCount < previous.crankRevolutionsCount) {
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
return;
}
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
value = Cadence.of(crankDiff, timeDiff);
}
}
@NonNull
@Override
public String toString() {
return super.toString() + " cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof CyclingCadence)) return false;
CyclingCadence comp = (CyclingCadence) obj;
if (hasData() && comp.hasData() == hasData()) {
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
} else {
return false;
}
}
}
public static class DistanceSpeed extends SensorData<DistanceSpeed.Data> {
private final Long wheelRevolutionsCount; // UINT32
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
public DistanceSpeed(String sensorAddress) {
super(sensorAddress);
this.wheelRevolutionsCount = null;
this.wheelRevolutionsTime = null;
}
public DistanceSpeed(String sensorAddress, String sensorName, long wheelRevolutionsCount, int wheelRevolutionsTime) {
super(sensorAddress, sensorName);
this.wheelRevolutionsCount = wheelRevolutionsCount;
this.wheelRevolutionsTime = wheelRevolutionsTime;
}
public boolean hasData() {
return wheelRevolutionsCount != null && wheelRevolutionsTime != null;
}
public long getWheelRevolutionsCount() {
return wheelRevolutionsCount;
}
public int getWheelRevolutionsTime() {
return wheelRevolutionsTime;
}
@NonNull
@Override
protected Data getNoneValue() {
if (value != null) {
return new Data(value.distance, value.distanceOverall, Speed.zero());
} else {
return new Data(Distance.of(0), Distance.of(0), Speed.zero());
}
}
public void compute(DistanceSpeed previous, Distance wheelCircumference) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute speed.");
value = null;
return;
}
if (wheelRevolutionsCount < previous.wheelRevolutionsCount) {
Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed.");
return;
}
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX);
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
Distance distanceOverall = distance;
if (previous.hasValue()) {
distanceOverall = distance.plus(previous.getValue().distanceOverall);
}
Speed speed_mps = Speed.of(distance, timeDiff);
value = new Data(distance, distanceOverall, speed_mps);
}
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.distance, Distance.of(0), value.speed);
}
}
@NonNull
@Override
public String toString() {
return super.toString() + " data=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof DistanceSpeed)) return false;
DistanceSpeed comp = (DistanceSpeed) obj;
if (!(hasData() && comp.hasData())) {
return false;
}
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
}
public static class Data {
private final Distance distance;
private final Distance distanceOverall;
private final Speed speed;
private Data(Distance distance, Distance distanceOverall, Speed speed) {
this.distance = distance;
this.distanceOverall = distanceOverall;
this.speed = speed;
}
public Distance getDistance() {
return distance;
}
public Distance getDistanceOverall() {
return distanceOverall;
}
public Speed getSpeed() {
return speed;
}
@NonNull
@Override
public String toString() {
return "Data{" +
"distance=" + getDistance() +
", distance_overall=" + getDistanceOverall() +
", speed=" + getSpeed() +
'}';
}
}
}
public static class CadenceAndSpeed extends SensorData<Pair<CyclingCadence, DistanceSpeed>> {
public CadenceAndSpeed(String sensorAddress, String sensorName, @Nullable CyclingCadence cadence, @Nullable DistanceSpeed distanceSpeed) {
super(sensorAddress, sensorName);
this.value = new Pair<>(cadence, distanceSpeed);
}
public CyclingCadence getCadence() {
return this.value != null ? this.value.first : null;
}
public DistanceSpeed getDistanceSpeed() {
return this.value != null ? this.value.second : null;
}
@NonNull
@Override
protected Pair<CyclingCadence, DistanceSpeed> getNoneValue() {
return new Pair<>(null, null);
}
}
}
@@ -0,0 +1,88 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.UintUtils;
public class SensorDataCyclingCadence extends SensorData<Cadence> {
private final String TAG = SensorDataCyclingCadence.class.getSimpleName();
private final Long crankRevolutionsCount; // UINT32
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
public SensorDataCyclingCadence(String sensorAddress) {
super(sensorAddress);
this.crankRevolutionsCount = null;
this.crankRevolutionsTime = null;
}
public SensorDataCyclingCadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
super(sensorAddress, sensorName);
this.crankRevolutionsCount = crankRevolutionsCount;
this.crankRevolutionsTime = crankRevolutionsTime;
}
public boolean hasData() {
return crankRevolutionsCount != null && crankRevolutionsTime != null;
}
public long getCrankRevolutionsCount() {
return crankRevolutionsCount;
}
public int getCrankRevolutionsTime() {
return crankRevolutionsTime;
}
@NonNull
@Override
protected Cadence getNoneValue() {
return Cadence.of(0);
}
public void compute(SensorDataCyclingCadence previous) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null;
return;
}
// TODO We have to treat with overflow according to the documentation: read https://github.com/OpenTracksApp/OpenTracks/pull/953#discussion_r711625268
if (crankRevolutionsCount < previous.crankRevolutionsCount) {
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
return;
}
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
value = Cadence.of(crankDiff, timeDiff);
}
}
@NonNull
@Override
public String toString() {
return super.toString() + " cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SensorDataCyclingCadence)) return false;
SensorDataCyclingCadence comp = (SensorDataCyclingCadence) obj;
if (hasData() && comp.hasData() == hasData()) {
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
} else {
return false;
}
}
}
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
public class SensorDataCyclingCadenceAndDistanceSpeed extends SensorData<Pair<SensorDataCyclingCadence, SensorDataCyclingDistanceSpeed>> {
public SensorDataCyclingCadenceAndDistanceSpeed(String sensorAddress, String sensorName, @Nullable SensorDataCyclingCadence cadence, @Nullable SensorDataCyclingDistanceSpeed distanceSpeed) {
super(sensorAddress, sensorName);
this.value = new Pair<>(cadence, distanceSpeed);
}
public SensorDataCyclingCadence getCadence() {
return this.value != null ? this.value.first : null;
}
public SensorDataCyclingDistanceSpeed getDistanceSpeed() {
return this.value != null ? this.value.second : null;
}
@NonNull
@Override
protected Pair<SensorDataCyclingCadence, SensorDataCyclingDistanceSpeed> getNoneValue() {
return new Pair<>(null, null);
}
}
@@ -0,0 +1,139 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.UintUtils;
public class SensorDataCyclingDistanceSpeed extends SensorData<SensorDataCyclingDistanceSpeed.Data> {
private final String TAG = SensorDataCyclingDistanceSpeed.class.getSimpleName();
private final Long wheelRevolutionsCount; // UINT32
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
public SensorDataCyclingDistanceSpeed(String sensorAddress) {
super(sensorAddress);
this.wheelRevolutionsCount = null;
this.wheelRevolutionsTime = null;
}
public SensorDataCyclingDistanceSpeed(String sensorAddress, String sensorName, long wheelRevolutionsCount, int wheelRevolutionsTime) {
super(sensorAddress, sensorName);
this.wheelRevolutionsCount = wheelRevolutionsCount;
this.wheelRevolutionsTime = wheelRevolutionsTime;
}
public boolean hasData() {
return wheelRevolutionsCount != null && wheelRevolutionsTime != null;
}
public long getWheelRevolutionsCount() {
return wheelRevolutionsCount;
}
public int getWheelRevolutionsTime() {
return wheelRevolutionsTime;
}
@NonNull
@Override
protected Data getNoneValue() {
if (value != null) {
return new Data(value.distance, value.distanceOverall, Speed.zero());
} else {
return new Data(Distance.of(0), Distance.of(0), Speed.zero());
}
}
public void compute(SensorDataCyclingDistanceSpeed previous, Distance wheelCircumference) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute speed.");
value = null;
return;
}
if (wheelRevolutionsCount < previous.wheelRevolutionsCount) {
Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed.");
return;
}
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX);
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
Distance distanceOverall = distance;
if (previous.hasValue()) {
distanceOverall = distance.plus(previous.getValue().distanceOverall);
}
Speed speed_mps = Speed.of(distance, timeDiff);
value = new Data(distance, distanceOverall, speed_mps);
}
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.distance, Distance.of(0), value.speed);
}
}
@NonNull
@Override
public String toString() {
return super.toString() + " data=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SensorDataCyclingDistanceSpeed)) return false;
SensorDataCyclingDistanceSpeed comp = (SensorDataCyclingDistanceSpeed) obj;
if (!(hasData() && comp.hasData())) {
return false;
}
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
}
public static class Data {
private final Distance distance;
private final Distance distanceOverall;
private final Speed speed;
private Data(Distance distance, Distance distanceOverall, Speed speed) {
this.distance = distance;
this.distanceOverall = distanceOverall;
this.speed = speed;
}
public Distance getDistance() {
return distance;
}
public Distance getDistanceOverall() {
return distanceOverall;
}
public Speed getSpeed() {
return speed;
}
@NonNull
@Override
public String toString() {
return "Data{" +
"distance=" + getDistance() +
", distance_overall=" + getDistanceOverall() +
", speed=" + getSpeed() +
'}';
}
}
}
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.data.models.Power;
@@ -17,13 +18,40 @@ public class SensorDataCyclingPower extends SensorData<Power> {
@NonNull
@Override
public String toString() {
return super.toString() + " power=" + value;
protected Power getNoneValue() {
return Power.of(0f);
}
@NonNull
@Override
protected Power getNoneValue() {
return Power.of(0f);
public String toString() {
return super.toString() + " data=" + value;
}
public static class Data {
private final SensorDataCyclingPower power;
private final SensorDataCyclingCadence cadence;
public Data(SensorDataCyclingPower power, @Nullable SensorDataCyclingCadence cadence) {
this.power = power;
this.cadence = cadence;
}
public SensorDataCyclingPower getPower() {
return power;
}
public SensorDataCyclingCadence getCadence() {
return cadence;
}
@NonNull
@Override
public String toString() {
return "Data{" +
"power=" + power +
", cadence=" + cadence +
'}';
}
}
}
@@ -13,9 +13,9 @@ public final class SensorDataSet {
private SensorDataHeartRate heartRate;
private SensorDataCycling.CyclingCadence cyclingCadence;
private SensorDataCyclingCadence cyclingCadence;
private SensorDataCycling.DistanceSpeed cyclingDistanceSpeed;
private SensorDataCyclingDistanceSpeed cyclingDistanceSpeed;
private SensorDataCyclingPower cyclingPower;
@@ -64,11 +64,11 @@ public final class SensorDataSet {
return null;
}
public SensorDataCycling.CyclingCadence getCyclingCadence() {
public SensorDataCyclingCadence getCyclingCadence() {
return cyclingCadence;
}
public SensorDataCycling.DistanceSpeed getCyclingDistanceSpeed() {
public SensorDataCyclingDistanceSpeed getCyclingDistanceSpeed() {
return cyclingDistanceSpeed;
}
@@ -146,12 +146,12 @@ public final class SensorDataSet {
return;
}
if (type instanceof SensorDataCycling.CyclingCadence) {
this.cyclingCadence = (SensorDataCycling.CyclingCadence) data;
if (type instanceof SensorDataCyclingCadence) {
this.cyclingCadence = (SensorDataCyclingCadence) data;
return;
}
if (type instanceof SensorDataCycling.DistanceSpeed) {
this.cyclingDistanceSpeed = (SensorDataCycling.DistanceSpeed) data;
if (type instanceof SensorDataCyclingDistanceSpeed) {
this.cyclingDistanceSpeed = (SensorDataCyclingDistanceSpeed) data;
return;
}
@@ -22,7 +22,6 @@ import java.util.concurrent.Executors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class TrackDeleteService extends Service {
@@ -267,10 +267,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
@Override
public void newGpsStatus(GpsStatusValue gpsStatusValue) {
notificationManager.updateContent(getString(gpsStatusValue.message));
//TODO This check should not be necessary, but prevents a crash; somehow the shutdown is not working correctly as we should not receive a notification then.
// It is likely a race condition as the LocationManager provides location updates without using the Handler.
if (gpsStatusObservable != null) {
notificationManager.updateContent(getString(gpsStatusValue.message));
gpsStatusObservable.postValue(gpsStatusValue);
}
}
@@ -33,8 +33,8 @@ class VoiceAnnouncementUtils {
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
SpannableStringBuilder builder = new SpannableStringBuilder();
Distance distance = trackStatistics.getTotalDistance();
Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
Distance totalDistance = trackStatistics.getTotalDistance();
Speed averageMovingSpeed = trackStatistics.getAverageMovingSpeed();
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
int perUnitStringId;
@@ -68,17 +68,17 @@ class VoiceAnnouncementUtils {
throw new RuntimeException("Not implemented");
}
double distanceInUnit = distance.toKM_Miles(unitSystem);
double distanceInUnit = totalDistance.toKM_Miles(unitSystem);
if (shouldVoiceAnnounceTotalDistance()) {
builder.append(context.getString(R.string.total_distance));
// Units should always be english singular for TTS.
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 2, unitDistanceTTS);
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 1, unitDistanceTTS);
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
}
if (distance.isZero()) {
if (totalDistance.isZero()) {
return builder;
}
@@ -91,7 +91,7 @@ class VoiceAnnouncementUtils {
if (isReportSpeed) {
if (shouldVoiceAnnounceAverageSpeedPace()) {
double speedInUnit = distancePerTime.to(unitSystem);
double speedInUnit = averageMovingSpeed.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.speed));
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
@@ -108,7 +108,7 @@ class VoiceAnnouncementUtils {
}
} else {
if (shouldVoiceAnnounceAverageSpeedPace()) {
Duration time = distancePerTime.toPace(unitSystem);
Duration time = averageMovingSpeed.toPace(unitSystem);
builder.append(" ")
.append(context.getString(R.string.pace));
appendDuration(context, builder, time);
@@ -3,13 +3,16 @@ package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.location.LocationListenerCompat;
import androidx.core.location.LocationManagerCompat;
import androidx.core.location.LocationRequestCompat;
import java.time.Duration;
import java.time.Instant;
@@ -22,12 +25,16 @@ import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
public class GPSHandler implements LocationListenerCompat, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
private final String TAG = GPSHandler.class.getSimpleName();
public static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
private final TrackPointCreator trackPointCreator;
private Context context;
private Handler handler;
private LocationManager locationManager;
private GpsStatus gpsStatus;
private Duration gpsInterval;
@@ -37,8 +44,10 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
this.trackPointCreator = trackPointCreator;
}
public void onStart(@NonNull Context context) {
public void onStart(@NonNull Context context, @NonNull Handler handler) {
this.context = context;
this.handler = handler;
PreferencesUtils.registerOnSharedPreferenceChangeListener(this);
gpsStatus = new GpsStatus(context, this);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
@@ -55,10 +64,11 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
public void onStop() {
if (locationManager != null && context != null) {
if (PermissionRequester.GPS.hasPermission(context)) {
locationManager.removeUpdates(this);
LocationManagerCompat.removeUpdates(locationManager, this);
}
locationManager = null;
context = null;
handler = null;
}
if (gpsStatus != null) {
@@ -152,9 +162,21 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
Log.e(TAG, "Not started.");
return;
}
if (!LocationManagerCompat.hasProvider(locationManager, LOCATION_PROVIDER)) {
Log.e(TAG, "Device doesn't have GPS.");
return;
}
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(gpsInterval.toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
if (PermissionRequester.GPS.hasPermission(context)) {
try {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, gpsInterval.toMillis(), 0, this);
final Handler HANDLER = handler;
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, HANDLER::post, this);
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
}
@@ -52,7 +52,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
public synchronized void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context;
gpsHandler.onStart(context);
gpsHandler.onStart(context, handler);
remoteSensorManager = new BluetoothRemoteSensorManager(context, handler, this);
altitudeSumManager = new AltitudeSumManager();
@@ -66,13 +66,6 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
return context != null;
}
@Deprecated
//There should be a cooler way to do this; we want to send fake locations without getting affected by real GPS data.
@VisibleForTesting
public void stopGPS() {
gpsHandler.onStop();
}
public synchronized void reset() {
if (remoteSensorManager == null || altitudeSumManager == null) {
Log.d(TAG, "No recording running and no reset necessary.");
@@ -73,9 +73,7 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
private void setFilenameTemplate() {
EditTextPreference preference = findPreference(getString(R.string.export_filename_format_key));
preference.setOnBindEditTextListener(t -> {
t.setHint(getString(R.string.export_filename_format_default));
});
preference.setOnBindEditTextListener(t -> t.setHint(getString(R.string.export_filename_format_default)));
preference.setDialogMessage(TrackFilenameGenerator.getAllOptions());
preference.setOnPreferenceChangeListener((p, newValue) -> new TrackFilenameGenerator(newValue.toString()).isValid());
@@ -42,11 +42,14 @@ import java.util.stream.Stream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.HeartRateZones;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayoutIO;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -197,6 +200,14 @@ public class PreferencesUtils {
return getBoolean(R.string.publicapi_dashboard_enabled_key, resources.getBoolean(R.bool.publicapi_dashboard_enabled_default));
}
public static boolean shouldShowIntroduction() {
return getBoolean(R.string.show_introduction_screen_key, resources.getBoolean(R.bool.show_introduction_screen_default));
}
public static void setShowIntroduction(boolean introduction) {
setBoolean(R.string.show_introduction_screen_key, introduction);
}
public static UnitSystem getUnitSystem() {
final String STATS_UNIT_DEFAULT = resources.getString(R.string.stats_units_default);
@@ -270,6 +281,12 @@ public class PreferencesUtils {
return getBoolean(R.string.settings_sensor_bluetooth_service_filter_enabled_key, DEFAULT);
}
public static HeartRateZones getHeartRateZones() {
final int DEFAULT = Integer.parseInt(resources.getString(R.string.settings_sensor_heart_rate_max_default));
int value = getInt(R.string.settings_sensor_heart_rate_max_key, DEFAULT);
return new HeartRateZones(HeartRate.of(value));
}
public static boolean shouldShowStatsOnLockscreen() {
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = resources.getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
return getBoolean(R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
@@ -745,35 +762,35 @@ public class PreferencesUtils {
/**
* @return custom layout selected or the first one if any has been selected or the one selected is not exists anymore.
*/
public static Layout getCustomLayout() {
public static RecordingLayout getCustomLayout() {
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
String[] csvLines = csvCustomLayouts.split(CsvLayoutUtils.LINE_SEPARATOR);
String layoutSelected = getString(R.string.stats_custom_layout_selected_layout_key, null);
if (layoutSelected == null) {
return Layout.fromCsv(csvLines[0], resources);
return RecordingLayoutIO.fromCsv(csvLines[0], resources);
}
for (String line : csvLines) {
Layout layout = Layout.fromCsv(line, resources);
if (layout.sameName(layoutSelected)) {
return layout;
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsv(line, resources);
if (recordingLayout.sameName(layoutSelected)) {
return recordingLayout;
}
}
return Layout.fromCsv(csvLines[0], resources);
return RecordingLayoutIO.fromCsv(csvLines[0], resources);
}
public static void updateCustomLayouts(@NonNull List<Layout> layouts) {
setString(R.string.stats_custom_layouts_key, layouts.stream().map(Layout::toCsv).collect(Collectors.joining(CsvLayoutUtils.LINE_SEPARATOR)));
public static void updateCustomLayouts(@NonNull List<RecordingLayout> recordingLayouts) {
setString(R.string.stats_custom_layouts_key, RecordingLayoutIO.toCSV(recordingLayouts));
}
public static void updateCustomLayout(@NonNull Layout layout) {
List<Layout> preferenceLayouts = PreferencesUtils.getAllCustomLayouts();
Optional<Layout> layoutToBeUpdated = preferenceLayouts.stream().filter(l -> l.sameName(layout)).findFirst();
public static void updateCustomLayout(@NonNull RecordingLayout recordingLayout) {
List<RecordingLayout> preferenceRecordingLayouts = PreferencesUtils.getAllCustomLayouts();
Optional<RecordingLayout> layoutToBeUpdated = preferenceRecordingLayouts.stream().filter(l -> l.sameName(recordingLayout)).findFirst();
if (layoutToBeUpdated.isPresent()) {
layoutToBeUpdated.get().replaceAllFields(layout.getFields());
layoutToBeUpdated.get().setColumnsPerRow(layout.getColumnsPerRow());
PreferencesUtils.updateCustomLayouts(preferenceLayouts);
layoutToBeUpdated.get().replaceAllFields(recordingLayout.getFields());
layoutToBeUpdated.get().setColumnsPerRow(recordingLayout.getColumnsPerRow());
PreferencesUtils.updateCustomLayouts(preferenceRecordingLayouts);
}
}
@@ -787,19 +804,19 @@ public class PreferencesUtils {
setString(R.string.stats_custom_layout_selected_layout_key, layoutName);
}
public static List<Layout> getAllCustomLayouts() {
List<Layout> layouts = new ArrayList<>();
public static List<RecordingLayout> getAllCustomLayouts() {
List<RecordingLayout> recordingLayouts = new ArrayList<>();
String csvCustomLayout = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
String[] csvLines = csvCustomLayout.split(CsvLayoutUtils.LINE_SEPARATOR);
for (String line : csvLines) {
layouts.add(Layout.fromCsv(line, resources));
recordingLayouts.add(RecordingLayoutIO.fromCsv(line, resources));
}
return layouts;
return recordingLayouts;
}
public static List<String> getAllCustomLayoutNames() {
return getAllCustomLayouts().stream().map(Layout::getName).collect(Collectors.toList());
return getAllCustomLayouts().stream().map(RecordingLayout::getName).collect(Collectors.toList());
}
public static void resetCustomLayoutPreferences() {

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