Compare commits

...

161 Commits

Author SHA1 Message Date
Dennis Guse d6da5f0862 Release: v3.22.0 2021-11-14 21:28:31 +01:00
Dennis Guse c3d55127fe Bugfix: running sensor data total is distance is UINT_32. 2021-11-14 21:20:24 +01:00
Román Martínez b1032f898a New field for recording layout: clock.
With this change fields can be: short, wide, clock short or clock wide.
Fixes #1015.
2021-11-14 16:27:37 +01:00
Dennis Guse d4a3762641 Merge remote-tracking branch 'weblate/main' 2021-11-13 20:10:19 +01:00
Dennis Guse ce5eec6a68 Bugfix: prevent NPE when disabling voice announcements while recording.
Fixes #940.
2021-11-13 20:07:32 +01:00
FTno cf71829bec Translated using Weblate (Norwegian Bokmål)
Currently translated at 92.1% (70 of 76 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nb_NO/
2021-11-11 08:50:24 +01:00
FTno 39606ccd9f Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-11-11 08:50:24 +01:00
Dennis Guse 9dd67e4db7 CI: nightly build with flexible file names. 2021-11-11 08:50:02 +01:00
Peter Storch 212345dea1 Set distinct versionCode and name for nightlies 2021-11-11 08:50:02 +01:00
Dennis Guse 08ba7d50c7 Only record a TrackPoint if the traveled distance is greater than recording distance interval.
Code seemed to be a leftover from the removed idle/non-idle functionality.

Fixed #1007.

Co-authored-by: vlmendz <91810638+vlmendz@users.noreply.github.com>
2021-11-08 22:42:14 +01:00
Dennis Guse 62401b2c5c Cleanup. 2021-11-07 22:19:02 +01:00
Dennis Guse 7cfdacecc3 UI: Remove "Sensor:" prefix from speed description. 2021-11-07 17:58:28 +01:00
mondstern 8f2e234c63 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-11-04 23:06:38 +01:00
mondstern dcd101a20a Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-11-04 23:06:38 +01:00
mondstern cb5f3ace5a Translated using Weblate (Galician)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-11-04 23:06:37 +01:00
mondstern 8e889fbce2 Translated using Weblate (Czech)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-11-04 23:06:36 +01:00
mondstern ec390c4a81 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-11-04 01:55:09 +01:00
Vinicius 30e9e930ab Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.7% (438 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-11-03 06:08:52 +01:00
mondstern 3719d9ac8f Translated using Weblate (Czech)
Currently translated at 98.8% (429 of 434 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-11-03 06:08:51 +01:00
dependabot[bot] e33cfa8666 Bump actions/checkout from 2.3.5 to 2.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.5 to 2.4.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.5...v2.4.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 06:08:33 +01:00
Dennis Guse 8ff1db6a23 README: add link to Amazon App Store 2021-11-02 22:03:09 +01:00
Dennis Guse a9228017fc Setting: add option to adjust idle speed.
New default: 0.5km/h (replaces 0.5mp/h).

Fixes #1004.
2021-11-01 22:36:29 +01:00
Román Martínez 41e5e71804 Bugfix: intervals expected gain and loss to be null or not (both or neither). But only one of them could be null. Fixes #996. 2021-10-31 18:12:28 +01:00
Dennis Guse 9f858168f8 Release: v3.21.0 2021-10-29 07:54:59 +02:00
Andrij Mizyk cfdcea2d88 Translated using Weblate (Ukrainian)
Currently translated at 92.6% (402 of 434 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-10-29 05:58:50 +02:00
Quang Trung 2d2db41c5d Translated using Weblate (Vietnamese)
Currently translated at 100.0% (434 of 434 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-10-29 05:58:50 +02:00
Xosé M 2c73d0645a Translated using Weblate (Galician)
Currently translated at 100.0% (434 of 434 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-10-29 05:58:50 +02:00
☆Verdulo ed1027eccc Translated using Weblate (Esperanto)
Currently translated at 92.0% (69 of 75 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/eo/
2021-10-29 05:58:49 +02:00
Dennis Guse 0dec17eba5 Bugfix: prevent NPE in onDestroy. 2021-10-29 05:58:22 +02:00
dependabot[bot] 774d701a09 Bump reactivecircus/android-emulator-runner from 2.20.0 to 2.21.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.20.0 to 2.21.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.20.0...v2.21.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>
2021-10-25 20:18:39 +02:00
Dennis Guse 23bcbf09a5 GPX importer supports speed without namespace (compatibility with other tools).
Fixed #774.
2021-10-20 21:50:51 +02:00
Dennis Guse 115966cbdf Bugfix: NPE using a cycling sensor. 2021-10-20 19:44:47 +02:00
Dennis Guse 1862bcc6f4 Cleanup. 2021-10-19 17:53:08 +02:00
Dennis Guse 900910e9f7 While recording: show cadence and speed from different sensor types correctly.
Fixes #970.
2021-10-19 17:53:08 +02:00
Dennis Guse 45571c68c9 Rename TrackPoint's cycling cadence to cadence as it may contain running data.
Part of #970.
2021-10-19 17:53:08 +02:00
Román Martínez 083b620e41 GPS data recording: remove idle/non-idle changes due to reported speed greater than 0.5mph.
This avoids that to storing GPS locations if the user r actually is not moving (likely bad GPS reception).

Fixes #968.

Co-authored-by: vlmendz <91810638+vlmendz@users.noreply.github.com>
Co-authored-by: Dennis Guse <dennis.guse@alumni.tu-berlin.de>
2021-10-18 23:26:07 +02:00
Dennis Guse 2f7a56bb17 Bugfix: importing files without speed resulted in moving time 0s and max speed of 0.
Fixes #982.
2021-10-18 18:41:53 +02:00
dependabot[bot] d8988663cb Bump actions/checkout from 2.3.4 to 2.3.5
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.3.5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.4...v2.3.5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 12:42:34 +02:00
FTno c742dae6e0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (434 of 434 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-10-16 10:23:21 +02:00
FTno 292437e6bd Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (429 of 429 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-10-16 10:23:20 +02:00
J. Lavoie 8e9cde7222 Translated using Weblate (French)
Currently translated at 100.0% (429 of 429 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-10-16 10:23:20 +02:00
Dennis Guse d07ab3cc22 TrackRecordingManager: do not store altitude changes twice (previous …
…TrackPoint's storage was delayed).

Fixes #969.
2021-10-16 10:23:09 +02:00
Dennis Guse 90493ae498 TrackRecordingManager: do not store sensor distance twice (previous TrackPoint's storage was delayed).
Fixes #969.
2021-10-16 10:23:09 +02:00
Dennis Guse f6b994572a TrackRecordingManager: cache last stored TrackPoint. 2021-10-16 10:23:09 +02:00
Dennis Guse 24826ff442 TrackRecordingManager: cleanup. 2021-10-16 10:23:09 +02:00
Dennis Guse d97e5ab1ef TrackRecordingManager: only keep lastTrackPoint if was not stored. 2021-10-16 10:23:09 +02:00
Dennis Guse 52a110d998 Bugfix: reset on SensorDataSet should only take place if the TrackPoint was stored
Fixes #969.
2021-10-16 10:23:09 +02:00
Román Martínez 12e18b0bcd Bugfix: activity type preference has to update the icon when user change the activity type. Fixes #985. 2021-10-15 22:16:17 +02:00
Román Martínez 66dac4b0ca Reorganize settings into multiple pages. The main page has icons, titles and summaries. Fixes #960. 2021-10-15 22:04:54 +02:00
Dennis Guse 1bb8cbb82a Running sensor Stryd works.
Fixes #962.
2021-10-15 07:57:29 +02:00
Dennis Guse 2f5b7b63e4 Opening from notification: pressing back goes to TrackListActivity.
Fixes #928.
2021-10-14 18:33:44 +02:00
Ristello 58ca78b976 Removed interface AppConfig, since it is technically not needed. 2021-10-13 22:02:38 +02:00
Ristello 8079a25a42 Use an in-memory database when running instrumented tests.
- added new TestRunner and new TestApplication
- The TestRunner loads TestApplication as the apps Application instance when running the tests
- added new interface AppConfig to retrieve the database name
- changed Startup (Application) so that it implements AppConfig
- changed CustomSQLiteOpenHelper so that it retrieves the database name from the Application instance
- Startup provides the production database name
- TestApplication provides null which yields an in-mememory database
2021-10-13 22:02:38 +02:00
Dennis Guse 0527e4ab14 Cleanup: move AnnouncementUtils to VoiceAnnouncement.
Part of #963
2021-10-12 22:24:51 +02:00
Dennis Guse b6b4c5aa39 Cleanup: move PreferencesUtils into settings package.
Part of #963
2021-10-12 22:24:51 +02:00
Dennis Guse 8c5c9d7947 Cleanup: extract preference functionality from StringUtils to PreferenceHelper.
Part of #963.
2021-10-12 22:24:51 +02:00
Dennis Guse eac3d4b3db Cleanup: SharedPreference as static variable.
Fixes #961.
2021-10-12 22:24:51 +02:00
dependabot[bot] d7bc550b29 Bump gradle from 7.0.2 to 7.0.3
Bumps gradle from 7.0.2 to 7.0.3.

---
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>
2021-10-12 22:16:13 +02:00
Dennis Guse d76d5673a8 Bugfix: running sensor's data should be null if not connected.
Fixes #950.
2021-10-09 14:24:50 +02:00
dependabot[bot] 847be686ea Bump mockito-android from 3.12.4 to 4.0.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.12.4 to 4.0.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.12.4...v4.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-08 07:12:11 +02:00
Dennis Guse 1e3445cfa2 Nightly build: app name with suffix.
Part of #952.
2021-10-05 19:23:00 +02:00
Román Martínez d0f7939f1a Added phones with barometric sensor information. 2021-10-01 16:16:16 +02:00
vsevjednom-cz 000b8149f8 Translated using Weblate (Czech)
Currently translated at 99.0% (425 of 429 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-09-28 11:06:25 +02:00
Quang Trung 05ffa5a373 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (429 of 429 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-09-25 00:06:30 +02:00
Vinicius e92f1107aa Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (429 of 429 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-09-25 00:06:30 +02:00
Quang Trung 2ad1fab670 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (427 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-09-22 22:30:43 +02:00
mondstern 8c04129ef9 Translated using Weblate (Vietnamese)
Currently translated at 98.3% (420 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-09-22 22:30:43 +02:00
Mario 400d9fe948 Translated using Weblate (Spanish)
Currently translated at 100.0% (427 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-09-22 22:30:42 +02:00
Mario 5d79510750 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (427 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2021-09-22 22:30:42 +02:00
vsevjednom-cz 18bb64580f Translated using Weblate (Slovak)
Currently translated at 71.6% (306 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sk/
2021-09-22 22:30:42 +02:00
vsevjednom-cz 7adebfc0d1 Translated using Weblate (Czech)
Currently translated at 97.8% (418 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-09-22 22:30:42 +02:00
Román Martínez 2178ff97d3 Release: v3.20.2 2021-09-22 22:30:16 +02:00
Timo Schwarzer f070495fb4 Ask for permission before using Bluetooth 2021-09-22 22:14:53 +02:00
Román Martínez d99fb5ea47 Cadence set to 0 when connected but user is not pedaling. Fixes #931.
Speed set to 0 when connected but last value is outdated. Fixes #930.
Bug: overflow when reset sensors (cadence and speed). Fixes #833.
Moving time stops when is not moving (the last track point is not moving) and restarts when last track point is moving.
2021-09-21 18:07:23 +02:00
Román Martínez d69a726e0e Renaming variables. 2021-09-18 16:17:29 +02:00
Román Martínez e0111c2ffd Gain and loss are integers. 2021-09-18 16:17:29 +02:00
Timo Schwarzer 487bd6c3a0 Set FLAG_IMMUTABLE for PendingIntents
This fixes a crash on Android 12, since it explicitly requires setting either FLAG_MUTABLE or FLAG_IMMUTABLE
2021-09-07 23:18:14 +02:00
Román Martínez 40aab9444a Time interval setting is in minutes. 2021-09-06 22:06:48 +02:00
Dennis Guse 82b460b57b Release: v3.20.1 2021-09-03 19:46:51 +02:00
Mario 68baad16e2 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (427 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2021-09-02 21:55:28 +02:00
Dennis Guse 26ed9b24d3 CI: build nightly on push.
Fixes #894.
2021-09-02 21:55:25 +02:00
dependabot[bot] 9f6089e7f1 Bump gradle from 7.0.1 to 7.0.2
Bumps gradle from 7.0.1 to 7.0.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-02 21:55:15 +02:00
FTno d0eef44bf8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (427 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-08-31 08:45:48 +02:00
Mario eead3b5721 Translated using Weblate (Spanish)
Currently translated at 100.0% (427 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-08-31 06:47:45 +02:00
Mario b8f0c59198 Translated using Weblate (Spanish (Latin America))
Currently translated at 99.5% (425 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2021-08-31 06:47:45 +02:00
vsevjednom-cz 8f408dceba Translated using Weblate (Slovak)
Currently translated at 68.7% (292 of 425 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sk/
2021-08-31 06:47:45 +02:00
vsevjednom-cz d1dfaa5443 Translated using Weblate (Czech)
Currently translated at 96.7% (411 of 425 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-08-31 06:47:45 +02:00
vsevjednom-cz b2da849e25 Translated using Weblate (Czech)
Currently translated at 95.7% (407 of 425 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-08-31 06:47:45 +02:00
Xosé M 88579be0a2 Translated using Weblate (Galician)
Currently translated at 100.0% (427 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-08-31 06:47:45 +02:00
FTno 70d971d8fd Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (427 of 427 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-08-31 06:47:45 +02:00
mondstern 70b0dac4b6 Translated using Weblate (Tagalog)
Currently translated at 72.7% (297 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tl/
2021-08-31 06:47:45 +02:00
Carl Dea 038e8d3e06 Using Zulu builds of OpenJDK in GH actions
The AdoptOpenJDK has been discontinued since July 2021. When using Zulu you get all the latest updated (TCK Tested) builds for all versions of OpenJDK.
2021-08-31 06:46:34 +02:00
Dennis Guse 932bb5ed5a KMLTrackExporter: uses NumberFormat to export sensor data.
Fixes #832.
2021-08-30 17:48:01 +02:00
Dennis Guse 8f2d1722ad Announcements: can be triggered by time and distance (instead of either).
Fixes #921.
2021-08-30 17:24:51 +02:00
Dennis Guse 9cb3d8c1bb Finished renaming: TrackPointCreator was still referenced as HandlerServer. 2021-08-30 17:24:51 +02:00
Dennis Guse fa02ba9c69 Voice announcements: if TTS is not available play a sound file.
Fixes #862.
2021-08-30 17:24:51 +02:00
Dennis Guse 4f31607e3e Voice announcements are now triggered by the TrackRecordingService's timer that is also used for UI updates. 2021-08-30 17:24:51 +02:00
Dennis Guse b5165de7f0 PeriodicTaskExecutor: use a Handler instead of Timer. 2021-08-30 17:24:51 +02:00
Dennis Guse b32e87e10b PeriodTaskFactory: refactor interfaces. 2021-08-30 17:24:51 +02:00
dependabot[bot] 29a1daa177 Bump reactivecircus/android-emulator-runner from 2.19.1 to 2.20.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.19.1 to 2.20.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.19.1...v2.20.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>
2021-08-30 17:23:53 +02:00
Dennis Guse d5cfd118f6 Cleanup. 2021-08-28 19:00:06 +02:00
Dennis Guse 5981340bb9 TrackRecordingManager: only keeps reference to TrackId. 2021-08-28 19:00:06 +02:00
dependabot[bot] f562d51d9a Bump mockito-android from 3.12.3 to 3.12.4
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.12.3 to 3.12.4.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.12.3...v3.12.4)

---
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>
2021-08-26 12:15:22 +02:00
Dennis Guse d77b9243af RecordingStatus provides methods for status changes. 2021-08-25 22:58:45 +02:00
dependabot[bot] 2c7e665206 Bump mockito-android from 3.12.1 to 3.12.3
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.12.1 to 3.12.3.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.12.1...v3.12.3)

---
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>
2021-08-25 07:00:23 +02:00
Dennis Guse 1aa6b94f19 Revert "Translated using Weblate (Slovenian)"
This reverts commit 8f39bf13
2021-08-24 18:38:12 +02:00
Dennis Guse 68c447f77e Removed AdaptiveLocationListenerPolicy.
Fixes #886.
2021-08-24 18:26:09 +02:00
Román Martínez 6b3a2eae5e Added Garmin HRM-Dual heart rate sensor 2021-08-23 13:13:11 +02:00
dependabot[bot] 3869dba72b Bump mockito-android from 3.12.0 to 3.12.1
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.12.0 to 3.12.1.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.12.0...v3.12.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>
2021-08-23 12:02:42 +02:00
Dennis Guse 5594a125d0 Release: v3.20.0 2021-08-22 22:08:23 +02:00
Román Martínez 13380afef0 TrackRecordingManager only updates track's statistics every time a new track point is inserted. Fixes #916. 2021-08-22 20:01:38 +02:00
Román Martínez 9c4c8131f5 Recording stats layout customizable.
Users can select the order, visibility and importance (primary or secondary) of all stats for recording layout from the Settings.
Also, users can select the number of columns per row in the recording layout: 1, 2 or 3.
Finally, show altitude/coordinate from settings have been removed (not needed anymore).
Fixes #659.
Fixes #302.
Fixes #820.
2021-08-21 19:40:31 +02:00
dependabot[bot] 7f10ae5c3f Bump mockito-android from 3.10.0 to 3.12.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.10.0 to 3.12.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.10.0...v3.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-20 07:37:15 +02:00
dependabot[bot] 1fa45a3393 Bump gradle from 7.0.0 to 7.0.1
Bumps gradle from 7.0.0 to 7.0.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>
2021-08-19 10:11:22 +02:00
Dennis Guse f1b0c83196 Merge branch 'translations' 2021-08-16 22:17:29 +02:00
Dennis Guse f0f7bca54b Translations: fix fr. 2021-08-16 22:16:58 +02:00
mondstern 8f39bf139c Translated using Weblate (Slovenian)
Currently translated at 71.5% (292 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sl/
2021-08-16 22:15:27 +02:00
mondstern 85585969bd Translated using Weblate (Russian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-08-16 22:15:27 +02:00
mondstern 976e7be370 Translated using Weblate (French)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-08-16 22:15:27 +02:00
Jaroslav Lichtblau 48dc64d4d7 Translated using Weblate (Czech)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-08-16 22:15:27 +02:00
Pablo 16e69e850b Translated using Weblate (Spanish (Latin America))
Currently translated at 98.2% (401 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2021-08-16 22:15:27 +02:00
FTno 5ece301ac0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.5% (70 of 71 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nb_NO/
2021-08-16 22:15:27 +02:00
☆Verdulo a5e500cdb1 Translated using Weblate (Esperanto)
Currently translated at 97.1% (69 of 71 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/eo/
2021-08-16 22:15:27 +02:00
Román Martínez 8433ed82fe Release: v3.19.1 2021-08-15 20:19:08 +02:00
Dennis Guse 7c7defa233 Merge pull request #912 from OpenTracksApp/raceCondition
Bugfix: LocationHandler should ignore locations after stopping.
2021-08-15 15:53:30 +02:00
Dennis Guse 1f84e44a8d Bugfix: LocationHandler should ignore locations after stopping. 2021-08-15 14:30:57 +02:00
Dennis Guse 858a8f20a8 Merge pull request #910 from OpenTracksApp/translations
Translations
2021-08-14 20:36:20 +02:00
Dennis Guse 6761c41dbf Translations: removed invalid plurals. 2021-08-14 19:46:40 +02:00
FTno 59e55be214 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.5% (70 of 71 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nb_NO/
2021-08-09 20:06:09 +02:00
☆Verdulo 1b68cba1ba Translated using Weblate (Esperanto)
Currently translated at 97.1% (69 of 71 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/eo/
2021-08-09 20:06:08 +02:00
mondstern 32f47340a9 Translated using Weblate (Latvian)
Currently translated at 99.0% (404 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/lv/
2021-08-09 20:06:07 +02:00
mondstern e25814c674 Translated using Weblate (Lithuanian)
Currently translated at 98.2% (401 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/lt/
2021-08-09 20:06:05 +02:00
FTno 522baa5193 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-08-09 11:54:21 +02:00
mondstern b597b56b84 Translated using Weblate (Romanian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ro/
2021-08-07 19:37:23 +02:00
mondstern d956e8d3f0 Translated using Weblate (Romanian)
Currently translated at 99.0% (404 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ro/
2021-08-07 19:37:23 +02:00
mondstern 274781ccfa Translated using Weblate (Romanian)
Currently translated at 83.3% (340 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ro/
2021-08-07 19:37:23 +02:00
mondstern e7e3ef23ff Translated using Weblate (Lithuanian)
Currently translated at 97.5% (398 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/lt/
2021-08-07 19:37:23 +02:00
Quang Trung 3289fa27e4 Translated using Weblate (Vietnamese)
Currently translated at 88.7% (362 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-08-07 19:37:23 +02:00
mondstern 96fc074a4b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-08-07 19:37:23 +02:00
mondstern eaab09747c Translated using Weblate (Swedish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sv/
2021-08-07 19:37:23 +02:00
mondstern bdb2a9dbc6 Translated using Weblate (Russian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-08-07 19:37:23 +02:00
mondstern 7e26571124 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-08-07 19:37:23 +02:00
mondstern 33b23a6e8f Translated using Weblate (Polish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-08-07 19:37:23 +02:00
mondstern 584adc7c57 Translated using Weblate (Dutch)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-08-07 19:37:23 +02:00
mondstern 667da76c4f Translated using Weblate (Italian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-08-07 19:37:23 +02:00
mondstern df6a3a7f04 Translated using Weblate (Hungarian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2021-08-07 19:37:23 +02:00
mondstern b254f5e40b Translated using Weblate (Galician)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-08-07 19:37:23 +02:00
mondstern 7a9c3efb3b Translated using Weblate (French)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-08-07 19:37:23 +02:00
mondstern 53dd7ada1f Translated using Weblate (Finnish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-08-07 19:37:23 +02:00
mondstern 6a5edcbdef Translated using Weblate (Basque)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-08-07 19:37:23 +02:00
mondstern 6e954261bb Translated using Weblate (Estonian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-08-07 19:37:23 +02:00
mondstern 277061ad27 Translated using Weblate (Spanish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-08-07 19:37:23 +02:00
mondstern c03f9fc792 Translated using Weblate (Danish)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-08-07 19:37:23 +02:00
mondstern 657b82b57a Translated using Weblate (Czech)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-08-07 19:37:23 +02:00
mondstern 4608edec56 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/bg/
2021-08-07 19:37:23 +02:00
FTno b086c30696 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-08-07 19:37:23 +02:00
mondstern 7eca033a8e Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (408 of 408 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-08-07 19:37:23 +02:00
Dennis Guse 6f3efd9ff2 Bugfix: reset TrackRecordingManager properly.
Fixes #901.
2021-08-07 19:37:14 +02:00
A Rabbit 7a2741e508 Fix: README 2021-08-06 16:42:22 +02:00
Román Martínez 017eee8b68 Bugfix: generated fields for include and merge tags. Fixes #902. 2021-08-05 18:01:34 +02:00
Dennis Guse 024d7f8652 PlayStore: describe metadata upload. 2021-08-04 17:11:29 +02:00
236 changed files with 6495 additions and 3967 deletions
+34
View File
@@ -0,0 +1,34 @@
#https://github.com/marketplace/actions/android-emulator-runner
name: Build nightly
on: [ push ]
jobs:
nightly-build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2.4.0
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Assemble nighlty APK
shell: bash
run: |
echo ${NIGHTLY_STORE_FILE} | base64 -d > KEY_NIGHTLY.jks
./gradlew assembleNightly -Dnightly_store_file=KEY_NIGHTLY.jks -Dnightly_store_password="${NIGHTLY_STORE_PASSWORD}" -Dnightly_key_alias="${NIGHTLY_KEY_ALIAS}" -Dnightly_key_password="${NIGHTLY_STORE_PASSWORD}"
env:
NIGHTLY_STORE_FILE: ${{ secrets.NIGHTLY_STORE_FILE }}
NIGHTLY_STORE_PASSWORD: ${{ secrets.NIGHTLY_STORE_PASSWORD }}
NIGHTLY_KEY_ALIAS: ${{ secrets.NIGHTLY_KEY_ALIAS }}
- name: Archive
if: always()
uses: actions/upload-artifact@v2
with:
name: OpenTracks-nightly.apk
path: ./build/outputs/apk/nightly/*.apk
retention-days: 7
+2 -2
View File
@@ -9,11 +9,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2.3.4 uses: actions/checkout@v2.4.0
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'adopt' distribution: 'zulu'
java-version: '11' java-version: '11'
- run: ./gradlew lint - run: ./gradlew lint
+3 -3
View File
@@ -11,15 +11,15 @@ jobs:
target: [ default ] target: [ default ]
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2.3.4 uses: actions/checkout@v2.4.0
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'adopt' distribution: 'zulu'
java-version: '11' java-version: '11'
- name: run tests - name: run tests
uses: reactivecircus/android-emulator-runner@v2.19.1 uses: reactivecircus/android-emulator-runner@v2.21.0
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }} target: ${{ matrix.target }}
+17 -8
View File
@@ -47,20 +47,29 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</td> </td>
<td></td> <td></td>
</tr> </tr>
<tr>
<td></td>
<td align="center">
<a href="https://www.amazon.de/dp/B09K2KNQ7X/">
<img alt="Get it on Amazon App Store" src="https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-appstore-badge-english-white.png" height="40" align="middle">
</a>
</td>
<td></td>
</tr>
</table> </table>
## Screenshots ## Screenshots
<div> <div>
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png"> <img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot2.png"> <img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot3.png"> <img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot3.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot4.png"> <img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot4.png">
</div> </div>
<div> <div>
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot5.png"> <img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot5.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot6.png"> <img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot6.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot7.png"> <img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot7.png">
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png"> <img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot8.png">
</div> </div>
## Features ## Features
+4 -3
View File
@@ -28,9 +28,10 @@ Requirement: `gem install fastlane`
(requires keystore) (requires keystore)
2. Upload signed apk to [Google Play Console](https://play.google.com/apps/publish) 2. Upload signed apk to [Google Play Console](https://play.google.com/apps/publish)
3. Update store meta data (e.g., description and screenshots) 3. Update store meta data (e.g., description and screenshots)
`fastlane supply --version_code VERSION_CODE --skip_upload_apk` `fastlane metadata`
(use the version code from the manifest) (requires API key configured in `Appfile`)
(requires API key)
Supported languages: https://support.google.com/googleplay/android-developer/table/4419860
## Translations (localization) ## Translations (localization)
+26 -4
View File
@@ -1,6 +1,6 @@
# Overview of tested Bluetooth LE sensors # Overview of tested Bluetooth LE sensors
## HRS: Heartrate Service ## 0x180D: Heartrate Service
* Decathlon|Geonaute|Kalenji Dual HRM Belt * Decathlon|Geonaute|Kalenji Dual HRM Belt
* moofit heart rate monitor * moofit heart rate monitor
@@ -11,8 +11,9 @@
* Wahoo Tickr (Model: WFBTHR02) * Wahoo Tickr (Model: WFBTHR02)
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor) * Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
* Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again) * Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again)
* Garmin HRM-Dual (Reference: 010-12883-00)
## CSCP: Cycling Cadence and Speed Service ## 0x1816: Cycling Cadence and Speed Service
Please note that the according to the specification these sensors _may_ provide speed as well as cadence. Please note that the according to the specification these sensors _may_ provide speed as well as cadence.
However, often only one value is provided. However, often only one value is provided.
@@ -35,7 +36,7 @@ However, often only one value is provided.
This sensor reports cadence data as speed. This sensor reports cadence data as speed.
A workaround is in place. A workaround is in place.
## Cycling Power Measurement ## 0x1818: Power Measurement
We only support _Instantaneous Power_. We only support _Instantaneous Power_.
@@ -43,6 +44,7 @@ _WARNING_: we only support physically movements with valid GPS coordinates.
Using a device indoors (no GPS available) will only show the current power values, but won't store anything. Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
Also the distance is not computed. Also the distance is not computed.
For cycling:
* Assioma Duo Powermeter * Assioma Duo Powermeter
* Elite drivo II * Elite drivo II
* QUARQ Red DZero Powermeter * QUARQ Red DZero Powermeter
@@ -50,8 +52,28 @@ Also the distance is not computed.
* Wahoo Kickr v4.0 * 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 propietary @ firmware v1.061)
## RSC: running cadence and speed For running:
* Stryd
## 0x1814: running cadence and speed
We do not support type of movement (i.e., walking vs running). We do not support type of movement (i.e., walking vs running).
* Polar Stride * Polar Stride
* Stryd (incl. power)
## Tested Barometric Sensor Smartphones
There are smartphones that have a barometric sensor that OpenTracks uses to compute altitude gain and loss.
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|
+35 -6
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.0' classpath 'com.android.tools.build:gradle:7.0.3'
} }
} }
@@ -30,6 +30,12 @@ def getVersionName = { ->
} }
} }
def versionCodeFromDate = { ->
def date = new Date()
def formattedDate = date.format('yyyyMMdd')
return Integer.valueOf(formattedDate)
}
android { android {
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
@@ -41,7 +47,7 @@ android {
compileSdkVersion 31 compileSdkVersion 31
buildFeatures { buildFeatures {
viewBinding = true viewBinding true
} }
aaptOptions { aaptOptions {
@@ -50,8 +56,8 @@ android {
defaultConfig { defaultConfig {
applicationId "de.dennisguse.opentracks" applicationId "de.dennisguse.opentracks"
versionCode 4358 versionCode 4519
versionName "v3.19.0" versionName "v3.22.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -59,9 +65,19 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
} }
signingConfigs {
nightly {
if (System.getProperty("nightly_store_file") != null) {
storeFile file(System.getProperty("nightly_store_file"))
storePassword System.getProperty("nightly_store_password")
keyAlias System.getProperty("nightly_key_alias")
keyPassword System.getProperty("nightly_key_password")
}
}
}
buildTypes { buildTypes {
debug { debug {
@@ -69,6 +85,11 @@ android {
versionNameSuffix "-debug" versionNameSuffix "-debug"
} }
nightly {
signingConfig signingConfigs.nightly
applicationIdSuffix ".nightly"
}
release { release {
minifyEnabled false minifyEnabled false
} }
@@ -81,6 +102,14 @@ android {
applicationVariants.all { variant -> applicationVariants.all { variant ->
variant.resValue "string", "applicationId", variant.applicationId variant.resValue "string", "applicationId", variant.applicationId
if (variant.buildType.name == 'nightly') {
variant.outputs.all {
setVersionCodeOverride(versionCodeFromDate())
setVersionNameOverride(getVersionName())
outputFileName = "${applicationId}_${variant.versionCode}.apk"
}
}
} }
} }
@@ -98,7 +127,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'org.mockito:mockito-android:3.10.0' androidTestImplementation 'org.mockito:mockito-android:4.0.0'
androidTestUtil 'androidx.test:orchestrator:1.4.0' androidTestUtil 'androidx.test:orchestrator:1.4.0'
} }
+20 -1
View File
@@ -43,7 +43,6 @@ class TrackPoint {
- sensor_power - sensor_power
} }
class Marker { class Marker {
- id - id
- trackId - trackId
@@ -58,6 +57,26 @@ class Marker {
- photoUrl - photoUrl
} }
class DataField {
- key
- title
- isVisible
- isPrimary
- isWide
}
class StatisticData {
- value
- unit
- description
}
StatisticData o-- DataField
class Layout {
- profile
}
Layout "1" o-- "many" DataField
note left of TrackPoint note left of TrackPoint
As of OpenTracks version 3.15.0, all times are using device time. As of OpenTracks version 3.15.0, all times are using device time.
Before that TrackPoint.time contained GPS time (determined by GPS hardware). Before that TrackPoint.time contained GPS time (determined by GPS hardware).
+3
View File
@@ -0,0 +1,3 @@
README.md
Appfile
report.xml
+6 -18
View File
@@ -16,23 +16,11 @@
default_platform(:android) default_platform(:android)
platform :android do platform :android do
desc "Runs all the tests" desc "Upload metadata"
lane :test do lane :metadata do
gradle(task: "test") upload_to_play_store(
end skip_upload_apk: true,
skip_upload_changelogs: true
desc "Submit a new Beta Build to Crashlytics Beta" )
lane :beta do
gradle(task: "clean assembleRelease")
crashlytics
# sh "your_script.sh"
# You can also use other beta testing services here
end
desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(task: "clean assembleRelease")
upload_to_play_store
end end
end end
@@ -0,0 +1,8 @@
v3.19.1: OpenTracks
Changes:
- Updated translations.
Bugfix:
- Export All Tracks bug fixed.
- Resume Track bug fixed.
@@ -0,0 +1,9 @@
v3.20.0: OpenTracks
Changes:
- While recording: statistics UI is now configurable
Bugfix:
- Changing track name/ category while recording was ignored (introduced in v3.19.0)
@@ -0,0 +1,7 @@
v3.20.1: OpenTracks
Changes:
- Announcements can now be used by both distance and time
- Announcements beep if no Text-to-speech software is installed
- Removed adaptive GPS location frequency
@@ -0,0 +1,11 @@
v3.20.2: OpenTracks
Changes:
- Elevation gain and loss are integers instead of floats
- Ask for permission before using Bluetooth by a Toast message
Bugfix:
- Set FLAG_IMMUTABLE for PendingIntents to avoid crash on Android 12
- Cadence sensor returns 0 instead of None when user is not pedaling
- Speed sensor returns 0 instead of the last speed when it's stopped
- With sensors, moving time stops when is not moving
@@ -0,0 +1,19 @@
v3.21.0: OpenTracks
Changes:
- While recording: remove idle/non-idle creation of TrackPoints
- Settings are now in separate folders
- Opening OpenTracks from notification (while recording) and pressing back opens track list
- GPX compatibilty: support <speed> for import
Bugfixes:
- Altitude and distance (from BlE sensor): only reset counters if data was actually stored
- Import without speed, requires computing speed
- BLE running sensor not connected, should not be reported as present
- BLE running sensor: show data while recording
- Activity type icon was not updated while changing activity
Developer:
- SharedPrefences via static reference
- Tests are executed against in-memory database
- TrackRecordingManager keeps last TrackPoint in memory
@@ -0,0 +1,11 @@
v3.22.0: OpenTracks
Changes:
- Locations are only stored if traveled distance > recording distance interval
(removed left over functionality from idle/non-idle detection)
- UI can be configured to show a clock
- Idle speed is now configurable
Bugfix:
- Intervals: crash when gain but not loss was stored
- While recording: prevent crash when disabling voice announcements

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 574 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 456 KiB

Before

Width:  |  Height:  |  Size: 630 KiB

After

Width:  |  Height:  |  Size: 630 KiB

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 416 KiB

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 508 KiB

Before

Width:  |  Height:  |  Size: 798 KiB

After

Width:  |  Height:  |  Size: 798 KiB

@@ -1,11 +1,11 @@
OpenTracks estas aplikaĵo por registri vian sportan aktivecon, kiu plene respektas vian privatecon. OpenTracks estas aplikaĵo por registri vian sportan aktivecon, kiu plene respektas vian privatecon.
Eblaĵoj: Ebloj:
• registri vian sportan kaj alian aktivecon eksterdome • registri vian sportan kaj alian aktivecon eksterdome
• fotoj kaj markoj: marki interesajn lokojn dum registri aktivecon • fotoj kaj markoj: marki interesajn lokojn dum registri aktivecon
• registri ŝanĝon de altitudo uzante barometron • registri ŝanĝon de altitudo uzante barometron
• montri altitudon en EGM2008 ()super averaĝa marnivelo); elporti ilin kiel WGS84 • montri altitudon en EGM2008 (super averaĝa marnivelo); elporti ilin kiel WGS84
• voĉaj anoncoj • voĉaj anoncoj
• subteno por sentiloj BLE por registri vian pulson; rapidon, veturigitan distancon, pedal-rapidon, povumon (biciklado) • subteno por sentiloj BLE por registri vian pulson, rapidon, veturigitan distancon, pedal-rapidon, povumon (biciklado)
• elporti datumojn al dosieroj KMZ (kun fotoj), KML aŭ GPX • elporti datumojn al dosieroj KMZ (kun fotoj), KML aŭ GPX
• funkcias malkonekte: interreta permeso ne estas uzata • funkcias malkonekte: interreta permeso ne estas uzata
@@ -1,9 +1,11 @@
OpenTracks er en app for opptak av sportsaktivitet som respekterer ditt personvern.
Funksjoner: Funksjoner:
* Sportssoring: Spor dine sports- og utendørsaktiviteter * Sportsporing: Registrer dine sports- og utendørsaktiviteter
* Bilder og markører: Marker interessante steder under sporing * Bilder og markører: Marker interessante steder under sporing
* Høydeendringer: via barometer * Total stigning og nedstigning: via barometersensor
* Høyde vist i EGM2008 (over havnivå); eksportert som WGS84 * Høyde vist i EGM2008 (over havnivå); eksportert som WGS84
* Stemmekunngjøringer * Talemeldinger
* Støtter Blåtann LE-sensorer: pulse, hastighet og distanse (sykling), takt (sykling), og effektmeter (sykling) * Støtter Blåtann LE-sensorer: puls, hastighet og distanse (sykling), kadens (sykling) og wattmåler (sykling)
* Eksport av data: Enten som KMZ (inkl. bilder), KML, eller GPX * Eksport av data: Enten som KMZ (inkl. bilder), KML eller GPX
* Ingen tilgang til Internett: Ingenting lastes opp noensteds hen * Ingen internettilgang: Internett brukes ikke
@@ -1 +1 @@
En sportsregistrerer som respekterer ditt personvern. En opptaker av sportsaktivitet som respekterer ditt personvern.
@@ -1,191 +0,0 @@
package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.is;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.test.espresso.DataInteraction;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
/**
* Espresso test that checks the unit for activity category selected.
* Also it checks that unit is properly when user change from a pace activity to an speed one.
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoCategoryUnitTest {
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public TestRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
@Rule
public TestRule mLocaleRule = new PreferenceMetricUnitRule(context, true);
@Test
public void changeBetweenCategoryUpdateProperlyUnitTest() {
// Start record.
ViewInteraction appCompatImageButton = onView(
allOf(withId(R.id.controller_record), withContentDescription("Record"),
childAtPosition(
allOf(withId(R.id.controller_container),
childAtPosition(
withId(R.id.controller_fragment),
0)),
0),
isDisplayed()));
appCompatImageButton.perform(click());
{
// Change to running activity (pace activity).
ViewInteraction appCompatImageView = onView(
allOf(withId(R.id.stats_activity_type_icon),
childAtPosition(
childAtPosition(
withClassName(is("android.widget.ScrollView")),
0),
7)));
appCompatImageView.perform(scrollTo(), click());
DataInteraction imageView = onData(anything())
.inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view),
childAtPosition(
withClassName(is("android.widget.LinearLayout")),
0)))
.atPosition(1);
imageView.perform(click());
}
// Pace (min/km) in all fields.
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit)));
textView.perform(waitFor(2000));
textView.check(matches(withText("min/km")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit)));
textView.check(matches(withText("min/km")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit)));
textView.check(matches(withText("min/km")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit)));
textView.check(matches(withText("min/km")));
}
{
// Change to biking activity (speed activity).
ViewInteraction appCompatImageView = onView(
allOf(withId(R.id.stats_activity_type_icon),
childAtPosition(
childAtPosition(
withClassName(is("android.widget.ScrollView")),
0),
7)));
appCompatImageView.perform(scrollTo(), click());
DataInteraction imageView = onData(anything())
.inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view),
childAtPosition(
withClassName(is("android.widget.LinearLayout")),
0)))
.atPosition(9);
imageView.perform(click());
}
// Speed (km/h) in all fields.
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit)));
textView.perform(waitFor(2000));
textView.check(matches(withText("km/h")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit)));
textView.check(matches(withText("km/h")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit)));
textView.check(matches(withText("km/h")));
}
{
ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit)));
textView.check(matches(withText("km/h")));
}
}
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
private static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Wait for milliseconds.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
}
};
}
}
@@ -0,0 +1,136 @@
package de.dennisguse.opentracks;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewAssertion;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import android.content.Context;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hamcrest.CoreMatchers.is;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoCustomLayoutTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Rule
public ActivityTestRule<TrackListActivity> mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule =
GrantPermissionRule.grant(
"android.permission.ACCESS_FINE_LOCATION");
@Test
public void customLayoutTest() {
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
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.controller_stop));
trackControllerStopButton.perform(veryLongTouch(1600));
}
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));
}
}
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Perform long touch.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
// Get view absolute position
int[] location = new int[2];
view.getLocationOnScreen(location);
// Offset coordinates by view position
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
// Send down event, pause, and send up
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
uiController.loopMainThreadForAtLeast(duration_ms);
MotionEvents.sendUp(uiController, down, coordinates);
}
};
}
}
@@ -143,7 +143,7 @@ public class EspressoDeleteTrackTest {
private static Matcher<View> childAtPosition( private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) { final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() { return new TypeSafeMatcher<>() {
@Override @Override
public void describeTo(Description description) { public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent "); description.appendText("Child at position " + position + " in parent ");
@@ -160,12 +160,14 @@ public class EspressoDeleteTrackTest {
} }
public static Matcher<View> withListSize (final int size) { public static Matcher<View> withListSize (final int size) {
return new TypeSafeMatcher<View> () { return new TypeSafeMatcher<>() {
@Override public boolean matchesSafely (final View view) { @Override
public boolean matchesSafely(final View view) {
return ((ListView) view).getCount() == size; return ((ListView) view).getCount() == size;
} }
@Override public void describeTo (final Description description) { @Override
public void describeTo(final Description description) {
description.appendText("ListView should have " + size + " items"); description.appendText("ListView should have " + size + " items");
} }
}; };
@@ -0,0 +1,261 @@
package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.core.IsInstanceOf;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeoutException;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoEditTrackRecordingTest {
@Rule
public ActivityTestRule<TrackListActivity> mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule =
GrantPermissionRule.grant(
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION");
@LargeTest
@Test
public void espressoEditTrackRecordingTest() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
// wait; stay recording
trackControllerRecordButton.perform(waitFor(15000));
// open menu
ViewInteraction overflowMenuButton = onView(
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(R.id.toolbar),
2),
2),
isDisplayed()));
overflowMenuButton.perform(click());
// click on edit
ViewInteraction appCompatTextView = onView(
allOf(withId(R.id.title), withText("Edit"),
childAtPosition(
childAtPosition(
withId(R.id.content),
0),
0),
isDisplayed()));
appCompatTextView.perform(click());
// change name for "New Name"
ViewInteraction textInputEditText = onView(
allOf(withId(R.id.track_edit_name),
childAtPosition(
childAtPosition(
withClassName(is("com.google.android.material.textfield.TextInputLayout")),
0),
1)));
textInputEditText.perform(scrollTo(), replaceText("New Name"));
ViewInteraction textInputEditText2 = onView(
allOf(withId(R.id.track_edit_name), withText("New Name"),
childAtPosition(
childAtPosition(
withClassName(is("com.google.android.material.textfield.TextInputLayout")),
0),
1),
isDisplayed()));
textInputEditText2.perform(closeSoftKeyboard());
// save edition
ViewInteraction appCompatButton = onView(
allOf(withId(R.id.track_edit_save),
childAtPosition(
childAtPosition(
withClassName(is("android.widget.LinearLayout")),
3),
1),
isDisplayed()));
appCompatButton.perform(click());
//pause
trackControllerRecordButton.perform(veryLongTouch(1600));
// wait; stay paused
trackControllerRecordButton.perform(waitFor(1000));
// resume
trackControllerRecordButton.perform(click());
// wait; stay recording
trackControllerRecordButton.perform(waitFor(1000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
// check track's name is "New Name"
ViewInteraction textView = onView(
allOf(withId(R.id.stats_name_value),
withParent(withParent(IsInstanceOf.instanceOf(android.widget.ScrollView.class))),
isDisplayed()));
textView.check(matches(withText("New Name")));
}
}
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
public static ViewAction waitId(final int viewId, final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
}
@Override
public void perform(final UiController uiController, final View view) {
uiController.loopMainThreadUntilIdle();
final long startTime = System.currentTimeMillis();
final long endTime = startTime + millis;
final Matcher<View> viewMatcher = withId(viewId);
do {
for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
// found view with required ID
if (viewMatcher.matches(child)) {
return;
}
}
uiController.loopMainThreadForAtLeast(50);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(new TimeoutException())
.build();
}
};
}
private static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Wait for milliseconds.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
}
};
}
private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Perform long touch.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
// Get view absolute position
int[] location = new int[2];
view.getLocationOnScreen(location);
// Offset coordinates by view position
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
// Send down event, pause, and send up
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
uiController.loopMainThreadForAtLeast(duration_ms);
MotionEvents.sendUp(uiController, down, coordinates);
}
};
}
}
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks; package de.dennisguse.opentracks;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -9,7 +8,7 @@ import org.junit.rules.TestRule;
import org.junit.runner.Description; import org.junit.runner.Description;
import org.junit.runners.model.Statement; import org.junit.runners.model.Statement;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
public class PreferenceMetricUnitRule implements TestRule { public class PreferenceMetricUnitRule implements TestRule {
private final Context context; private final Context context;
@@ -25,14 +24,13 @@ public class PreferenceMetricUnitRule implements TestRule {
return new Statement() { return new Statement() {
@Override @Override
public void evaluate() throws Throwable { public void evaluate() throws Throwable {
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); final boolean previousMetricUnits = PreferencesUtils.isMetricUnits();
final boolean previousMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
try { try {
PreferencesUtils.setMetricUnits(sharedPreferences, context, metricUnits); PreferencesUtils.setMetricUnits(metricUnits);
base.evaluate(); base.evaluate();
} finally { } finally {
PreferencesUtils.setMetricUnits(sharedPreferences, context, previousMetricUnits); PreferencesUtils.setMetricUnits(previousMetricUnits);
} }
} }
}; };
@@ -0,0 +1,9 @@
package de.dennisguse.opentracks;
public class TestApplication extends Startup {
@Override
public String getDatabaseName() {
// null will make SQLiteOpenHelper create an in-memory database
return null;
}
}
@@ -0,0 +1,13 @@
package de.dennisguse.opentracks;
import android.app.Application;
import android.content.Context;
import androidx.test.runner.AndroidJUnitRunner;
public class TestRunner extends AndroidJUnitRunner {
@Override
public Application newApplication(ClassLoader cl, String className, Context context) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return super.newApplication(cl, TestApplication.class.getName(), context);
}
}
@@ -78,7 +78,7 @@ public class ChartPointTest {
// given // given
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint(); TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setHeartRate_bpm(100f); trackPoint.setHeartRate_bpm(100f);
trackPoint.setCyclingCadence_rpm(101f); trackPoint.setCadence_rpm(101f);
trackPoint.setPower(102f); trackPoint.setPower(102f);
TrackStatistics statistics = new TrackStatistics(); TrackStatistics statistics = new TrackStatistics();
@@ -120,7 +120,7 @@ public class TestDataUtil {
trackPoint.setSpeed(Speed.of(5f + (i / 10f))); trackPoint.setSpeed(Speed.of(5f + (i / 10f)));
trackPoint.setHeartRate_bpm(100f + i); trackPoint.setHeartRate_bpm(100f + i);
trackPoint.setCyclingCadence_rpm(300f + i); trackPoint.setCadence_rpm(300f + i);
trackPoint.setPower(400f + i); trackPoint.setPower(400f + i);
trackPoint.setAltitudeGain(ALTITUDE_GAIN); trackPoint.setAltitudeGain(ALTITUDE_GAIN);
trackPoint.setAltitudeLoss(ALTITUDE_LOSS); trackPoint.setAltitudeLoss(ALTITUDE_LOSS);
@@ -17,7 +17,7 @@ public class TestSensorDataUtil {
tp.setLatitude(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0); tp.setLatitude(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0);
tp.setLongitude(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0); tp.setLongitude(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0);
tp.setHeartRate_bpm(hr); tp.setHeartRate_bpm(hr);
tp.setCyclingCadence_rpm(cadence); tp.setCadence_rpm(cadence);
tp.setPower(power); tp.setPower(power);
tp.setHorizontalAccuracy(Distance.of(1f)); tp.setHorizontalAccuracy(Distance.of(1f));
tp.setAltitude(1f); tp.setAltitude(1f);
@@ -835,7 +835,7 @@ public class CustomContentProviderUtilsTest {
TrackPoint trackPoint = TestDataUtil.createTrackPoint(5); TrackPoint trackPoint = TestDataUtil.createTrackPoint(5);
trackPoint.setHeartRate_bpm(1F); trackPoint.setHeartRate_bpm(1F);
trackPoint.setCyclingCadence_rpm(2F); trackPoint.setCadence_rpm(2F);
trackPoint.setPower(3F); trackPoint.setPower(3F);
// when // when
@@ -845,7 +845,7 @@ public class CustomContentProviderUtilsTest {
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId); List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertTrue(trackPoints.get(10).hasHeartRate()); assertTrue(trackPoints.get(10).hasHeartRate());
assertEquals(trackPoint.getHeartRate_bpm(), trackPoints.get(10).getHeartRate_bpm(), 0.01); assertEquals(trackPoint.getHeartRate_bpm(), trackPoints.get(10).getHeartRate_bpm(), 0.01);
assertEquals(trackPoint.getCyclingCadence_rpm(), trackPoints.get(10).getCyclingCadence_rpm(), 0.01); assertEquals(trackPoint.getCadence_rpm(), trackPoints.get(10).getCadence_rpm(), 0.01);
assertEquals(trackPoint.getPower(), trackPoints.get(10).getPower(), 0.01); assertEquals(trackPoint.getPower(), trackPoints.get(10).getPower(), 0.01);
} }
@@ -934,7 +934,7 @@ public class CustomContentProviderUtilsTest {
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1); TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
trackPoint.setType(TrackPoint.Type.TRACKPOINT); trackPoint.setType(TrackPoint.Type.TRACKPOINT);
trackPoint.setPower(null); trackPoint.setPower(null);
trackPoint.setCyclingCadence_rpm(null); trackPoint.setCadence_rpm(null);
trackPoint.setHeartRate_bpm(null); trackPoint.setHeartRate_bpm(null);
trackPointList.add(trackPoint); trackPointList.add(trackPoint);
Track.Id trackId = new Track.Id(System.currentTimeMillis()); Track.Id trackId = new Track.Id(System.currentTimeMillis());
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.content.sensor;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -11,6 +12,7 @@ import de.dennisguse.opentracks.util.UintUtils;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class SensorDataCyclingTest { public class SensorDataCyclingTest {
@@ -81,7 +83,9 @@ public class SensorDataCyclingTest {
assertEquals(60, current.getValue(), 0.01); assertEquals(60, current.getValue(), 0.01);
} }
@Ignore("Disabled from #953")
@Test @Test
@Deprecated
public void compute_cadence_rollOverCount() { public void compute_cadence_rollOverCount() {
// given // given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024); SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
@@ -94,6 +98,19 @@ public class SensorDataCyclingTest {
assertEquals(60, current.getValue(), 0.01); assertEquals(60, current.getValue(), 0.01);
} }
@Test
public void compute_cadence_overflow() {
// given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous);
// then
assertNull(current.getValue());
}
@Test @Test
public void compute_speed() { public void compute_speed() {
// given // given
@@ -108,7 +125,9 @@ public class SensorDataCyclingTest {
assertEquals(1.20, current.getValue().getSpeed().toMPS(), 0.01); assertEquals(1.20, current.getValue().getSpeed().toMPS(), 0.01);
} }
@Ignore("Disabled from #953")
@Test @Test
@Deprecated
public void compute_speed_rollOverCount() { public void compute_speed_rollOverCount() {
// given // given
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024); SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
@@ -122,6 +141,19 @@ public class SensorDataCyclingTest {
assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01); assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
} }
@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);
// when
current.compute(previous, Distance.ofMM(2000));
// then
assertNull(current.getValue());
}
@Test @Test
public void equals_speed_with_no_data() { public void equals_speed_with_no_data() {
// given // given
@@ -112,41 +112,41 @@ public class ExportImportTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService(); .getService();
service.getHandlerServer().setClock(Clock.systemUTC()); service.getTrackPointCreator().setClock(Clock.systemUTC());
} }
public void setUp(boolean hasSensorDistance) throws TimeoutException { public void setUp(boolean hasSensorDistance) throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService(); .getService();
TrackPointCreator handlerServer = service.getHandlerServer(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
trackId = service.startNewTrack(); trackId = service.startNewTrack();
Distance sensorDistance = hasSensorDistance ? Distance.of(5) : null; Distance sensorDistance = hasSensorDistance ? Distance.of(5) : null;
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:03Z"), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance); sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:03Z"), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null); service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:04Z"), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance); sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:04Z"), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:05Z"), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, sensorDistance); sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:05Z"), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null); service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:06Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:06Z"), ZoneId.of("CET")));
handlerServer.setRemoteSensorManager(new BluetoothRemoteSensorManager(context)); trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context));
service.pauseCurrentTrack(); service.pauseCurrentTrack();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:20Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:20Z"), ZoneId.of("CET")));
service.resumeCurrentTrack(); service.resumeCurrentTrack();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:21Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:21Z"), ZoneId.of("CET")));
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:21Z"), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, sensorDistance); sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:21Z"), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:22Z"), 3, 16, 10, 15, 10, 0, 66, 3, 50, sensorDistance); sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:22Z"), 3, 16, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(handlerServer, Instant.parse("2020-02-02T02:02:23Z"), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance); sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:23Z"), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:24Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:24Z"), ZoneId.of("CET")));
handlerServer.setRemoteSensorManager(new BluetoothRemoteSensorManager(context)); trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context));
service.endCurrentTrack(); service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
@@ -380,7 +380,7 @@ public class ExportImportTest {
} }
} }
private void sendLocation(TrackPointCreator handlerServer, Instant time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) { private void sendLocation(TrackPointCreator trackPointCreator, Instant time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) {
Location location = new Location("mock"); Location location = new Location("mock");
location.setLatitude(latitude); location.setLatitude(latitude);
location.setLongitude(longitude); location.setLongitude(longitude);
@@ -388,7 +388,7 @@ public class ExportImportTest {
location.setSpeed(speed); location.setSpeed(speed);
location.setAltitude(altitude); location.setAltitude(altitude);
handlerServer.setAltitudeSumManager(new AltitudeSumManager() { trackPointCreator.setAltitudeSumManager(new AltitudeSumManager() {
@Override @Override
public void fill(@NonNull TrackPoint trackPoint) { public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(altitudeGain); trackPoint.setAltitudeGain(altitudeGain);
@@ -396,7 +396,7 @@ public class ExportImportTest {
} }
}); });
handlerServer.setRemoteSensorManager(new BluetoothRemoteSensorManager(context) { trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
@Override @Override
public SensorDataSet fill(@NonNull TrackPoint trackPoint) { public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
SensorDataSet sensorDataSet = new SensorDataSet(); SensorDataSet sensorDataSet = new SensorDataSet();
@@ -422,7 +422,7 @@ public class ExportImportTest {
} }
}); });
handlerServer.setClock(Clock.fixed(time, ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(time, ZoneId.of("CET")));
handlerServer.getLocationHandler().onLocationChanged(location); trackPointCreator.getLocationHandler().onLocationChanged(location);
} }
} }
@@ -14,6 +14,7 @@ import org.junit.runners.JUnit4;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
@@ -23,6 +24,7 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@@ -67,7 +69,7 @@ public class GPXImportTest {
importTrackId = importer.importFile(inputStream).get(0); importTrackId = importer.importFile(inputStream).get(0);
// then // then
// 1. track // 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId); Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack); assertNotNull(importedTrack);
assertEquals("the category", importedTrack.getCategory()); assertEquals("the category", importedTrack.getCategory());
@@ -75,6 +77,8 @@ public class GPXImportTest {
assertEquals("2021-01-07 22:51", importedTrack.getName()); assertEquals("2021-01-07 22:51", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon()); assertEquals("UNKNOWN", importedTrack.getIcon());
//TODO Check trackstatistics
// 3. trackpoints // 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId); List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(6, importedTrackPoints.size()); assertEquals(6, importedTrackPoints.size());
@@ -106,4 +110,102 @@ public class GPXImportTest {
.setAltitude(10) .setAltitude(10)
), importedTrackPoints); ), importedTrackPoints);
} }
@LargeTest
@Test
public void gpx_without_speed() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx11_without_speed);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getDescription());
assertEquals("20210907_213924.gpx", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
assertEquals(1.44, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(53), trackStatistics.getMovingTime());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2021-09-07T22:10:19Z"))
.setLatitude(30.14185982)
.setLongitude(-40.3863038)
.setAltitude(-5),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2021-09-07T22:11:07Z"))
.setLatitude(30.14184657)
.setLongitude(-40.38670089)
.setAltitude(-5)
.setSpeed(Speed.of(0.7976524233818054)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2021-09-07T22:12:00Z"))
.setLatitude(30.14185982)
.setLongitude(-40.3863038)
.setAltitude(-5)
.setSpeed(Speed.of(0.7224021553993225))
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_speed_no_namespace() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx11_with_speed_no_namespace);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getDescription());
assertEquals("20210907_213924.gpx", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
assertEquals(4, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2021-09-07T22:10:19Z"))
.setLatitude(30.14185982)
.setLongitude(-40.3863038)
.setAltitude(-5)
.setSpeed(Speed.of(5)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2021-09-07T22:11:07Z"))
.setLatitude(30.14184657)
.setLongitude(-40.38670089)
.setAltitude(-5)
.setSpeed(Speed.of(0.7976524233818054))
.setSpeed(Speed.of(4)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2021-09-07T22:12:00Z"))
.setLatitude(30.14185982)
.setLongitude(-40.3863038)
.setAltitude(-5)
.setSpeed(Speed.of(3))
), importedTrackPoints);
}
} }
@@ -73,9 +73,9 @@ public class TrackPointAssert {
Assert.assertEquals(expected.getPower(), actual.getPower(), delta); Assert.assertEquals(expected.getPower(), actual.getPower(), delta);
} }
Assert.assertEquals(expected.hasCyclingCadence(), actual.hasCyclingCadence()); Assert.assertEquals(expected.hasCadence(), actual.hasCadence());
if (expected.hasCyclingCadence()) { if (expected.hasCadence()) {
Assert.assertEquals(expected.getCyclingCadence_rpm(), actual.getCyclingCadence_rpm(), delta); Assert.assertEquals(expected.getCadence_rpm(), actual.getCadence_rpm(), delta);
} }
} }
@@ -83,14 +83,14 @@ public class TrackPointAssert {
try { try {
Assert.assertEquals(expected.size(), actual.size()); Assert.assertEquals(expected.size(), actual.size());
} catch (AssertionError e) { } catch (AssertionError e) {
throw new AssertionError("Expected: " + expected + " actual: " + actual); throw new AssertionError("Expected: " + expected + "\n actual: " + actual);
} }
for (int i = 0; i < expected.size(); i++) { for (int i = 0; i < expected.size(); i++) {
try { try {
assertEquals(expected.get(i), actual.get(i)); assertEquals(expected.get(i), actual.get(i));
} catch (AssertionError e) { } catch (AssertionError e) {
throw new AssertionError("Expected: " + expected.get(i) + " actual: " + actual.get(i), e); throw new AssertionError("Expected: " + expected.get(i) + "\n actual: " + actual.get(i), e);
} }
} }
Assert.assertEquals(expected.size(), actual.size()); Assert.assertEquals(expected.size(), actual.size());
@@ -1,83 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.time.Duration;
import de.dennisguse.opentracks.services.handlers.AdaptiveLocationListenerPolicy;
import static org.junit.Assert.assertEquals;
/**
* Tests the {@link AdaptiveLocationListenerPolicy}.
*
* @author youtaol
*/
@RunWith(JUnit4.class)
public class AdaptiveLocationListenerPolicyTest {
private static final Duration MIN = Duration.ofMillis(1000);
private static final Duration MAX = Duration.ofMillis(3000);
private static final int MIN_DISTANCE = 10;
private static final Duration NEW_IDLE_TIME_BIG = Duration.ofMillis(10000);
private static final Duration NEW_IDLE_TIME_NORMAL = Duration.ofMillis(5000);
private static final Duration NEW_IDLE_TIME_SMALL = Duration.ofMillis(2000);
private static final Duration NEW_IDLE_TIME_LESS_THAN_MIN = Duration.ofMillis(500);
private AdaptiveLocationListenerPolicy adaptiveLocationListenerPolicy;
@Before
public void setUp() {
adaptiveLocationListenerPolicy = new AdaptiveLocationListenerPolicy(MIN, MAX, MIN_DISTANCE);
}
/**
* Tests the {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four situations.
* <ul>
* <li>The newIdleTime is bigger than max interval.</li>
* <li>The newIdleTime is between min and max interval.</li>
* <li>The newIdleTime is smaller than max interval.</li>
* <li>The newIdleTime is smaller than the smallest interval unit.</li>
* </ul>
*/
@Test
public void testGetDesiredPollingInterval() {
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_BIG);
assertEquals(MAX, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
// First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the nearest second.
assertEquals(NEW_IDLE_TIME_NORMAL.dividedBy(2).getSeconds(), adaptiveLocationListenerPolicy.getDesiredPollingInterval().getSeconds());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_LESS_THAN_MIN);
assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
}
/**
* Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance_m()}.
*/
@Test
public void testGetMinDistance() {
assertEquals(MIN_DISTANCE, adaptiveLocationListenerPolicy.getMinDistance_m());
}
}
@@ -25,7 +25,6 @@ import static org.junit.Assert.assertTrue;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
@@ -66,15 +65,12 @@ import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager; import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
/** /**
* Tests for the track recording service. * Tests for the track recording service.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* <p>
* //TODO ATTENTION: This tests deletes all stored tracks in the database.
* So, if it is executed on a real device, data might be lost.
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTest { public class TrackRecordingServiceTest {
@@ -97,7 +93,6 @@ public class TrackRecordingServiceTest {
} }
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private final Track.Id trackId = new Track.Id(Math.abs(new Random().nextLong())); private final Track.Id trackId = new Track.Id(Math.abs(new Random().nextLong()));
@@ -124,7 +119,7 @@ public class TrackRecordingServiceTest {
contentProviderUtils = new ContentProviderUtils(context); contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values. // Let's use default values.
sharedPreferences.edit().clear().commit(); PreferencesUtils.clear();
// Ensure that the database is empty before every test // Ensure that the database is empty before every test
contentProviderUtils.deleteAllTracks(context); contentProviderUtils.deleteAllTracks(context);
@@ -138,7 +133,7 @@ public class TrackRecordingServiceTest {
if (service.isRecording() || service.isPaused()) { if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack(); service.endCurrentTrack();
} }
service.getHandlerServer().setClock(Clock.systemUTC()); service.getTrackPointCreator().setClock(Clock.systemUTC());
// Ensure that the database is empty after every test // Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context); contentProviderUtils.deleteAllTracks(context);
@@ -212,15 +207,15 @@ public class TrackRecordingServiceTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService(); .getService();
TrackPointCreator handlerServer = service.getHandlerServer(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
handlerServer.stopGPS(); trackPointCreator.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager); trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when // when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.endCurrentTrack(); service.endCurrentTrack();
// then // then
@@ -242,22 +237,22 @@ public class TrackRecordingServiceTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService(); .getService();
TrackPointCreator handlerServer = service.getHandlerServer(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
handlerServer.stopGPS(); trackPointCreator.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager); trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when // when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.pauseCurrentTrack(); service.pauseCurrentTrack();
// then // then
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount()); assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
//when //when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.resumeTrack(trackId); service.resumeTrack(trackId);
// then // then
@@ -281,23 +276,23 @@ public class TrackRecordingServiceTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService(); .getService();
TrackPointCreator handlerServer = service.getHandlerServer(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
handlerServer.stopGPS(); trackPointCreator.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager); trackPointCreator.setAltitudeSumManager(altitudeSumManager);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.endCurrentTrack(); service.endCurrentTrack();
// when // when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.resumeTrack(trackId); service.resumeTrack(trackId);
handlerServer.stopGPS(); trackPointCreator.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager); trackPointCreator.setAltitudeSumManager(altitudeSumManager);
handlerServer.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z")), Distance.of(50)); trackPointCreator.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z")), Distance.of(50));
// then // then
assertTrue(service.isRecording()); assertTrue(service.isRecording());
@@ -323,18 +318,18 @@ public class TrackRecordingServiceTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService(); .getService();
TrackPointCreator handlerServer = service.getHandlerServer(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
handlerServer.stopGPS(); trackPointCreator.stopGPS();
handlerServer.setAltitudeSumManager(altitudeSumManager); trackPointCreator.setAltitudeSumManager(altitudeSumManager);
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.pauseCurrentTrack(); service.pauseCurrentTrack();
// when // when
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.endCurrentTrack(); service.endCurrentTrack();
// then // then
@@ -405,14 +400,14 @@ public class TrackRecordingServiceTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService(); .getService();
TrackPointCreator handlerServer = service.getHandlerServer(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
handlerServer.stopGPS(); trackPointCreator.stopGPS();
handlerServer.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET"))); trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording()); assertTrue(service.isRecording());
handlerServer.onNewTrackPoint( trackPointCreator.onNewTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:03Z")) new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:03Z"))
.setLatitude(10) .setLatitude(10)
.setLongitude(10) .setLongitude(10)
@@ -467,6 +462,6 @@ public class TrackRecordingServiceTest {
.setSpeed(Speed.of(speed)) .setSpeed(Speed.of(speed))
.setBearing(3.0f); .setBearing(3.0f);
trackRecordingService.getHandlerServer().onNewTrackPoint(trackPoint, Distance.of(50)); trackRecordingService.getTrackPointCreator().onNewTrackPoint(trackPoint, Distance.of(50));
} }
} }
@@ -1,8 +1,9 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertFalse;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -19,11 +20,14 @@ import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.internal.debugging.LocationImpl;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed; import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -32,19 +36,15 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider; import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate; import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert; import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager; import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import static org.junit.Assert.assertFalse;
/** /**
* Tests insert location. * Tests insert location.
* <p>
* //TODO ATTENTION: This tests deletes all stored tracks in the database.
* So, if it is executed on a real device, data might be lost.
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
//TODO Implement as mock test; no need to store data in database //TODO Implement as mock test; no need to store data in database
@@ -91,12 +91,11 @@ public class TrackRecordingServiceTestLocation {
tearDown(); tearDown();
// Let's use default values. // Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); PreferencesUtils.clear();
sharedPreferences.edit().clear().commit();
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context))) service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService(); .getService();
service.getHandlerServer().stopGPS(); service.getTrackPointCreator().stopGPS();
} }
@After @After
@@ -107,10 +106,10 @@ public class TrackRecordingServiceTestLocation {
@MediumTest @MediumTest
@Test @Test
public void testOnLocationChangedAsync_movingAccurate() throws Exception { public void testOnLocationChangedAsync_movingAccurate() {
// given // given
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager); service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when // when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15); TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
@@ -180,10 +179,10 @@ public class TrackRecordingServiceTestLocation {
@MediumTest @MediumTest
@Test @Test
public void testOnLocationChangedAsync_slowMovingAccurate() throws Exception { public void testOnLocationChangedAsync_slowMovingAccurate() {
// given // given
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager); service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when // when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15); TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
@@ -191,7 +190,7 @@ public class TrackRecordingServiceTestLocation {
TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 3, 15); TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 3, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 4, 15); TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 4, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000004, 35.0, 5, 15); TrackRecordingServiceTest.newTrackPoint(service, 45.000004, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000005, 35.0, 6, 15); TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 6, 15);
service.endCurrentTrack(); service.endCurrentTrack();
@@ -211,7 +210,7 @@ public class TrackRecordingServiceTestLocation {
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f), .setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null) new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0005) .setLatitude(45.0002)
.setLongitude(35) .setLongitude(35)
.setHorizontalAccuracy(Distance.of(6)) .setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15)) .setSpeed(Speed.of(15))
@@ -221,14 +220,17 @@ public class TrackRecordingServiceTestLocation {
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f) .setAltitudeLoss(0f)
), trackPoints); ), trackPoints);
} }
@MediumTest @MediumTest
@Test @Test
public void testOnLocationChangedAsync_idle() throws Exception { public void testOnLocationChangedAsync_idle() {
// given // given
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager); service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when // when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0); TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
@@ -255,20 +257,6 @@ public class TrackRecordingServiceTestLocation {
.setSpeed(Speed.of(0)) .setSpeed(Speed.of(0))
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f), .setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null) //TODO Why is this added? Systems is idle and not moving at all.
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null) new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f) .setAltitudeLoss(0f)
@@ -277,11 +265,11 @@ public class TrackRecordingServiceTestLocation {
@MediumTest @MediumTest
@Test @Test
public void testOnLocationChangedAsync_idle_withMovement() throws Exception { public void testOnLocationChangedAsync_idle_withMovement() {
// given // given
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager); service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().stopGPS(); service.getTrackPointCreator().stopGPS();
// when // when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15); TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
@@ -308,27 +296,6 @@ public class TrackRecordingServiceTestLocation {
.setSpeed(Speed.of(15)) .setSpeed(Speed.of(15))
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f), .setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null) //TODO Check why this trackPoint is inserted.
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null) //TODO Check why this trackPoint is inserted.
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null) new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f) .setAltitudeLoss(0f)
@@ -337,11 +304,11 @@ public class TrackRecordingServiceTestLocation {
@MediumTest @MediumTest
@Test @Test
public void testOnLocationChangedAsync_idle_withSensorData() throws Exception { public void testOnLocationChangedAsync_idle_withSensorData() {
// given // given
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager); service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
service.getHandlerServer().setRemoteSensorManager(new BluetoothRemoteSensorManager(context) { service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
@@ -430,12 +397,86 @@ public class TrackRecordingServiceTestLocation {
), trackPoints); ), trackPoints);
} }
@Deprecated // Will be superseded when fixing #500
@MediumTest @MediumTest
@Test @Test
public void testOnLocationChangedAsync_segment() throws Exception { public void testOnLocationChangedAsync_idle_withSensorDistance() {
BluetoothRemoteSensorManager remoteSensorManager = new BluetoothRemoteSensorManager(context) {
@Override
public boolean isEnabled() {
return true;
}
};
AltitudeSumManager altitudeSumManager = new AltitudeSumManager();
// given // given
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
service.getHandlerServer().setAltitudeSumManager(altitudeSumManager); service.getTrackPointCreator().setRemoteSensorManager(remoteSensorManager);
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
altitudeSumManager.stop(service);
altitudeSumManager.setConnected(true);
// when
altitudeSumManager.addAltitudeGain_m(6f);
altitudeSumManager.addAltitudeLoss_m(6f);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(0)));
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(2)));
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(12)));
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 15);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(13)));
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 15);
altitudeSumManager.addAltitudeGain_m(6f);
altitudeSumManager.addAltitudeLoss_m(6f);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(14)));
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 15);
altitudeSumManager.addAltitudeGain_m(6f);
altitudeSumManager.addAltitudeLoss_m(6f);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(16)));
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(5))
.setAltitudeGain(6f)
.setAltitudeLoss(6f)
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setSensorDistance(Distance.of(11))
.setSpeed(Speed.of(5))
.setAltitudeGain(12f)
.setAltitudeLoss(12f)
.setSensorDistance(Distance.of(4))
), trackPoints);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_segment() {
// given
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when // when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0); TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
@@ -470,13 +511,6 @@ public class TrackRecordingServiceTestLocation {
.setSpeed(Speed.of(0)) .setSpeed(Speed.of(0))
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f), .setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.1)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(3))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, null) new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, null)
.setLatitude(45.2) .setLatitude(45.2)
@@ -485,13 +519,6 @@ public class TrackRecordingServiceTestLocation {
.setSpeed(Speed.of(0)) .setSpeed(Speed.of(0))
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f), .setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.2)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null) new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f) .setAltitudeGain(0f)
.setAltitudeLoss(0f) .setAltitudeLoss(0f)
@@ -32,7 +32,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider; import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -43,9 +43,6 @@ import static org.junit.Assert.assertTrue;
* Tests for the track recording service, which require a {@link Looper}. * Tests for the track recording service, which require a {@link Looper}.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* <p>
* ATTENTION: This tests deletes all stored tracks in the database.
* So, if it is executed on a real device, data might be lost.
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestLooper { public class TrackRecordingServiceTestLooper {
@@ -54,7 +51,6 @@ public class TrackRecordingServiceTestLooper {
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
@@ -79,7 +75,7 @@ public class TrackRecordingServiceTestLooper {
contentProviderUtils = new ContentProviderUtils(context); contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values. // Let's use default values.
sharedPreferences.edit().clear().apply(); PreferencesUtils.clear();
// Ensure that the database is empty before every test // Ensure that the database is empty before every test
contentProviderUtils.deleteAllTracks(context); contentProviderUtils.deleteAllTracks(context);
@@ -107,34 +103,6 @@ public class TrackRecordingServiceTestLooper {
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest
@Test
public void testWithProperties_voiceFrequencyDefault() throws TimeoutException {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.voice_frequency_key), Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default)));
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByDistance() throws TimeoutException {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.voice_frequency_key), -1);
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByTime() throws TimeoutException {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.voice_frequency_key), 1);
editor.commit();
fullRecordingSession();
}
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException { public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException {
@@ -174,14 +142,14 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_metricUnitsDefault() throws TimeoutException { public void testWithProperties_metricUnitsDefault() throws TimeoutException {
PreferencesUtils.setString(sharedPreferences, context, R.string.stats_units_key, context.getString(R.string.stats_units_default)); PreferencesUtils.setString(R.string.stats_units_key, context.getString(R.string.stats_units_default));
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_metricUnitsDisabled() throws TimeoutException { public void testWithProperties_metricUnitsDisabled() throws TimeoutException {
PreferencesUtils.setString(sharedPreferences, context, R.string.stats_units_key, context.getString(R.string.stats_units_imperial)); PreferencesUtils.setString(R.string.stats_units_key, context.getString(R.string.stats_units_imperial));
fullRecordingSession(); fullRecordingSession();
} }
@@ -235,16 +203,16 @@ public class TrackRecordingServiceTestLooper {
// Insert a few points, markers and statistics. // Insert a few points, markers and statistics.
for (int i = 0; i < 30; i++) { for (int i = 0; i < 30; i++) {
//TODO Should send locations to LocationHandler instead of TrackPoints to HandlerServer? //TODO Should send locations to LocationHandler instead of TrackPoints to TrackPointCreator?
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, service.getHandlerServer().createNow()) TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, service.getTrackPointCreator().createNow())
.setLongitude(35.0f + i / 10.0f) .setLongitude(35.0f + i / 10.0f)
.setLatitude(45.0f - i / 5.0f) .setLatitude(45.0f - i / 5.0f)
.setHorizontalAccuracy(Distance.of(5)) .setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(10)) .setSpeed(Speed.of(10))
.setBearing(3.0f); .setBearing(3.0f);
Distance prefAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy(sharedPreferences, context); Distance prefAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
service.getHandlerServer().onNewTrackPoint(trackPoint, prefAccuracy); service.getTrackPointCreator().onNewTrackPoint(trackPoint, prefAccuracy);
if (i % 7 == 0) { if (i % 7 == 0) {
service.insertMarker(null, null, null, null); service.insertMarker(null, null, null, null);
@@ -5,7 +5,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.os.Looper; import android.os.Looper;
@@ -24,16 +23,15 @@ import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class LocationHandlerTest { public class LocationHandlerTest {
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@Mock @Mock
private TrackPointCreator handlerServer; private TrackPointCreator trackPointCreator;
@InjectMocks @InjectMocks
private LocationHandler locationHandler; private LocationHandler locationHandler;
@@ -47,15 +45,12 @@ public class LocationHandlerTest {
@Before @Before
public void setUp() { public void setUp() {
// Let's use default values. // Let's use default values.
sharedPreferences.edit().clear().commit(); PreferencesUtils.clear();
Mockito.when(handlerServer.createNow()) Mockito.when(trackPointCreator.createNow())
.thenReturn(Instant.now()); .thenReturn(Instant.now());
//TODO REMOVE locationHandler.onStart(context);
// locationHandler.onSharedPreferenceChanged(context, sharedPreferences, context.getString(R.string.recording_gps_accuracy_key));
// locationHandler.onSharedPreferenceChanged(context, sharedPreferences, context.getString(R.string.min_recording_interval_key));
locationHandler.onStart(context, sharedPreferences);
} }
/** /**
@@ -67,7 +62,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis())); locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis()));
// then // then
verify(handlerServer, times(1)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class)); verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
} }
/** /**
@@ -83,7 +78,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis())); locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis()));
// then // then
verify(handlerServer, times(0)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class)); verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
} }
/** /**
@@ -92,14 +87,14 @@ public class LocationHandlerTest {
@Test @Test
public void testOnLocationChanged_poorAccuracy() { public void testOnLocationChanged_poorAccuracy() {
// given // given
Distance prefAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy(sharedPreferences, context); Distance prefAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
// when // when
locationHandler.onLocationChanged(createLocation(45f, 35f, (float) (prefAccuracy.toM() + 1), 5, System.currentTimeMillis())); locationHandler.onLocationChanged(createLocation(45f, 35f, (float) (prefAccuracy.toM() + 1), 5, System.currentTimeMillis()));
// then // then
// no newTrackPoint called // no newTrackPoint called
verify(handlerServer, times(0)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class)); verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
} }
@Test @Test
@@ -112,7 +107,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis())); locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
// then // then
verify(handlerServer, times(1)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class)); verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
} }
/** /**
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.util; package de.dennisguse.opentracks.services.tasks;
import android.content.Context; import android.content.Context;
import android.util.Pair; import android.util.Pair;
@@ -6,6 +6,7 @@ import android.util.Pair;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -18,7 +19,9 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.services.tasks.AnnouncementUtils;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -68,7 +71,7 @@ public class AnnouncementUtilsTest {
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval); String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval);
// then // then
assertEquals( Assert.assertEquals(
"total distance " + "total distance " +
StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first + StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first +
" kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + " kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
@@ -15,6 +15,7 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@@ -25,7 +26,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class PeriodicTaskExecutorTest { public class VoiceAnnouncementManagerTest {
@Rule @Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@@ -53,17 +54,38 @@ public class PeriodicTaskExecutorTest {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService(); .getService();
PeriodicTaskExecutor periodicTaskExecutor = new PeriodicTaskExecutor(service, new AnnouncementPeriodicTask.Factory()); VoiceAnnouncementManager voiceAnnouncementManager = new VoiceAnnouncementManager(service);
periodicTaskExecutor.setMetricUnits(true); voiceAnnouncementManager.setFrequency(Distance.ofKilometer(5));
periodicTaskExecutor.setTaskFrequency(-5);
// when // when
TrackStatistics statistics = new TrackStatistics(); TrackStatistics statistics = new TrackStatistics();
statistics.setTotalDistance(Distance.of(13000)); statistics.setTotalDistance(Distance.ofKilometer(13));
assertEquals(Distance.of(15000), periodicTaskExecutor.calculateNextTaskDistance(statistics)); voiceAnnouncementManager.restore(statistics);
assertEquals(Distance.of(15000), voiceAnnouncementManager.getNextTotalDistance());
statistics.setTotalDistance(Distance.of(15100)); statistics.setTotalDistance(Distance.of(15100));
assertEquals(Distance.of(20000), periodicTaskExecutor.calculateNextTaskDistance(statistics)); voiceAnnouncementManager.restore(statistics);
assertEquals(Distance.of(20000), voiceAnnouncementManager.getNextTotalDistance());
}
@Test
public void calculateNextTotalTime() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
VoiceAnnouncementManager voiceAnnouncementManager = new VoiceAnnouncementManager(service);
voiceAnnouncementManager.setFrequency(Duration.ofSeconds(5));
// when
TrackStatistics statistics = new TrackStatistics();
statistics.setTotalTime(Duration.ofSeconds(91));
voiceAnnouncementManager.restore(statistics);
assertEquals(Duration.ofSeconds(95), voiceAnnouncementManager.getNextTotalTime());
statistics.setTotalTime(Duration.ofSeconds(95));
voiceAnnouncementManager.restore(statistics);
assertEquals(Duration.ofSeconds(100), voiceAnnouncementManager.getNextTotalTime());
} }
} }
@@ -93,8 +93,8 @@ public class BluetoothUtilsTest {
@Test @Test
public void parseRunningSpeedAndCadence_with_distance() { public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, 0, 0); BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID, 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, 12, 1}); characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
// when // when
SensorDataRunning sensor = BluetoothUtils.parseRunningSpeedAndCadence("address", "sensorName", characteristic); SensorDataRunning sensor = BluetoothUtils.parseRunningSpeedAndCadence("address", "sensorName", characteristic);
@@ -102,6 +102,6 @@ public class BluetoothUtilsTest {
// then // then
assertEquals(Speed.of(5), sensor.getSpeed()); assertEquals(Speed.of(5), sensor.getSpeed());
assertEquals(80, sensor.getCadence(), 0.01); assertEquals(80, sensor.getCadence(), 0.01);
assertEquals(Distance.of(26.8), sensor.getTotalDistance()); assertEquals(Distance.of(6553.5 + 1677721.6), sensor.getTotalDistance());
} }
} }
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.util;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -10,9 +11,15 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class PreferencesUtilsTest { public class PreferencesUtilsTest {
@@ -22,14 +29,14 @@ public class PreferencesUtilsTest {
@Test @Test
public void ExportTrackFileFormat_ok() { public void ExportTrackFileFormat_ok() {
// given // given
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit(); SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.export_trackfileformat_key), TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.name()); editor.putString(context.getString(R.string.export_trackfileformat_key), TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.name());
editor.commit(); editor.commit();
// when // when
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(sharedPreferences, context); TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
// then // then
assertEquals(TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat); assertEquals(TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
@@ -38,13 +45,13 @@ public class PreferencesUtilsTest {
@Test @Test
public void ExportTrackFileFormat_invalid() { public void ExportTrackFileFormat_invalid() {
// given // given
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit(); SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.export_trackfileformat_key), "invalid"); editor.putString(context.getString(R.string.export_trackfileformat_key), "invalid");
editor.commit(); editor.commit();
// when // when
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(sharedPreferences, context); TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
// then // then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat); assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
@@ -53,15 +60,142 @@ public class PreferencesUtilsTest {
@Test @Test
public void ExportTrackFileFormat_noValue() { public void ExportTrackFileFormat_noValue() {
// given // given
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit(); SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear(); editor.clear();
editor.commit(); editor.commit();
// when // when
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(sharedPreferences, context); TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
// then // then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat); assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
} }
@Test
public void testGetCustomLayout_default() {
// given
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear();
editor.commit();
// when
Layout layout = PreferencesUtils.getCustomLayout();
// then
assertTrue(layout.getFields().size() > 0);
assertEquals(layout.getProfile(), context.getString(R.string.default_activity_default));
assertTrue(layout.getFields().stream().anyMatch(DataField::isVisible));
}
@Test
public void testGetCustomLayout_1() {
// given
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(
context.getString(R.string.stats_custom_layout_fields_key),
"run;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;");
editor.apply();
// when
Layout layout = PreferencesUtils.getCustomLayout();
// then
assertEquals(layout.getFields().size(), 4);
assertEquals(layout.getProfile(), "run");
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(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(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(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());
}
@Test
public void testGetCustomLayout_coordinatesIsWide() {
// given
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(
context.getString(R.string.stats_custom_layout_fields_key),
"walking;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_coordinates_key) + ",0,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;");
editor.apply();
// when
Layout layout = PreferencesUtils.getCustomLayout();
// then
assertEquals(layout.getFields().size(), 4);
assertEquals(layout.getProfile(), "walking");
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(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(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(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());
}
@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));
// when
PreferencesUtils.setCustomLayout(layoutSrc);
// then
String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layout_fields_key), null);
assertNotNull(csv);
assertEquals(csv,
"road cycling;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;");
Layout layoutDst = PreferencesUtils.getCustomLayout();
assertEquals(layoutSrc.getProfile(), layoutDst.getProfile());
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());
}
}
} }
@@ -0,0 +1,68 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
@RunWith(AndroidJUnit4.class)
public class StatisticsUtilsTest extends TestCase {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void testEmptyValue() {
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_distance_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_total_time_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_moving_time_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_max_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_speed_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_altitude_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_gain_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_loss_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_fastest_pace_key)));
assertEquals(context.getString(R.string.stats_empty_value_coordinates), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_coordinates_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_heart_rate_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_cadence_key)));
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_power_key)));
}
@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));
// when
Layout resultTrue = StatisticsUtils.filterVisible(layout, true);
Layout resultFalse = StatisticsUtils.filterVisible(layout, false);
// then
assertEquals(resultTrue.getFields().size(), 3);
assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key1")));
assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key3")));
assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key5")));
assertEquals(resultFalse.getFields().size(), 2);
assertTrue(resultFalse.getFields().stream().anyMatch(f -> f.getKey().equals("key2")));
assertTrue(resultFalse.getFields().stream().anyMatch(f -> f.getKey().equals("key4")));
}
}
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks.util; package de.dennisguse.opentracks.util;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -31,6 +30,7 @@ import java.util.Locale;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -47,14 +47,12 @@ public class TrackNameUtilsTest {
private static final Context CONTEXT = ApplicationProvider.getApplicationContext(); private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
private static final SharedPreferences SHARED_PREFERENCES = PreferencesUtils.getSharedPreferences(CONTEXT);
/** /**
* Tests when the track_name_key is settings_recording_track_name_date_local_value. * Tests when the track_name_key is settings_recording_track_name_date_local_value.
*/ */
@Test @Test
public void testTrackName_date_local() { public void testTrackName_date_local() {
PreferencesUtils.setString(SHARED_PREFERENCES, CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value)); PreferencesUtils.setString(R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value));
assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME)); assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
} }
@@ -63,7 +61,7 @@ public class TrackNameUtilsTest {
*/ */
@Test @Test
public void testTrackName_date_iso_8601() { public void testTrackName_date_iso_8601() {
PreferencesUtils.setString(SHARED_PREFERENCES, CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value)); PreferencesUtils.setString(R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US); SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US);
assertEquals(simpleDateFormat.format(START_TIME.toEpochMilli()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME)); assertEquals(simpleDateFormat.format(START_TIME.toEpochMilli()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
} }
@@ -73,7 +71,7 @@ public class TrackNameUtilsTest {
*/ */
@Test @Test
public void testTrackName_number() { public void testTrackName_number() {
PreferencesUtils.setString(SHARED_PREFERENCES, CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value)); PreferencesUtils.setString(R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value));
assertEquals("Track " + TRACK_ID.getId(), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME)); assertEquals("Track " + TRACK_ID.getId(), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
} }
} }
@@ -23,6 +23,7 @@ import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class IntervalStatisticsTest { public class IntervalStatisticsTest {
@@ -115,9 +116,40 @@ public class IntervalStatisticsTest {
whenAndThen(10000, distanceInterval); whenAndThen(10000, distanceInterval);
} }
@Test
public void testWithNoLossTrackPoints() {
// TrackPoints with elevation gain but without elevation loss.
// given
float distanceInterval = 1000f;
int numberOfPoints = 10000;
Track dummyTrack = new Track();
dummyTrack.setId(new Track.Id(System.currentTimeMillis()));
dummyTrack.setName("Dummy Track Without Elevation Loss");
contentProviderUtils.insertTrack(dummyTrack);
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
for (int i = 0; i < numberOfPoints; i++) {
TrackPoint tp = TestDataUtil.createTrackPoint(i);
tp.setAltitudeLoss(null);
contentProviderUtils.insertTrackPoint(tp, dummyTrack.getId());
trackStatisticsUpdater.addTrackPoint(tp, Distance.of(0));
}
dummyTrack.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(dummyTrack);
Pair<Track.Id, TrackStatistics> trackWithStats = new Pair<>(dummyTrack.getId(), trackStatisticsUpdater.getTrackStatistics());
// when and then
whenAndThen(trackWithStats, numberOfPoints, distanceInterval);
}
private void whenAndThen(int numberOfPoints, float distanceInterval) { private void whenAndThen(int numberOfPoints, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(distanceInterval), Distance.of(0));
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints); Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
whenAndThen(trackWithStats, numberOfPoints, distanceInterval);
}
private void whenAndThen(Pair<Track.Id, TrackStatistics> trackWithStats, int numberOfPoints, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(distanceInterval), Distance.of(0));
Track.Id trackId = trackWithStats.first; Track.Id trackId = trackWithStats.first;
TrackStatistics trackStatistics = trackWithStats.second; TrackStatistics trackStatistics = trackWithStats.second;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) { try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
@@ -127,18 +159,39 @@ public class IntervalStatisticsTest {
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList(); List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
Distance totalDistance = Distance.of(0); Distance totalDistance = Distance.of(0);
float totalTime = 0L; float totalTime = 0L;
float totalGain = 0f; Float totalGain = null;
Float totalLoss = null;
for (IntervalStatistics.Interval i : intervalList) { for (IntervalStatistics.Interval i : intervalList) {
totalDistance = totalDistance.plus(i.getDistance()); totalDistance = totalDistance.plus(i.getDistance());
totalTime += i.getDistance().toM() / i.getSpeed().toMPS(); totalTime += i.getDistance().toM() / i.getSpeed().toMPS();
if (totalGain == null) {
totalGain = i.getGain_m();
} else if (i.getGain_m() != null) {
totalGain += i.getGain_m(); totalGain += i.getGain_m();
} }
if (totalLoss == null) {
totalLoss = i.getLoss_m();
} else if (i.getLoss_m() != null) {
totalLoss += i.getLoss_m();
}
}
// then // then
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01); assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1); assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval)); assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
if (totalGain != null) {
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1); assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
} else {
assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(IntervalStatistics.Interval::hasGain));
}
if (totalLoss != null) {
assertEquals(totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
} else {
assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(IntervalStatistics.Interval::hasLoss));
}
for (int i = 0; i < intervalList.size() - 1; i++) { for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).getDistance().toM(), distanceInterval, 0.001); assertEquals(intervalList.get(i).getDistance().toM(), distanceInterval, 0.001);
@@ -0,0 +1,195 @@
package de.dennisguse.opentracks.viewmodels;
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.content.data.Distance;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class StatisticDataBuilderTest extends TestCase {
private final Context context = ApplicationProvider.getApplicationContext();
@Mock
private Track trackMock;
@Mock
private TrackStatistics trackStatisticsMock;
@Mock
private TrackRecordingService.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, true);
// 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, true);
// 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(200f);
when(sensorStatisticsMock.getAvgHeartRate()).thenReturn(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(90f);
when(sensorStatisticsMock.getMaxCadence()).thenReturn(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(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(200f);
when(sensorStatisticsMock.getAvgHeartRate()).thenReturn(150f);
when(sensorStatisticsMock.hasCadence()).thenReturn(true);
when(sensorStatisticsMock.getAvgCadence()).thenReturn(90f);
when(sensorStatisticsMock.getMaxCadence()).thenReturn(110f);
when(sensorStatisticsMock.hasPower()).thenReturn(true);
when(sensorStatisticsMock.getAvgPower()).thenReturn(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))));
}
}
@@ -0,0 +1,26 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>20210907_213924.gpx</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<speed>5</speed>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<speed>4</speed>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<speed>3</speed>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -0,0 +1,23 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>20210907_213924.gpx</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
</trkpt>
</trkseg>
</trk>
</gpx>
+11 -2
View File
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.dennisguse.opentracks" package="de.dennisguse.opentracks"
android:installLocation="auto"> android:installLocation="auto">
@@ -52,7 +53,10 @@ limitations under the License.
<uses-permission <uses-permission
android:name="android.permission.BLUETOOTH_ADMIN" android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" /> android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<application <application
@@ -304,7 +308,11 @@ limitations under the License.
<activity android:name=".TrackRecordedActivity" /> <activity android:name=".TrackRecordedActivity" />
<activity android:name=".TrackRecordingActivity" /> <activity android:name=".TrackRecordingActivity" android:parentActivityName=".TrackListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".TrackListActivity" />
</activity>
<activity android:name=".TrackEditActivity" /> <activity android:name=".TrackEditActivity" />
@@ -316,6 +324,7 @@ limitations under the License.
<activity android:name=".io.file.exporter.ExportActivity" /> <activity android:name=".io.file.exporter.ExportActivity" />
<activity android:name=".settings.SettingsActivity" /> <activity android:name=".settings.SettingsActivity" />
<activity android:name=".settings.SettingsCustomLayoutActivity" />
<activity <activity
android:name=".ShowOnMapProxyActivity$KML" android:name=".ShowOnMapProxyActivity$KML"
@@ -22,8 +22,7 @@ import android.view.View;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import de.dennisguse.opentracks.services.tasks.VoiceAnnouncement;
import de.dennisguse.opentracks.util.TTSUtils;
/** /**
* @author Jimmy Shih * @author Jimmy Shih
@@ -35,7 +34,7 @@ public abstract class AbstractActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Set volume control stream for text to speech // Set volume control stream for text to speech
setVolumeControlStream(TTSUtils.getTTSStream()); setVolumeControlStream(VoiceAnnouncement.AUDIO_STREAM);
setContentView(getRootView()); setContentView(getRootView());
@@ -1,14 +1,12 @@
package de.dennisguse.opentracks; package de.dennisguse.opentracks;
import android.app.Application; import android.app.Application;
import android.content.SharedPreferences;
import android.os.StrictMode; import android.os.StrictMode;
import android.util.Log; import android.util.Log;
import java.util.Locale; import java.util.Locale;
import de.dennisguse.opentracks.util.ActivityUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/** /**
* Code that is executed when the application starts. * Code that is executed when the application starts.
@@ -27,13 +25,14 @@ public class Startup extends Application {
// Include version information into stack traces. // Include version information into stack traces.
Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE); Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE);
PreferencesUtils.initPreferences(this, getResources());
// Set default values of preferences on first start. // Set default values of preferences on first start.
SharedPreferences sharedPreferences = PreferencesUtils.resetPreferences(this, false); PreferencesUtils.resetPreferences(this, false);
if (PreferencesUtils.getString(sharedPreferences, this, R.string.stats_units_key, "").equals("")) { if (PreferencesUtils.getString( R.string.stats_units_key, "").equals("")) {
PreferencesUtils.setMetricUnits(sharedPreferences, this, !Locale.US.equals(Locale.getDefault())); PreferencesUtils.setMetricUnits(!Locale.US.equals(Locale.getDefault()));
} }
PreferencesUtils.applyNightMode();
ActivityUtils.applyNightMode(sharedPreferences, this);
// In debug builds: show thread and VM warnings. // In debug builds: show thread and VM warnings.
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@@ -41,4 +40,13 @@ public class Startup extends Application {
StrictMode.enableDefaults(); StrictMode.enableDefaults();
} }
} }
/**
* Returns the name of the database used by SQLiteOpenHelper.
* See {@link android.database.sqlite.SQLiteOpenHelper} for details.
* @return SQLite database name.
*/
public String getDatabaseName() {
return "database.db";
}
} }
@@ -21,7 +21,6 @@ import android.app.ActivityOptions;
import android.app.SearchManager; import android.app.SearchManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.database.Cursor; import android.database.Cursor;
@@ -64,7 +63,7 @@ import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils; import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -77,8 +76,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private static final String TAG = TrackListActivity.class.getSimpleName(); private static final String TAG = TrackListActivity.class.getSimpleName();
// The following are set in onCreate // The following are setFrequency in onCreate
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private ResourceCursorAdapter resourceCursorAdapter; private ResourceCursorAdapter resourceCursorAdapter;
@@ -110,8 +108,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}; };
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, TrackListActivity.this); metricUnits = PreferencesUtils.isMetricUnits();
} }
if (key != null) { if (key != null) {
runOnUiThread(() -> { runOnUiThread(() -> {
@@ -161,8 +159,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback); trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView); viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
@@ -231,8 +227,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(this); trackRecordingServiceConnection.startConnection(this);
} }
@@ -249,7 +244,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind(this); trackRecordingServiceConnection.unbind(this);
} }
@@ -47,7 +47,7 @@ import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils; import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
/** /**
* An activity to show the track detail, record a new track or resumes an existing one. * An activity to show the track detail, record a new track or resumes an existing one.
@@ -66,9 +66,8 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key"; private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
// The following are set in onCreate. // The following are setFrequency in onCreate.
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private TrackRecordedBinding viewBinding; private TrackRecordedBinding viewBinding;
@@ -96,8 +95,8 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(TrackRecordedActivity.this, R.string.recording_distance_interval_key, key)) { if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordedActivity.this)); trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval());
} }
} }
}; };
@@ -107,7 +106,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
contentProviderUtils = new ContentProviderUtils(this); contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
handleIntent(getIntent()); handleIntent(getIntent());
@@ -130,8 +128,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackDataHub.start(); trackDataHub.start();
} }
@@ -155,7 +152,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
super.onStop(); super.onStop();
trackRecordingServiceConnection.unbind(this); trackRecordingServiceConnection.unbind(this);
trackDataHub.stop(); trackDataHub.stop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
} }
@Override @Override
@@ -31,7 +31,7 @@ import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils; import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils; import de.dennisguse.opentracks.util.TrackUtils;
@@ -51,9 +51,8 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key"; private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
// The following are set in onCreate // The following are setFrequency in onCreate
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
@@ -96,17 +95,17 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) { if (PreferencesUtils.isKey(R.string.stats_show_on_lockscreen_while_recording_key, key)) {
setLockscreenPolicy(); setLockscreenPolicy();
} }
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_keep_screen_on_while_recording_key, key)) { if (PreferencesUtils.isKey(R.string.stats_keep_screen_on_while_recording_key, key)) {
setScreenOnPolicy(); setScreenOnPolicy();
} }
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_fullscreen_while_recording_key, key)) { if (PreferencesUtils.isKey(R.string.stats_fullscreen_while_recording_key, key)) {
setFullscreenPolicy(); setFullscreenPolicy();
} }
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_distance_interval_key, key)) { if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordingActivity.this)); trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval());
} }
if (key == null) return; if (key == null) return;
@@ -121,7 +120,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
contentProviderUtils = new ContentProviderUtils(this); contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackId = null; trackId = null;
if (savedInstanceState != null) { if (savedInstanceState != null) {
//Activity was recreated. //Activity was recreated.
@@ -156,7 +154,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
} }
private void setLockscreenPolicy() { private void setLockscreenPolicy() {
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(sharedPreferences, TrackRecordingActivity.this); boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen); setShowWhenLocked(showOnLockScreen);
@@ -168,7 +166,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
} }
private void setScreenOnPolicy() { private void setScreenOnPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldKeepScreenOn(sharedPreferences, TrackRecordingActivity.this); boolean keepScreenOn = PreferencesUtils.shouldKeepScreenOn();
if (keepScreenOn) { if (keepScreenOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -178,7 +176,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
} }
private void setFullscreenPolicy() { private void setFullscreenPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldUseFullscreen(sharedPreferences, TrackRecordingActivity.this); boolean keepScreenOn = PreferencesUtils.shouldUseFullscreen();
if (keepScreenOn) { if (keepScreenOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
@@ -191,8 +189,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(this); trackRecordingServiceConnection.startConnection(this);
trackDataHub.start(); trackDataHub.start();
@@ -230,7 +227,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind(this); trackRecordingServiceConnection.unbind(this);
trackDataHub.stop(); trackDataHub.stop();
} }
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks.adapters; package de.dennisguse.opentracks.adapters;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.util.Pair; import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -11,7 +10,7 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.viewmodels.AggregatedStatistics; import de.dennisguse.opentracks.viewmodels.AggregatedStatistics;
@@ -58,7 +57,7 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
viewHolder = (ViewHolder) convertView.getTag(); viewHolder = (ViewHolder) convertView.getTag();
} }
if (TrackIconUtils.isSpeedIcon(context, aggregatedStatistic.getCategory())) { if (TrackIconUtils.isSpeedIcon(context.getResources(), aggregatedStatistic.getCategory())) {
viewHolder.setSpeed(aggregatedStatistic); viewHolder.setSpeed(aggregatedStatistic);
} else { } else {
viewHolder.setPace(aggregatedStatistic); viewHolder.setPace(aggregatedStatistic);
@@ -139,9 +138,8 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
private void setCommonValues(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) { private void setCommonValues(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
String category = aggregatedStatistic.getCategory(); String category = aggregatedStatistic.getCategory();
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); reportSpeed = PreferencesUtils.isReportSpeed(category);
reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category); metricsUnits = PreferencesUtils.isMetricUnits();
metricsUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
sportIcon.setImageResource(getIcon(aggregatedStatistic)); sportIcon.setImageResource(getIcon(aggregatedStatistic));
typeLabel.setText(category); typeLabel.setText(category);
@@ -59,8 +59,8 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed(), metricUnits, isReportSpeed)); viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed(), metricUnits, isReportSpeed));
viewHolder.gain.setText(StringUtils.formatAltitude(context, interval.getGain_m(), metricUnits)); viewHolder.gain.setText(StringUtils.formatAltitudeChange(context, interval.getGain_m(), metricUnits));
viewHolder.loss.setText(StringUtils.formatAltitude(context, interval.getLoss_m(), metricUnits)); viewHolder.loss.setText(StringUtils.formatAltitudeChange(context, interval.getLoss_m(), metricUnits));
} }
@@ -1,91 +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.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<SensorDataModel> sensorDataList;
private final Context context;
public SensorsAdapter(Context context) {
this.context = context;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.sensor_item, parent, false);
return new SensorsAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
SensorDataModel sensorDataModel = sensorDataList.get(position);
viewHolder.setData(sensorDataModel);
}
@Override
public int getItemCount() {
if (sensorDataList == null) {
return 0;
} else {
return sensorDataList.size();
}
}
public List<SensorDataModel> swapData(List<SensorDataModel> data) {
if (sensorDataList == data) {
return null;
}
sensorDataList = data;
if (data != null) {
this.notifyDataSetChanged();
}
return data;
}
private class ViewHolder extends RecyclerView.ViewHolder {
private final TextView label;
private final TextView sensorValue;
private final TextView value;
private final TextView unit;
public ViewHolder(@NonNull View itemView) {
super(itemView);
label = itemView.findViewById(R.id.stats_sensor_label);
sensorValue = itemView.findViewById(R.id.stats_sensor_sensor_value);
value = itemView.findViewById(R.id.stats_sensor_value);
unit = itemView.findViewById(R.id.stats_sensor_unit);
}
public void setData(SensorDataModel sensorDataModel) {
String sensorName = sensorDataModel.getSensorName();
String sensorValue = sensorDataModel.hasValue() ? sensorDataModel.getSensorValue() : context.getString(R.string.value_unknown);
this.label.setText(context.getString(sensorDataModel.getLabelId()));
if (sensorName == null) {
this.sensorValue.setVisibility(View.GONE);
} else {
this.sensorValue.setText(sensorName);
}
this.value.setText(sensorValue);
this.unit.setText(context.getString(sensorDataModel.getUnitId()));
}
}
}
@@ -0,0 +1,113 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Optional;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.util.StatisticsUtils;
public class SettingsCustomLayoutAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Layout layout;
private final Context context;
private final SettingsCustomLayoutItemClickListener itemClickListener;
public SettingsCustomLayoutAdapter(Context context, SettingsCustomLayoutItemClickListener itemClickListener, Layout layout) {
this.context = context;
this.itemClickListener = itemClickListener;
this.layout = layout;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.custom_stats_item, parent, false);
return new SettingsCustomLayoutAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SettingsCustomLayoutAdapter.ViewHolder viewHolder = (SettingsCustomLayoutAdapter.ViewHolder) holder;
DataField field = layout.getFields().get(position);
viewHolder.itemView.setTag(field.getKey());
viewHolder.title.setText(field.getTitle());
viewHolder.value.setText(StatisticsUtils.emptyValue(context, field.getKey()));
viewHolder.title.setTextAppearance(context, field.isVisible() ? (field.isPrimary() ? R.style.StatsPrimaryLabel : R.style.StatsSecondaryLabel) : R.style.StatsLabelHidden);
viewHolder.value.setTextAppearance(context, field.isVisible() ? (field.isPrimary() ? R.style.StatsPrimaryValue : R.style.StatsSecondaryValue) : R.style.StatsValueHidden);
viewHolder.statusIcon.setVisibility(field.isVisible() ? View.GONE : View.VISIBLE);
viewHolder.statusIcon.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_baseline_visibility_off_24));
viewHolder.moveIcon.setVisibility(field.isVisible() ? View.VISIBLE : View.GONE);
}
@Override
public int getItemCount() {
if (layout == null) {
return 0;
} else {
return layout.getFields().size();
}
}
public boolean isItemWide(int position) {
return layout.getFields().get(position).isWide();
}
public DataField getItem(int position) {
return layout.getFields().get(position);
}
public void swapValues(Layout data) {
this.layout = data;
if (this.layout != null) {
this.notifyDataSetChanged();
}
}
public Layout move(int fromPosition, int toPosition) {
layout.moveField(fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
return layout;
}
private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView title;
final TextView value;
final TextView unit;
final ImageView statusIcon;
final ImageView moveIcon;
public ViewHolder(@NonNull View itemView) {
super(itemView);
title = itemView.findViewById(R.id.stats_description_main);
value = itemView.findViewById(R.id.stats_value);
unit = itemView.findViewById(R.id.stats_unit);
statusIcon = itemView.findViewById(R.id.stats_icon_show_status);
moveIcon = itemView.findViewById(R.id.stats_iv_drag_indicator);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
String statTitle = (String) view.getTag();
Optional<DataField> optionalField = layout.getFields().stream().filter(f -> f.getKey().equals(statTitle)).findFirst();
optionalField.ifPresent(itemClickListener::onSettingsCustomLayoutItemClicked);
}
}
public interface SettingsCustomLayoutItemClickListener {
void onSettingsCustomLayoutItemClicked(@NonNull DataField field);
}
}
@@ -0,0 +1,176 @@
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.content.data.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.StatsPrimaryValue : R.style.StatsSecondaryValue);
this.unit.setText(statisticData.getUnit());
this.descMain.setText(statisticData.getField().getTitle());
this.descMain.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.StatsPrimaryLabel : R.style.StatsSecondaryLabel);
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.StatsPrimaryValue : R.style.StatsSecondaryValue);
this.descMain.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.StatsPrimaryLabel : R.style.StatsSecondaryLabel);
}
}
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);
}
}
}
}
@@ -41,8 +41,8 @@ public class ChartPoint {
if (trackPoint.hasHeartRate()) { if (trackPoint.hasHeartRate()) {
heartRate = (double) trackPoint.getHeartRate_bpm(); heartRate = (double) trackPoint.getHeartRate_bpm();
} }
if (trackPoint.hasCyclingCadence()) { if (trackPoint.hasCadence()) {
cadence = (double) trackPoint.getCyclingCadence_rpm(); cadence = (double) trackPoint.getCadence_rpm();
} }
if (trackPoint.hasPower()) { if (trackPoint.hasPower()) {
power = (double) trackPoint.getPower(); power = (double) trackPoint.getPower();
@@ -77,6 +77,7 @@ public class ChartPoint {
return power; return power;
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return "ChartPoint{" + "timeOrDistance=" + timeOrDistance + '}'; return "ChartPoint{" + "timeOrDistance=" + timeOrDistance + '}';
@@ -0,0 +1,16 @@
package de.dennisguse.opentracks.content.data;
public enum CustomLayoutFieldType {
GENERIC(1),
CLOCK(2);
private final int value;
CustomLayoutFieldType(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
@@ -0,0 +1,112 @@
package de.dennisguse.opentracks.content.data;
import android.content.Context;
import android.content.res.Resources;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.CsvConstants;
public class DataField {
public static final String YES_VALUE = "1";
public static final String NOT_VALUE = "0";
private final String key;
private final String title;
private boolean isVisible;
private boolean isPrimary;
private final boolean isWide;
public DataField(String key, String title, boolean isVisible, boolean isPrimary, boolean isWide) {
this.key = key;
this.title = title;
this.isVisible = isVisible;
this.isPrimary = isPrimary;
this.isWide = isWide;
}
public String getKey() {
return key;
}
public String getTitle() {
return title;
}
public boolean isVisible() {
return isVisible;
}
public void toggleVisibility() {
isVisible = !isVisible;
}
public boolean isPrimary() {
return isPrimary;
}
public void togglePrimary() {
isPrimary = !isPrimary;
}
public boolean isWide() {
return isWide;
}
public CustomLayoutFieldType getType(Context context) {
if (key.equals(context.getString(R.string.stats_custom_layout_clock_key))) {
return CustomLayoutFieldType.CLOCK;
} else {
return CustomLayoutFieldType.GENERIC;
}
}
public String toCsv() {
String visible = this.isVisible ? YES_VALUE : NOT_VALUE;
String primary = this.isPrimary ? YES_VALUE : NOT_VALUE;
return key + CsvConstants.ITEM_SEPARATOR + visible + CsvConstants.ITEM_SEPARATOR + primary;
}
public static String getTitleByKey(Resources resources, String key) {
if (key.equals(resources.getString(R.string.stats_custom_layout_total_time_key))) {
return resources.getString(R.string.stats_total_time);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_moving_time_key))) {
return resources.getString(R.string.stats_moving_time);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_distance_key))) {
return resources.getString(R.string.stats_distance);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_speed_key))) {
return resources.getString(R.string.stats_speed);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_pace_key))) {
return resources.getString(R.string.stats_pace);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_average_moving_speed_key))) {
return resources.getString(R.string.stats_average_moving_speed);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_average_speed_key))) {
return resources.getString(R.string.stats_average_speed);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_max_speed_key))) {
return resources.getString(R.string.stats_max_speed);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_average_moving_pace_key))) {
return resources.getString(R.string.stats_average_moving_pace);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_average_pace_key))) {
return resources.getString(R.string.stats_average_pace);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_fastest_pace_key))) {
return resources.getString(R.string.stats_fastest_pace);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_altitude_key))) {
return resources.getString(R.string.stats_altitude);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_gain_key))) {
return resources.getString(R.string.stats_gain);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_loss_key))) {
return resources.getString(R.string.stats_loss);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_coordinates_key))) {
return resources.getString(R.string.stats_coordinates);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_heart_rate_key))) {
return resources.getString(R.string.stats_sensors_heart_rate);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_cadence_key))) {
return resources.getString(R.string.stats_sensors_cadence);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_power_key))) {
return resources.getString(R.string.stats_sensors_power);
} else if (key.equals(resources.getString(R.string.stats_custom_layout_clock_key))) {
return resources.getString(R.string.stats_clock);
} else {
throw new RuntimeException("It doesn't exists a field with key: " + key);
}
}
}
@@ -123,6 +123,7 @@ public class Distance {
return Objects.hash(distance_m); return Objects.hash(distance_m);
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Distance{" + return "Distance{" +
@@ -0,0 +1,42 @@
package de.dennisguse.opentracks.content.data;
import java.util.ArrayList;
import java.util.List;
public class Layout {
private final String profile;
private final List<DataField> dataFields = new ArrayList<>();
public Layout(String profile) {
this.profile = profile;
}
public void addField(String key, String title, boolean visible, boolean primary, boolean isWide) {
dataFields.add(new DataField(key, title, visible, primary, isWide));
}
public void addField(DataField dataField) {
dataFields.add(dataField);
}
public void addFields(List<DataField> dataFields) {
this.dataFields.addAll(dataFields);
}
public void removeField(DataField dataField) {
dataFields.remove(dataField);
}
public List<DataField> getFields() {
return new ArrayList<>(dataFields);
}
public void moveField(int from, int to) {
DataField dataFieldToMove = dataFields.remove(from);
dataFields.add(to, dataFieldToMove);
}
public String getProfile() {
return profile;
}
}
@@ -1,8 +1,11 @@
package de.dennisguse.opentracks.content.data; package de.dennisguse.opentracks.content.data;
import androidx.annotation.NonNull;
import java.time.Duration; import java.time.Duration;
import java.util.Objects; import java.util.Objects;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
public class Speed { public class Speed {
@@ -23,6 +26,10 @@ public class Speed {
return of(Float.parseFloat(speed_mps)); return of(Float.parseFloat(speed_mps));
} }
public static Speed ofKMH(double speed_kmh) {
return of(Distance.ofKilometer(speed_kmh), Duration.ofHours(1));
}
public static Speed zero() { public static Speed zero() {
return of(0.0); return of(0.0);
} }
@@ -40,10 +47,6 @@ public class Speed {
return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps)); return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps));
} }
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private final double speed_mps; private final double speed_mps;
private Speed(double speed_mps) { private Speed(double speed_mps) {
@@ -63,7 +66,7 @@ public class Speed {
} }
public boolean isMoving() { public boolean isMoving() {
return !isInvalid() && speed_mps >= MAX_NO_MOVEMENT_SPEED; return !isInvalid() && greaterThan(PreferencesUtils.getIdleSpeed());
} }
public boolean lessThan(Speed speed) { public boolean lessThan(Speed speed) {
@@ -132,6 +135,7 @@ public class Speed {
return Objects.hash(speed_mps); return Objects.hash(speed_mps);
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Speed{" + return "Speed{" +
@@ -91,7 +91,7 @@ public class TrackPoint {
private Type type; private Type type;
private Float heartRate_bpm = null; private Float heartRate_bpm = null;
private Float cyclingCadence_rpm = null; private Float cadence_rpm = null;
private Float power = null; private Float power = null;
private Float altitudeGain_m = null; private Float altitudeGain_m = null;
private Float altitudeLoss_m = null; private Float altitudeLoss_m = null;
@@ -310,11 +310,11 @@ public class TrackPoint {
} }
@Nullable @Nullable
public Distance distanceToPrevious(@NonNull TrackPoint previous) { public Distance distanceToPrevious(TrackPoint previous) {
if (hasSensorDistance()) { if (hasSensorDistance()) {
return getSensorDistance(); return getSensorDistance();
} }
if (!(hasLocation() && previous.hasLocation())) { if (previous == null || !(hasLocation() && previous.hasLocation())) {
return null; return null;
} }
@@ -350,7 +350,7 @@ public class TrackPoint {
} }
public boolean hasSensorData() { public boolean hasSensorData() {
return hasHeartRate() || hasCyclingCadence() || hasPower(); return hasHeartRate() || hasCadence() || hasPower();
} }
public boolean hasHeartRate() { public boolean hasHeartRate() {
@@ -366,16 +366,16 @@ public class TrackPoint {
return this; return this;
} }
public boolean hasCyclingCadence() { public boolean hasCadence() {
return cyclingCadence_rpm != null; return cadence_rpm != null;
} }
public float getCyclingCadence_rpm() { public float getCadence_rpm() {
return cyclingCadence_rpm; return cadence_rpm;
} }
public TrackPoint setCyclingCadence_rpm(Float cyclingCadence_rpm) { public TrackPoint setCadence_rpm(Float cadence_rpm) {
this.cyclingCadence_rpm = cyclingCadence_rpm; this.cadence_rpm = cadence_rpm;
return this; return this;
} }
@@ -396,15 +396,17 @@ public class TrackPoint {
@Override @Override
public String toString() { public String toString() {
String result = "time=" + getTime() + " (type=" + getType() + ")"; String result = "time=" + getTime() + " (type=" + getType() + ")";
if (!hasLocation()) { if (hasLocation()) {
return result;
}
result += ": lat=" + getLatitude() + " lng=" + getLongitude(); result += ": lat=" + getLatitude() + " lng=" + getLongitude();
if (!hasHorizontalAccuracy()) { }
return result; if (hasHorizontalAccuracy()) {
result += " acc=" + getHorizontalAccuracy();
}
if (hasSensorDistance()) {
result += " distance=" + getSensorDistance();
} }
return result + " acc=" + getHorizontalAccuracy(); return result;
} }
public static class Id { public static class Id {
@@ -290,6 +290,31 @@ public class ContentProviderUtils {
return values; return values;
} }
public void updateTrackStatistics(@NonNull Track.Id trackId, @NonNull TrackStatistics trackStatistics) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
}
private ContentValues createContentValues(TrackStatistics trackStatistics) {
ContentValues values = new ContentValues();
if (trackStatistics.getStartTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
}
if (trackStatistics.getStopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
}
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed().toMPS());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed().toMPS());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed().toMPS());
values.put(TracksColumns.MIN_ALTITUDE, trackStatistics.getMinAltitude());
values.put(TracksColumns.MAX_ALTITUDE, trackStatistics.getMaxAltitude());
values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.getTotalAltitudeGain());
values.put(TracksColumns.ALTITUDE_LOSS, trackStatistics.getTotalAltitudeLoss());
return values;
}
public Marker createMarker(Cursor cursor) { public Marker createMarker(Cursor cursor) {
int idIndex = cursor.getColumnIndexOrThrow(MarkerColumns._ID); int idIndex = cursor.getColumnIndexOrThrow(MarkerColumns._ID);
int nameIndex = cursor.getColumnIndexOrThrow(MarkerColumns.NAME); int nameIndex = cursor.getColumnIndexOrThrow(MarkerColumns.NAME);
@@ -533,7 +558,7 @@ public class ContentProviderUtils {
trackPoint.setHeartRate_bpm(cursor.getFloat(indexes.sensorHeartRateIndex)); trackPoint.setHeartRate_bpm(cursor.getFloat(indexes.sensorHeartRateIndex));
} }
if (!cursor.isNull(indexes.sensorCadenceIndex)) { if (!cursor.isNull(indexes.sensorCadenceIndex)) {
trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex)); trackPoint.setCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex));
} }
if (!cursor.isNull(indexes.sensorDistanceIndex)) { if (!cursor.isNull(indexes.sensorDistanceIndex)) {
trackPoint.setSensorDistance(Distance.of(cursor.getFloat(indexes.sensorDistanceIndex))); trackPoint.setSensorDistance(Distance.of(cursor.getFloat(indexes.sensorDistanceIndex)));
@@ -694,8 +719,8 @@ public class ContentProviderUtils {
if (trackPoint.hasHeartRate()) { if (trackPoint.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate_bpm()); values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate_bpm());
} }
if (trackPoint.hasCyclingCadence()) { if (trackPoint.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm()); values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCadence_rpm());
} }
if (trackPoint.hasSensorDistance()) { if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM()); values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM());
@@ -11,6 +11,7 @@ import androidx.annotation.VisibleForTesting;
import java.util.UUID; import java.util.UUID;
import de.dennisguse.opentracks.Startup;
import de.dennisguse.opentracks.content.data.MarkerColumns; import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPointsColumns; import de.dennisguse.opentracks.content.data.TrackPointsColumns;
@@ -27,11 +28,8 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 31; private static final int DATABASE_VERSION = 31;
@VisibleForTesting
public static final String DATABASE_NAME = "database.db";
public CustomSQLiteOpenHelper(Context context) { public CustomSQLiteOpenHelper(Context context) {
this(context, DATABASE_NAME); this(context, ((Startup) context.getApplicationContext()).getDatabaseName());
} }
@VisibleForTesting @VisibleForTesting
@@ -50,9 +50,18 @@ public abstract class SensorData<T> {
return value != null; return value != null;
} }
@NonNull
protected abstract T getNoneValue();
public T getValue() { public T getValue() {
if (!hasValue()) {
return null;
}
if (isRecent()) {
return value; return value;
} }
return getNoneValue();
}
/** /**
* Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance. * Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance.
@@ -63,7 +72,7 @@ public abstract class SensorData<T> {
/** /**
* Is the data recent considering the current time. * Is the data recent considering the current time.
*/ */
public boolean isRecent() { private boolean isRecent() {
return Instant.now() return Instant.now()
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)); .isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
} }
@@ -59,19 +59,32 @@ public final class SensorDataCycling {
return crankRevolutionsTime; return crankRevolutionsTime;
} }
@NonNull
@Override
protected Float getNoneValue() {
return 0f;
}
public void compute(Cadence previous) { public void compute(Cadence previous) {
if (hasData() && previous != null && previous.hasData()) { if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS; float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) { if (timeDiff_ms <= 0) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence."); Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null; value = null;
} else { 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); long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
float cadence_ms = crankDiff / timeDiff_ms; float cadence_ms = crankDiff / timeDiff_ms;
value = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN); value = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
} }
} }
}
@NonNull @NonNull
@Override @Override
@@ -121,6 +134,16 @@ public final class SensorDataCycling {
return wheelRevolutionsTime; 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) { public void compute(DistanceSpeed previous, Distance wheelCircumference) {
if (hasData() && previous != null && previous.hasData()) { if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS; float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
@@ -131,6 +154,10 @@ public final class SensorDataCycling {
return; 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); long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX);
Distance distance = wheelCircumference.multipliedBy(wheelDiff); Distance distance = wheelCircumference.multipliedBy(wheelDiff);
@@ -191,6 +218,7 @@ public final class SensorDataCycling {
return speed; return speed;
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Data{" + return "Data{" +
@@ -216,6 +244,12 @@ public final class SensorDataCycling {
public DistanceSpeed getDistanceSpeed() { public DistanceSpeed getDistanceSpeed() {
return this.value != null ? this.value.second : null; return this.value != null ? this.value.second : null;
} }
@NonNull
@Override
protected Pair<Cadence, DistanceSpeed> getNoneValue() {
return new Pair<>(null, null);
}
} }
} }
@@ -18,4 +18,10 @@ public class SensorDataCyclingPower extends SensorData<Float> {
public String toString() { public String toString() {
return super.toString() + " power=" + value; return super.toString() + " power=" + value;
} }
@NonNull
@Override
protected Float getNoneValue() {
return 0f;
}
} }
@@ -18,4 +18,10 @@ public class SensorDataHeartRate extends SensorData<Float> {
public String toString() { public String toString() {
return super.toString() + " heart=" + value; return super.toString() + " heart=" + value;
} }
@NonNull
@Override
protected Float getNoneValue() {
return 0f;
}
} }
@@ -1,5 +1,6 @@
package de.dennisguse.opentracks.content.sensor; package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@@ -42,7 +43,6 @@ public final class SensorDataRunning extends SensorData<SensorDataRunning.Data>
return cadence; return cadence;
} }
@VisibleForTesting
public Speed getSpeed() { public Speed getSpeed() {
return speed; return speed;
} }
@@ -52,9 +52,20 @@ public final class SensorDataRunning extends SensorData<SensorDataRunning.Data>
return totalDistance; return totalDistance;
} }
@NonNull
@Override
protected Data getNoneValue() {
if (value != null) {
return new Data(Speed.zero(), 0f, value.distance);
} else {
return new Data(Speed.zero(), 0f, Distance.of(0));
}
}
public void compute(SensorDataRunning previous) { public void compute(SensorDataRunning previous) {
if (speed != null && hasTotalDistance()) {
Distance overallDistance = null; Distance overallDistance = null;
if (hasTotalDistance() && previous != null && previous.hasTotalDistance()) { if (previous != null && previous.hasTotalDistance()) {
overallDistance = this.totalDistance.minus(previous.totalDistance); overallDistance = this.totalDistance.minus(previous.totalDistance);
if (previous.hasValue() && previous.getValue().getDistance() != null) { if (previous.hasValue() && previous.getValue().getDistance() != null) {
overallDistance = overallDistance.plus(previous.getValue().getDistance()); overallDistance = overallDistance.plus(previous.getValue().getDistance());
@@ -63,6 +74,7 @@ public final class SensorDataRunning extends SensorData<SensorDataRunning.Data>
value = new Data(speed, cadence, overallDistance); value = new Data(speed, cadence, overallDistance);
} }
}
@Override @Override
public void reset() { public void reset() {
@@ -92,10 +104,12 @@ public final class SensorDataRunning extends SensorData<SensorDataRunning.Data>
return cadence; return cadence;
} }
@Nullable
public Distance getDistance() { public Distance getDistance() {
return distance; return distance;
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Data{" + return "Data{" +
@@ -1,7 +1,10 @@
package de.dennisguse.opentracks.content.sensor; package de.dennisguse.opentracks.content.sensor;
import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
public final class SensorDataSet { public final class SensorDataSet {
@@ -27,8 +30,36 @@ public final class SensorDataSet {
this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence; this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence;
} }
public SensorDataHeartRate getHeartRate() { public Pair<Float, String> getHeartRate() {
return heartRate; if (heartRate != null) {
return new Pair<>(heartRate.getValue(), heartRate.getSensorNameOrAddress());
}
return null;
}
public Pair<Float, String> getCadence() {
if (cyclingCadence != null) {
return new Pair<>(cyclingCadence.getValue(), cyclingCadence.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null) {
return new Pair<>(runningDistanceSpeedCadence.getCadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
}
public Pair<Speed, String> getSpeed() {
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue().getSpeed() != null) {
return new Pair<>(cyclingDistanceSpeed.getValue().getSpeed(), cyclingDistanceSpeed.getSensorNameOrAddress());
}
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.getValue().getSpeed() != null) {
return new Pair<>(runningDistanceSpeedCadence.getSpeed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
}
return null;
} }
public SensorDataCycling.Cadence getCyclingCadence() { public SensorDataCycling.Cadence getCyclingCadence() {
@@ -64,17 +95,20 @@ public final class SensorDataSet {
} }
public void fillTrackPoint(TrackPoint trackPoint) { public void fillTrackPoint(TrackPoint trackPoint) {
if (heartRate != null) { if (getHeartRate() != null) {
trackPoint.setHeartRate_bpm(heartRate.getValue()); trackPoint.setHeartRate_bpm(getHeartRate().first);
} }
if (cyclingCadence != null && cyclingCadence.hasValue()) { if (getCadence() != null) {
trackPoint.setCyclingCadence_rpm(cyclingCadence.getValue()); trackPoint.setCadence_rpm(getCadence().first);
}
if (getSpeed() != null) {
trackPoint.setSpeed(getSpeed().first);
} }
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) { if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().getDistanceOverall()); trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().getDistanceOverall());
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().getSpeed());
} }
if (cyclingPower != null && cyclingPower.hasValue()) { if (cyclingPower != null && cyclingPower.hasValue()) {
@@ -83,8 +117,6 @@ public final class SensorDataSet {
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) { if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) {
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().getDistance()); trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().getDistance());
trackPoint.setSpeed(runningDistanceSpeedCadence.getValue().getSpeed());
trackPoint.setCyclingCadence_rpm(runningDistanceSpeedCadence.getValue().getCadence());
} }
} }
@@ -93,18 +125,17 @@ public final class SensorDataSet {
if (cyclingCadence != null) cyclingCadence.reset(); if (cyclingCadence != null) cyclingCadence.reset();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset(); if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset();
if (cyclingPower != null) cyclingPower.reset(); if (cyclingPower != null) cyclingPower.reset();
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset(); if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset();
} }
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {
return (getHeartRate() != null ? "" + getHeartRate() : "") return (heartRate != null ? "" + heartRate : "")
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "") + (cyclingCadence != null ? " " + cyclingCadence : "")
+ (getCyclingDistanceSpeed() != null ? " " + getCyclingDistanceSpeed() : "") + (cyclingDistanceSpeed != null ? " " + cyclingDistanceSpeed : "")
+ (getCyclingPower() != null ? " " + getCyclingPower() : "") + (cyclingPower != null ? " " + cyclingPower : "")
+ (getRunningDistanceSpeedCadence() != null ? " " + getRunningDistanceSpeedCadence() : ""); + (runningDistanceSpeedCadence != null ? " " + runningDistanceSpeedCadence : "");
} }
private void set(@NonNull SensorData<?> type, SensorData<?> data) { private void set(@NonNull SensorData<?> type, SensorData<?> data) {
@@ -42,7 +42,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding; import de.dennisguse.opentracks.databinding.ChartBinding;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
/** /**
* A fragment to display track chart to the user. * A fragment to display track chart to the user.
@@ -64,8 +64,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
return chartFragment; return chartFragment;
} }
private SharedPreferences sharedPreferences;
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
// Stats gathered from the received data // Stats gathered from the received data
@@ -80,8 +78,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext()); boolean metricUnits = PreferencesUtils.isMetricUnits();
if (metricUnits != viewBinding.chartView.getMetricUnits()) { if (metricUnits != viewBinding.chartView.getMetricUnits()) {
viewBinding.chartView.setMetricUnits(metricUnits); viewBinding.chartView.setMetricUnits(metricUnits);
runOnUiThread(() -> { runOnUiThread(() -> {
@@ -91,8 +89,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}); });
} }
} }
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { if (PreferencesUtils.isKey(R.string.stats_rate_key, key)) {
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category); boolean reportSpeed = PreferencesUtils.isReportSpeed(category);
if (reportSpeed != viewBinding.chartView.getReportSpeed()) { if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
viewBinding.chartView.setReportSpeed(reportSpeed); viewBinding.chartView.setReportSpeed(reportSpeed);
viewBinding.chartView.applyReportSpeed(); viewBinding.chartView.applyReportSpeed();
@@ -108,7 +106,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}; };
/** /**
* A runnable that will set the orange pointer as appropriate and redraw. * A runnable that will setFrequency the orange pointer as appropriate and redraw.
*/ */
private final Runnable updateChart = new Runnable() { private final Runnable updateChart = new Runnable() {
@Override @Override
@@ -141,9 +139,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
resumeTrackDataHub(); resumeTrackDataHub();
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
checkChartSettings(); checkChartSettings();
getActivity().runOnUiThread(updateChart); getActivity().runOnUiThread(updateChart);
@@ -153,7 +150,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
pauseTrackDataHub(); pauseTrackDataHub();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
} }
@Override @Override
@@ -162,12 +159,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
viewBinding = null; viewBinding = null;
} }
@Override
public void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
@Override @Override
public void onTrackUpdated(Track track) { public void onTrackUpdated(Track track) {
if (isResumed()) { if (isResumed()) {
@@ -177,7 +168,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
category = track.getCategory(); category = track.getCategory();
boolean reportSpeed = PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category); boolean reportSpeed = PreferencesUtils.isReportSpeed(category);
if (reportSpeed != viewBinding.chartView.getReportSpeed()) { if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
viewBinding.chartView.setReportSpeed(reportSpeed); viewBinding.chartView.setReportSpeed(reportSpeed);
viewBinding.chartView.applyReportSpeed(); viewBinding.chartView.applyReportSpeed();
@@ -23,7 +23,7 @@ import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.IntervalListViewBinding; import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel; import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
@@ -46,15 +46,13 @@ public class IntervalsFragment extends Fragment {
private IntervalStatisticsAdapter adapter; private IntervalStatisticsAdapter adapter;
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter; private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter;
private SharedPreferences sharedPreferences;
private boolean isReportSpeed; private boolean isReportSpeed;
private IntervalListViewBinding viewBinding; private IntervalListViewBinding viewBinding;
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key) || PreferencesUtils.isKey(R.string.stats_rate_key, key)) {
updateIntervals(PreferencesUtils.isMetricUnits(sharedPreferences, getContext()), selectedInterval); updateIntervals(PreferencesUtils.isMetricUnits(), selectedInterval);
if (spinnerAdapter != null) { if (spinnerAdapter != null) {
spinnerAdapter.notifyDataSetChanged(); spinnerAdapter.notifyDataSetChanged();
} }
@@ -93,8 +91,6 @@ public class IntervalsFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, metricUnits, isReportSpeed); adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, metricUnits, isReportSpeed);
viewBinding.intervalList.setLayoutManager(new LinearLayoutManager(getContext())); viewBinding.intervalList.setLayoutManager(new LinearLayoutManager(getContext()));
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView); // TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
@@ -136,13 +132,12 @@ public class IntervalsFragment extends Fragment {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getContext()); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getContext());
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
if (track != null) { if (track != null) {
isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), track.getCategory()); isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
} }
viewModel = new ViewModelProvider(getActivity()).get(IntervalStatisticsModel.class); viewModel = new ViewModelProvider(getActivity()).get(IntervalStatisticsModel.class);
@@ -152,7 +147,9 @@ public class IntervalsFragment extends Fragment {
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
if (viewModel != null) { if (viewModel != null) {
viewModel.onPause(); viewModel.onPause();
} }
@@ -168,8 +165,6 @@ public class IntervalsFragment extends Fragment {
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
sharedPreferences = null;
adapter = null; adapter = null;
viewModel = null; viewModel = null;
} }
@@ -254,9 +254,9 @@ public class MarkerDetailFragment extends Fragment {
private void setLocation(boolean addShadow) { private void setLocation(boolean addShadow) {
String value = "[" + getString(R.string.stats_latitude) + " " String value = "[" + getString(R.string.stats_latitude) + " "
+ StringUtils.formatCoordinate(marker.getLatitude()) + ", " + StringUtils.formatCoordinate(getContext(), marker.getLatitude()) + ", "
+ getString(R.string.stats_longitude) + " " + getString(R.string.stats_longitude) + " "
+ StringUtils.formatCoordinate(marker.getLongitude()) + "]"; + StringUtils.formatCoordinate(getContext(), marker.getLongitude()) + "]";
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerLocation, value, addShadow); ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerLocation, value, addShadow);
} }
@@ -31,21 +31,21 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity; import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.adapters.SensorsAdapter; import de.dennisguse.opentracks.adapters.StatisticsAdapter;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding; import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.viewmodels.SensorDataModel; 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}. * A fragment to display track statistics to the user for a recorded {@link Track}.
@@ -69,7 +69,7 @@ public class StatisticsRecordedFragment extends Fragment {
} }
private SensorStatistics sensorStatistics; private SensorStatistics sensorStatistics;
private SensorsAdapter sensorsAdapter; private StatisticsAdapter.WithRecordedLayout sensorsAdapter;
private Track.Id trackId; private Track.Id trackId;
@Nullable // Lazily loaded. @Nullable // Lazily loaded.
@@ -79,21 +79,20 @@ public class StatisticsRecordedFragment extends Fragment {
private StatisticsRecordedBinding viewBinding; private StatisticsRecordedBinding viewBinding;
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits; private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed; private boolean preferenceReportSpeed;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false; boolean updateUInecessary = false;
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true; updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext()); preferenceMetricUnits = PreferencesUtils.isMetricUnits();
} }
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && track != null) { if (PreferencesUtils.isKey(R.string.stats_rate_key, key) && track != null) {
updateUInecessary = true; updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), track.getCategory()); preferenceReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
} }
if (key != null && updateUInecessary && isResumed()) { if (key != null && updateUInecessary && isResumed()) {
@@ -112,9 +111,7 @@ public class StatisticsRecordedFragment extends Fragment {
trackId = getArguments().getParcelable(TRACK_ID_KEY); trackId = getArguments().getParcelable(TRACK_ID_KEY);
contentProviderUtils = new ContentProviderUtils(getContext()); contentProviderUtils = new ContentProviderUtils(getContext());
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext()); sensorsAdapter = new StatisticsAdapter.WithRecordedLayout(getContext());
sensorsAdapter = new SensorsAdapter(getContext());
} }
@Override @Override
@@ -132,24 +129,22 @@ public class StatisticsRecordedFragment extends Fragment {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
loadStatistics(); loadStatistics();
} }
@Override
public void onPause() {
super.onPause();
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
viewBinding = null; viewBinding = null;
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
public void onDestroy() {
super.onDestroy();
sharedPreferences = null;
} }
public void loadStatistics() { public void loadStatistics() {
@@ -168,7 +163,7 @@ public class StatisticsRecordedFragment extends Fragment {
boolean prefsChanged = this.track == null || (!this.track.getCategory().equals(track.getCategory())); boolean prefsChanged = this.track == null || (!this.track.getCategory().equals(track.getCategory()));
this.track = track; this.track = track;
if (prefsChanged) { if (prefsChanged) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key)); sharedPreferenceChangeListener.onSharedPreferenceChanged(null, getString(R.string.stats_rate_key));
} }
loadTrackDescription(track); loadTrackDescription(track);
@@ -238,22 +233,21 @@ public class StatisticsRecordedFragment extends Fragment {
// Set altitude gain and loss // Set altitude gain and loss
{ {
// Make altitude visible?
boolean show = PreferencesUtils.isShowStatsAltitude(sharedPreferences, getContext());
viewBinding.statsAltitudeGroup.setVisibility(show ? View.VISIBLE : View.GONE);
Float altitudeGain_m = trackStatistics.getTotalAltitudeGain(); Float altitudeGain_m = trackStatistics.getTotalAltitudeGain();
Float altitudeLoss_m = trackStatistics.getTotalAltitudeLoss(); Float altitudeLoss_m = trackStatistics.getTotalAltitudeLoss();
Pair<String, String> parts; Pair<String, String> parts;
parts = StringUtils.getAltitudeParts(getContext(), altitudeGain_m, preferenceMetricUnits); parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeGain_m, preferenceMetricUnits);
viewBinding.statsAltitudeGainValue.setText(parts.first); viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second); viewBinding.statsAltitudeGainUnit.setText(parts.second);
parts = StringUtils.getAltitudeParts(getContext(), altitudeLoss_m, preferenceMetricUnits); parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeLoss_m, preferenceMetricUnits);
viewBinding.statsAltitudeLossValue.setText(parts.first); viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second); viewBinding.statsAltitudeLossUnit.setText(parts.second);
boolean show = altitudeGain_m != null && altitudeLoss_m != null;
viewBinding.statsAltitudeGroup.setVisibility(show ? View.VISIBLE : View.GONE);
} }
} }
@@ -262,18 +256,7 @@ public class StatisticsRecordedFragment extends Fragment {
return; return;
} }
List<SensorDataModel> sensorDataList = new ArrayList<>(); List<StatisticData> sensorDataList = StatisticDataBuilder.fromSensorStatistics(getContext(), sensorStatistics);
if (sensorStatistics.hasHeartRate()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_max, R.string.sensor_unit_beats_per_minute, sensorStatistics.getMaxHeartRate()));
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_avg, R.string.sensor_unit_beats_per_minute, sensorStatistics.getAvgHeartRate()));
}
if (sensorStatistics.hasCadence()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_max, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getMaxCadence()));
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_avg, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getAvgCadence()));
}
if (sensorStatistics.hasPower()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_power_avg, R.string.sensor_unit_power, sensorStatistics.getAvgPower()));
}
if (sensorDataList.size() > 0) { if (sensorDataList.size() > 0) {
sensorsAdapter.swapData(sensorDataList); sensorsAdapter.swapData(sensorDataList);
} }
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -11,28 +10,25 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; 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.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordingActivity; import de.dennisguse.opentracks.adapters.StatisticsAdapter;
import de.dennisguse.opentracks.adapters.SensorsAdapter; import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding; import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.viewmodels.StatisticData;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.viewmodels.StatisticsDataModel;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
/** /**
* A fragment to display track statistics to the user for a currently recording {@link Track}. * A fragment to display track statistics to the user for a currently recording {@link Track}.
@@ -51,37 +47,31 @@ public class StatisticsRecordingFragment extends Fragment {
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackRecordingService.RecordingData recordingData = TrackRecordingService.NOT_RECORDING; private TrackRecordingService.RecordingData recordingData = TrackRecordingService.NOT_RECORDING;
private TrackPoint latestTrackPoint; private TrackPoint latestTrackPoint;
private Layout layout;
private StatisticsRecordingBinding viewBinding; private StatisticsRecordingBinding viewBinding;
private SensorsAdapter sensorsAdapter; private StatisticsAdapter statisticsAdapter;
private GridLayoutManager gridLayoutManager;
private StatisticsDataModel viewModel;
private LiveData<List<StatisticData>> statisticsLiveData;
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits; private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private boolean preferenceShowAltitude;
private boolean preferenceShowCoordinate;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false; boolean updateUInecessary = false;
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
updateUInecessary = true; updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext()); preferenceMetricUnits = PreferencesUtils.isMetricUnits();
} }
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && recordingData != null) { if (PreferencesUtils.isKey(R.string.stats_custom_layout_fields_key, key)) {
updateUInecessary = true; updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), recordingData.getTrackCategory()); layout = PreferencesUtils.getCustomLayout();
} }
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_altitude_key, key)) { if (PreferencesUtils.isKey(R.string.stats_custom_layout_columns_key, key)) {
updateUInecessary = true; gridLayoutManager.setSpanCount(PreferencesUtils.getLayoutColumns());
preferenceShowAltitude = PreferencesUtils.isShowStatsAltitude(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_coordinate_key, key)) {
updateUInecessary = true;
preferenceShowCoordinate = PreferencesUtils.isStatsShowCoordinate(sharedPreferences, getContext());
} }
if (key != null && updateUInecessary && isResumed()) { if (key != null && updateUInecessary && isResumed()) {
@@ -106,20 +96,30 @@ public class StatisticsRecordingFragment extends Fragment {
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback); trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
sensorsAdapter = new SensorsAdapter(getContext()); statisticsAdapter = new StatisticsAdapter(getContext());
} }
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false); viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(recordingData.getTrackCategory()));
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView; RecyclerView recyclerView = viewBinding.statsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), RecyclerView.VERTICAL));
sensorsRecyclerView.setAdapter(sensorsAdapter); final int numColumns = PreferencesUtils.getLayoutColumns();
gridLayoutManager = new GridLayoutManager(getContext(), numColumns);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (statisticsAdapter.isItemWide(position)) {
return numColumns;
}
return 1;
}
});
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.setAdapter(statisticsAdapter);
return viewBinding.getRoot(); return viewBinding.getRoot();
} }
@@ -128,8 +128,11 @@ public class StatisticsRecordingFragment extends Fragment {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
viewModel = new ViewModelProvider(getActivity()).get(StatisticsDataModel.class);
statisticsLiveData = viewModel.getStatsData();
statisticsLiveData.observe(getActivity(), statsDataList -> statisticsAdapter.swapData(statsDataList));
trackRecordingServiceConnection.startConnection(getContext()); trackRecordingServiceConnection.startConnection(getContext());
} }
@@ -137,8 +140,7 @@ public class StatisticsRecordingFragment extends Fragment {
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
} }
@Override @Override
@@ -157,168 +159,27 @@ public class StatisticsRecordingFragment extends Fragment {
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
trackRecordingServiceConnection = null; trackRecordingServiceConnection = null;
sharedPreferences = null; viewModel = null;
if (statisticsLiveData != null) {
statisticsLiveData.removeObservers(getActivity());
}
statisticsLiveData = null;
} }
private void updateUI() { private void updateUI() {
if (isResumed()) { if (isResumed()) {
updateStats(); viewModel.update(recordingData, layout, preferenceMetricUnits);
setLocationValues();
updateSensorDataUI();
} }
} }
/**
* Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}.
* Also sets altitude gain and loss.
*/
private void updateSensorDataUI() {
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
if (sensorDataSet != null) {
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorDataSet.getHeartRate() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
}
if (sensorDataSet.getCyclingCadence() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
}
if (sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
}
sensorsAdapter.swapData(sensorDataList);
}
{
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), recordingData.getTrackStatistics().getTotalAltitudeGain(), preferenceMetricUnits);
viewBinding.statsAltitudeGainValue.setText(parts.first);
viewBinding.statsAltitudeGainUnit.setText(parts.second);
}
{
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), recordingData.getTrackStatistics().getTotalAltitudeLoss(), preferenceMetricUnits);
viewBinding.statsAltitudeLossValue.setText(parts.first);
viewBinding.statsAltitudeLossUnit.setText(parts.second);
}
}
private void updateStats() {
// Set activity type
String trackIconValue = TrackIconUtils.getIconValue(getContext(), recordingData.getTrackCategory());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
// Set time
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(recordingData.getTrackStatistics().getMovingTime()));
updateTotalTime();
// Set average speed/pace
{
Speed speed = recordingData.getTrackStatistics().getAverageSpeed();
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
// Set max speed/pace
{
Speed speed = recordingData.getTrackStatistics().getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
// Set moving speed/pace
{
Speed speed = recordingData.getTrackStatistics().getAverageMovingSpeed();
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
// Set altitude gain and loss
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
}
private void updateTotalTime() {
Duration totalTime = recordingData.getTrackStatistics().getTotalTime();
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
}
private void setLocationValues() {
// Set speed/pace
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null) {
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
if (data.hasValue() && data.isRecent()) {
speed = data.getValue().getSpeed();
}
}
setSpeed(speed);
// Set distance
{
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), recordingData.getTrackStatistics().getTotalDistance(), preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
// Set altitude
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
if (preferenceShowAltitude) {
// Current altitude
Float altitude = null;
int labelId = R.string.value_unknown;
if (latestTrackPoint != null && latestTrackPoint.hasAltitude()) {
altitude = (float) latestTrackPoint.getAltitude().toM();
labelId = latestTrackPoint.getAltitude().getLabelId();
}
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
viewBinding.statsAltitudeCurrentLabelEgm.setText(labelId);
}
// Set coordinate
viewBinding.statsCoordinateGroup.setVisibility(preferenceShowCoordinate ? View.VISIBLE : View.GONE);
if (preferenceShowCoordinate) {
String latitudeText = getContext().getString(R.string.value_unknown);
String longitudeText = getContext().getString(R.string.value_unknown);
if (latestTrackPoint != null && latestTrackPoint.hasLocation()) {
latitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLatitude());
longitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLongitude());
}
viewBinding.statsLatitudeValue.setText(latitudeText);
viewBinding.statsLongitudeValue.setText(longitudeText);
}
}
private void setSpeed(Speed speed) {
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsSpeedValue.setText(parts.first);
viewBinding.statsSpeedUnit.setText(parts.second);
}
private void onRecordingDataChanged(TrackRecordingService.RecordingData recordingData) { private void onRecordingDataChanged(TrackRecordingService.RecordingData recordingData) {
String oldCategory = this.recordingData.getTrackCategory(); String oldCategory = this.recordingData.getTrackCategory();
String newCategory = recordingData.getTrackCategory(); String newCategory = recordingData.getTrackCategory();
this.recordingData = recordingData; this.recordingData = recordingData;
if (!oldCategory.equals(newCategory)) { if (!oldCategory.equals(newCategory)) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key)); sharedPreferenceChangeListener.onSharedPreferenceChanged(null, getString(R.string.stats_rate_key));
} }
latestTrackPoint = recordingData.getLatestTrackPoint(); latestTrackPoint = recordingData.getLatestTrackPoint();
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns; import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.ExportActivityBinding; import de.dennisguse.opentracks.databinding.ExportActivityBinding;
import de.dennisguse.opentracks.databinding.ToolbarBinding;
import de.dennisguse.opentracks.io.file.ErrorListDialog; import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.ExportUtils; import de.dennisguse.opentracks.util.ExportUtils;
@@ -134,6 +135,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewBinding = ExportActivityBinding.inflate(getLayoutInflater()); viewBinding = ExportActivityBinding.inflate(getLayoutInflater());
ToolbarBinding mergedBinding = ToolbarBinding.bind(viewBinding.getRoot());
setContentView(viewBinding.getRoot()); setContentView(viewBinding.getRoot());
directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY); directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
@@ -144,7 +146,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri); DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri);
String directoryDisplayName = FileUtils.getPath(documentFile); String directoryDisplayName = FileUtils.getPath(documentFile);
viewBinding.toolbar.toolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName)); mergedBinding.toolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
resultReceiver = new ExportServiceResultReceiver(new Handler(), this); resultReceiver = new ExportServiceResultReceiver(new Handler(), this);
@@ -285,7 +285,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>"); printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
if (trackPoint.hasSpeed() || trackPoint.hasHeartRate() || trackPoint.hasCyclingCadence() || trackPoint.hasAltitudeGain() || trackPoint.hasAltitudeLoss()) { if (trackPoint.hasSpeed() || trackPoint.hasHeartRate() || trackPoint.hasCadence() || trackPoint.hasAltitudeGain() || trackPoint.hasAltitudeLoss()) {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>"); printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
if (trackPoint.hasSpeed()) { if (trackPoint.hasSpeed()) {
@@ -296,8 +296,8 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate_bpm()) + "</gpxtpx:hr>"); printWriter.println("<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate_bpm()) + "</gpxtpx:hr>");
} }
if (trackPoint.hasCyclingCadence()) { if (trackPoint.hasCadence()) {
printWriter.println("<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCyclingCadence_rpm()) + "</gpxtpx:cad>"); printWriter.println("<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence_rpm()) + "</gpxtpx:cad>");
} }
if (trackPoint.hasPower()) { if (trackPoint.hasPower()) {
@@ -26,10 +26,12 @@ import androidx.annotation.VisibleForTesting;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.text.NumberFormat;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -67,6 +69,13 @@ public class KMLTrackExporter implements TrackExporter {
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png"; private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png"; private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private static final NumberFormat SENSOR_DATA_FORMAT = NumberFormat.getInstance(Locale.US);
static {
SENSOR_DATA_FORMAT.setMaximumFractionDigits(1);
SENSOR_DATA_FORMAT.setGroupingUsed(false);
}
private final Context context; private final Context context;
private final boolean exportPhotos; private final boolean exportPhotos;
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
@@ -373,7 +382,7 @@ public class KMLTrackExporter implements TrackExporter {
distanceList.add(trackPoint.hasSensorDistance() ? (float) trackPoint.getSensorDistance().toM() : null); distanceList.add(trackPoint.hasSensorDistance() ? (float) trackPoint.getSensorDistance().toM() : null);
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null); heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null);
cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null); cadenceList.add(trackPoint.hasCadence() ? trackPoint.getCadence_rpm() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null); powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null);
altitudeGainList.add(trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : null); altitudeGainList.add(trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : null);
@@ -394,7 +403,7 @@ public class KMLTrackExporter implements TrackExporter {
if (value == null) { if (value == null) {
printWriter.println("<gx:value />"); printWriter.println("<gx:value />");
} else { } else {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>"); printWriter.println("<gx:value>" + SENSOR_DATA_FORMAT.format(value) + "</gx:value>");
} }
} }
printWriter.println("</gx:SimpleArrayData>"); printWriter.println("</gx:SimpleArrayData>");
@@ -14,7 +14,7 @@ import java.io.OutputStream;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.databinding.TrackSharePictureBinding; import de.dennisguse.opentracks.databinding.TrackSharePictureBinding;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
/** /**
@@ -61,12 +61,12 @@ public class SharePictureTrackExporter implements TrackExporter {
TrackSharePictureBinding viewBinding = TrackSharePictureBinding.inflate(LayoutInflater.from(context)); TrackSharePictureBinding viewBinding = TrackSharePictureBinding.inflate(LayoutInflater.from(context));
viewBinding.sharePictureCategory.setText(track.getCategory()); viewBinding.sharePictureCategory.setText(track.getCategory());
boolean metricUnits = PreferencesUtils.isMetricUnits(PreferencesUtils.getSharedPreferences(context), context); boolean metricUnits = PreferencesUtils.isMetricUnits();
TrackStatistics trackStatistics = track.getTrackStatistics(); TrackStatistics trackStatistics = track.getTrackStatistics();
viewBinding.sharePictureTotalTime.setText(StringUtils.formatElapsedTimeWithHour(track.getTrackStatistics().getTotalTime())); viewBinding.sharePictureTotalTime.setText(StringUtils.formatElapsedTimeWithHour(track.getTrackStatistics().getTotalTime()));
viewBinding.sharePictureTotalDistance.setText(StringUtils.formatDistance(context, track.getTrackStatistics().getTotalDistance(), metricUnits)); viewBinding.sharePictureTotalDistance.setText(StringUtils.formatDistance(context, track.getTrackStatistics().getTotalDistance(), metricUnits));
viewBinding.sharePictureGain.setText(StringUtils.formatAltitude(context, trackStatistics.getTotalAltitudeGain(), metricUnits)); viewBinding.sharePictureGain.setText(StringUtils.formatAltitudeChange(context, trackStatistics.getTotalAltitudeGain(), metricUnits));
// prepare rendering // prepare rendering
View view = viewBinding.getRoot(); View view = viewBinding.getRoot();
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.io.file.importer;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
@@ -13,20 +12,16 @@ import androidx.documentfile.provider.DocumentFile;
import de.dennisguse.opentracks.io.file.exporter.ExportActivity; import de.dennisguse.opentracks.io.file.exporter.ExportActivity;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
public abstract class DirectoryChooserActivity extends AppCompatActivity { public abstract class DirectoryChooserActivity extends AppCompatActivity {
private static final int DIRECTORY_PICKER_REQUEST_CODE = 6; private static final int DIRECTORY_PICKER_REQUEST_CODE = 6;
protected SharedPreferences sharedPreferences;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
DocumentFile directoryUri = configureDirectoryChooserIntent(intent); DocumentFile directoryUri = configureDirectoryChooserIntent(intent);
if (directoryUri == null) { if (directoryUri == null) {
@@ -43,12 +38,6 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
onActivityResultCustom(requestCode, resultCode, resultData); onActivityResultCustom(requestCode, resultCode, resultData);
} }
@Override
protected void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
protected void onActivityResultCustom(int requestCode, int resultCode, @Nullable Intent resultData) { protected void onActivityResultCustom(int requestCode, int resultCode, @Nullable Intent resultData) {
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) { if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
@@ -91,14 +80,14 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
super.configureDirectoryChooserIntent(intent); super.configureDirectoryChooserIntent(intent);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
return PreferencesUtils.getDefaultExportDirectoryUri(sharedPreferences, this); return PreferencesUtils.getDefaultExportDirectoryUri(this);
} }
@Override @Override
protected Intent createNextActivityIntent(Uri directoryUri) { protected Intent createNextActivityIntent(Uri directoryUri) {
Intent intent = IntentUtils.newIntent(this, ExportActivity.class); Intent intent = IntentUtils.newIntent(this, ExportActivity.class);
intent.putExtra(ExportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri); intent.putExtra(ExportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri);
intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat(sharedPreferences, this)); intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat());
return intent; return intent;
} }
} }
@@ -112,11 +101,11 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
case RESULT_OK: case RESULT_OK:
Uri directoryUri = resultData.getData(); Uri directoryUri = resultData.getData();
PreferencesUtils.setDefaultExportDirectoryUri(sharedPreferences, this, directoryUri); PreferencesUtils.setDefaultExportDirectoryUri(directoryUri);
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), directoryUri); IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), directoryUri);
break; break;
case RESULT_CANCELED: case RESULT_CANCELED:
PreferencesUtils.setDefaultExportDirectoryUri(sharedPreferences, this, null); PreferencesUtils.setDefaultExportDirectoryUri(null);
//TODO Remove stored permission contentResolver.releasePersistableUriPermission //TODO Remove stored permission contentResolver.releasePersistableUriPermission
break; break;
} }
@@ -129,8 +118,8 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
protected DocumentFile configureDirectoryChooserIntent(Intent intent) { protected DocumentFile configureDirectoryChooserIntent(Intent intent) {
super.configureDirectoryChooserIntent(intent); super.configureDirectoryChooserIntent(intent);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (PreferencesUtils.isDefaultExportDirectoryUri(sharedPreferences, this)) { if (PreferencesUtils.isDefaultExportDirectoryUri(this)) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri(sharedPreferences, this).getUri()); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri(this).getUri());
} }
return null; return null;
} }
@@ -70,6 +70,10 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private static final String ATTRIBUTE_LON = "lon"; private static final String ATTRIBUTE_LON = "lon";
private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed"; private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed";
/**
* Often speed is exported without the proper namespace.
*/
private static final String TAG_EXTENSION_SPEED_COMPAT = "speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr"; private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad"; private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts"; private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
@@ -192,6 +196,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
} }
break; break;
case TAG_EXTENSION_SPEED: case TAG_EXTENSION_SPEED:
case TAG_EXTENSION_SPEED_COMPAT:
if (content != null) { if (content != null) {
speed = content.trim(); speed = content.trim();
} }
@@ -295,7 +300,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
if (cadence != null) { if (cadence != null) {
try { try {
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence)); trackPoint.setCadence_rpm(Float.parseFloat(cadence));
} catch (Exception e) { } catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e); throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
} }
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.io.file.importer;
import android.app.job.JobService; import android.app.job.JobService;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.ResultReceiver; import android.os.ResultReceiver;
@@ -22,7 +21,7 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils; import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
public class ImportService extends JobIntentService { public class ImportService extends JobIntentService {
@@ -54,10 +53,9 @@ public class ImportService extends JobIntentService {
String fileExtension = FileUtils.getExtension(file); String fileExtension = FileUtils.getExtension(file);
try { try {
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(this); Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance();
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, this); Distance recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval();
Distance recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, this); boolean preventReimport = PreferencesUtils.getPreventReimportTracks();
boolean preventReimport = PreferencesUtils.getPreventReimportTracks(sharedPreferences, this);
TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), recordingDistanceInterval, maxRecordingDistance, preventReimport); TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), recordingDistanceInterval, maxRecordingDistance, preventReimport);
@@ -316,7 +316,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
trackPoint.setHeartRate_bpm(sensorHeartRateList.get(i)); trackPoint.setHeartRate_bpm(sensorHeartRateList.get(i));
} }
if (i < sensorCadenceList.size()) { if (i < sensorCadenceList.size()) {
trackPoint.setCyclingCadence_rpm(sensorCadenceList.get(i)); trackPoint.setCadence_rpm(sensorCadenceList.get(i));
} }
if (i < sensorPowerList.size()) { if (i < sensorPowerList.size()) {
trackPoint.setPower(sensorPowerList.get(i)); trackPoint.setPower(sensorPowerList.get(i));
@@ -144,6 +144,8 @@ public class TrackImporter {
return 0; return 0;
}); });
adjustTrackPoints();
TrackStatisticsUpdater updater = new TrackStatisticsUpdater(); TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
updater.addTrackPoints(trackPoints, recordingDistanceInterval); updater.addTrackPoints(trackPoints, recordingDistanceInterval);
track.setTrackStatistics(updater.getTrackStatistics()); track.setTrackStatistics(updater.getTrackStatistics());
@@ -151,7 +153,6 @@ public class TrackImporter {
Track.Id trackId = contentProviderUtils.insertTrack(track); Track.Id trackId = contentProviderUtils.insertTrack(track);
// Store TrackPoints // Store TrackPoints
adjustTrackPoints();
contentProviderUtils.bulkInsertTrackPoint(trackPoints, trackId); contentProviderUtils.bulkInsertTrackPoint(trackPoints, trackId);
// Store Markers // Store Markers
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.services;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
@@ -20,7 +19,7 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackNameUtils; import de.dennisguse.opentracks.util.TrackNameUtils;
@@ -34,12 +33,11 @@ class TrackRecordingManager {
private Distance recordingDistanceInterval; private Distance recordingDistanceInterval;
private Distance maxRecordingDistance; private Distance maxRecordingDistance;
private Track track; private Track.Id trackId;
private TrackStatisticsUpdater trackStatisticsUpdater; private TrackStatisticsUpdater trackStatisticsUpdater;
private TrackPoint lastTrackPoint; private boolean currentSegmentHasTrackPoint;
private TrackPoint lastValidTrackPoint; private TrackPoint lastStoredTrackPoint;
private boolean isIdle;
TrackRecordingManager(Context context) { TrackRecordingManager(Context context) {
this.context = context; this.context = context;
@@ -47,70 +45,74 @@ class TrackRecordingManager {
} }
Track.Id start(TrackPoint segmentStartTrackPoint) { Track.Id start(TrackPoint segmentStartTrackPoint) {
// Insert a track // Create new track
track = new Track(); Track track = new Track();
track.setId(contentProviderUtils.insertTrack(track)); trackId = contentProviderUtils.insertTrack(track);
track.setId(trackId);
trackStatisticsUpdater = new TrackStatisticsUpdater(); trackStatisticsUpdater = new TrackStatisticsUpdater();
insertTrackPoint(track, segmentStartTrackPoint); insertTrackPoint(trackId, segmentStartTrackPoint);
//TODO Pass TrackPoint //TODO Pass TrackPoint
track.setName(TrackNameUtils.getTrackName(context, track.getId(), segmentStartTrackPoint.getTime())); track.setName(TrackNameUtils.getTrackName(context, trackId, segmentStartTrackPoint.getTime()));
String category = PreferencesUtils.getDefaultActivity(PreferencesUtils.getSharedPreferences(context), context); //TODO Re-use sharedpreferences String category = PreferencesUtils.getDefaultActivity();
track.setCategory(category); track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(context, category)); track.setIcon(TrackIconUtils.getIconValue(context, category));
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics()); track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track); //TODO Could we do this in the insert? contentProviderUtils.updateTrack(track);
return track.getId(); currentSegmentHasTrackPoint = false;
return trackId;
} }
//TODO Handle non-existing trackId? Start a new track or exception? //TODO Handle non-existing trackId? Start a new track or exception?
void resume(@NonNull Track.Id trackId, @NonNull TrackPoint segmentStartTrackPoint) { void resume(@NonNull Track.Id resumeTrackId, @NonNull TrackPoint segmentStartTrackPoint) {
track = contentProviderUtils.getTrack(trackId); trackId = resumeTrackId;
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) { if (track == null) {
Log.e(TAG, "Ignore resumeTrack. Track " + trackId.getId() + " does not exists."); Log.e(TAG, "Ignore resumeTrack. Track " + trackId.getId() + " does not exists.");
return; return;
} }
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics()); trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(track, segmentStartTrackPoint); insertTrackPoint(trackId, segmentStartTrackPoint);
currentSegmentHasTrackPoint = false;
lastStoredTrackPoint = null; }
void pause(TrackPointCreator trackPointCreator) {
insertTrackPoint(trackId, trackPointCreator.createSegmentEnd());
currentSegmentHasTrackPoint = false;
lastStoredTrackPoint = null;
} }
void pause(TrackPointCreator handlerServer) { void end(TrackPointCreator trackPointCreator) {
if (lastTrackPoint != null) { TrackPoint segmentEnd = trackPointCreator.createSegmentEnd();
insertTrackPointIfNewer(track, lastTrackPoint); insertTrackPoint(trackId, segmentEnd);
}
insertTrackPoint(track, handlerServer.createSegmentEnd());
}
void end(TrackPointCreator handlerServer) { trackId = null;
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
}
TrackPoint segmentEnd = handlerServer.createSegmentEnd();
insertTrackPoint(track, segmentEnd);
track = null;
trackStatisticsUpdater = null; trackStatisticsUpdater = null;
lastTrackPoint = null; lastStoredTrackPoint = null;
lastValidTrackPoint = null; currentSegmentHasTrackPoint = false;
isIdle = false;
} }
Pair<Track, Pair<TrackPoint, SensorDataSet>> get(TrackPointCreator handlerServer) { Pair<Track, Pair<TrackPoint, SensorDataSet>> get(TrackPointCreator trackPointCreator) {
if (handlerServer == null) { if (trackPointCreator == null) {
return null; return null;
} }
TrackStatisticsUpdater tmpTrackStatisticsUpdater = getTrackStatisticsUpdater(); TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater);
Pair<TrackPoint, SensorDataSet> current = handlerServer.createCurrentTrackPoint(lastTrackPoint); Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastStoredTrackPoint);
tmpTrackStatisticsUpdater.addTrackPoint(current.first, recordingDistanceInterval); tmpTrackStatisticsUpdater.addTrackPoint(current.first, recordingDistanceInterval);
Track track = getTrack(); //Get copy Track track = contentProviderUtils.getTrack(trackId); //Get copy
if (track == null) {
Log.w(TAG, "Requesting data if not recording is taking place, should not be done.");
return null;
}
track.setTrackStatistics(tmpTrackStatisticsUpdater.getTrackStatistics()); track.setTrackStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
return new Pair<>(track, current); return new Pair<>(track, current);
@@ -118,15 +120,14 @@ class TrackRecordingManager {
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) { public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
if (name == null) { if (name == null) {
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(track.getId()); Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(trackId);
if (nextMarkerNumber == null) { if (nextMarkerNumber == null) {
nextMarkerNumber = 1; nextMarkerNumber = 1;
} }
name = context.getString(R.string.marker_name_format, nextMarkerNumber + 1); name = context.getString(R.string.marker_name_format, nextMarkerNumber + 1);
} }
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(track.getId()); if (lastStoredTrackPoint == null) {
if (trackPoint == null) {
Log.i(TAG, "Could not create a marker as trackPoint is unknown."); Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
return null; return null;
} }
@@ -136,138 +137,51 @@ class TrackRecordingManager {
String icon = context.getString(R.string.marker_icon_url); String icon = context.getString(R.string.marker_icon_url);
photoUrl = photoUrl != null ? photoUrl : ""; photoUrl = photoUrl != null ? photoUrl : "";
// Insert marker // Insert marker
Marker marker = new Marker(name, description, category, icon, track.getId(), getTrackStatistics(), trackPoint, photoUrl); Marker marker = new Marker(name, description, category, icon, trackId, getTrackStatistics(), lastStoredTrackPoint, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker); Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri)); return new Marker.Id(ContentUris.parseId(uri));
} }
void onNewTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) { boolean onNewTrackPoint(TrackPoint trackPoint) {
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
//Storing trackPoint //Storing trackPoint
// Always insert the first segment location // Always insert the first segment location
if (!currentSegmentHasTrackPoint()) { if (!currentSegmentHasTrackPoint) {
insertTrackPoint(track, trackPoint); insertTrackPoint(trackId, trackPoint);
lastTrackPoint = trackPoint; currentSegmentHasTrackPoint = true;
return; return true;
} }
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint); Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastStoredTrackPoint);
if (distanceToLastTrackLocation != null) {
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) { if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
insertTrackPointIfNewer(track, lastTrackPoint);
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC); trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(track, trackPoint); insertTrackPoint(trackId, trackPoint);
return true;
isIdle = false;
lastTrackPoint = trackPoint;
return;
} }
if (trackPoint.hasSensorData() || distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval)) { if (trackPoint.hasSensorData() || (distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval) && trackPoint.isMoving())) {
insertTrackPointIfNewer(track, lastTrackPoint); insertTrackPoint(trackId, trackPoint);
return true;
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
} }
if (!isIdle && !trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = true;
lastTrackPoint = trackPoint;
return;
}
if (isIdle && trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
} }
Log.d(TAG, "Not recording TrackPoint, idle"); Log.d(TAG, "Not recording TrackPoint, idle");
lastTrackPoint = trackPoint; return false;
}
Track getTrack() {
return contentProviderUtils.getTrack(track.getId()); //Copy in memory; not via DB
}
//Functionality that uses this method should happen here.
@Deprecated
TrackStatisticsUpdater getTrackStatisticsUpdater() {
return new TrackStatisticsUpdater(trackStatisticsUpdater);
} }
TrackStatistics getTrackStatistics() { TrackStatistics getTrackStatistics() {
return trackStatisticsUpdater.getTrackStatistics(); return trackStatisticsUpdater.getTrackStatistics();
} }
/** private void insertTrackPoint(@NonNull Track.Id trackId, @NonNull TrackPoint trackPoint) {
* Gets the last valid track point in the current segment.
*
* @param trackId the track id
* @return the location or null
*/
@Deprecated
//Use lastValidTrackPoint
private TrackPoint getLastValidTrackPointInCurrentSegment(Track.Id trackId) {
if (!currentSegmentHasTrackPoint()) {
return null;
}
return contentProviderUtils.getLastValidTrackPoint(trackId);
}
private boolean currentSegmentHasTrackPoint() {
return lastTrackPoint != null;
}
/**
* Inserts a trackPoint if this trackPoint is different than lastValidTrackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPointIfNewer(@NonNull Track track, @NonNull TrackPoint trackPoint) {
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
if (lastValidTrackPoint != null && trackPoint.getTime().equals(lastValidTrackPoint.getTime())) {
// Do not insert if inserted already
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid track point time.");
return;
}
insertTrackPoint(track, trackPoint);
}
/**
* Inserts a trackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPoint(@NonNull Track track, @NonNull TrackPoint trackPoint) {
try { try {
contentProviderUtils.insertTrackPoint(trackPoint, track.getId()); contentProviderUtils.insertTrackPoint(trackPoint, trackId);
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track); contentProviderUtils.updateTrackStatistics(trackId, trackStatisticsUpdater.getTrackStatistics());
lastStoredTrackPoint = trackPoint;
} catch (SQLiteException e) { } catch (SQLiteException e) {
/* /*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY). * Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
@@ -277,12 +191,12 @@ class TrackRecordingManager {
} }
} }
public void onSharedPreferenceChanged(@NonNull SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(String key) {
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) { if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context); recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval();
} }
if (PreferencesUtils.isKey(context, R.string.max_recording_distance_key, key)) { if (PreferencesUtils.isKey(R.string.max_recording_distance_key, key)) {
maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance();
} }
} }
} }
@@ -49,14 +49,12 @@ import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver;
import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager; import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue; import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTask; import de.dennisguse.opentracks.services.tasks.VoiceAnnouncementManager;
import de.dennisguse.opentracks.services.tasks.PeriodicTask;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.ExportUtils; import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils; import de.dennisguse.opentracks.util.SystemUtils;
/** /**
@@ -71,12 +69,12 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1); private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
public static final RecordingStatus STATUS_DEFAULT = new RecordingStatus(null, false); public static final RecordingStatus STATUS_DEFAULT = RecordingStatus.notRecording();
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null); public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE; public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
// The following variables are set in onCreate: // The following variables are setFrequency in onCreate:
private PeriodicTaskExecutor voiceExecutor; private VoiceAnnouncementManager voiceAnnouncementManager;
private TrackRecordingServiceNotificationManager notificationManager; private TrackRecordingServiceNotificationManager notificationManager;
private TrackRecordingManager trackRecordingManager; private TrackRecordingManager trackRecordingManager;
@@ -98,33 +96,31 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
}; };
private SharedPreferences sharedPreferences;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Context context = TrackRecordingService.this; if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) { boolean metricUnits = PreferencesUtils.isMetricUnits();
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
voiceExecutor.setMetricUnits(metricUnits);
notificationManager.setMetricUnits(metricUnits); notificationManager.setMetricUnits(metricUnits);
} }
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) { if (PreferencesUtils.isKey(R.string.voice_announcement_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(sharedPreferences, context)); voiceAnnouncementManager.setFrequency(PreferencesUtils.getVoiceAnnouncementFrequency());
}
if (PreferencesUtils.isKey(new int[]{R.string.voice_announcement_distance_key, R.string.stats_units_key}, key)) {
voiceAnnouncementManager.setFrequency(PreferencesUtils.getVoiceAnnouncementDistance());
} }
trackPointCreator.onSharedPreferenceChanged(key);
handlerServer.onSharedPreferenceChanged(sharedPreferences, key); trackRecordingManager.onSharedPreferenceChanged(key);
trackRecordingManager.onSharedPreferenceChanged(sharedPreferences, key);
} }
}; };
// The following variables are set when recording: // The following variables are setFrequency when recording:
private WakeLock wakeLock; private WakeLock wakeLock;
private final Binder binder = new Binder(); private final Binder binder = new Binder();
private TrackPointCreator handlerServer; //TODO Move to TrackRecordingManager? private TrackPointCreator trackPointCreator; //TODO Move to TrackRecordingManager?
private RecordingStatus recordingStatus; private RecordingStatus recordingStatus;
private MutableLiveData<RecordingStatus> recordingStatusObservable; private MutableLiveData<RecordingStatus> recordingStatusObservable;
@@ -143,15 +139,13 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
recordingDataObservable = new MutableLiveData<>(NOT_RECORDING); recordingDataObservable = new MutableLiveData<>(NOT_RECORDING);
trackRecordingManager = new TrackRecordingManager(this); trackRecordingManager = new TrackRecordingManager(this);
handlerServer = new TrackPointCreator(this); trackPointCreator = new TrackPointCreator(this);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTask.Factory()); voiceAnnouncementManager = new VoiceAnnouncementManager(this);
notificationManager = new TrackRecordingServiceNotificationManager(this); notificationManager = new TrackRecordingServiceNotificationManager(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
} }
@Override @Override
@@ -168,19 +162,18 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public void onDestroy() { public void onDestroy() {
handler = null; handler = null;
handlerServer.stop(); trackPointCreator.stop();
handlerServer = null; trackPointCreator = null;
// Reverse order from onCreate // Reverse order from onCreate
showNotification(false); //TODO Why? showNotification(false); //TODO Why?
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferences = null;
try { try {
voiceExecutor.shutdown(); voiceAnnouncementManager.shutdown();
} finally { } finally {
voiceExecutor = null; voiceAnnouncementManager = null;
} }
// This should be the next to last operation // This should be the next to last operation
@@ -209,10 +202,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return trackRecordingManager.insertMarker(name, category, description, photoUrl); return trackRecordingManager.insertMarker(name, category, description, photoUrl);
} }
public void run(@NonNull PeriodicTask periodicTask) {
periodicTask.run(recordingStatus.getTrackId(), trackRecordingManager.getTrackStatistics());
}
/** /**
* Starts a new track. * Starts a new track.
* *
@@ -225,8 +214,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
// Set recording status // Set recording status
Track.Id trackId = trackRecordingManager.start(handlerServer.createSegmentStartManual()); Track.Id trackId = trackRecordingManager.start(trackPointCreator.createSegmentStartManual());
updateRecordingStatus(new RecordingStatus(trackId, false)); updateRecordingStatus(RecordingStatus.record(trackId));
startRecording(); startRecording();
return trackId; return trackId;
@@ -239,11 +228,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
* @param trackId the id of the track to be resumed. * @param trackId the id of the track to be resumed.
*/ */
public void resumeTrack(Track.Id trackId) { public void resumeTrack(Track.Id trackId) {
handlerServer.resetSensorData(); trackPointCreator.resetSensorData();
trackRecordingManager.resume(trackId, handlerServer.createSegmentStartManual()); trackRecordingManager.resume(trackId, trackPointCreator.createSegmentStartManual());
// Set recording status // Set recording status
updateRecordingStatus(new RecordingStatus(trackId, false)); updateRecordingStatus(RecordingStatus.record(trackId));
startRecording(); startRecording();
} }
@@ -267,8 +256,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
startGps(); startGps();
// Restore periodic tasks voiceAnnouncementManager.restore(trackRecordingManager.getTrackStatistics());
voiceExecutor.restore();
} }
public void tryStartGps() { public void tryStartGps() {
@@ -279,7 +267,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private void startGps() { private void startGps() {
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock); wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
handlerServer.start(this); trackPointCreator.start(this);
showNotification(true); showNotification(true);
} }
@@ -298,7 +286,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
updateRecordingStatus(STATUS_DEFAULT); updateRecordingStatus(STATUS_DEFAULT);
if (!wasPause) { if (!wasPause) {
trackRecordingManager.end(handlerServer); trackRecordingManager.end(trackPointCreator);
} }
ExportUtils.postWorkoutExport(this, trackId, new ExportServiceResultReceiver(new Handler(), this)); ExportUtils.postWorkoutExport(this, trackId, new ExportServiceResultReceiver(new Handler(), this));
@@ -314,9 +302,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
// Set recording status // Set recording status
updateRecordingStatus(new RecordingStatus(recordingStatus.getTrackId(), true)); updateRecordingStatus(recordingStatus.pause());
trackRecordingManager.pause(handlerServer); trackRecordingManager.pause(trackPointCreator);
endRecording(false); endRecording(false);
@@ -337,10 +325,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
// Shutdown periodic tasks // Shutdown periodic tasks
voiceExecutor.shutdown(); voiceAnnouncementManager.shutdown();
// Update instance variables // Update instance variables
handlerServer.stop(); trackPointCreator.stop();
stopGps(trackStopped); stopGps(trackStopped);
} }
@@ -357,7 +345,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
void stopGps(boolean shutdown) { void stopGps(boolean shutdown) {
if (!isRecording()) return; if (!isRecording()) return;
handlerServer.stop(); trackPointCreator.stop();
showNotification(false); showNotification(false);
wakeLock = SystemUtils.releaseWakeLock(wakeLock); wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (shutdown) { if (shutdown) {
@@ -366,15 +354,14 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
@Override @Override
public void newTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) { public boolean newTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) {
if (!isRecording() || isPaused()) { if (!isRecording() || isPaused()) {
Log.w(TAG, "Ignore newTrackPoint. Not recording or paused."); Log.w(TAG, "Ignore newTrackPoint. Not recording or paused.");
return; return false;
} }
trackRecordingManager.onNewTrackPoint(trackPoint, thresholdHorizontalAccuracy);
notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy); notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy);
voiceExecutor.update(recordingStatus.getTrackId(), trackRecordingManager.getTrackStatistics()); return trackRecordingManager.onNewTrackPoint(trackPoint);
} }
@Override @Override
@@ -389,12 +376,16 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private void showNotification(boolean isGpsStarted) { private void showNotification(boolean isGpsStarted) {
if (isRecording()) { if (isRecording()) {
Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class) Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class);
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, recordingStatus.getTrackId());
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = TaskStackBuilder.create(this) PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackRecordingActivity.class) .addNextIntentWithParentStack(intent)
.addNextIntent(intent) .getPendingIntent(0, pendingIntentFlags);
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
notificationManager.updatePendingIntent(pendingIntent); notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting)); notificationManager.updateContent(getString(R.string.gps_starting));
@@ -402,9 +393,15 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
if (!isRecording() && isGpsStarted) { if (!isRecording() && isGpsStarted) {
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class); Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
int pendingIntentFlags = 0;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
pendingIntentFlags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT;
}
PendingIntent pendingIntent = TaskStackBuilder.create(this) PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackListActivity.class)
.addNextIntent(intent) .addNextIntent(intent)
.getPendingIntent(0, 0); .getPendingIntent(0, pendingIntentFlags);
notificationManager.updatePendingIntent(pendingIntent); notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting)); notificationManager.updateContent(getString(R.string.gps_starting));
@@ -416,21 +413,17 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
} }
public Duration getTotalTime() {
return trackRecordingManager.getTrackStatistics().getTotalTime();
}
@Deprecated @Deprecated
@VisibleForTesting @VisibleForTesting
public TrackPointCreator getHandlerServer() { public TrackPointCreator getTrackPointCreator() {
return handlerServer; return trackPointCreator;
} }
public LiveData<GpsStatusValue> getGpsStatusObservable() { public LiveData<GpsStatusValue> getGpsStatusObservable() {
return gpsStatusObservable; return gpsStatusObservable;
} }
public MutableLiveData<RecordingData> getRecordingDataObservable() { public LiveData<RecordingData> getRecordingDataObservable() {
return recordingDataObservable; return recordingDataObservable;
} }
@@ -442,17 +435,24 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
// Compute temporary track statistics using sensorData and update time. // Compute temporary track statistics using sensorData and update time.
TrackPointCreator localHandlerServer = this.handlerServer; TrackPointCreator localTrackPointCreator = this.trackPointCreator;
if (localHandlerServer == null) { VoiceAnnouncementManager localVoiceAnnouncementManager = this.voiceAnnouncementManager;
if (localTrackPointCreator == null || localVoiceAnnouncementManager == null) {
// when this happens, no recording is running and we should not send any notifications. // when this happens, no recording is running and we should not send any notifications.
//TODO This implementation is not a good idea; rather solve the issue for this properly //TODO This implementation is not a good idea; rather solve the issue for this properly
return; return;
} }
Pair<Track, Pair<TrackPoint, SensorDataSet>> data = trackRecordingManager.get(handlerServer); Pair<Track, Pair<TrackPoint, SensorDataSet>> data = trackRecordingManager.get(trackPointCreator);
if (data == null) {
Log.w(TAG, "Requesting data if not recording is taking place, should not be done.");
return;
}
TrackPoint trackPoint = data.second.first; TrackPoint trackPoint = data.second.first;
egm2008CorrectionManager.correctAltitude(this, trackPoint); egm2008CorrectionManager.correctAltitude(this, trackPoint);
localVoiceAnnouncementManager.update(data.first);
recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second)); recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second));
} }
@@ -493,7 +493,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private final Track.Id trackId; private final Track.Id trackId;
private final boolean paused; private final boolean paused;
public RecordingStatus(Track.Id trackId, boolean paused) { private RecordingStatus(Track.Id trackId, boolean paused) {
this.trackId = trackId; this.trackId = trackId;
this.paused = paused; this.paused = paused;
} }
@@ -514,20 +514,23 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return isRecording() && !isPaused(); return isRecording() && !isPaused();
} }
// TODO Use private static RecordingStatus notRecording() {
// public RecordingStatus pause() { return new RecordingStatus(null, false);
// return new RecordingStatus(getTrackId(), true); }
// }
// private static RecordingStatus record(@NonNull Track.Id trackId) {
// public RecordingStatus record(@NonNull Track.Id trackId) { return new RecordingStatus(trackId, false);
// return new RecordingStatus(trackId, false); }
// }
private RecordingStatus pause() {
return new RecordingStatus(getTrackId(), true);
}
public RecordingStatus stop() { public RecordingStatus stop() {
return STATUS_DEFAULT; return STATUS_DEFAULT;
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return "RecordingStatus{" + return "RecordingStatus{" +
@@ -1,53 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.handlers;
import java.time.Duration;
/**
* This is a simple location listener policy that will always dictate the same polling interval.
*
* @author Sandor Dornbush
*/
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
private final Duration interval;
/**
* Constructor.
*
* @param interval the interval to request for gps signal
*/
public AbsoluteLocationListenerPolicy(Duration interval) {
this.interval = interval;
}
@Override
public Duration getDesiredPollingInterval() {
return interval;
}
@Override
public int getMinDistance_m() {
return 0;
}
@Override
public void updateIdleTime(Duration idleTime) {
// Ignore
}
}

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