Compare commits

...

119 Commits

Author SHA1 Message Date
Dennis Guse 52226c8ef4 Release: v4.7.0 2023-08-04 08:11:00 +02:00
Dennis Guse c254a6ec75 Bug: GPXExporter - TrackPoint of type SEGMENT_START_AUTOMATIC might have no location.
Fixes #1647.
2023-08-03 19:31:10 +02:00
Dennis Guse 0ecf4863a7 Bug: CSVExporter format for distance used decimal grouping. 2023-08-03 18:29:33 +02:00
Dennis Guse 325196159d Add barometer test procedure. 2023-07-30 22:15:40 +02:00
Leon Marz 6adbf39e52 Fix alignment of Statistics 2023-07-29 20:17:52 +02:00
Leon Marz db1a37f015 Reimplement sensor statistics in UI
Closes: #1426
2023-07-29 19:17:23 +02:00
dependabot[bot] 7ca9423fbf Bump androidx.preference:preference from 1.2.0 to 1.2.1
Bumps androidx.preference:preference from 1.2.0 to 1.2.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-27 07:11:15 +02:00
Dennis Guse 11c5ec43e2 Statistics: distance, total time, moving time are counted between automatic segments.
Fixes #1566.
2023-07-26 19:16:27 +02:00
Dennis Guse 3f891f7fb4 Release: v4.6.2 2023-07-26 07:10:43 +02:00
Dennis Guse 49260a0517 Translations fr: removed restored voice announcement plurals. 2023-07-26 07:06:01 +02:00
Fjuro 1392c5d92a Translated using Weblate (Czech)
Currently translated at 98.2% (510 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/cs/
2023-07-25 23:39:26 +02:00
Fjuro c641b436ce Translated using Weblate (Czech)
Currently translated at 94.0% (488 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/cs/
2023-07-25 23:39:26 +02:00
Lorin-lab 6948c2db41 Translated using Weblate (French)
Currently translated at 100.0% (519 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/fr/
2023-07-25 23:39:26 +02:00
josé m fb904f4a0b Translated using Weblate (Galician)
Currently translated at 100.0% (519 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2023-07-25 23:39:26 +02:00
josé m 6c006cc9c7 Translated using Weblate (Galician)
Currently translated at 7.2% (8 of 111 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/gl/
2023-07-25 23:39:26 +02:00
ovl-1 7a73757ab1 Translated using Weblate (Norwegian Bokmål)
Currently translated at 95.7% (497 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2023-07-25 23:39:26 +02:00
Dennis Guse 9725cca2bf Upgrade gradlew to 8.2.0 2023-07-25 23:39:16 +02:00
Dennis Guse 252f71e75f Bump com.android.tools.build:gradle from 8.1.0-rc01 to 8.1.0 2023-07-25 23:01:47 +02:00
Dennis Guse fc2341515c Bug: TrackStatistics cannot compute average moving speed (division by zero). 2023-07-23 12:00:09 +02:00
michel 77719f5bcd Bluetooth settings: handle missing permissions
fixes #1637
2023-07-15 14:15:19 +03:00
michel 565ef7f12b Fix aggregated stats don't show imperial units for avg pace
fixes #1634
2023-07-12 19:44:09 +03:00
michel f4208e2f6b Fix clipped x graph in ChartView
fixes #1627
2023-07-12 17:02:49 +03:00
blackberrypineapple 4b3ff45609 Bugfix: crash on GPS preferences if using imperial with meters
Fixes #1629
2023-07-05 14:27:14 +02:00
blackberrypineapple 6dd9630bae Bugfix: crash on voice announcement if using imperial with meters
Fixes #1626
2023-07-05 14:26:46 +02:00
Dennis Guse a4ec68def6 Release: v4.6.1 2023-06-30 21:59:28 +02:00
plum7x d80be8b2a9 Translated using Weblate (Chinese (Traditional))
Currently translated at 96.5% (501 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hant/
2023-06-29 13:52:38 +02:00
Dennis Guse 7d7ab5778a Track list: re-added aggregated stats for selections.
Removed in 9f1ea4d745

Fixes #1623.
2023-06-29 07:17:15 +02:00
Dennis Guse 1749c776ab Release: v4.6.0 2023-06-28 08:09:39 +02:00
Dennis Guse 336b9eaa9f Bugfix: Android 13 couldn't open Gallery.
Fixes #1620.
2023-06-27 21:07:35 +02:00
Dennis Guse 0fc2209ceb Merge pull request #1618 from OpenTracksApp/exportType#1608
Refactor activity type / category / icon
2023-06-27 16:10:31 +02:00
Dennis Guse 477819d730 Cleanup: split ActivityType and ActivityIcon.
Part of #1608.
2023-06-27 16:06:25 +02:00
Dennis Guse 425ea0f216 Cleanup: use ActivityType instead of passing it's id around as String.
Part of #1608.
2023-06-27 15:53:54 +02:00
Dennis Guse 779681088c Refactor Track.icon is now Track.activityTypeId.
Part of #1608.
2023-06-27 15:53:54 +02:00
Dennis Guse f824b31891 Cleanup: inline TrackIconUtils methods.
Part of #1608.
2023-06-27 15:53:54 +02:00
Dennis Guse d316a63fca Cleanup: ActivityType lookup.
Part of #1608.
2023-06-27 15:53:54 +02:00
s-k-e-t-c-h 55cf60ef2f Translated using Weblate (German)
Currently translated at 99.6% (517 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/de/
2023-06-27 15:48:40 +02:00
DerDilla 01598d0a55 Translated using Weblate (German)
Currently translated at 98.0% (509 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/de/
2023-06-27 15:48:40 +02:00
s-k-e-t-c-h 4bcba07706 Translated using Weblate (German)
Currently translated at 98.0% (509 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/de/
2023-06-27 15:48:39 +02:00
josé m ed979bdc80 Translated using Weblate (Galician)
Currently translated at 5.5% (6 of 109 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/gl/
2023-06-27 15:48:39 +02:00
josé m 1d81070d9e Translated using Weblate (Galician)
Currently translated at 98.2% (510 of 519 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2023-06-27 15:48:39 +02:00
josé m 357a1de07d Translated using Weblate (Galician)
Currently translated at 100.0% (510 of 510 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2023-06-27 15:48:39 +02:00
josé m 84380d8373 Translated using Weblate (Galician)
Currently translated at 100.0% (509 of 509 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2023-06-27 15:48:39 +02:00
Dennis Guse 97c3e5a938 Commit .editorconfig for code style. 2023-06-27 15:48:25 +02:00
Dennis Guse 8fbcf5dcc0 Bump com.android.tools.build:gradle from 8.1.0-beta04 to 8.1.0-rc01 2023-06-26 23:57:47 +02:00
Dennis Guse 8c364ba7e2 Support altitude correction via Android's native AltitudeConverter (API34+).
Fixes #1492.
2023-06-22 22:50:34 +02:00
Dennis Guse 0c5c49cd92 CONTRIBUTING.md: add translations. 2023-06-22 07:35:32 +02:00
Dennis Guse 62d9e81b23 VoiceAnnouncement: migrated es, pt, zh, fr, de to ICU plural format.
Follow-up to #1584.
2023-06-22 07:25:52 +02:00
Dennis Guse 1a5acc8f2d Cleanup using AndroidStudio analyze. 2023-06-21 23:15:04 +02:00
Dennis Guse 7e9b99ae1c Merge pull request #1607 from OpenTracksApp/proofOfConcept#1584
Plural quantities via ICU.
2023-06-21 19:25:17 +02:00
Dennis Guse 1f86250e46 ICU's MessageFormat handles plurals for floats (getQuantityString does not).
Fixes #1584.
2023-06-21 19:19:53 +02:00
Dennis Guse 7f6e3f7c69 Raise minSDK from 21 to 24.
Part of #1584
2023-06-21 19:18:55 +02:00
Dennis Guse e0b84df8f6 Release: v4.5.0 2023-06-20 07:32:15 +02:00
Dennis Guse a431850793 Material3: less customizing for night
mode.
2023-06-20 07:28:00 +02:00
Dennis Guse 5c17c8ffcf Material3: less customizing for day mode. 2023-06-20 07:28:00 +02:00
dependabot[bot] 0691524dfa Bump org.mockito:mockito-android from 5.3.1 to 5.4.0
Bumps [org.mockito:mockito-android](https://github.com/mockito/mockito) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.3.1...v5.4.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>
2023-06-19 08:32:17 +02:00
Dennis Guse 1c825b052e Bugfix: prevent crash if sensor that need computation disconnect (or connection timeout). 2023-06-16 22:48:49 +02:00
Dennis Guse eb8d28dfc1 Units: add imperial with meters.
Fixes of #1609.
2023-06-16 00:00:31 +02:00
Dennis Guse 9f1ea4d745 Migrate to Material3 design.
Fixes #1586.
2023-06-15 23:56:46 +02:00
Dennis Guse dbd64a8075 Cleanup: rename category to activityType. 2023-06-15 18:45:11 +02:00
Dennis Guse 75a88ca0e1 Bugfix: prevent crash if sensor that need computation disconnect (or connection timeout).
java.lang.Object.equals(java.lang.Object)' on a null object reference
	at de.dennisguse.opentracks.sensors.sensorData.SensorDataSet.set(SensorDataSet.java:171)
	at de.dennisguse.opentracks.sensors.sensorData.SensorDataSet.remove(SensorDataSet.java:93)
	at de.dennisguse.opentracks.sensors.SensorManager$1.onDisconnect(SensorManager.java:35)
	at de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager.onDisconnecting(BluetoothRemoteSensorManager.java:173)
2023-06-14 22:35:38 +02:00
Dennis Guse 2af19b00e5 Settings: make Bluetooth filter option readable.
Fixes #1602.
2023-06-14 22:21:02 +02:00
Dennis Guse 11db0d1a4f Rewrote GpsStatus check and added tests. 2023-06-11 21:27:50 +02:00
Dennis Guse 6a4adee2f2 Cleanup: GPS Status only reports current status. 2023-06-11 21:27:50 +02:00
Dennis Guse d04b420c56 Bugfix: GPS disabled warning was shown repeatedly.
Fixes #1429.
2023-06-11 21:27:50 +02:00
Dennis Guse 0122539d0f Bugfix: crash on starting a recording with at least one BLE sensor configured.
Introduced in f336a1da12
2023-06-11 15:21:22 +02:00
Dennis Guse 0b8c2a0ef6 Cleanup using AndroidStudio's analyze feature. 2023-06-08 21:28:35 +02:00
Dennis Guse cef363562c Merge pull request #1516 from OpenTracksApp/android14#1496
Target Android 14/API34
2023-06-08 07:19:52 +02:00
Dennis Guse e681531186 Merge remote-tracking branch 'weblate/main' 2023-06-08 07:19:07 +02:00
Dennis Guse 65c4b85bc4 EspressoTests: only grant POST_NOTIFICATION if running on Android 13. 2023-06-08 07:15:33 +02:00
Dennis Guse d7022a07a2 Cleanup: use Java17' Records.
Fixes #1526.
2023-06-08 07:06:22 +02:00
Dennis Guse 81b090893b Target Android 14/API34.
Fixes #1496.
2023-06-08 06:44:15 +02:00
Dennis Guse 3dca9304c4 Merge pull request #1568 from OpenTracksApp/pressure#1433
Preparing for use BLE Environmental Sensing sensor for elevation gain/loss
2023-06-08 06:38:49 +02:00
mm4c 7fc6492e13 Translated using Weblate (Dutch)
Currently translated at 100.0% (507 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nl/
2023-06-07 10:50:38 +02:00
Dennis Guse 80d968dd0f Cleanup. 2023-06-04 13:16:22 +02:00
Dennis Guse f336a1da12 Move GPSManager into SensorManager.
Includes Bugfix: SensorManager might not have been stopped.

Part of #1424.
2023-06-04 13:16:22 +02:00
Dennis Guse 1657bf5ec1 Cleanup: removed SensorDataSetChangeObserver interface.
Part of #1424.
2023-06-04 13:16:22 +02:00
Dennis Guse d046ca1cd8 SensorManager handles SensorDataSet.
Part of #1424.
2023-06-04 13:16:22 +02:00
Dennis Guse 6c852091ce AltitudeSumManager: extracted Barometer driver.
Part of #1424.
2023-06-04 13:16:22 +02:00
Dennis Guse 4906d3f942 Introduced AtmosphericPressure.
Part of #1424.
2023-06-04 13:16:22 +02:00
denta58 0395394e26 Bluetooth pressure data parse 2023-06-04 13:16:22 +02:00
Dennis Guse ebcc7dc46e Bump to gradle wrapper 8.1.1 2023-06-04 12:36:10 +02:00
Dennis Guse 1e4246e73a Bump com.android.tools.build:gradle from 8.1.0-beta03 to 8.1.0-beta04 2023-06-04 12:27:01 +02:00
Chris 3ca8381504 App shortcut for aggregated statistics.
Part of #1563.
2023-06-04 11:57:25 +02:00
ovl-1 b92457cb23 Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.8% (496 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2023-06-02 17:51:32 +02:00
josé m 4baec1e672 Translated using Weblate (Galician)
Currently translated at 100.0% (507 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2023-06-02 17:51:31 +02:00
Dennis Guse c702c5a4a0 Bugfix: translation for settings_recording_min_required_accuracy_summary needs to be inverted.
Fixes #1573.
2023-05-31 17:12:15 +02:00
Dennis Guse 24c2c5fc1b Release: v4.4.0 2023-05-29 19:48:47 +02:00
ovl-1 8797b033a3 Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.6% (493 of 505 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2023-05-29 19:48:47 +02:00
Dennis Guse b2c4cdc4aa Merge pull request #1318 from OpenTracksApp/exportOne#448
Export all tracks into one file
2023-05-29 09:08:13 +02:00
Dennis Guse ae37ec9c33 Bugfix: set file suffix for "one file"-exports explicitly.
KML/KMZ was added by AndroidOS, but not GPX.

Part of #1318.
2023-05-28 21:30:09 +02:00
Dennis Guse ccc18739a4 Bugfix: KML exporter for multiple tracks created invalid XML.
Part of #1318.
2023-05-28 21:30:09 +02:00
Dennis Guse f48c4806b5 Simplify TrackExporter interface. 2023-05-28 21:30:09 +02:00
Dennis Guse 3aadf6d5d8 Cleanup: use an enum for export's conflict resolution. 2023-05-28 21:30:09 +02:00
Dennis Guse 2f2679c775 Export: implement export to one file.
Fixes #1318
2023-05-28 21:30:09 +02:00
Dennis Guse 442e23b106 Cleanup: EGM2008CorrectionManager returns Altitude. 2023-05-27 08:07:33 +02:00
Dennis Guse 65249f11b2 Move Help from Settings to TrackList (overflow menu).
Fixes #956.
2023-05-26 13:24:43 +02:00
Dennis Guse 34b5c2ab18 BLE: code for fetching and parsing battery level (not used).
Part of #24.
2023-05-25 22:09:27 +02:00
Dennis Guse 93919183be Cleanup: use ActivityUtils.vibrate(). 2023-05-24 20:18:35 +02:00
Dennis Guse 1915641d78 Fix tests for FileUtils.buildUniqueFileName(). 2023-05-23 23:28:25 +02:00
Dennis Guse c2c7cad3eb Warning: do not change MAIN/LAUNCHER activity name due to use by icon packs. 2023-05-23 21:22:08 +02:00
Dennis Guse e4957e45af Espresso UI tests: get required permissions for Android 13. 2023-05-20 15:59:44 +02:00
Dennis Guse 77bce9a5eb Bump com.android.tools.build:gradle from 8.1.0-beta02 to 8.1.0-beta03 2023-05-20 15:59:37 +02:00
Dennis Guse 6538f0d587 Release: v4.3.5 2023-05-15 17:37:40 +02:00
rjo12 5b0e7e5065 Bugfix: Off-by-one bug in subtracting UINT. Fixes #1557 2023-05-15 08:19:49 +02:00
Dennis Guse d6cf8ba90e Bump com.android.tools.build:gradle from 8.1.0-beta01 to 0.1.0-beta02 2023-05-13 13:43:33 +02:00
Dennis Guse 29568d0e3f Bugfix: prevent crash when creating a marker (no camera or gallery application present).
Fixes #1561.
2023-05-13 13:41:07 +02:00
Dennis Guse a571c4c93b Merge remote-tracking branch 'weblate/main' 2023-05-13 13:39:17 +02:00
mm4c 073fed6c84 Translated using Weblate (Dutch)
Currently translated at 9.5% (10 of 105 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-05-11 15:50:15 +02:00
mm4c 6847e1f899 Translated using Weblate (Dutch)
Currently translated at 99.4% (502 of 505 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nl/
2023-05-11 15:50:14 +02:00
Dennis Guse 427048ac11 Bugfix: crash when changing layout from one with 3 column layout to one with 2 columns.
Fixes #1554.
2023-05-08 23:34:43 +02:00
Shimon 240ea47c43 Translated using Weblate (Czech)
Currently translated at 99.8% (504 of 505 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/cs/
2023-05-06 15:48:40 +02:00
dependabot[bot] 068d56a850 Bump com.google.android.material:material from 1.8.0 to 1.9.0
Bumps [com.google.android.material:material](https://github.com/material-components/material-components-android) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.8.0...1.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-05 07:59:57 +02:00
plum7x 76d02da973 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (505 of 505 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hant/
2023-05-04 10:49:21 +02:00
dependabot[bot] 4664895ae3 Bump androidx.mediarouter:mediarouter from 1.3.1 to 1.4.0
Bumps androidx.mediarouter:mediarouter from 1.3.1 to 1.4.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-04 07:18:21 +02:00
Rens Hijdra 5235c8cf83 Translated using Weblate (Dutch)
Currently translated at 97.6% (493 of 505 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nl/
2023-05-02 16:01:51 +02:00
Dennis Guse 4f040ac79f Release: v4.3.4 2023-05-01 12:53:35 +02:00
Dennis Guse dcad42bf4d Build: remove resConfigs limiting included languages.
Fixes #1527.
2023-05-01 12:44:19 +02:00
mm4c 2eea93a4a3 Translated using Weblate (Dutch)
Currently translated at 9.5% (10 of 105 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-04-29 15:50:49 +02:00
mm4c 55e419475a Translated using Weblate (Dutch)
Currently translated at 96.6% (488 of 505 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nl/
2023-04-29 15:50:48 +02:00
254 changed files with 4474 additions and 2783 deletions
+965
View File
@@ -0,0 +1,965 @@
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = false
max_line_length = 100
tab_width = 4
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
ij_visual_guides = none
ij_wrap_on_typing = false
[*.java]
ij_java_align_consecutive_assignments = false
ij_java_align_consecutive_variable_declarations = false
ij_java_align_group_field_declarations = false
ij_java_align_multiline_annotation_parameters = false
ij_java_align_multiline_array_initializer_expression = false
ij_java_align_multiline_assignment = false
ij_java_align_multiline_binary_operation = false
ij_java_align_multiline_chained_methods = false
ij_java_align_multiline_deconstruction_list_components = true
ij_java_align_multiline_extends_list = false
ij_java_align_multiline_for = true
ij_java_align_multiline_method_parentheses = false
ij_java_align_multiline_parameters = true
ij_java_align_multiline_parameters_in_calls = false
ij_java_align_multiline_parenthesized_expression = false
ij_java_align_multiline_records = true
ij_java_align_multiline_resources = true
ij_java_align_multiline_ternary_operation = false
ij_java_align_multiline_text_blocks = false
ij_java_align_multiline_throws_list = false
ij_java_align_subsequent_simple_methods = false
ij_java_align_throws_keyword = false
ij_java_align_types_in_multi_catch = true
ij_java_annotation_parameter_wrap = off
ij_java_array_initializer_new_line_after_left_brace = false
ij_java_array_initializer_right_brace_on_new_line = false
ij_java_array_initializer_wrap = off
ij_java_assert_statement_colon_on_next_line = false
ij_java_assert_statement_wrap = off
ij_java_assignment_wrap = off
ij_java_binary_operation_sign_on_next_line = false
ij_java_binary_operation_wrap = off
ij_java_blank_lines_after_anonymous_class_header = 0
ij_java_blank_lines_after_class_header = 0
ij_java_blank_lines_after_imports = 1
ij_java_blank_lines_after_package = 1
ij_java_blank_lines_around_class = 1
ij_java_blank_lines_around_field = 0
ij_java_blank_lines_around_field_in_interface = 0
ij_java_blank_lines_around_initializer = 1
ij_java_blank_lines_around_method = 1
ij_java_blank_lines_around_method_in_interface = 1
ij_java_blank_lines_before_class_end = 0
ij_java_blank_lines_before_imports = 1
ij_java_blank_lines_before_method_body = 0
ij_java_blank_lines_before_package = 0
ij_java_block_brace_style = end_of_line
ij_java_block_comment_add_space = false
ij_java_block_comment_at_first_column = true
ij_java_builder_methods = none
ij_java_call_parameters_new_line_after_left_paren = false
ij_java_call_parameters_right_paren_on_new_line = false
ij_java_call_parameters_wrap = off
ij_java_case_statement_on_separate_line = true
ij_java_catch_on_new_line = false
ij_java_class_annotation_wrap = split_into_lines
ij_java_class_brace_style = end_of_line
ij_java_class_count_to_use_import_on_demand = 99
ij_java_class_names_in_javadoc = 1
ij_java_deconstruction_list_wrap = normal
ij_java_do_not_indent_top_level_class_members = false
ij_java_do_not_wrap_after_single_annotation = false
ij_java_do_not_wrap_after_single_annotation_in_parameter = false
ij_java_do_while_brace_force = never
ij_java_doc_add_blank_line_after_description = true
ij_java_doc_add_blank_line_after_param_comments = false
ij_java_doc_add_blank_line_after_return = false
ij_java_doc_add_p_tag_on_empty_lines = true
ij_java_doc_align_exception_comments = true
ij_java_doc_align_param_comments = true
ij_java_doc_do_not_wrap_if_one_line = false
ij_java_doc_enable_formatting = true
ij_java_doc_enable_leading_asterisks = true
ij_java_doc_indent_on_continuation = false
ij_java_doc_keep_empty_lines = true
ij_java_doc_keep_empty_parameter_tag = true
ij_java_doc_keep_empty_return_tag = true
ij_java_doc_keep_empty_throws_tag = true
ij_java_doc_keep_invalid_tags = true
ij_java_doc_param_description_on_new_line = false
ij_java_doc_preserve_line_breaks = false
ij_java_doc_use_throws_not_exception_tag = true
ij_java_else_on_new_line = false
ij_java_enum_constants_wrap = off
ij_java_extends_keyword_wrap = off
ij_java_extends_list_wrap = off
ij_java_field_annotation_wrap = split_into_lines
ij_java_finally_on_new_line = false
ij_java_for_brace_force = never
ij_java_for_statement_new_line_after_left_paren = false
ij_java_for_statement_right_paren_on_new_line = false
ij_java_for_statement_wrap = off
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false
ij_java_if_brace_force = never
ij_java_imports_layout = $android.**,$androidx.**,$com.**,$junit.**,$net.**,$org.**,$java.**,$javax.**,$*,|,android.**,|,androidx.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,|
ij_java_indent_case_from_switch = true
ij_java_insert_inner_class_imports = false
ij_java_insert_override_annotation = true
ij_java_keep_blank_lines_before_right_brace = 2
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
ij_java_keep_blank_lines_in_code = 2
ij_java_keep_blank_lines_in_declarations = 2
ij_java_keep_builder_methods_indents = false
ij_java_keep_control_statement_in_one_line = true
ij_java_keep_first_column_comment = true
ij_java_keep_indents_on_empty_lines = false
ij_java_keep_line_breaks = true
ij_java_keep_multiple_expressions_in_one_line = false
ij_java_keep_simple_blocks_in_one_line = false
ij_java_keep_simple_classes_in_one_line = false
ij_java_keep_simple_lambdas_in_one_line = false
ij_java_keep_simple_methods_in_one_line = false
ij_java_label_indent_absolute = false
ij_java_label_indent_size = 0
ij_java_lambda_brace_style = end_of_line
ij_java_layout_static_imports_separately = true
ij_java_line_comment_add_space = false
ij_java_line_comment_add_space_on_reformat = false
ij_java_line_comment_at_first_column = true
ij_java_method_annotation_wrap = split_into_lines
ij_java_method_brace_style = end_of_line
ij_java_method_call_chain_wrap = off
ij_java_method_parameters_new_line_after_left_paren = false
ij_java_method_parameters_right_paren_on_new_line = false
ij_java_method_parameters_wrap = off
ij_java_modifier_list_wrap = false
ij_java_multi_catch_types_wrap = normal
ij_java_names_count_to_use_import_on_demand = 99
ij_java_new_line_after_lparen_in_annotation = false
ij_java_new_line_after_lparen_in_deconstruction_pattern = true
ij_java_new_line_after_lparen_in_record_header = false
ij_java_parameter_annotation_wrap = off
ij_java_parentheses_expression_new_line_after_left_paren = false
ij_java_parentheses_expression_right_paren_on_new_line = false
ij_java_place_assignment_sign_on_next_line = false
ij_java_prefer_longer_names = true
ij_java_prefer_parameters_wrap = false
ij_java_record_components_wrap = normal
ij_java_repeat_synchronized = true
ij_java_replace_instanceof_and_cast = false
ij_java_replace_null_check = true
ij_java_replace_sum_lambda_with_method_ref = true
ij_java_resource_list_new_line_after_left_paren = false
ij_java_resource_list_right_paren_on_new_line = false
ij_java_resource_list_wrap = off
ij_java_rparen_on_new_line_in_annotation = false
ij_java_rparen_on_new_line_in_deconstruction_pattern = true
ij_java_rparen_on_new_line_in_record_header = false
ij_java_space_after_closing_angle_bracket_in_type_argument = false
ij_java_space_after_colon = true
ij_java_space_after_comma = true
ij_java_space_after_comma_in_type_arguments = true
ij_java_space_after_for_semicolon = true
ij_java_space_after_quest = true
ij_java_space_after_type_cast = true
ij_java_space_before_annotation_array_initializer_left_brace = false
ij_java_space_before_annotation_parameter_list = false
ij_java_space_before_array_initializer_left_brace = false
ij_java_space_before_catch_keyword = true
ij_java_space_before_catch_left_brace = true
ij_java_space_before_catch_parentheses = true
ij_java_space_before_class_left_brace = true
ij_java_space_before_colon = true
ij_java_space_before_colon_in_foreach = true
ij_java_space_before_comma = false
ij_java_space_before_deconstruction_list = false
ij_java_space_before_do_left_brace = true
ij_java_space_before_else_keyword = true
ij_java_space_before_else_left_brace = true
ij_java_space_before_finally_keyword = true
ij_java_space_before_finally_left_brace = true
ij_java_space_before_for_left_brace = true
ij_java_space_before_for_parentheses = true
ij_java_space_before_for_semicolon = false
ij_java_space_before_if_left_brace = true
ij_java_space_before_if_parentheses = true
ij_java_space_before_method_call_parentheses = false
ij_java_space_before_method_left_brace = true
ij_java_space_before_method_parentheses = false
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
ij_java_space_before_quest = true
ij_java_space_before_switch_left_brace = true
ij_java_space_before_switch_parentheses = true
ij_java_space_before_synchronized_left_brace = true
ij_java_space_before_synchronized_parentheses = true
ij_java_space_before_try_left_brace = true
ij_java_space_before_try_parentheses = true
ij_java_space_before_type_parameter_list = false
ij_java_space_before_while_keyword = true
ij_java_space_before_while_left_brace = true
ij_java_space_before_while_parentheses = true
ij_java_space_inside_one_line_enum_braces = false
ij_java_space_within_empty_array_initializer_braces = false
ij_java_space_within_empty_method_call_parentheses = false
ij_java_space_within_empty_method_parentheses = false
ij_java_spaces_around_additive_operators = true
ij_java_spaces_around_annotation_eq = true
ij_java_spaces_around_assignment_operators = true
ij_java_spaces_around_bitwise_operators = true
ij_java_spaces_around_equality_operators = true
ij_java_spaces_around_lambda_arrow = true
ij_java_spaces_around_logical_operators = true
ij_java_spaces_around_method_ref_dbl_colon = false
ij_java_spaces_around_multiplicative_operators = true
ij_java_spaces_around_relational_operators = true
ij_java_spaces_around_shift_operators = true
ij_java_spaces_around_type_bounds_in_type_parameters = true
ij_java_spaces_around_unary_operator = false
ij_java_spaces_within_angle_brackets = false
ij_java_spaces_within_annotation_parentheses = false
ij_java_spaces_within_array_initializer_braces = false
ij_java_spaces_within_braces = false
ij_java_spaces_within_brackets = false
ij_java_spaces_within_cast_parentheses = false
ij_java_spaces_within_catch_parentheses = false
ij_java_spaces_within_deconstruction_list = false
ij_java_spaces_within_for_parentheses = false
ij_java_spaces_within_if_parentheses = false
ij_java_spaces_within_method_call_parentheses = false
ij_java_spaces_within_method_parentheses = false
ij_java_spaces_within_parentheses = false
ij_java_spaces_within_record_header = false
ij_java_spaces_within_switch_parentheses = false
ij_java_spaces_within_synchronized_parentheses = false
ij_java_spaces_within_try_parentheses = false
ij_java_spaces_within_while_parentheses = false
ij_java_special_else_if_treatment = true
ij_java_subclass_name_suffix = Impl
ij_java_ternary_operation_signs_on_next_line = false
ij_java_ternary_operation_wrap = off
ij_java_test_name_suffix = Test
ij_java_throws_keyword_wrap = off
ij_java_throws_list_wrap = off
ij_java_use_external_annotations = false
ij_java_use_fq_class_names = false
ij_java_use_relative_indents = false
ij_java_use_single_class_imports = true
ij_java_variable_annotation_wrap = off
ij_java_visibility = public
ij_java_while_brace_force = never
ij_java_while_on_new_line = false
ij_java_wrap_comments = false
ij_java_wrap_first_method_in_call_chain = false
ij_java_wrap_long_lines = false
[*.properties]
ij_properties_align_group_field_declarations = false
ij_properties_keep_blank_lines = false
ij_properties_key_value_delimiter = equals
ij_properties_spaces_around_key_value_delimiter = false
[.editorconfig]
ij_editorconfig_align_group_field_declarations = false
ij_editorconfig_space_after_colon = false
ij_editorconfig_space_after_comma = true
ij_editorconfig_space_before_colon = false
ij_editorconfig_space_before_comma = false
ij_editorconfig_spaces_around_assignment_operators = true
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_continuation_indent_size = 4
ij_xml_align_attributes = false
ij_xml_align_text = false
ij_xml_attribute_wrap = normal
ij_xml_block_comment_add_space = false
ij_xml_block_comment_at_first_column = true
ij_xml_keep_blank_lines = 2
ij_xml_keep_indents_on_empty_lines = false
ij_xml_keep_line_breaks = false
ij_xml_keep_line_breaks_in_text = true
ij_xml_keep_whitespaces = false
ij_xml_keep_whitespaces_around_cdata = preserve
ij_xml_keep_whitespaces_inside_cdata = false
ij_xml_line_comment_at_first_column = true
ij_xml_space_after_tag_name = false
ij_xml_space_around_equals_in_attribute = false
ij_xml_space_inside_empty_tag = true
ij_xml_text_wrap = normal
ij_xml_use_custom_settings = true
[{*.apinotes,*.yaml,*.yml}]
indent_size = 2
ij_yaml_align_values_properties = do_not_align
ij_yaml_autoinsert_sequence_marker = true
ij_yaml_block_mapping_on_new_line = false
ij_yaml_indent_sequence_value = true
ij_yaml_keep_indents_on_empty_lines = false
ij_yaml_keep_line_breaks = true
ij_yaml_sequence_on_new_line = false
ij_yaml_space_before_colon = false
ij_yaml_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true
[{*.bash,*.sh,*.zsh}]
indent_size = 2
tab_width = 2
ij_shell_binary_ops_start_line = false
ij_shell_keep_column_alignment_padding = false
ij_shell_minify_program = false
ij_shell_redirect_followed_by_space = false
ij_shell_switch_cases_indented = false
ij_shell_use_unix_line_separator = true
[{*.c,*.c++,*.cc,*.cp,*.cpp,*.cppm,*.cu,*.cuh,*.cxx,*.h,*.h++,*.hh,*.hp,*.hpp,*.hxx,*.i,*.icc,*.ii,*.inl,*.ino,*.ipp,*.ixx,*.m,*.mm,*.mxx,*.pch,*.tcc,*.tpp}]
ij_c_add_brief_tag = false
ij_c_add_getter_prefix = true
ij_c_add_setter_prefix = true
ij_c_align_dictionary_pair_values = false
ij_c_align_group_field_declarations = false
ij_c_align_init_list_in_columns = true
ij_c_align_multiline_array_initializer_expression = true
ij_c_align_multiline_assignment = true
ij_c_align_multiline_binary_operation = true
ij_c_align_multiline_chained_methods = false
ij_c_align_multiline_for = true
ij_c_align_multiline_ternary_operation = true
ij_c_array_initializer_comma_on_next_line = false
ij_c_array_initializer_new_line_after_left_brace = false
ij_c_array_initializer_right_brace_on_new_line = false
ij_c_array_initializer_wrap = normal
ij_c_assignment_wrap = off
ij_c_binary_operation_sign_on_next_line = false
ij_c_binary_operation_wrap = normal
ij_c_blank_lines_after_class_header = 0
ij_c_blank_lines_after_imports = 1
ij_c_blank_lines_around_class = 1
ij_c_blank_lines_around_field = 0
ij_c_blank_lines_around_field_in_interface = 0
ij_c_blank_lines_around_method = 1
ij_c_blank_lines_around_method_in_interface = 1
ij_c_blank_lines_around_namespace = 0
ij_c_blank_lines_around_properties_in_declaration = 0
ij_c_blank_lines_around_properties_in_interface = 0
ij_c_blank_lines_before_imports = 1
ij_c_blank_lines_before_method_body = 0
ij_c_block_brace_placement = end_of_line
ij_c_block_brace_style = end_of_line
ij_c_block_comment_at_first_column = true
ij_c_catch_on_new_line = false
ij_c_class_brace_style = end_of_line
ij_c_class_constructor_init_list_align_multiline = true
ij_c_class_constructor_init_list_comma_on_next_line = false
ij_c_class_constructor_init_list_new_line_after_colon = never
ij_c_class_constructor_init_list_new_line_before_colon = if_long
ij_c_class_constructor_init_list_wrap = normal
ij_c_copy_is_deep = false
ij_c_create_interface_for_categories = true
ij_c_declare_generated_methods = true
ij_c_description_include_member_names = true
ij_c_discharged_short_ternary_operator = false
ij_c_do_not_add_breaks = false
ij_c_do_while_brace_force = never
ij_c_else_on_new_line = false
ij_c_enum_constants_comma_on_next_line = false
ij_c_enum_constants_wrap = on_every_item
ij_c_for_brace_force = never
ij_c_for_statement_new_line_after_left_paren = false
ij_c_for_statement_right_paren_on_new_line = false
ij_c_for_statement_wrap = off
ij_c_function_brace_placement = end_of_line
ij_c_function_call_arguments_align_multiline = true
ij_c_function_call_arguments_align_multiline_pars = false
ij_c_function_call_arguments_comma_on_next_line = false
ij_c_function_call_arguments_new_line_after_lpar = false
ij_c_function_call_arguments_new_line_before_rpar = false
ij_c_function_call_arguments_wrap = normal
ij_c_function_non_top_after_return_type_wrap = normal
ij_c_function_parameters_align_multiline = true
ij_c_function_parameters_align_multiline_pars = false
ij_c_function_parameters_comma_on_next_line = false
ij_c_function_parameters_new_line_after_lpar = false
ij_c_function_parameters_new_line_before_rpar = false
ij_c_function_parameters_wrap = normal
ij_c_function_top_after_return_type_wrap = normal
ij_c_generate_additional_eq_operators = true
ij_c_generate_additional_rel_operators = true
ij_c_generate_class_constructor = true
ij_c_generate_comparison_operators_use_std_tie = false
ij_c_generate_instance_variables_for_properties = ask
ij_c_generate_operators_as_members = true
ij_c_header_guard_style_pattern = ${PROJECT_NAME}_${FILE_NAME}_${EXT}
ij_c_if_brace_force = never
ij_c_in_line_short_ternary_operator = true
ij_c_indent_block_comment = true
ij_c_indent_c_struct_members = 4
ij_c_indent_case_from_switch = true
ij_c_indent_class_members = 4
ij_c_indent_directive_as_code = false
ij_c_indent_implementation_members = 0
ij_c_indent_inside_code_block = 4
ij_c_indent_interface_members = 0
ij_c_indent_interface_members_except_ivars_block = false
ij_c_indent_namespace_members = 4
ij_c_indent_preprocessor_directive = 0
ij_c_indent_visibility_keywords = 0
ij_c_insert_override = true
ij_c_insert_virtual_with_override = false
ij_c_introduce_auto_vars = false
ij_c_introduce_const_params = false
ij_c_introduce_const_vars = false
ij_c_introduce_generate_property = false
ij_c_introduce_generate_synthesize = true
ij_c_introduce_globals_to_header = true
ij_c_introduce_prop_to_private_category = false
ij_c_introduce_static_consts = true
ij_c_introduce_use_ns_types = false
ij_c_ivars_prefix = _
ij_c_keep_blank_lines_before_end = 2
ij_c_keep_blank_lines_before_right_brace = 2
ij_c_keep_blank_lines_in_code = 2
ij_c_keep_blank_lines_in_declarations = 2
ij_c_keep_case_expressions_in_one_line = false
ij_c_keep_control_statement_in_one_line = true
ij_c_keep_directive_at_first_column = true
ij_c_keep_first_column_comment = true
ij_c_keep_line_breaks = true
ij_c_keep_nested_namespaces_in_one_line = false
ij_c_keep_simple_blocks_in_one_line = true
ij_c_keep_simple_methods_in_one_line = true
ij_c_keep_structures_in_one_line = false
ij_c_lambda_capture_list_align_multiline = false
ij_c_lambda_capture_list_align_multiline_bracket = false
ij_c_lambda_capture_list_comma_on_next_line = false
ij_c_lambda_capture_list_new_line_after_lbracket = false
ij_c_lambda_capture_list_new_line_before_rbracket = false
ij_c_lambda_capture_list_wrap = off
ij_c_line_comment_add_space = false
ij_c_line_comment_at_first_column = true
ij_c_method_brace_placement = end_of_line
ij_c_method_call_arguments_align_by_colons = true
ij_c_method_call_arguments_align_multiline = false
ij_c_method_call_arguments_special_dictionary_pairs_treatment = true
ij_c_method_call_arguments_wrap = off
ij_c_method_call_chain_wrap = off
ij_c_method_parameters_align_by_colons = true
ij_c_method_parameters_align_multiline = false
ij_c_method_parameters_wrap = off
ij_c_namespace_brace_placement = end_of_line
ij_c_parentheses_expression_new_line_after_left_paren = false
ij_c_parentheses_expression_right_paren_on_new_line = false
ij_c_place_assignment_sign_on_next_line = false
ij_c_property_nonatomic = true
ij_c_put_ivars_to_implementation = true
ij_c_refactor_compatibility_aliases_and_classes = true
ij_c_refactor_properties_and_ivars = true
ij_c_release_style = ivar
ij_c_retain_object_parameters_in_constructor = true
ij_c_semicolon_after_method_signature = false
ij_c_shift_operation_align_multiline = true
ij_c_shift_operation_wrap = normal
ij_c_show_non_virtual_functions = false
ij_c_space_after_colon = true
ij_c_space_after_colon_in_foreach = true
ij_c_space_after_colon_in_selector = false
ij_c_space_after_comma = true
ij_c_space_after_cup_in_blocks = false
ij_c_space_after_dictionary_literal_colon = true
ij_c_space_after_for_semicolon = true
ij_c_space_after_init_list_colon = true
ij_c_space_after_method_parameter_type_parentheses = false
ij_c_space_after_method_return_type_parentheses = false
ij_c_space_after_pointer_in_declaration = false
ij_c_space_after_quest = true
ij_c_space_after_reference_in_declaration = false
ij_c_space_after_reference_in_rvalue = false
ij_c_space_after_structures_rbrace = true
ij_c_space_after_superclass_colon = true
ij_c_space_after_type_cast = true
ij_c_space_after_visibility_sign_in_method_declaration = true
ij_c_space_before_autorelease_pool_lbrace = true
ij_c_space_before_catch_keyword = true
ij_c_space_before_catch_left_brace = true
ij_c_space_before_catch_parentheses = true
ij_c_space_before_category_parentheses = true
ij_c_space_before_chained_send_message = true
ij_c_space_before_class_left_brace = true
ij_c_space_before_colon = true
ij_c_space_before_colon_in_foreach = false
ij_c_space_before_comma = false
ij_c_space_before_dictionary_literal_colon = false
ij_c_space_before_do_left_brace = true
ij_c_space_before_else_keyword = true
ij_c_space_before_else_left_brace = true
ij_c_space_before_export_lbrace = true
ij_c_space_before_for_left_brace = true
ij_c_space_before_for_parentheses = true
ij_c_space_before_for_semicolon = false
ij_c_space_before_if_left_brace = true
ij_c_space_before_if_parentheses = true
ij_c_space_before_init_list = false
ij_c_space_before_init_list_colon = true
ij_c_space_before_method_call_parentheses = false
ij_c_space_before_method_left_brace = true
ij_c_space_before_method_parentheses = false
ij_c_space_before_namespace_lbrace = true
ij_c_space_before_pointer_in_declaration = true
ij_c_space_before_property_attributes_parentheses = false
ij_c_space_before_protocols_brackets = true
ij_c_space_before_quest = true
ij_c_space_before_reference_in_declaration = true
ij_c_space_before_superclass_colon = true
ij_c_space_before_switch_left_brace = true
ij_c_space_before_switch_parentheses = true
ij_c_space_before_template_call_lt = false
ij_c_space_before_template_declaration_lt = false
ij_c_space_before_try_left_brace = true
ij_c_space_before_while_keyword = true
ij_c_space_before_while_left_brace = true
ij_c_space_before_while_parentheses = true
ij_c_space_between_adjacent_brackets = false
ij_c_space_between_operator_and_punctuator = false
ij_c_space_within_empty_array_initializer_braces = false
ij_c_spaces_around_additive_operators = true
ij_c_spaces_around_assignment_operators = true
ij_c_spaces_around_bitwise_operators = true
ij_c_spaces_around_equality_operators = true
ij_c_spaces_around_lambda_arrow = true
ij_c_spaces_around_logical_operators = true
ij_c_spaces_around_multiplicative_operators = true
ij_c_spaces_around_pm_operators = false
ij_c_spaces_around_relational_operators = true
ij_c_spaces_around_shift_operators = true
ij_c_spaces_around_unary_operator = false
ij_c_spaces_within_array_initializer_braces = false
ij_c_spaces_within_braces = true
ij_c_spaces_within_brackets = false
ij_c_spaces_within_cast_parentheses = false
ij_c_spaces_within_catch_parentheses = false
ij_c_spaces_within_category_parentheses = false
ij_c_spaces_within_empty_braces = false
ij_c_spaces_within_empty_function_call_parentheses = false
ij_c_spaces_within_empty_function_declaration_parentheses = false
ij_c_spaces_within_empty_lambda_capture_list_bracket = false
ij_c_spaces_within_empty_template_call_ltgt = false
ij_c_spaces_within_empty_template_declaration_ltgt = false
ij_c_spaces_within_for_parentheses = false
ij_c_spaces_within_function_call_parentheses = false
ij_c_spaces_within_function_declaration_parentheses = false
ij_c_spaces_within_if_parentheses = false
ij_c_spaces_within_lambda_capture_list_bracket = false
ij_c_spaces_within_method_parameter_type_parentheses = false
ij_c_spaces_within_method_return_type_parentheses = false
ij_c_spaces_within_parentheses = false
ij_c_spaces_within_property_attributes_parentheses = false
ij_c_spaces_within_protocols_brackets = false
ij_c_spaces_within_send_message_brackets = false
ij_c_spaces_within_structured_binding_list_bracket = false
ij_c_spaces_within_switch_parentheses = false
ij_c_spaces_within_template_call_ltgt = false
ij_c_spaces_within_template_declaration_ltgt = false
ij_c_spaces_within_template_double_gt = true
ij_c_spaces_within_while_parentheses = false
ij_c_special_else_if_treatment = true
ij_c_structured_binding_list_align_multiline = false
ij_c_structured_binding_list_align_multiline_bracket = false
ij_c_structured_binding_list_comma_on_next_line = false
ij_c_structured_binding_list_new_line_after_lbracket = false
ij_c_structured_binding_list_new_line_before_rbracket = false
ij_c_structured_binding_list_wrap = off
ij_c_superclass_list_after_colon = never
ij_c_superclass_list_align_multiline = true
ij_c_superclass_list_before_colon = if_long
ij_c_superclass_list_comma_on_next_line = false
ij_c_superclass_list_wrap = on_every_item
ij_c_tag_prefix_of_block_comment = at
ij_c_tag_prefix_of_line_comment = back_slash
ij_c_template_call_arguments_align_multiline = false
ij_c_template_call_arguments_align_multiline_pars = false
ij_c_template_call_arguments_comma_on_next_line = false
ij_c_template_call_arguments_new_line_after_lt = false
ij_c_template_call_arguments_new_line_before_gt = false
ij_c_template_call_arguments_wrap = off
ij_c_template_declaration_function_body_indent = false
ij_c_template_declaration_function_wrap = split_into_lines
ij_c_template_declaration_struct_body_indent = false
ij_c_template_declaration_struct_wrap = split_into_lines
ij_c_template_parameters_align_multiline = false
ij_c_template_parameters_align_multiline_pars = false
ij_c_template_parameters_comma_on_next_line = false
ij_c_template_parameters_new_line_after_lt = false
ij_c_template_parameters_new_line_before_gt = false
ij_c_template_parameters_wrap = off
ij_c_ternary_operation_signs_on_next_line = true
ij_c_ternary_operation_wrap = normal
ij_c_type_qualifiers_placement = before
ij_c_use_modern_casts = true
ij_c_use_setters_in_constructor = true
ij_c_while_brace_force = never
ij_c_while_on_new_line = false
ij_c_wrap_property_declaration = off
[{*.cmake,CMakeLists.txt}]
ij_cmake_align_multiline_parameters_in_calls = false
ij_cmake_force_commands_case = 2
ij_cmake_keep_blank_lines_in_code = 2
ij_cmake_space_before_for_parentheses = true
ij_cmake_space_before_if_parentheses = true
ij_cmake_space_before_method_call_parentheses = false
ij_cmake_space_before_method_parentheses = false
ij_cmake_space_before_while_parentheses = true
ij_cmake_spaces_within_for_parentheses = false
ij_cmake_spaces_within_if_parentheses = false
ij_cmake_spaces_within_method_call_parentheses = false
ij_cmake_spaces_within_method_parentheses = false
ij_cmake_spaces_within_while_parentheses = false
[{*.gant,*.groovy,*.gy}]
ij_groovy_align_group_field_declarations = false
ij_groovy_align_multiline_array_initializer_expression = false
ij_groovy_align_multiline_assignment = false
ij_groovy_align_multiline_binary_operation = false
ij_groovy_align_multiline_chained_methods = false
ij_groovy_align_multiline_extends_list = false
ij_groovy_align_multiline_for = true
ij_groovy_align_multiline_list_or_map = true
ij_groovy_align_multiline_method_parentheses = false
ij_groovy_align_multiline_parameters = true
ij_groovy_align_multiline_parameters_in_calls = false
ij_groovy_align_multiline_resources = true
ij_groovy_align_multiline_ternary_operation = false
ij_groovy_align_multiline_throws_list = false
ij_groovy_align_named_args_in_map = true
ij_groovy_align_throws_keyword = false
ij_groovy_array_initializer_new_line_after_left_brace = false
ij_groovy_array_initializer_right_brace_on_new_line = false
ij_groovy_array_initializer_wrap = off
ij_groovy_assert_statement_wrap = off
ij_groovy_assignment_wrap = off
ij_groovy_binary_operation_wrap = off
ij_groovy_blank_lines_after_class_header = 0
ij_groovy_blank_lines_after_imports = 1
ij_groovy_blank_lines_after_package = 1
ij_groovy_blank_lines_around_class = 1
ij_groovy_blank_lines_around_field = 0
ij_groovy_blank_lines_around_field_in_interface = 0
ij_groovy_blank_lines_around_method = 1
ij_groovy_blank_lines_around_method_in_interface = 1
ij_groovy_blank_lines_before_imports = 1
ij_groovy_blank_lines_before_method_body = 0
ij_groovy_blank_lines_before_package = 0
ij_groovy_block_brace_style = end_of_line
ij_groovy_block_comment_add_space = false
ij_groovy_block_comment_at_first_column = true
ij_groovy_call_parameters_new_line_after_left_paren = false
ij_groovy_call_parameters_right_paren_on_new_line = false
ij_groovy_call_parameters_wrap = off
ij_groovy_catch_on_new_line = false
ij_groovy_class_annotation_wrap = split_into_lines
ij_groovy_class_brace_style = end_of_line
ij_groovy_class_count_to_use_import_on_demand = 5
ij_groovy_do_while_brace_force = never
ij_groovy_else_on_new_line = false
ij_groovy_enable_groovydoc_formatting = true
ij_groovy_enum_constants_wrap = off
ij_groovy_extends_keyword_wrap = off
ij_groovy_extends_list_wrap = off
ij_groovy_field_annotation_wrap = split_into_lines
ij_groovy_finally_on_new_line = false
ij_groovy_for_brace_force = never
ij_groovy_for_statement_new_line_after_left_paren = false
ij_groovy_for_statement_right_paren_on_new_line = false
ij_groovy_for_statement_wrap = off
ij_groovy_ginq_general_clause_wrap_policy = 2
ij_groovy_ginq_having_wrap_policy = 1
ij_groovy_ginq_indent_having_clause = true
ij_groovy_ginq_indent_on_clause = true
ij_groovy_ginq_on_wrap_policy = 1
ij_groovy_ginq_space_after_keyword = true
ij_groovy_if_brace_force = never
ij_groovy_import_annotation_wrap = 2
ij_groovy_imports_layout = *,|,javax.**,java.**,|,$*
ij_groovy_indent_case_from_switch = true
ij_groovy_indent_label_blocks = true
ij_groovy_insert_inner_class_imports = false
ij_groovy_keep_blank_lines_before_right_brace = 2
ij_groovy_keep_blank_lines_in_code = 2
ij_groovy_keep_blank_lines_in_declarations = 2
ij_groovy_keep_control_statement_in_one_line = true
ij_groovy_keep_first_column_comment = true
ij_groovy_keep_indents_on_empty_lines = false
ij_groovy_keep_line_breaks = true
ij_groovy_keep_multiple_expressions_in_one_line = false
ij_groovy_keep_simple_blocks_in_one_line = false
ij_groovy_keep_simple_classes_in_one_line = true
ij_groovy_keep_simple_lambdas_in_one_line = true
ij_groovy_keep_simple_methods_in_one_line = true
ij_groovy_label_indent_absolute = false
ij_groovy_label_indent_size = 0
ij_groovy_lambda_brace_style = end_of_line
ij_groovy_layout_static_imports_separately = true
ij_groovy_line_comment_add_space = false
ij_groovy_line_comment_add_space_on_reformat = false
ij_groovy_line_comment_at_first_column = true
ij_groovy_method_annotation_wrap = split_into_lines
ij_groovy_method_brace_style = end_of_line
ij_groovy_method_call_chain_wrap = off
ij_groovy_method_parameters_new_line_after_left_paren = false
ij_groovy_method_parameters_right_paren_on_new_line = false
ij_groovy_method_parameters_wrap = off
ij_groovy_modifier_list_wrap = false
ij_groovy_names_count_to_use_import_on_demand = 3
ij_groovy_packages_to_use_import_on_demand = java.awt.*,javax.swing.*
ij_groovy_parameter_annotation_wrap = off
ij_groovy_parentheses_expression_new_line_after_left_paren = false
ij_groovy_parentheses_expression_right_paren_on_new_line = false
ij_groovy_prefer_parameters_wrap = false
ij_groovy_resource_list_new_line_after_left_paren = false
ij_groovy_resource_list_right_paren_on_new_line = false
ij_groovy_resource_list_wrap = off
ij_groovy_space_after_assert_separator = true
ij_groovy_space_after_colon = true
ij_groovy_space_after_comma = true
ij_groovy_space_after_comma_in_type_arguments = true
ij_groovy_space_after_for_semicolon = true
ij_groovy_space_after_quest = true
ij_groovy_space_after_type_cast = true
ij_groovy_space_before_annotation_parameter_list = false
ij_groovy_space_before_array_initializer_left_brace = false
ij_groovy_space_before_assert_separator = false
ij_groovy_space_before_catch_keyword = true
ij_groovy_space_before_catch_left_brace = true
ij_groovy_space_before_catch_parentheses = true
ij_groovy_space_before_class_left_brace = true
ij_groovy_space_before_closure_left_brace = true
ij_groovy_space_before_colon = true
ij_groovy_space_before_comma = false
ij_groovy_space_before_do_left_brace = true
ij_groovy_space_before_else_keyword = true
ij_groovy_space_before_else_left_brace = true
ij_groovy_space_before_finally_keyword = true
ij_groovy_space_before_finally_left_brace = true
ij_groovy_space_before_for_left_brace = true
ij_groovy_space_before_for_parentheses = true
ij_groovy_space_before_for_semicolon = false
ij_groovy_space_before_if_left_brace = true
ij_groovy_space_before_if_parentheses = true
ij_groovy_space_before_method_call_parentheses = false
ij_groovy_space_before_method_left_brace = true
ij_groovy_space_before_method_parentheses = false
ij_groovy_space_before_quest = true
ij_groovy_space_before_record_parentheses = false
ij_groovy_space_before_switch_left_brace = true
ij_groovy_space_before_switch_parentheses = true
ij_groovy_space_before_synchronized_left_brace = true
ij_groovy_space_before_synchronized_parentheses = true
ij_groovy_space_before_try_left_brace = true
ij_groovy_space_before_try_parentheses = true
ij_groovy_space_before_while_keyword = true
ij_groovy_space_before_while_left_brace = true
ij_groovy_space_before_while_parentheses = true
ij_groovy_space_in_named_argument = true
ij_groovy_space_in_named_argument_before_colon = false
ij_groovy_space_within_empty_array_initializer_braces = false
ij_groovy_space_within_empty_method_call_parentheses = false
ij_groovy_spaces_around_additive_operators = true
ij_groovy_spaces_around_assignment_operators = true
ij_groovy_spaces_around_bitwise_operators = true
ij_groovy_spaces_around_equality_operators = true
ij_groovy_spaces_around_lambda_arrow = true
ij_groovy_spaces_around_logical_operators = true
ij_groovy_spaces_around_multiplicative_operators = true
ij_groovy_spaces_around_regex_operators = true
ij_groovy_spaces_around_relational_operators = true
ij_groovy_spaces_around_shift_operators = true
ij_groovy_spaces_within_annotation_parentheses = false
ij_groovy_spaces_within_array_initializer_braces = false
ij_groovy_spaces_within_braces = true
ij_groovy_spaces_within_brackets = false
ij_groovy_spaces_within_cast_parentheses = false
ij_groovy_spaces_within_catch_parentheses = false
ij_groovy_spaces_within_for_parentheses = false
ij_groovy_spaces_within_gstring_injection_braces = false
ij_groovy_spaces_within_if_parentheses = false
ij_groovy_spaces_within_list_or_map = false
ij_groovy_spaces_within_method_call_parentheses = false
ij_groovy_spaces_within_method_parentheses = false
ij_groovy_spaces_within_parentheses = false
ij_groovy_spaces_within_switch_parentheses = false
ij_groovy_spaces_within_synchronized_parentheses = false
ij_groovy_spaces_within_try_parentheses = false
ij_groovy_spaces_within_tuple_expression = false
ij_groovy_spaces_within_while_parentheses = false
ij_groovy_special_else_if_treatment = true
ij_groovy_ternary_operation_wrap = off
ij_groovy_throws_keyword_wrap = off
ij_groovy_throws_list_wrap = off
ij_groovy_use_flying_geese_braces = false
ij_groovy_use_fq_class_names = false
ij_groovy_use_fq_class_names_in_javadoc = true
ij_groovy_use_relative_indents = false
ij_groovy_use_single_class_imports = true
ij_groovy_variable_annotation_wrap = off
ij_groovy_while_brace_force = never
ij_groovy_while_on_new_line = false
ij_groovy_wrap_chain_calls_after_dot = false
ij_groovy_wrap_long_lines = false
[{*.har,*.json}]
indent_size = 2
ij_json_array_wrapping = split_into_lines
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_keep_trailing_comma = false
ij_json_object_wrapping = split_into_lines
ij_json_property_alignment = do_not_align
ij_json_space_after_colon = true
ij_json_space_after_comma = true
ij_json_space_before_colon = false
ij_json_space_before_comma = false
ij_json_spaces_within_braces = false
ij_json_spaces_within_brackets = false
ij_json_wrap_long_lines = false
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
ij_html_align_attributes = true
ij_html_align_text = false
ij_html_attribute_wrap = normal
ij_html_block_comment_add_space = false
ij_html_block_comment_at_first_column = true
ij_html_do_not_align_children_of_min_lines = 0
ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p
ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
ij_html_enforce_quotes = false
ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var
ij_html_keep_blank_lines = 2
ij_html_keep_indents_on_empty_lines = false
ij_html_keep_line_breaks = true
ij_html_keep_line_breaks_in_text = true
ij_html_keep_whitespaces = false
ij_html_keep_whitespaces_inside = span,pre,textarea
ij_html_line_comment_at_first_column = true
ij_html_new_line_after_last_attribute = never
ij_html_new_line_before_first_attribute = never
ij_html_quote_style = double
ij_html_remove_new_line_before_tags = br
ij_html_space_after_tag_name = false
ij_html_space_around_equality_in_attribute = false
ij_html_space_inside_empty_tag = false
ij_html_text_wrap = normal
[{*.kt,*.kts}]
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_allow_trailing_comma = false
ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_assignment_wrap = off
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_add_space = false
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = false
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_call_parameters_wrap = off
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = split_into_lines
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = true
ij_kotlin_continuation_indent_in_if_conditions = true
ij_kotlin_continuation_indent_in_parameter_lists = true
ij_kotlin_continuation_indent_in_supertype_lists = true
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = off
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
ij_kotlin_keep_blank_lines_in_declarations = 2
ij_kotlin_keep_first_column_comment = true
ij_kotlin_keep_indents_on_empty_lines = false
ij_kotlin_keep_line_breaks = true
ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_break_after_multiline_when_entry = true
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_add_space_on_reformat = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = off
ij_kotlin_method_parameters_new_line_after_left_paren = false
ij_kotlin_method_parameters_right_paren_on_new_line = false
ij_kotlin_method_parameters_wrap = off
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
ij_kotlin_space_after_type_colon = true
ij_kotlin_space_before_catch_parentheses = true
ij_kotlin_space_before_comma = false
ij_kotlin_space_before_extend_colon = true
ij_kotlin_space_before_for_parentheses = true
ij_kotlin_space_before_if_parentheses = true
ij_kotlin_space_before_lambda_arrow = true
ij_kotlin_space_before_type_colon = false
ij_kotlin_space_before_when_parentheses = true
ij_kotlin_space_before_while_parentheses = true
ij_kotlin_spaces_around_additive_operators = true
ij_kotlin_spaces_around_assignment_operators = true
ij_kotlin_spaces_around_equality_operators = true
ij_kotlin_spaces_around_function_type_arrow = true
ij_kotlin_spaces_around_logical_operators = true
ij_kotlin_spaces_around_multiplicative_operators = true
ij_kotlin_spaces_around_range = false
ij_kotlin_spaces_around_relational_operators = true
ij_kotlin_spaces_around_unary_operator = false
ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_use_custom_formatting_for_modifiers = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 0
ij_kotlin_wrap_first_method_in_call_chain = false
[{*.markdown,*.md}]
ij_markdown_force_one_space_after_blockquote_symbol = true
ij_markdown_force_one_space_after_header_symbol = true
ij_markdown_force_one_space_after_list_bullet = true
ij_markdown_force_one_space_between_words = true
ij_markdown_format_tables = true
ij_markdown_insert_quote_arrows_on_wrap = true
ij_markdown_keep_indents_on_empty_lines = false
ij_markdown_keep_line_breaks_inside_text_blocks = true
ij_markdown_max_lines_around_block_elements = 1
ij_markdown_max_lines_around_header = 1
ij_markdown_max_lines_between_paragraphs = 1
ij_markdown_min_lines_around_block_elements = 1
ij_markdown_min_lines_around_header = 1
ij_markdown_min_lines_between_paragraphs = 1
ij_markdown_wrap_text_if_long = true
ij_markdown_wrap_text_inside_blockquotes = true
[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}]
ij_toml_keep_indents_on_empty_lines = false
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+10 -3
View File
@@ -1,18 +1,25 @@
# How to contribute?
## Testing
## Testing
Just use OpenTracks and tell us what can be improved by opening an issue [here](https://github.com/OpenTracksApp/OpenTracks/issues/new/choose)
Just use OpenTracks and tell us what can be improved by opening an
issue [here](https://github.com/OpenTracksApp/OpenTracks/issues/new/choose)
If we broke things (happens now and then), please tell us also.
## Translations
Translations are hosted on [hosted.weblate.org](https://hosted.weblate.org/engage/opentracks/).
## Coding new features
You have a cool idea for a feature that is missing in OpenTracks?
And you would like to help by implementing it?
If not, you can just take a look at the [issue tracker](https://github.com/OpenTracksApp/OpenTracks/issues).
If not, you can just take a look at
the [issue tracker](https://github.com/OpenTracksApp/OpenTracks/issues).
Advices:
* creating an issue beforehand might be a good idea
Sometimes it is helpful to discuss your idea and also your implementation approach first.
We may be able to give you some hints on how to tackle the problem.
+33 -11
View File
@@ -59,16 +59,38 @@ We do not support type of movement (i.e., walking vs running).
## Tested Barometric Sensor Smartphones
There are smartphones that have a barometric sensor that OpenTracks uses to compute altitude gain and loss.
OpenTracks derives the altitude gain and altitude loss using the internal barometric sensor of a device (if present).
However, barometric sensor seem to be prone to a lot of noise.
This may either be a malfunctioning sensor, a case that prevents the device from measuring, or something else.
In our tests there are smartphones whose barometric sensor works well and others don't. Here is a list of the smartphones tested:
To check if the barometric sensor of a device is working with OpenTracks:
1. Record a track (ca. 1min) with the device stationary.
Expectation: 0m gain and 0m loss should.
2. Record a track (ca. 1min) while shaking the device.
Expectation: 0m gain and 0m loss should.
3. Record a track (ca. 1min) while moving the device up (less than 3m difference between highest and lowest point).
Expectation: 0m gain and 0m loss should.
4. Record a track walking up some stairs or taking an elevator.
Expectation: some gain and some loss is recorded that seems realistic.
| 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 |
If any of these tests do **not** show expected values for gain/loss, OpenTracks will not show useful values.
Please note that OpenTracks:
* only records gain/loss if +/- 3m to the initial/previous of are measured to the previous
* solely uses the barometric sensor for gain/loss; GPS is not used for this.
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 |
| Google | Nexus 4 | LineageOS 18.1 | Ok |
| Samsung | S4 | Android 11 | Ok |
| Sony | Xperia XZ1 Compact | LineageOS 16.1 | Failure |
+10 -12
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.0-beta01'
classpath 'com.android.tools.build:gradle:8.1.0'
}
}
@@ -51,7 +51,7 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
compileSdk 33
compileSdk 34
buildFeatures {
viewBinding true
@@ -64,18 +64,16 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 5194
versionName "v4.3.3"
versionCode 5313
versionName "v4.7.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdk 21
targetSdk 33
minSdk 24
targetSdk 34
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
resConfigs "ar", "bg", "ca", "cs", "da", "de", "el", "en", "es", "et", "eu", "fa", "fi", "fr", "ga", "gl", "hi", "hr", "hu", "in", "it", "iw", "ja", "ko", "lt", "lv", "mo", "ms", "nb", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sr", "sv", "th", "tl", "tr", "uk", "vi", "zh"
}
signingConfigs {
nightly {
@@ -130,20 +128,20 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.mediarouter:mediarouter:1.3.1'
implementation 'androidx.mediarouter:mediarouter:1.4.0'
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'org.mockito:mockito-android:5.3.1'
androidTestImplementation 'org.mockito:mockito-android:5.4.0'
androidTestUtil 'androidx.test:orchestrator:1.4.2'
}
@@ -0,0 +1,4 @@
v4.3.4: OpenTracks
Bugfix:
- Android 13: regional locales could not be selected (introduced in v4.3.0)
@@ -0,0 +1,6 @@
v4.3.5: OpenTracks
Bugfix:
- Sensor parsing had a overflow one off bug (#1559)
- Insert marker: don't crash if no camera app is installed (#1561)
@@ -0,0 +1,4 @@
v4.4.0: OpenTracks
Changes:
- Export all tracks as one file
@@ -0,0 +1,14 @@
v4.5.0: OpenTracks
Changes:
- Material3
- App shortcut for aggregated statistics
- New Unit: miles with meters
Bugfix:
- GPS disabled warnings shown repeatedly (#1429)
Developer:
- Use Java Records
- Target API34
- Refactor sensor handling
@@ -0,0 +1,12 @@
v4.6.0: OpenTracks
Changes:
- Voice announcements use ICU plural format (en, es, pt, zh, dr, de) due to fractional quanitites #1584
- Requires Android 7.0 / SDK24+ due to ICU plurals
- On Android 14+: use Android's AltitudeConverter instead of internal EGM2008
Bugfix:
- Android 13 couldn't select images from gallery
Developer:
- Split ActivityType and ActivityIcon
@@ -0,0 +1,5 @@
v4.6.1: OpenTracks
Bugfix:
- Re-added aggregated statistics per selected tracks
@@ -0,0 +1,8 @@
v4.6.2: OpenTracks
Bugfix:
- Crash on voice annoucement for imperial+meter
- Crash on settings for imperial+meter
- Fixed clipping of charts (Material3 upgrade)
- Bluetooth settings handle missing permission
- Aggregated stats didn't show imperial units for avg speed
@@ -0,0 +1,11 @@
v4.7.0: OpenTracks
Changes:
- Statistics: distance, total time, and moving time are included between automatic segments (created due to distance too previous location too long)
- Recorded tracks show sensor statistics again
Bugfix:
- GPX exporter: for automatic segments their might be no location
- CSV exporter: distance didn't use decimal grouping
@@ -0,0 +1,4 @@
v4.4.0: OpenTracks
Cambios:
- Exportar todas as rutas nun único ficheiro
@@ -0,0 +1,14 @@
v4.5.0: OpenTracks
Cambios:
- Material3
- Atallo na app para estatísticas acumuladas
- Nova unidade: millas con metros
Arranxo:
- avisos repetidos do GPS desactivado (#1429)
Desenvolvemento:
- Utilizar Java Records
- Obxectivo API34
- Refactorización da xestión dos sensores
@@ -0,0 +1,12 @@
v4.6.0: OpenTracks
Cambios:
- Anuncios por voz usando o formato de plurais ICU (en, es, pt, zh, dr, de) debido ás cantidades en fraccións #1584
- Require Android 7.0 / SDK24+ debido aos plurais ICU
- En Android 14+: usar o conversor AltitudeConverter de Android no lugar do interno EGM2008
Arrranxo:
- Non se podía escoller unha imaxe da galería en Android 13
Desenvolvemento:
- Separamos ActivityType e ActivityIcon
@@ -0,0 +1,4 @@
v4.6.1: OpenTracks
Arranxo:
- Volvemos a engadir estatísticas conxuntas para as rutas seleccionadas
@@ -0,0 +1,10 @@
(versionName): OpenTracks
Cambios:
-
Arranxos:
-
Programadora:
-
@@ -0,0 +1,22 @@
Gravador de actividades deportivas que respecta a túa privacidade.
As actividades deportivas e ao aire libre proporcionanche felicidade, destreza e confianza.
Dalle valor á túa saúde facendo un seguimento dos teus adestramentos.
Grava mentras saes a correr ou camiñar, e será un ciclocomputador cunha pantalla máis grande e clara.
Marca as localizacións interesantes na ruta podendo engadir unha imaxe.
Garda un rexistro das túas estatísticas con gran detalle e análise.
Comparte só os datos que outras persoas poden ver.
* Anuncios por voz.
* Soporte para sensores Bluetooth LE: de frecuencia cardíaca, velocidade e distancia (ciclismo), cadencia (ciclismo), e de potencia (ciclismo).
* Gañancia e perda de altitude: a través do sensor barométrico.
* Altitude mostrada en EGM2008 (sobre o nivel do mar); exportada como WGS84.
* Exporta os datos como rutas tanto como KMZ (inclúe fotos), KML, ou GPX.
* Non precisa conexión a internet ou permisos extra.
* Decorado escuro, claro, ou seguindo os axustes do sistema.
* Sen publicidade.
Software Libre / Free Software / Open Source
significa que podes usar, estudar, cambiar e compartir o código fonte.
Licenza Apache 2.0
@@ -0,0 +1 @@
Gravador de actividades que respecta a túa privacidade.
@@ -0,0 +1 @@
OpenTracks
@@ -2,13 +2,13 @@ v3.19.0: OpenTracks
Wijzigingen:
- Ondersteuning voor Running Speed en Cadence (RCS) sensoren
- Bij het delen van een track wordt een afbeelding gemaakt
- Bij delen van een track: maakt een deel-afbeelding
- Niet trillen bij starten van opnemen
- Bij opnemen: UI toont recentst ontvangen hoogte, snelheid en richting (ook al is deze niet opgeslagen)
- Bij opnemen: UI toont meest recente hoogte, snelheid en richting (zelfs als niet opgeslagen)
Bugfix:
- Berekening van totale tijd mag geen pauze-tijd bevatten (vereist opnieuw importeren)
- (opnieuw) Gebruik aangepaste overzichten indien aanwezig
- Berekening totale tijd mag geen pauze-tijd bevatten (vereist opnieuw importeren)
- (opnieuw) Gebruik aanwezige aangepaste overzichten
Ontwikkelaar:
- Upgrade naar SDK31 en AGP7.0.0 (vereist JDK11)
@@ -5,4 +5,4 @@ Wijzigingen:
- Gesproken meldingen van hartslag
Bugfix:
- Crash due to race condition
- Vastloper door race condities
Binary file not shown.
+2 -1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+12 -7
View File
@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,10 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -143,12 +140,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -193,6 +194,10 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
Vendored
+1
View File
@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -11,6 +11,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
@@ -30,7 +31,7 @@ public class EspressoDeleteTrackTest {
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Test
public void espressoDeleteTrackTest() {
@@ -51,7 +52,15 @@ public class EspressoDeleteTrackTest {
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(longClick());
// open menu and delete selected track
onView(allOf(withContentDescription("More options"), isDisplayed()))
//TODO openActionBarOverflowOrOptionsMenu(); doesn't work
onView(
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(androidx.appcompat.R.id.action_mode_bar),
1),
2),
isDisplayed()))
.perform(click());
onView(withText("Delete")).perform(click());
@@ -38,7 +38,7 @@ public class EspressoEditTrackRecordingTest {
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@LargeTest
@Test
@@ -31,8 +31,7 @@ public class EspressoUITest {
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@LargeTest
@Test
public void record_stop_resume_stop_finish() {
@@ -0,0 +1,16 @@
package de.dennisguse.opentracks;
import android.Manifest;
import android.os.Build;
import androidx.test.rule.GrantPermissionRule;
public class TestUtil {
public static GrantPermissionRule createGrantPermissionRule() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.POST_NOTIFICATIONS);
}
return GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.BLUETOOTH_CONNECT);
}
}
@@ -27,7 +27,7 @@ public class ChartPointTest {
statistics.setTotalTime(Duration.ofSeconds(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), false, UnitSystem.IMPERIAL);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), false, UnitSystem.IMPERIAL_FEET);
// then
assertEquals(1000000, (long) point.getTimeOrDistance());
@@ -96,16 +96,7 @@ public class TestDataUtil {
return new TrackData(track, trackPoints, markers);
}
public static class TrackData {
public final Track track;
public final List<TrackPoint> trackPoints;
public final List<Marker> markers;
public TrackData(Track track, List<TrackPoint> trackPoints, List<Marker> markers) {
this.track = track;
this.trackPoints = trackPoints;
this.markers = markers;
}
public record TrackData(Track track, List<TrackPoint> trackPoints, List<Marker> markers) {
}
@@ -424,7 +424,7 @@ public class CustomContentProviderUtilsTest {
Marker.Id markerId = new Marker.Id(System.currentTimeMillis());
marker.setId(markerId);
ContentValues contentValues = contentProviderUtils.createContentValues(marker);
assertEquals(markerId.getId(), contentValues.get(MarkerColumns._ID));
assertEquals(markerId.id(), contentValues.get(MarkerColumns._ID));
assertEquals((int) (TestDataUtil.INITIAL_LONGITUDE * 1000000), contentValues.get(MarkerColumns.LONGITUDE));
assertEquals(TEST_DESC, contentValues.get(MarkerColumns.DESCRIPTION));
}
@@ -458,7 +458,7 @@ public class CustomContentProviderUtilsTest {
when(cursorMock.getLong(columnIndex++)).thenReturn(trackId);
Marker marker = contentProviderUtils.createMarker(cursorMock);
assertEquals(id, marker.getId().getId());
assertEquals(id, marker.getId().id());
assertEquals(name, marker.getName());
assertEquals(trackId, marker.getTrackId().getId());
}
@@ -67,7 +67,7 @@ public class TrackSelectionTest extends TestCase {
@Test
public void testFilterBuildSelection_onlyOneCategory() {
// given
TrackSelection filter = new TrackSelection().addCategory("running");
TrackSelection filter = new TrackSelection().addActivityType("running");
// when
SelectionData selection = filter.buildSelection();
@@ -82,10 +82,10 @@ public class TrackSelectionTest extends TestCase {
public void testFilterBuildSelection_severalCategories() {
// given
TrackSelection filter = new TrackSelection()
.addCategory("running")
.addCategory("road biking")
.addCategory("mountain biking")
.addCategory("trail walking");
.addActivityType("running")
.addActivityType("road biking")
.addActivityType("mountain biking")
.addActivityType("trail walking");
// when
SelectionData selection = filter.buildSelection();
@@ -136,8 +136,8 @@ public class TrackSelectionTest extends TestCase {
.addTrackId(trackId1)
.addTrackId(trackId2)
.addTrackId(trackId3)
.addCategory("running")
.addCategory("road biking");
.addActivityType("running")
.addActivityType("road biking");
// when
SelectionData selection = filter.buildSelection();
@@ -188,8 +188,8 @@ public class TrackSelectionTest extends TestCase {
long oneDay = 24 * 60 * 60 * 1000;
TrackSelection filter = new TrackSelection()
.addCategory("running")
.addCategory("road biking")
.addActivityType("running")
.addActivityType("road biking")
.addDateRange(instant, instant.plusMillis(oneDay));
// when
@@ -35,7 +35,7 @@ public class DistanceFormatterTest {
public void testFormatDistance_imperial() {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.IMPERIAL)
.setUnit(UnitSystem.IMPERIAL_FEET)
.build(context);
// A large number in imperial
@@ -36,7 +36,7 @@ public class SpeedFormatterTest {
public void testGetSpeedParts_pace_imperial() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.IMPERIAL)
.setUnit(UnitSystem.IMPERIAL_FEET)
.setReportSpeedOrPace(false)
.build(context);
@@ -72,7 +72,7 @@ public class SpeedFormatterTest {
public void testGetSpeedParts_speed_imperial() {
SpeedFormatter formatter = SpeedFormatter.Builder()
.setDecimalCount(2)
.setUnit(UnitSystem.IMPERIAL)
.setUnit(UnitSystem.IMPERIAL_FEET)
.setReportSpeedOrPace(true)
.build(context);
@@ -46,6 +46,7 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TimezoneRule;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
@@ -57,7 +58,6 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
@@ -96,8 +96,8 @@ public class ExportImportTest {
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private static final String TRACK_ICON = "the track icon";
private static final String TRACK_CATEGORY = "the category";
private static final ActivityType TRACK_ACTIVITY_TYPE = ActivityType.MOUNTAIN_BIKING;
private static final String TRACK_ACTIVITY_TYPE_LOCALIZED = "the activity type";
private static final String TRACK_DESCRIPTION = "the description";
private File tmpFile;
@@ -165,8 +165,7 @@ public class ExportImportTest {
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:18Z");
BluetoothRemoteSensorManager mockRemoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class);
trackPointCreator.getSensorManager().setBluetoothSensorManager(mockRemoteSensorManager);
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -179,12 +178,11 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:03:24Z");
trackPointCreator.getSensorManager().setBluetoothSensorManager(mockRemoteSensorManager);
service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId);
track.setIcon(TRACK_ICON);
track.setCategory(TRACK_CATEGORY);
track.setActivityType(TRACK_ACTIVITY_TYPE);
track.setActivityTypeLocalized(TRACK_ACTIVITY_TYPE_LOCALIZED);
track.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.updateTrack(track);
@@ -195,7 +193,7 @@ public class ExportImportTest {
assertEquals(2, markers.size());
}
//TODO Does not test images
//TODO Does not test marker images
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
@@ -204,11 +202,11 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context, contentProviderUtils);
TrackExporter trackExporter = TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.createTrackExporter(context, contentProviderUtils);
// when
// 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
@@ -219,10 +217,10 @@ public class ExportImportTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getActivityTypeLocalized(), importedTrack.getActivityTypeLocalized());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon());
assertEquals(track.getActivityType(), importedTrack.getActivityType());
// 2. trackpoints
TrackPointAssert a = new TrackPointAssert();
@@ -240,24 +238,24 @@ public class ExportImportTest {
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(19), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(20), importedTrackStatistics.getTotalTime());
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(222238.70, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(55559.67, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(6.58, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(11111.93, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(55559.67, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
// Altitude
assertEquals(originalTrackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
@@ -290,7 +288,7 @@ public class ExportImportTest {
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context, contentProviderUtils);
// when
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
@@ -314,7 +312,7 @@ public class ExportImportTest {
// when
// 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
@@ -326,7 +324,7 @@ public class ExportImportTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getActivityTypeLocalized(), importedTrack.getActivityTypeLocalized());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
@@ -396,16 +394,16 @@ public class ExportImportTest {
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:03:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(80), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(80), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(222349.85, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(8.34, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(8.34, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(8.34, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(2779.37, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(2779.37, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(2779.37, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
@@ -432,7 +430,7 @@ public class ExportImportTest {
// when
// 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
@@ -457,7 +455,7 @@ public class ExportImportTest {
// when
// 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// then
@@ -495,9 +493,6 @@ public class ExportImportTest {
}
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) {
BluetoothRemoteSensorManager remoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class);
Mockito.when(remoteSensorManager.fill(Mockito.any())).thenAnswer(invocation -> {
TrackPoint trackPoint = invocation.getArgument(0);
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
@@ -508,21 +503,20 @@ public class ExportImportTest {
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
sensorDataSet.set(cyclingCadence);
if (distance != null && speed != null) {
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed);
}
if (distance != null && speed != null) {
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed);
}
sensorDataSet.fillTrackPoint(trackPoint);
return sensorDataSet;
});
trackPointCreator.getSensorManager().setBluetoothSensorManager(remoteSensorManager);
trackPointCreator.onChange(new SensorDataSet());
trackPointCreator.getSensorManager().sensorDataSet = sensorDataSet;
trackPointCreator.onChange(sensorDataSet);
//TODO Might require: getSensorManager().sensorDataSet = new SensorDataSet()
}
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
@@ -545,6 +539,6 @@ public class ExportImportTest {
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(time);
trackPointCreator.getGpsHandler().onLocationChanged(location);
trackPointCreator.onChange(location);
}
}
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
@@ -79,10 +80,10 @@ public class GPXTrackImporterTest {
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("the category", importedTrack.getCategory());
assertEquals("the category", importedTrack.getActivityTypeLocalized());
assertEquals("the description", importedTrack.getDescription());
assertEquals("2021-01-07 22:51", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
//TODO Check trackstatistics
@@ -133,10 +134,10 @@ public class GPXTrackImporterTest {
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getActivityTypeLocalized());
assertEquals("", importedTrack.getDescription());
assertEquals("20210907_213924.gpx", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
@@ -181,10 +182,10 @@ public class GPXTrackImporterTest {
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getActivityTypeLocalized());
assertEquals("", importedTrack.getDescription());
assertEquals("20210907_213924.gpx", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
@@ -232,7 +233,7 @@ public class GPXTrackImporterTest {
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils);
trackExporter.writeTrack(importedTrack, outputStream);
trackExporter.writeTrack(List.of(importedTrack), outputStream);
// then
String expected = new BufferedReader(
@@ -22,6 +22,7 @@ import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
@@ -70,10 +71,10 @@ public class KMLTrackImporterTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getActivityTypeLocalized());
assertEquals("", importedTrack.getDescription());
assertEquals("", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkers(importTrackId).size());
@@ -119,10 +120,10 @@ public class KMLTrackImporterTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getActivityTypeLocalized());
assertEquals("", importedTrack.getDescription());
assertEquals("", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkers(importTrackId).size());
@@ -159,10 +160,10 @@ public class KMLTrackImporterTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("unknown", importedTrack.getCategory());
assertEquals("unknown", importedTrack.getActivityTypeLocalized());
assertEquals("Test Track", importedTrack.getDescription());
assertEquals("2020-11-28 18:06", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
assertEquals(ActivityType.UNKNOWN, importedTrack.getActivityType());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkers(importTrackId).size());
@@ -1,47 +1,55 @@
package de.dennisguse.opentracks.sensors;
import static org.mockito.Mockito.when;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
/**
* NOTE: Test data is completely artificial.
*/
@RunWith(MockitoJUnitRunner.class)
public class AltitudeSumManagerTest {
private final AltitudeSumManager subject = new AltitudeSumManager();
private static void addSensorValue(AltitudeSumManager altitudeSumManager, float[] values) {
for (float f : values) {
altitudeSumManager.onSensorValueChanged(f);
altitudeSumManager.onSensorValueChanged(AtmosphericPressure.ofHPA(f));
}
}
@Before
public void setUp() {
subject.reset();
}
@Mock
BarometerInternal driver;
@Test
public void getAltitudeGainLoss_downhill() {
// given
subject.setConnected(true);
AltitudeSumManager subject = new AltitudeSumManager(driver);
when(driver.isConnected()).thenReturn(true);
// then
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1015.14f, 1015.15f});
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
// then
Assert.assertEquals(0f, subject.getAltitudeGain_m(), 0.01);
Assert.assertEquals(48.0, subject.getAltitudeLoss_m(), 0.01);
Assert.assertEquals(15f, subject.getAltitudeLoss_m(), 0.01);
}
@Test
public void sensorUnavailable() {
// given
subject.setConnected(false);
AltitudeSumManager subject = new AltitudeSumManager(driver);
when(driver.isConnected()).thenReturn(true);
// then
subject.onSensorValueChanged(999f);
subject.onSensorValueChanged(AtmosphericPressure.ofHPA(999f));
// then
Assert.assertNull(subject.getAltitudeGain_m());
@@ -7,6 +7,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
@@ -20,7 +21,7 @@ public class BluetoothUtilsTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.getServiceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
@@ -33,7 +34,7 @@ public class BluetoothUtilsTest {
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.getServiceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
@@ -43,9 +44,22 @@ public class BluetoothUtilsTest {
assertEquals(HeartRate.of(257), heartRate);
}
@Test
public void parseEnvironmentalSensing_Pa() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
// when
AtmosphericPressure pressure = BluetoothUtils.parseEnvironmentalSensing(characteristic);
// then
assertEquals(AtmosphericPressure.ofPA(100165), pressure);
}
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
@@ -58,7 +72,7 @@ public class BluetoothUtilsTest {
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
// when
@@ -71,7 +85,7 @@ public class BluetoothUtilsTest {
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
@@ -84,34 +98,34 @@ public class BluetoothUtilsTest {
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.getServiceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
// then
assertEquals(40, powerCadence.getPower().getValue().getW(), 0.01);
assertEquals(40, powerCadence.power().getValue().getW(), 0.01);
}
@Test
public void parseCyclingPower_power_with_cadence() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.getServiceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
// when
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
// then
assertEquals(0, powerCadence.getPower().getValue().getW(), 0.01);
assertEquals(0, powerCadence.power().getValue().getW(), 0.01);
assertEquals(12, powerCadence.getCadence().getCrankRevolutionsCount());
assertEquals(17125, powerCadence.getCadence().getCrankRevolutionsTime());
assertEquals(12, powerCadence.cadence().getCrankRevolutionsCount());
assertEquals(17125, powerCadence.cadence().getCrankRevolutionsTime());
}
@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_SPEED_CADENCE.getServiceUUID(), 0, 0);
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
// when
@@ -5,6 +5,8 @@ import static org.junit.Assert.assertNull;
import org.junit.Test;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
public class PressureSensorUtilsTest {
// real data at 500Hz stationary
@@ -12,11 +14,11 @@ public class PressureSensorUtilsTest {
public void altitudeChanges_none() {
// given
float[] sensorValues_hPa = new float[]{1015.6876f, 1015.699f, 1015.70905f, 1015.71075f, 1015.7224f, 1015.72f, 1015.7373f, 1015.7481f, 1015.74133f, 1015.73553f, 1015.7462f, 1015.74896f, 1015.752f, 1015.754f, 1015.7456f, 1015.73303f, 1015.7288f, 1015.7239f, 1015.7298f, 1015.7387f, 1015.72534f, 1015.72577f, 1015.72797f, 1015.71796f, 1015.7113f, 1015.7097f, 1015.7096f, 1015.7013f, 1015.7072f, 1015.71063f, 1015.7088f, 1015.7115f, 1015.713f, 1015.7121f, 1015.7062f, 1015.7062f, 1015.6994f, 1015.69617f, 1015.68945f, 1015.698f, 1015.6913f, 1015.69696f, 1015.69293f, 1015.6955f, 1015.6928f, 1015.6939f, 1015.68787f, 1015.6881f, 1015.68536f, 1015.68726f, 1015.6879f, 1015.6904f, 1015.6937f, 1015.69476f, 1015.7046f, 1015.7019f, 1015.7079f, 1015.71124f, 1015.71216f, 1015.71436f, 1015.7171f, 1015.7186f, 1015.7186f, 1015.7246f, 1015.72046f, 1015.7211f, 1015.72375f, 1015.7194f, 1015.72125f, 1015.7161f, 1015.71185f, 1015.7188f, 1015.721f, 1015.7252f, 1015.72266f, 1015.7236f, 1015.72614f, 1015.7266f, 1015.7344f, 1015.7376f, 1015.7337f, 1015.7253f, 1015.71875f, 1015.7219f, 1015.7219f, 1015.7317f, 1015.7284f, 1015.73444f, 1015.73584f, 1015.7369f, 1015.73254f, 1015.7369f, 1015.7336f, 1015.73254f, 1015.73676f, 1015.74176f, 1015.74506f, 1015.7478f, 1015.755f, 1015.7515f, 1015.7543f, 1015.7459f, 1015.7468f, 1015.7492f, 1015.74585f, 1015.74756f, 1015.74756f, 1015.75085f, 1015.754f, 1015.75336f, 1015.7568f, 1015.75165f, 1015.761f, 1015.7527f, 1015.75684f, 1015.7635f, 1015.7585f, 1015.7552f, 1015.7475f, 1015.74506f, 1015.7542f, 1015.75586f, 1015.74176f, 1015.74414f, 1015.7501f, 1015.751f, 1015.7478f, 1015.755f, 1015.75836f, 1015.7592f, 1015.75256f, 1015.7559f, 1015.76654f, 1015.76154f, 1015.766f, 1015.77014f, 1015.76904f, 1015.76013f, 1015.7674f, 1015.76514f, 1015.7674f, 1015.7635f, 1015.76404f, 1015.7607f, 1015.754f, 1015.7574f, 1015.7607f, 1015.7607f, 1015.7607f, 1015.754f, 1015.7567f, 1015.7616f, 1015.7643f, 1015.7666f, 1015.7609f, 1015.766f, 1015.76764f, 1015.755f, 1015.75757f, 1015.74927f, 1015.74927f, 1015.7426f, 1015.745f, 1015.745f, 1015.7409f, 1015.7376f, 1015.7326f, 1015.74164f, 1015.74664f, 1015.745f, 1015.7459f, 1015.7502f, 1015.7493f, 1015.7476f, 1015.7493f, 1015.751f, 1015.7601f, 1015.76434f, 1015.766f, 1015.756f, 1015.7617f, 1015.7623f, 1015.76404f, 1015.76733f, 1015.7607f, 1015.7623f, 1015.76337f, 1015.7584f, 1015.7573f, 1015.7502f, 1015.7534f, 1015.7534f, 1015.7601f, 1015.75684f, 1015.7518f, 1015.75574f, 1015.7524f, 1015.7507f, 1015.7484f, 1015.7502f, 1015.7534f, 1015.7623f, 1015.75903f, 1015.75574f, 1015.75574f, 1015.7573f, 1015.7524f, 1015.75574f, 1015.75574f, 1015.7607f, 1015.7573f, 1015.754f, 1015.74677f, 1015.754f, 1015.75574f, 1015.75903f, 1015.7573f, 1015.7551f, 1015.7573f, 1015.754f, 1015.7507f, 1015.7374f, 1015.739f, 1015.73566f, 1015.73566f, 1015.7367f, 1015.7417f, 1015.7417f, 1015.739f, 1015.74835f, 1015.74567f, 1015.7517f, 1015.74066f, 1015.7374f, 1015.739f, 1015.7417f, 1015.73065f, 1015.72894f, 1015.73505f, 1015.73334f, 1015.74005f, 1015.74005f, 1015.7367f, 1015.7434f, 1015.744f, 1015.7423f, 1015.7384f, 1015.739f, 1015.7374f, 1015.7374f, 1015.7374f, 1015.7341f, 1015.73505f, 1015.7341f, 1015.7267f, 1015.72833f, 1015.7234f, 1015.7167f, 1015.725f, 1015.7217f, 1015.7234f, 1015.73004f, 1015.7324f, 1015.7423f, 1015.74005f, 1015.73505f, 1015.73004f, 1015.739f, 1015.7417f, 1015.74567f, 1015.74066f, 1015.74146f, 1015.74146f, 1015.73816f, 1015.7274f, 1015.72906f, 1015.7274f, 1015.7341f, 1015.739f, 1015.744f, 1015.7417f, 1015.7374f, 1015.7324f, 1015.73816f, 1015.74146f, 1015.7374f, 1015.7341f, 1015.7365f, 1015.7365f, 1015.74146f, 1015.74414f, 1015.7475f, 1015.7448f, 1015.74146f, 1015.74585f, 1015.7475f, 1015.75146f, 1015.75146f, 1015.7591f, 1015.75476f, 1015.7614f, 1015.75476f, 1015.7492f, 1015.74817f, 1015.7498f, 1015.7515f, 1015.75586f, 1015.7509f, 1015.7448f, 1015.7525f, 1015.7581f, 1015.7592f};
float firstSensorValue = sensorValues_hPa[0];
AtmosphericPressure firstSensorValue = AtmosphericPressure.ofHPA(sensorValues_hPa[0]);
// when // then
for (float v : sensorValues_hPa) {
assertNull(PressureSensorUtils.computeChanges(firstSensorValue, v));
assertNull(PressureSensorUtils.computeChanges(firstSensorValue, AtmosphericPressure.ofHPA(v)));
}
}
@@ -25,25 +27,25 @@ public class PressureSensorUtilsTest {
@Test
public void computeChanges_downhill() {
float[] sensorValues_hPa = new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f};
float firstSensorValue = sensorValues_hPa[0];
AtmosphericPressure firstSensorValue = AtmosphericPressure.ofHPA(sensorValues_hPa[0]);
// when
float altitudeGain_m = 0;
float altitudeLoss = 0;
float lastUsedPressureValue_hPa = firstSensorValue;
AtmosphericPressure lastUsedPressureValue = firstSensorValue;
for (float v : sensorValues_hPa) {
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChanges(lastUsedPressureValue_hPa, v);
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChanges(lastUsedPressureValue, AtmosphericPressure.ofHPA(v));
if (altitudeChange != null) {
altitudeGain_m += altitudeChange.getAltitudeGain_m();
altitudeLoss += altitudeChange.getAltitudeLoss_m();
lastUsedPressureValue_hPa = altitudeChange.getCurrentSensorValue_hPa();
lastUsedPressureValue = altitudeChange.currentSensorValue();
}
}
// then
assertEquals(0f, altitudeGain_m, 0.01);
assertEquals(15.0f, altitudeLoss, 0.01);
assertEquals(1016.80f, lastUsedPressureValue_hPa, 0.01); //Expect exponential smoothing
assertEquals(1016.80f, lastUsedPressureValue.getHPA(), 0.01); //Expect exponential smoothing
}
}
@@ -11,13 +11,44 @@ public class UintUtilsTest {
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT16_MAX));
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT16_MAX));
assertEquals(3, UintUtils.diff(5, 2, UintUtils.UINT16_MAX));
assertEquals(65534, UintUtils.diff(1, 2, UintUtils.UINT16_MAX));
assertEquals(65535, UintUtils.diff(1, 2, UintUtils.UINT16_MAX)); /* unsigned 16 arithmetic is modulo UINT16_MAX + 1, not modulo UINT16_MAX */
assertEquals(65530, UintUtils.diff(UintUtils.UINT16_MAX, 5, UintUtils.UINT16_MAX));
/* Test modulo arithmetic for arguments that are out of range */
if (false) { /* false means UintUtils.diff() throws if arguments out of range */
assertEquals(0, UintUtils.diff(65537, 1, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(1, 65537, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(65537, 65537, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(-65535, 1, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(1, -65535, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(-65535, -65535, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(-65535, 65537, UintUtils.UINT16_MAX));
assertEquals(0, UintUtils.diff(65537, -65535, UintUtils.UINT16_MAX));
}
/* The following tests are the above, but for 32-bit unsigned. */
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT32_MAX));
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT32_MAX));
assertEquals(3, UintUtils.diff(5, 2, UintUtils.UINT32_MAX));
assertEquals(4294967295L, UintUtils.diff(1, 2, UintUtils.UINT32_MAX));
assertEquals(4294967290L, UintUtils.diff(UintUtils.UINT32_MAX, 5, UintUtils.UINT32_MAX));
if (false) { /* false means UintUtils.diff() throws if arguments out of range */
assertEquals(0, UintUtils.diff(4294967297L, 1, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(1, 4294967297L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(4294967297L, 4294967297L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(-4294967295L, 1, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(1, -4294967295L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(-4294967295L, -4294967295L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(-4294967295L, 4294967297L, UintUtils.UINT32_MAX));
assertEquals(0, UintUtils.diff(65537, -4294967297L, UintUtils.UINT32_MAX));
}
}
@Test
public void realData() {
assertEquals(1, UintUtils.diff(381616, 381615, UintUtils.UINT32_MAX));
}
}
}
@@ -765,7 +765,7 @@ public class TrackRecordingServiceTestRecording {
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.1, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps2, 0, 1, 0, 0, 0f, 0f), contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new TrackStatistics(startTime, gps2, 11113.275390625, 4, 3, 3704.4251302083335f, 0f, 0f).toString(), contentProviderUtils.getTrack(trackId).getTrackStatistics().toString());
// when
@@ -774,8 +774,7 @@ public class TrackRecordingServiceTestRecording {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 7, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
assertEquals(new TrackStatistics(startTime, stopTime, 11113.275390625, 10, 3, 3704.4251302083335f, 0f, 0f).toString(), contentProviderUtils.getTrack(trackId).getTrackStatistics().toString());
// then
@@ -5,8 +5,6 @@ import android.location.Location;
import androidx.test.rule.ServiceTestRule;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -16,7 +14,7 @@ public class TrackRecordingServiceTestUtils {
//TODO Workaround as service is not stopped on API23; thus sharedpreferences are not reset between tests.
//TODO Anyhow, the service should re-create all it's resources if a recording starts and makes sure that there is no leftovers from previous recordings.
@Deprecated
public static void resetService(ServiceTestRule mServiceRule, Context context) throws TimeoutException {
public static void resetService(ServiceTestRule mServiceRule, Context context) {
// Let's use default values.
PreferencesUtils.clear();
}
@@ -30,6 +28,6 @@ public class TrackRecordingServiceTestUtils {
location.setSpeed(speed);
trackPointCreator.setClock(time);
trackPointCreator.getGpsHandler().onLocationChanged(location);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
}
}
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.services.announcement;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import android.icu.text.MessageFormat;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
@@ -15,6 +16,7 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.Locale;
import java.util.Map;
import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -185,12 +187,54 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
// then
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
}
@Test
public void getAnnouncement_imperial_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.ofMile(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
// then
assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
}
@Test
public void getAnnouncement_imperial_meter_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.ofMile(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_METER, true, null, null).toString();
// then
assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
}
@Test
public void getAnnouncement_metric_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.ofKilometer(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 1.1 kilometers. 1 hour. Average moving speed 1.1 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_withInterval_imperial_speed() {
// given
@@ -207,7 +251,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, lastInterval, null).toString();
// then
assertEquals("Total distance 8.8 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
@@ -223,7 +267,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, false, null, null).toString();
// then
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
@@ -245,7 +289,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, false, lastInterval, null).toString();
// then
assertEquals("Total distance 8.8 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
@@ -306,4 +350,19 @@ public class VoiceAnnouncementUtilsTest {
// then
assertEquals(" Current heart rate 133 bpm.", announcement);
}
@Test
public void ICUMessageDemo() {
// Android 7's ICU MessageFormat; working
String template = """
{n, plural,
one {1 mile}
other {{n,number,#.#} miles}
}""";
assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.1)));
assertEquals("1 mile", MessageFormat.format(template, Map.of("n", 1)));
assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.11)));
assertEquals("1.2 miles", MessageFormat.format(template, Map.of("n", 1.18)));
}
}
@@ -0,0 +1,146 @@
package de.dennisguse.opentracks.services.handlers;
import static org.junit.Assert.assertEquals;
import static de.dennisguse.opentracks.services.handlers.GpsStatusValue.GPS_DISABLED;
import static de.dennisguse.opentracks.services.handlers.GpsStatusValue.GPS_ENABLED;
import static de.dennisguse.opentracks.services.handlers.GpsStatusValue.GPS_NONE;
import static de.dennisguse.opentracks.services.handlers.GpsStatusValue.GPS_SIGNAL_BAD;
import static de.dennisguse.opentracks.services.handlers.GpsStatusValue.GPS_SIGNAL_FIX;
import static de.dennisguse.opentracks.services.handlers.GpsStatusValue.GPS_SIGNAL_LOST;
import android.content.Context;
import android.location.Location;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(AndroidJUnit4.class)
public class GpsStatusTest {
private final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
private final static Location badFix = new Location("gps");
private final static Location ok = new Location("gps");
static {
badFix.setAccuracy(50);
ok.setAccuracy(10);
}
@Test
public void testStartDisabledEnabledStop() {
ArrayList<GpsStatusValue> statusList = new ArrayList<>();
// given
GpsStatusManager subject = new GpsStatusManager(context, statusList::add, new Handler());
// when / then
subject.start();
assertEquals(List.of(GPS_ENABLED), statusList);
subject.onGpsDisabled();
assertEquals(List.of(GPS_ENABLED, GPS_DISABLED), statusList);
subject.onGpsEnabled();
assertEquals(List.of(GPS_ENABLED, GPS_DISABLED, GPS_ENABLED), statusList);
subject.stop();
assertEquals(List.of(GPS_ENABLED, GPS_DISABLED, GPS_ENABLED, GPS_NONE), statusList);
}
@Test
public void testStartBadfixOk() {
ArrayList<GpsStatusValue> statusList = new ArrayList<>();
// given
GpsStatusManager subject = new GpsStatusManager(context, statusList::add, new Handler());
subject.onRecordingDistanceChanged(Distance.of(10));
// when / then
subject.start();
subject.onNewTrackPoint(new TrackPoint(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD), statusList);
subject.onNewTrackPoint(new TrackPoint(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX), statusList);
subject.onNewTrackPoint(new TrackPoint(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX), statusList);
subject.onNewTrackPoint(new TrackPoint(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_BAD, GPS_SIGNAL_FIX, GPS_SIGNAL_BAD), statusList);
}
@Test
public void testStartSignalLost() {
ArrayList<GpsStatusValue> statusList = new ArrayList<>();
// given
GpsStatusManager subject = new GpsStatusManager(context, statusList::add, new Handler());
subject.onRecordingDistanceChanged(Distance.of(10));
subject.onMinRecordingIntervalChanged(GpsStatusManager.SIGNAL_LOST_THRESHOLD.multipliedBy(-1));
// when / then
subject.start();
subject.onNewTrackPoint(new TrackPoint(ok, Instant.now().minusMillis(1000)));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX), statusList);
subject.determineGpsStatusByTime(Instant.now());
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST), statusList);
subject.onNewTrackPoint(new TrackPoint(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_FIX), statusList);
}
@Test
public void testStartSignalLostByTimer() throws InterruptedException {
ArrayList<GpsStatusValue> statusList = new ArrayList<>();
final HandlerThread handlerThread = new HandlerThread("solution!");
handlerThread.start();
// given
GpsStatusManager subject = new GpsStatusManager(context, statusList::add, new Handler(handlerThread.getLooper()));
subject.onRecordingDistanceChanged(Distance.of(10));
subject.onMinRecordingIntervalChanged(GpsStatusManager.SIGNAL_LOST_THRESHOLD.multipliedBy(-1).plus(Duration.ofMillis(10)));
// when / then
subject.start();
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED), statusList);
subject.onNewTrackPoint(new TrackPoint(ok, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX), statusList);
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST), statusList);
subject.onNewTrackPoint(new TrackPoint(badFix, Instant.now()));
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_BAD), statusList);
Thread.sleep(100);
assertEquals(List.of(GPS_ENABLED, GPS_SIGNAL_FIX, GPS_SIGNAL_LOST, GPS_SIGNAL_BAD, GPS_SIGNAL_LOST), statusList);
}
}
@@ -1,57 +0,0 @@
package de.dennisguse.opentracks.services.handlers;
import static org.mockito.Mockito.verify;
import android.content.Context;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(MockitoJUnitRunner.class)
public class TrackPointCreatorTest {
@Mock
private Context context;
@Mock
private TrackPointCreator.Callback server;
@Mock
private GPSManager locationHandler;
private TrackPointCreator subject;
@Before
public void setUp() {
subject = new TrackPointCreator(locationHandler, server);
subject.start(context, null);
}
@After
public void tearDown() {
subject.stop();
}
@Ignore("ServiceExecutor disabled for #822")
@Test
public void sendTrackPoint() throws InterruptedException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, null);
Distance horizontalAccuracyThreshold = Distance.of(50);
// when
subject.onNewTrackPoint(trackPoint);
// then
Thread.sleep(10); // Wait for executor service
verify(server).newTrackPoint(trackPoint, horizontalAccuracyThreshold);
}
}
@@ -80,7 +80,7 @@ public class DescriptionGeneratorTest {
stats.setTotalAltitudeLoss(6000f);
stats.setStartTime(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
track.setActivityTypeLocalized("hiking");
String expected = //"Created by"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks (Debug)</a><p>"
+ "Name: -<br>"
@@ -118,7 +118,7 @@ public class DescriptionGeneratorTest {
stats.setTotalAltitudeLoss(6000f);
stats.setStartTime(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
track.setActivityTypeLocalized("hiking");
String expected = //"Created by"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks (Debug)</a><p>"
+ "Name: -<br>"
@@ -5,7 +5,6 @@ import static org.junit.Assert.assertNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -78,13 +77,13 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
data.trackPoints.forEach(subject::addTrackPoint);
data.trackPoints().forEach(subject::addTrackPoint);
// then
TrackStatistics statistics = subject.getTrackStatistics();
assertEquals(128.03, statistics.getTotalDistance().toM(), 0.01);
assertEquals(Duration.ofSeconds(13), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(9), statistics.getMovingTime());
assertEquals(142.26, statistics.getTotalDistance().toM(), 0.01);
assertEquals(Duration.ofSeconds(14), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(10), statistics.getMovingTime());
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
assertEquals(32.5, statistics.getMaxAltitude(), 0.01);
@@ -93,8 +92,8 @@ public class TrackStatisticsUpdaterTest {
assertEquals(14.226, statistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(14.226, statistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(9.84, statistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(106.85f, statistics.getAverageHeartRate().getBPM(), 0.01);
assertEquals(10.16, statistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(106.64f, statistics.getAverageHeartRate().getBPM(), 0.01);
}
@Test
@@ -226,25 +225,6 @@ public class TrackStatisticsUpdaterTest {
assertEquals(59.18, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_not_moving() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_moving() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_not_moving_and_sensor_speed() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_moving_and_sensor_speed() {
}
@Test
public void addTrackPoint_maxSpeed_ignore_above_acceleration() {
@@ -16,18 +16,18 @@ import java.time.Instant;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.TrackIconUtils;
@RunWith(JUnit4.class)
public class AggregatedStatisticsTest {
private final Context context = ApplicationProvider.getApplicationContext();
private static Track createTrack(Context context, Distance totalDistance, Duration totalTime, String category) {
private static Track createTrack(Context context, Distance totalDistance, Duration totalTime, String activityTypeLocalized) {
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime(Instant.ofEpochMilli(1000L)); // Resulting start time
statistics.setStopTime(statistics.getStartTime().plus(totalTime));
@@ -40,8 +40,8 @@ public class AggregatedStatisticsTest {
statistics.setMinAltitude(1200.0); // Resulting min altitude
Track track = new Track();
track.setIcon(TrackIconUtils.getIconValue(context, category));
track.setCategory(category);
track.setActivityType(ActivityType.findByLocalizedString(context, activityTypeLocalized));
track.setActivityTypeLocalized(activityTypeLocalized);
track.setTrackStatistics(statistics);
return track;
}
@@ -243,8 +243,8 @@ public class AggregatedStatisticsTest {
// Check order
{
assertEquals(biking, aggregatedStatistics.getItem(0).getCategory());
assertEquals(driving, aggregatedStatistics.getItem(3).getCategory());
assertEquals(biking, aggregatedStatistics.getItem(0).getActivityTypeLocalized());
assertEquals(driving, aggregatedStatistics.getItem(3).getActivityTypeLocalized());
}
}
}
@@ -51,7 +51,7 @@ public class EspressoAggregatedFilterTest {
@Before
public void setUp() {
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 20);
pair.first.setCategory(CATEGORY);
pair.first.setActivityTypeLocalized(CATEGORY);
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), pair.first, pair.second);
}
@@ -59,8 +59,8 @@ public class EspressoAggregatedFilterTest {
public void espressoAggregatedFilterTest() {
// open AggregatedStatisticsActivity through toolbar's menu item
onView(ViewMatchers.withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.track_list_aggregated_stats))));
onView(withId(R.id.track_list_aggregated_stats)).perform(click());
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_stats_button))));
onView(withId(R.id.aggregated_stats_button)).perform(click());
// open FilterDialogFragment through toolbar's menu item
onView(withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
@@ -79,7 +79,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-14.8980, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
assertEquals(-14.8980, altitude_egm2008.correctAltitude(trackPoint.getLocation()).toM(), MAX_BILINEAR_ERROR);
}
@Test
@@ -94,7 +94,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()).toM(), MAX_BILINEAR_ERROR);
}
@Test
@@ -109,7 +109,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()).toM(), MAX_BILINEAR_ERROR);
}
@Test
@@ -124,7 +124,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()).toM(), MAX_BILINEAR_ERROR);
}
@Test
@@ -139,7 +139,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-17.2260, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
assertEquals(-17.2260, altitude_egm2008.correctAltitude(trackPoint.getLocation()).toM(), MAX_BILINEAR_ERROR);
}
@Test
@@ -154,7 +154,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-39.4865, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
assertEquals(-39.4865, altitude_egm2008.correctAltitude(trackPoint.getLocation()).toM(), MAX_BILINEAR_ERROR);
}
@Test
@@ -174,7 +174,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint1.getLocation());
// then
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()).toM(), altitude_egm2008.correctAltitude(trackPoint2.getLocation()).toM(), 0.0001);
}
@Test
@@ -190,7 +190,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(22.99, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
assertEquals(22.99, altitude_egm2008.correctAltitude(trackPoint.getLocation()).toM(), MAX_BILINEAR_ERROR);
}
@Test
@@ -205,7 +205,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()).toM(), MAX_BILINEAR_ERROR);
}
@@ -17,11 +17,14 @@ package de.dennisguse.opentracks.util;
import static org.junit.Assert.assertEquals;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
/**
@@ -37,7 +40,7 @@ public class FileUtilsTest {
*/
@Test
public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext");
String filename = FileUtils.buildUniqueFileName(FileUtils.getPhotoDir(ApplicationProvider.getApplicationContext()), "Filename", "ext");
assertEquals("Filename.ext", filename);
}
@@ -45,9 +48,14 @@ public class FileUtilsTest {
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file exists already.
*/
@Test
public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator
String filename = FileUtils.buildUniqueFileName(new File("/"), "default", "prop");
public void testBuildUniqueFileName_exist() throws IOException {
// given
new File(FileUtils.getPhotoDir(ApplicationProvider.getApplicationContext()), "default.prop").createNewFile();
//when
String filename = FileUtils.buildUniqueFileName(FileUtils.getPhotoDir(ApplicationProvider.getApplicationContext()), "default", "prop");
// then
assertEquals("default(1).prop", filename);
}
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
android:icon="@drawable/ic_statistics_24dp"
android:shortcutId="stats"
android:shortcutLongLabel="@string/menu_aggregated_statistics"
android:shortcutShortLabel="@string/menu_aggregated_statistics">
<intent
android:action="android.intent.action.VIEW"
android:targetClass="de.dennisguse.opentracks.ui.aggregatedStatistics.AggregatedStatisticsActivity"
android:targetPackage="de.dennisguse.opentracks.debug" />
</shortcut>
</shortcuts>
+7
View File
@@ -89,6 +89,7 @@ limitations under the License.
</intent-filter>
</activity>
<!-- NOTE: the name of the MAIN/LAUNCHER is used by icon packs; if this is changed, icon packs break.-->
<activity
android:name=".introduction.IntroductionActivity"
android:exported="true"
@@ -98,6 +99,11 @@ limitations under the License.
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"
/>
</activity>
<activity android:name=".AboutActivity" />
@@ -348,6 +354,7 @@ limitations under the License.
<activity android:name=".ui.aggregatedStatistics.AggregatedStatisticsActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserOneFileActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity" />
<activity android:name=".io.file.exporter.ExportActivity" />
@@ -24,7 +24,7 @@ public class ShowErrorActivity extends AbstractActivity {
super.onCreate(savedInstanceState);
viewBinding.textViewError.setText(getIntent().getStringExtra(EXTRA_ERROR_TEXT));
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(createErrorTitle());
viewBinding.showErrorToolbar.setTitle(createErrorTitle());
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@@ -29,7 +29,6 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackEditBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
/**
@@ -48,7 +47,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private ContentProviderUtils contentProviderUtils;
private Track track;
private String iconValue;
private ActivityType activityType;
private TrackEditBinding viewBinding;
@@ -74,28 +73,31 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
viewBinding.fields.trackEditName.setText(track.getName());
viewBinding.fields.trackEditActivityType.setText(track.getCategory());
viewBinding.fields.trackEditActivityType.setText(track.getActivityTypeLocalized());
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, ActivityType.getLocalizedStrings(this));
viewBinding.fields.trackEditActivityType.setAdapter(adapter);
viewBinding.fields.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.fields.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.fields.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> {
String localizedActivityType = (String) viewBinding.fields.trackEditActivityType.getAdapter().getItem(position);
setActivityTypeIcon(ActivityType.findByLocalizedString(this, localizedActivityType));
});
viewBinding.fields.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, viewBinding.fields.trackEditActivityType.getText().toString()));
String localizedActivityType = viewBinding.fields.trackEditActivityType.getText().toString();
setActivityTypeIcon(ActivityType.findByLocalizedString(this, localizedActivityType));
}
});
iconValue = null;
activityType = null;
if (bundle != null) {
iconValue = bundle.getString(ICON_VALUE_KEY);
activityType = (ActivityType) bundle.getSerializable(ICON_VALUE_KEY);
}
if (iconValue == null) {
iconValue = track.getIcon();
if (activityType == null) {
activityType = track.getActivityType();
}
setActivityTypeIcon(iconValue);
viewBinding.fields.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.fields.trackEditActivityType.getText().toString()));
setActivityTypeIcon(activityType);
viewBinding.fields.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), this, viewBinding.fields.trackEditActivityType.getText().toString()));
viewBinding.fields.trackEditDescription.setText(track.getDescription());
@@ -109,7 +111,6 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
viewBinding.trackEditCancel.setOnClickListener(v -> finish());
viewBinding.trackEditCancel.setVisibility(View.VISIBLE);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.menu_edit));
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@@ -128,7 +129,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(ICON_VALUE_KEY, iconValue);
outState.putSerializable(ICON_VALUE_KEY, activityType);
}
@Override
@@ -137,14 +138,14 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return viewBinding.getRoot();
}
private void setActivityTypeIcon(String iconValue) {
this.iconValue = iconValue;
viewBinding.fields.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
private void setActivityTypeIcon(ActivityType activityType) {
this.activityType = activityType;
viewBinding.fields.trackEditActivityTypeIcon.setImageResource(activityType.getIconDrawableId());
}
@Override
public void onChooseActivityTypeDone(String value) {
setActivityTypeIcon(value);
viewBinding.fields.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
public void onChooseActivityTypeDone(ActivityType activityType) {
setActivityTypeIcon(activityType);
viewBinding.fields.trackEditActivityType.setText(getString(activityType.getLocalizedStringId()));
}
}
@@ -25,7 +25,6 @@ import android.database.Cursor;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Vibrator;
import android.provider.Settings;
import android.util.Pair;
import android.view.KeyEvent;
@@ -36,6 +35,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
@@ -43,6 +43,8 @@ import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import com.google.android.material.button.MaterialButton;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
@@ -50,6 +52,7 @@ import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.Track;
@@ -72,7 +75,6 @@ import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* An activity displaying a list of tracks.
@@ -83,7 +85,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private static final String TAG = TrackListActivity.class.getSimpleName();
// The following are setFrequency in onCreate
// The following are set in onCreate
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private ResourceCursorAdapter resourceCursorAdapter;
@@ -137,7 +139,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> {
service.getRecordingStatusObservable()
@@ -165,6 +166,25 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
viewBinding.aggregatedStatsButton.setOnClickListener((view) -> startActivity(IntentUtils.newIntent(this, AggregatedStatisticsActivity.class)));
viewBinding.sensorStartButton.setOnClickListener((view) -> {
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else {
// Invoke trackRecordingService
if (!gpsStatusValue.isGpsStarted()) {
trackRecordingServiceConnection.startAndBindWithCallback(this);
} else {
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopSensorsAndShutdown(); //TODO Handle this in TrackRecordingServiceConnection
}
trackRecordingServiceConnection.unbindAndStop(this);
}
}
});
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackIdId) -> {
Track.Id trackId = new Track.Id(trackIdId);
@@ -194,14 +214,15 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int startTimeOffsetIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET);
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
int activityTypeIndex = cursor.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE_LOCALIZED);
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
boolean isRecording = trackId.equals(recordingStatus.getTrackId());
String icon = cursor.getString(iconIndex);
int iconId = TrackIconUtils.getIconDrawable(icon);
int iconId = ActivityType.findBy(icon)
.getIconDrawableId();
String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
String totalDistance = DistanceFormatter.Builder()
@@ -210,13 +231,13 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex);
int startTimeOffset = cursor.getInt(startTimeOffsetIndex);
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
String activityType = icon != null && !icon.equals("") ? null : cursor.getString(activityTypeIndex);
String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording,
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
OffsetDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneOffset.ofTotalSeconds(startTimeOffset)),
category, description, false);
activityType, description, false);
}
};
viewBinding.trackList.setAdapter(resourceCursorAdapter);
@@ -246,7 +267,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
// Recording -> Stop
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
ActivityUtils.vibrate(this, 1000);
updateGpsMenuItem(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
viewBinding.trackListFabAction.setImageResource(R.drawable.ic_baseline_record_24);
@@ -254,7 +275,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
return true;
});
setSupportActionBar(viewBinding.bottomAppBar);
setSupportActionBar(viewBinding.trackListToolbar);
loadData(getIntent());
}
@@ -309,21 +330,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
getMenuInflater().inflate(R.menu.track_list, menu);
searchMenuItem = menu.findItem(R.id.track_list_search);
SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem);
searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setPadding(0, 0, 48, 0);
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(androidx.appcompat.R.id.search_src_text);
searchAutoComplete.setHintTextColor(ContextCompat.getColor(this, android.R.color.white));
searchAutoComplete.setTextColor(ContextCompat.getColor(this, android.R.color.white));
searchView.setOnCloseListener(() -> {
searchView.clearFocus();
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
});
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
ActivityUtils.configureSearchWidget(this, searchMenuItem);
return super.onCreateOptionsMenu(menu);
}
@@ -340,34 +347,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_list_start_gps) {
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else {
// Invoke trackRecordingService
if (!gpsStatusValue.isGpsStarted()) {
trackRecordingServiceConnection.startAndBindWithCallback(this);
} else {
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopSensorsAndShutdown(); //TODO Handle this in TrackRecordingServiceConnection
}
trackRecordingServiceConnection.unbindAndStop(this);
}
// Update menu after starting or stopping gps
this.invalidateOptionsMenu();
}
return true;
}
if (item.getItemId() == R.id.track_list_aggregated_stats) {
startActivity(IntentUtils.newIntent(this, AggregatedStatisticsActivity.class));
return true;
}
if (item.getItemId() == R.id.track_list_markers) {
startActivity(IntentUtils.newIntent(this, MarkerListActivity.class));
return true;
@@ -382,6 +361,12 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setIconified(false);
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
return true;
}
if (item.getItemId() == R.id.track_list_help) {
startActivity(IntentUtils.newIntent(this, HelpActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
@@ -455,14 +440,12 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
*/
//TODO Check if if can be avoided to call this outside of onGpsStatusChanged()
private void updateGpsMenuItem(boolean isGpsStarted, boolean isRecording) {
if (startGpsMenuItem != null) {
startGpsMenuItem.setVisible(!isRecording);
if (!isRecording) {
startGpsMenuItem.setTitle(isGpsStarted ? R.string.menu_stop_gps : R.string.menu_start_gps);
startGpsMenuItem.setIcon(isGpsStarted ? gpsStatusValue.icon : R.drawable.ic_gps_off_24dp);
if (startGpsMenuItem.getIcon() instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) startGpsMenuItem.getIcon()).start();
}
MaterialButton startGpsMenuItem = viewBinding.sensorStartButton;
startGpsMenuItem.setVisibility(!isRecording ? View.VISIBLE : View.INVISIBLE);
if (!isRecording) {
startGpsMenuItem.setIcon(AppCompatResources.getDrawable(this, isGpsStarted ? gpsStatusValue.icon : R.drawable.ic_gps_off_24dp));
if (startGpsMenuItem.getIcon() instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) startGpsMenuItem.getIcon()).start();
}
}
}
@@ -532,11 +515,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
public void setSearch(String searchQuery) {
this.searchQuery = searchQuery;
restart();
if (searchQuery != null) {
setTitle(searchQuery);
} else {
setTitle(R.string.app_name);
}
viewBinding.trackListToolbar.setTitle(searchQuery == null ? getString(R.string.app_name) : searchQuery);
}
public void restart() {
@@ -547,7 +526,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME, TracksColumns.STARTTIME_OFFSET,
TracksColumns.DESCRIPTION, TracksColumns.ACTIVITY_TYPE_LOCALIZED, TracksColumns.STARTTIME, TracksColumns.STARTTIME_OFFSET,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT};
final String sortOrder = TracksColumns.STARTTIME + " DESC";
@@ -557,7 +536,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
} else {
final String SEARCH_QUERY = TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
TracksColumns.ACTIVITY_TYPE_LOCALIZED + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, sortOrder);
}
@@ -581,7 +560,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private void setFloatButton() {
viewBinding.trackListFabAction.setImageResource(recordingStatus.isRecording() ? R.drawable.ic_baseline_stop_24 : R.drawable.ic_baseline_record_24);
viewBinding.trackListFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, recordingStatus.isRecording() ? R.color.opentracks_secondary_color : R.color.red_dark));
viewBinding.trackListFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, recordingStatus.isRecording() ? R.color.opentracks : R.color.red_dark));
}
private void onRecordingStatusChanged(RecordingStatus status) {
@@ -103,7 +103,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
Track track = contentProviderUtils.getTrack(trackId);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(track != null ? track.getName() : "");
viewBinding.bottomAppBarLayout.bottomAppBar.replaceMenu(R.menu.track_detail);
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
@@ -1,12 +1,10 @@
package de.dennisguse.opentracks;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -32,6 +30,7 @@ import de.dennisguse.opentracks.chart.ChartFragment;
import de.dennisguse.opentracks.chart.TrackDataHubInterface;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
@@ -45,9 +44,9 @@ import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
/**
@@ -134,10 +133,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
viewBinding.trackRecordingFabAction.setImageResource(R.drawable.ic_baseline_stop_24);
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setBackgroundColor(ContextCompat.getColor(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks));
viewBinding.trackRecordingFabAction.setBackgroundColor(ContextCompat.getColor(this, R.color.opentracks));
viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> {
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
ActivityUtils.vibrate(this, 1000);
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackStoppedActivity.class)
.putExtra(TrackStoppedActivity.EXTRA_TRACK_ID, trackId);
@@ -318,10 +317,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
public void onChooseActivityTypeDone(ActivityType activityType) {
Track track = contentProviderUtils.getTrack(trackId);
String category = getString(TrackIconUtils.getIconActivityType(iconValue));
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
String activityTypeLocalized = getString(activityType.getLocalizedStringId());
TrackUtils.updateTrack(this, track, null, activityTypeLocalized, null, contentProviderUtils);
}
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
@@ -20,7 +20,6 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
@@ -50,22 +49,23 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
viewBinding.trackEditName.setText(track.getName());
viewBinding.trackEditActivityType.setText(track.getCategory());
viewBinding.trackEditActivityType.setText(track.getActivityTypeLocalized());
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, ActivityType.getLocalizedStrings(this));
viewBinding.trackEditActivityType.setAdapter(adapter);
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> {
String localizedActivityType = (String) viewBinding.trackEditActivityType.getAdapter().getItem(position);
setActivityTypeIcon(ActivityType.findByLocalizedString(this, localizedActivityType));
});
viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackStoppedActivity.this, viewBinding.trackEditActivityType.getText().toString()));
String localizedActivityType = viewBinding.trackEditActivityType.getText().toString();
setActivityTypeIcon(ActivityType.findByLocalizedString(this, localizedActivityType));
}
});
String iconValue = track.getIcon();
setActivityTypeIcon(iconValue);
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
setActivityTypeIcon(track.getActivityType());
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), this, viewBinding.trackEditActivityType.getText().toString()));
viewBinding.trackEditDescription.setText(track.getDescription());
@@ -74,7 +74,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
{
Pair<String, String> parts = SpeedFormatter.Builder()
.setUnit(PreferencesUtils.getUnitSystem())
.setReportSpeedOrPace(PreferencesUtils.isReportSpeed(track.getCategory()))
.setReportSpeedOrPace(PreferencesUtils.isReportSpeed(track))
.build(this)
.getSpeedParts(track.getTrackStatistics().getAverageMovingSpeed());
viewBinding.speed.setText(parts.first);
@@ -125,14 +125,14 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
return viewBinding.getRoot();
}
private void setActivityTypeIcon(String iconValue) {
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
private void setActivityTypeIcon(ActivityType activityType) {
viewBinding.trackEditActivityTypeIcon.setImageResource(activityType.getIconDrawableId());
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
setActivityTypeIcon(iconValue);
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(iconValue)));
public void onChooseActivityTypeDone(ActivityType activityType) {
setActivityTypeIcon(activityType);
viewBinding.trackEditActivityType.setText(getString(activityType.getLocalizedStringId()));
}
private void resumeTrackAndFinish() {
@@ -65,7 +65,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
// Stats gathered from the received data
private final List<ChartPoint> pendingPoints = new ArrayList<>();
private String category = "";
private String activityTypeLocalized = "";
// Modes of operation
private boolean chartByDistance;
@@ -87,7 +87,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
}
}
if (PreferencesUtils.isKey(R.string.stats_rate_key, key)) {
boolean reportSpeed = PreferencesUtils.isReportSpeed(category);
boolean reportSpeed = PreferencesUtils.isReportSpeed(activityTypeLocalized);
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
viewBinding.chartView.setReportSpeed(reportSpeed);
viewBinding.chartView.applyReportSpeed();
@@ -160,12 +160,12 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
public void onTrackUpdated(Track track) {
if (isResumed()) {
if (track == null) {
category = "";
activityTypeLocalized = "";
return;
}
category = track.getCategory();
boolean reportSpeed = PreferencesUtils.isReportSpeed(category);
activityTypeLocalized = track.getActivityTypeLocalized();
boolean reportSpeed = PreferencesUtils.isReportSpeed(activityTypeLocalized);
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
viewBinding.chartView.setReportSpeed(reportSpeed);
viewBinding.chartView.applyReportSpeed();
@@ -212,7 +212,7 @@ abstract class ChartValueSeries {
int getTitleId(UnitSystem unitSystem) {
return switch (unitSystem) {
case METRIC -> metricTitleId;
case IMPERIAL -> imperialTitleId;
case IMPERIAL_FEET, IMPERIAL_METER -> imperialTitleId;
case NAUTICAL_IMPERIAL -> nauticalTitleId;
};
}
@@ -735,7 +735,7 @@ public class ChartView extends View {
if (chartByDistance) {
return switch (unitSystem) {
case METRIC -> context.getString(R.string.unit_kilometer);
case IMPERIAL -> context.getString(R.string.unit_mile);
case IMPERIAL_FEET, IMPERIAL_METER -> context.getString(R.string.unit_mile);
case NAUTICAL_IMPERIAL -> context.getString(R.string.unit_nautical_mile);
};
} else {
@@ -946,7 +946,7 @@ public class ChartView extends View {
*/
private void updateEffectiveDimensions() {
effectiveWidth = Math.max(0, width - leftBorder - rightBorder);
effectiveHeight = Math.max(0, height - topBorder - bottomBorder);
effectiveHeight = Math.max(0, height - topBorder - bottomBorder - spacer);
}
/**
@@ -40,6 +40,7 @@ import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance;
@@ -100,7 +101,7 @@ public class ContentProviderUtils {
int uuidIndex = cursor.getColumnIndexOrThrow(TracksColumns.UUID);
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
int activityTypeLocalizedIndex = cursor.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE_LOCALIZED);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int startTimeOffsetIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET);
int stopTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME);
@@ -128,8 +129,8 @@ public class ContentProviderUtils {
if (!cursor.isNull(descriptionIndex)) {
track.setDescription(cursor.getString(descriptionIndex));
}
if (!cursor.isNull(categoryIndex)) {
track.setCategory(cursor.getString(categoryIndex));
if (!cursor.isNull(activityTypeLocalizedIndex)) {
track.setActivityTypeLocalized(cursor.getString(activityTypeLocalizedIndex));
}
if (!cursor.isNull(startTimeIndex)) {
@@ -163,7 +164,7 @@ public class ContentProviderUtils {
trackStatistics.setTotalAltitudeLoss(cursor.getFloat(altitudeLossIndex));
}
if (!cursor.isNull(iconIndex)) {
track.setIcon(cursor.getString(iconIndex));
track.setActivityType(ActivityType.findBy(cursor.getString(iconIndex)));
}
return track;
}
@@ -290,7 +291,7 @@ public class ContentProviderUtils {
values.put(TracksColumns.UUID, UUIDUtils.toBytes(track.getUuid()));
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.CATEGORY, track.getCategory());
values.put(TracksColumns.ACTIVITY_TYPE_LOCALIZED, track.getActivityTypeLocalized());
values.put(TracksColumns.STARTTIME_OFFSET, track.getZoneOffset().getTotalSeconds());
if (trackStatistics.getStartTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
@@ -308,7 +309,7 @@ public class ContentProviderUtils {
values.put(TracksColumns.MAX_ALTITUDE, trackStatistics.getMaxAltitude());
values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.getTotalAltitudeGain());
values.put(TracksColumns.ALTITUDE_LOSS, trackStatistics.getTotalAltitudeLoss());
values.put(TracksColumns.ICON, track.getIcon());
values.put(TracksColumns.ICON, track.getActivityType() != null ? track.getActivityType().getIconId() : "");
return values;
}
@@ -403,7 +404,7 @@ public class ContentProviderUtils {
public void deleteMarker(Context context, Marker.Id markerId) {
final Marker marker = getMarker(markerId);
deleteMarkerPhoto(context, marker);
contentResolver.delete(MarkerColumns.CONTENT_URI, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.getId())});
contentResolver.delete(MarkerColumns.CONTENT_URI, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.id())});
}
/**
@@ -422,7 +423,7 @@ public class ContentProviderUtils {
}
public Marker getMarker(@NonNull Marker.Id markerId) {
try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.getId())}, MarkerColumns._ID, 1)) {
try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.id())}, MarkerColumns._ID, 1)) {
if (cursor != null && cursor.moveToFirst()) {
return createMarker(cursor);
}
@@ -442,7 +443,7 @@ public class ContentProviderUtils {
String[] selectionArgs;
if (minMarkerId != null) {
selection = MarkerColumns.TRACKID + "=? AND " + MarkerColumns._ID + ">=?";
selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(minMarkerId.getId())};
selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(minMarkerId.id())};
} else {
selection = MarkerColumns.TRACKID + "=?";
selectionArgs = new String[]{Long.toString(trackId.getId())};
@@ -495,7 +496,7 @@ public class ContentProviderUtils {
if (!updateMarker.hasPhoto()) {
deleteMarkerPhoto(context, savedMarker);
}
int rows = contentResolver.update(MarkerColumns.CONTENT_URI, createContentValues(updateMarker), MarkerColumns._ID + "=?", new String[]{Long.toString(updateMarker.getId().getId())});
int rows = contentResolver.update(MarkerColumns.CONTENT_URI, createContentValues(updateMarker), MarkerColumns._ID + "=?", new String[]{Long.toString(updateMarker.getId().id())});
return rows == 1;
}
@@ -503,7 +504,7 @@ public class ContentProviderUtils {
ContentValues values = new ContentValues();
if (marker.getId() != null) {
values.put(MarkerColumns._ID, marker.getId().getId());
values.put(MarkerColumns._ID, marker.getId().id());
}
values.put(MarkerColumns.NAME, marker.getName());
values.put(MarkerColumns.DESCRIPTION, marker.getDescription());
@@ -213,7 +213,7 @@ public class ShareContentProvider extends CustomContentProvider {
PipeDataWriter<String> pipeDataWriter = (output, uri1, mimeType, opts, args) -> {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
// TODO handle failure (i.e., do not export an empty file)
trackExporter.writeTrack(tracks.toArray(new Track[0]), fileOutputStream);
trackExporter.writeTrack(tracks, fileOutputStream);
} catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e);
}
@@ -39,7 +39,7 @@ import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager;
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
@@ -73,7 +73,7 @@ public class TrackDataHub {
private final ContentProviderUtils contentProviderUtils;
private final int targetNumPoints;
private final EGM2008CorrectionManager egm2008Correction = new EGM2008CorrectionManager();
private final AltitudeCorrectionManager egm2008Correction = new AltitudeCorrectionManager();
//TODO Check if this is needed.
private HandlerThread handlerThread;
@@ -30,9 +30,9 @@ public class TrackSelection implements ContentProviderUtils.ContentProviderSelec
return this;
}
public TrackSelection addCategory(String category) {
if (!this.categories.contains(category)) {
this.categories.add(category);
public TrackSelection addActivityType(String activityType) {
if (!this.categories.contains(activityType)) {
this.categories.add(activityType);
}
return this;
}
@@ -53,7 +53,7 @@ public class TrackSelection implements ContentProviderUtils.ContentProviderSelec
}
if (!categories.isEmpty()) {
selection += selection.isEmpty() ? "" : " AND ";
selection += String.format(TracksColumns.CATEGORY + " IN (%s)", TextUtils.join(",", Collections.nCopies(categories.size(), "?")));
selection += String.format(TracksColumns.ACTIVITY_TYPE_LOCALIZED + " IN (%s)", TextUtils.join(",", Collections.nCopies(categories.size(), "?")));
}
if (from != null && to != null) {
selection += selection.isEmpty() ? "" : " AND ";
@@ -0,0 +1,35 @@
package de.dennisguse.opentracks.data.models;
import de.dennisguse.opentracks.R;
public enum ActivityIcon {
AIRPLANE("AIRPLANE", R.drawable.ic_activity_flight_24dp),
BIKE("BIKE", R.drawable.ic_activity_bike_24dp),
MOUNTAIN_BIKE("MOUNTAIN_BIKE", R.drawable.ic_activity_mtb_24dp),
MOTOR_BIKE("MOTOR_BIKE", R.drawable.ic_activity_motorbike_24dp),
KAYAK("KAYAK", R.drawable.ic_activity_kayaking_24dp),
BOAT("BOAT", R.drawable.ic_activity_boat_24dp),
SAILING("SAILING", R.drawable.ic_activity_sailing_24dp),
DRIVE("DRIVE", R.drawable.ic_activity_drive_24dp),
RUN("RUN", R.drawable.ic_activity_run_24dp),
SKI("SKI", R.drawable.ic_activity_skiing_24dp),
SNOW_BOARDING("SNOW_BOARDING", R.drawable.ic_activity_snowboarding_24dp),
UNKNOWN("UNKNOWN", R.drawable.ic_logo_24dp),
WALK("WALK", R.drawable.ic_activity_walk_24dp),
ESCOOTER("ESCOOTER", R.drawable.ic_activity_escooter_24dp),
INLINE_SKATING("INLINES_SKATING", R.drawable.ic_activity_inline_skating_24dp),
SKATE_BOARDING("SKATE_BOARDING", R.drawable.ic_activity_skateboarding_24dp),
CLIMBING("CLIMBING", R.drawable.ic_activity_climbing_24dp),
SWIMMING("SWIMMING", R.drawable.ic_activity_swimming_24dp),
SWIMMING_OPEN("SWIMMING_OPEN", R.drawable.ic_activity_swimming_open_24dp),
WORKOUT("WORKOUT", R.drawable.ic_activity_workout_24dp);
@Deprecated //TODO should be removed.
final String iconId;
final int iconDrawableId;
ActivityIcon(String iconId, int iconDrawableId) {
this.iconId = iconId;
this.iconDrawableId = iconDrawableId;
}
}
@@ -1,8 +1,10 @@
package de.dennisguse.opentracks.data.models;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -10,70 +12,138 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
public enum ActivityType {
AIRPLANE("AIRPLANE", R.drawable.ic_activity_flight_24dp, true, R.string.activity_type_airplane, R.string.activity_type_commercial_airplane, R.string.activity_type_rc_airplane),
BIKE("BIKE", R.drawable.ic_activity_bike_24dp, true, R.string.activity_type_biking, R.string.activity_type_cycling, R.string.activity_type_dirt_bike, R.string.activity_type_road_biking, R.string.activity_type_track_cycling),
MOUNTAIN_BIKE("MOUNTAIN_BIKE", R.drawable.ic_activity_mtb_24dp, true, R.string.activity_type_mountain_biking),
MOTOR_BIKE("MOTOR_BIKE", R.drawable.ic_activity_motorbike_24dp, true, R.string.activity_type_motor_bike),
KAYAK("KAYAK", R.drawable.ic_activity_kayaking_24dp, true, R.string.activity_type_kayaking),
BOAT("BOAT", R.drawable.ic_activity_boat_24dp, true, R.string.activity_type_boat, R.string.activity_type_ferry, R.string.activity_type_motor_boating, R.string.activity_type_rc_boat),
SAILING("SAILING", R.drawable.ic_activity_sailing_24dp, true, R.string.activity_type_sailing),
DRIVE("DRIVE", R.drawable.ic_activity_drive_24dp, true, R.string.activity_type_atv, R.string.activity_type_driving, R.string.activity_type_driving_bus, R.string.activity_type_driving_car),
RUN("RUN", R.drawable.ic_activity_run_24dp, false, R.string.activity_type_running, R.string.activity_type_street_running, R.string.activity_type_track_running, R.string.activity_type_trail_running),
SKI("SKI", R.drawable.ic_activity_skiing_24dp, true, R.string.activity_type_cross_country_skiing, R.string.activity_type_skiing),
SNOW_BOARDING("SNOW_BOARDING", R.drawable.ic_activity_snowboarding_24dp, true, R.string.activity_type_snow_boarding),
UNKNOWN("UNKNOWN", R.drawable.ic_logo_24dp, true, R.string.activity_type_unknown),
WALK("WALK", R.drawable.ic_activity_walk_24dp, false, R.string.activity_type_hiking, R.string.activity_type_off_trail_hiking, R.string.activity_type_speed_walking, R.string.activity_type_trail_hiking, R.string.activity_type_walking),
ESCOOTER("ESCOOTER", R.drawable.ic_activity_escooter_24dp, true, R.string.activity_type_escooter),
INLINE_SKATING("INLINES_SKATING", R.drawable.ic_activity_inline_skating_24dp, true, R.string.activity_type_inline_skating),
SKATE_BOARDING("SKATE_BOARDING", R.drawable.ic_activity_skateboarding_24dp, true, R.string.activity_type_skate_boarding),
CLIMBING("CLIMBING", R.drawable.ic_activity_climbing_24dp, false, R.string.activity_type_climbing),
SWIMMING("SWIMMING", R.drawable.ic_activity_swimming_24dp, false, R.string.activity_type_swimming),
SWIMMING_OPEN("SWIMMING_OPEN", R.drawable.ic_activity_swimming_open_24dp, false, R.string.activity_type_swimming_open),
WORKOUT("WORKOUT", R.drawable.ic_activity_workout_24dp, false, R.string.activity_type_workout);
AIRPLANE("airplane", R.string.activity_type_airplane, ActivityIcon.AIRPLANE, true),
ATV("ATV", R.string.activity_type_atv, ActivityIcon.DRIVE, true),
BIKING("biking", R.string.activity_type_biking, ActivityIcon.BIKE, true),
BLIMP("blimp", R.string.activity_type_blimp, ActivityIcon.UNKNOWN, true),
BOAT("boat", R.string.activity_type_boat, ActivityIcon.BOAT, true),
CLIMBING("climbing", R.string.activity_type_climbing, ActivityIcon.CLIMBING, false),
COMMERCIAL_AIRPLANE("commercial airplane", R.string.activity_type_commercial_airplane, ActivityIcon.AIRPLANE, true),
CROSS_COUNTRY_SKIING("cross-country skiing", R.string.activity_type_cross_country_skiing, ActivityIcon.SKI, true),
CYCLING("cycling", R.string.activity_type_cycling, ActivityIcon.BIKE, true),
DIRT_BIKE("dirt bike", R.string.activity_type_dirt_bike, ActivityIcon.MOTOR_BIKE, true),
DONKEY_BACK_RIDING("donkey back riding", R.string.activity_type_donkey_back_riding, ActivityIcon.UNKNOWN, true),
DRIVING("driving", R.string.activity_type_driving, ActivityIcon.DRIVE, true),
DRIVING_BUS("driving bus", R.string.activity_type_driving_bus, ActivityIcon.DRIVE, true),
DRIVING_CAR("driving car", R.string.activity_type_driving_car, ActivityIcon.DRIVE, true),
ESCOOTER("escooter", R.string.activity_type_escooter, ActivityIcon.ESCOOTER, true),
FERRY("ferry", R.string.activity_type_ferry, ActivityIcon.BOAT, true),
FRISBEE("frisbee", R.string.activity_type_frisbee, ActivityIcon.UNKNOWN, true),
GLIDING("gliding", R.string.activity_type_gliding, ActivityIcon.UNKNOWN, true),
HANG_GLIDING("hang gliding", R.string.activity_type_hang_gliding, ActivityIcon.UNKNOWN, true),
HELICOPTER("helicopter", R.string.activity_type_helicopter, ActivityIcon.UNKNOWN, true),
HIKING("hiking", R.string.activity_type_hiking, ActivityIcon.WALK, false),
HORSE_BACK_RIDING("horse back riding", R.string.activity_type_horse_back_riding, ActivityIcon.UNKNOWN, true),
HOT_AIR_BALLOON("hot air balloon", R.string.activity_type_hot_air_balloon, ActivityIcon.UNKNOWN, true),
ICE_SAILING("ice sailing", R.string.activity_type_ice_sailing, ActivityIcon.UNKNOWN, true),
INLINE_SKATING("inline skating", R.string.activity_type_inline_skating, ActivityIcon.INLINE_SKATING, true),
KAYAKING("kayaking", R.string.activity_type_kayaking, ActivityIcon.KAYAK, true),
KITE_SURFING("kite surfing", R.string.activity_type_kite_surfing, ActivityIcon.UNKNOWN, true),
LAND_SAILING("land sailing", R.string.activity_type_land_sailing, ActivityIcon.UNKNOWN, true),
MIXED_TYPE("mixed type", R.string.activity_type_mixed_type, ActivityIcon.UNKNOWN, true),
MOTOR_BIKE("motor bike", R.string.activity_type_motor_bike, ActivityIcon.MOTOR_BIKE, true),
MOTOR_BOATING("motor boating", R.string.activity_type_motor_boating, ActivityIcon.BOAT, true),
MOUNTAIN_BIKING("mountain biking", R.string.activity_type_mountain_biking, ActivityIcon.MOUNTAIN_BIKE, true),
OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_hiking, ActivityIcon.WALK, false),
OTHER("other", R.string.activity_type_other, ActivityIcon.UNKNOWN, true),
PADDLING("paddling", R.string.activity_type_paddling, ActivityIcon.UNKNOWN, true),
PARA_GLIDING("para gliding", R.string.activity_type_para_gliding, ActivityIcon.UNKNOWN, true),
RC_AIRPLANE("RC airplane", R.string.activity_type_rc_airplane, ActivityIcon.AIRPLANE, true),
RC_BOAT("RC boat", R.string.activity_type_rc_boat, ActivityIcon.BOAT, true),
RC_HELICOPTER("RC helicopter", R.string.activity_type_rc_helicopter, ActivityIcon.UNKNOWN, true),
RIDING("riding", R.string.activity_type_horse_back_riding, ActivityIcon.UNKNOWN, true),
ROAD_BIKING("road biking", R.string.activity_type_road_biking, ActivityIcon.BIKE, true),
ROLLER_SKIING("roller skiing", R.string.activity_type_roller_skiing, ActivityIcon.UNKNOWN, true),
ROWING("rowing", R.string.activity_type_rowing, ActivityIcon.UNKNOWN, true),
RUNNING("running", R.string.activity_type_running, ActivityIcon.RUN, false),
SAILING("sailing", R.string.activity_type_sailing, ActivityIcon.SAILING, true),
SEAPLANE("seaplane", R.string.activity_type_seaplane, ActivityIcon.AIRPLANE, true),
SKATE_BOARDING("skate boarding", R.string.activity_type_skate_boarding, ActivityIcon.SKATE_BOARDING, true),
SKATING("skating", R.string.activity_type_skating, ActivityIcon.UNKNOWN, true),
SKIING("skiing", R.string.activity_type_skiing, ActivityIcon.SKI, true),
SKY_JUMPING("sky jumping", R.string.activity_type_sky_jumping, ActivityIcon.UNKNOWN, true),
SLED("sled", R.string.activity_type_sled, ActivityIcon.UNKNOWN, true),
SNOW_BOARDING("snow boarding", R.string.activity_type_snow_boarding, ActivityIcon.SNOW_BOARDING, true),
SNOW_SHOEING("snow shoeing", R.string.activity_type_snow_shoeing, ActivityIcon.UNKNOWN, true),
SPEED_WALKING("speed walking", R.string.activity_type_speed_walking, ActivityIcon.WALK, false),
STREET_RUNNING("street running", R.string.activity_type_street_running, ActivityIcon.RUN, false),
SURFING("surfing", R.string.activity_type_surfing, ActivityIcon.UNKNOWN, true),
TRACK_CYCLING("track cycling", R.string.activity_type_track_cycling, ActivityIcon.BIKE, true),
TRACK_RUNNING("track running", R.string.activity_type_trail_running, ActivityIcon.RUN, false),
TRAIL_HIKING("trail hiking", R.string.activity_type_trail_hiking, ActivityIcon.WALK, false),
TRAIL_RUNNING("trail running", R.string.activity_type_trail_running, ActivityIcon.RUN, false),
TRAIN("train", R.string.activity_type_train, ActivityIcon.UNKNOWN, true),
ULTIMATE_FRISBEE("ultimate frisbee", R.string.activity_type_ultimate_frisbee, ActivityIcon.UNKNOWN, true),
WAKEBOARDING("wakeboarding", R.string.activity_type_wakeboarding, ActivityIcon.UNKNOWN, true),
WALKING("walking", R.string.activity_type_walking, ActivityIcon.WALK, false),
WATER_SKIING("water skiing", R.string.activity_type_water_skiing, ActivityIcon.UNKNOWN, true),
WIND_SURFING("wind surfing", R.string.activity_type_wind_surfing, ActivityIcon.UNKNOWN, true),
SWIMMING("swimming", R.string.activity_type_swimming, ActivityIcon.SWIMMING, false),
SWIMMING_OPEN("swimming in open water", R.string.activity_type_swimming_open, ActivityIcon.SWIMMING_OPEN, false),
WORKOUT("workout", R.string.activity_type_workout, ActivityIcon.WORKOUT, false),
UNKNOWN("unknown", R.string.activity_type_unknown, ActivityIcon.UNKNOWN, true);
final String id;
final int iconId;
final ActivityIcon icon;
final boolean showSpeedPreferred;
final int[] localizedStringIds;
final int localizedStringId;
//isSpeed?
ActivityType(String id, int iconId, boolean showSpeedPreferred, int... localizedStringIds) {
ActivityType(String id, int localizedStringId, ActivityIcon icon, boolean showSpeedPreferred) {
this.id = id;
this.iconId = iconId;
this.localizedStringId = localizedStringId;
this.icon = icon;
this.showSpeedPreferred = showSpeedPreferred;
this.localizedStringIds = localizedStringIds;
}
public String getId() {
return id;
}
public int getIconId() {
return iconId;
@Deprecated
public String getIconId() {
return icon.iconId;
}
public int getIconDrawableId() {
return icon.iconDrawableId;
}
public int getLocalizedStringId() {
return localizedStringId;
}
public boolean isShowSpeedPreferred() {
return showSpeedPreferred;
}
public int[] getLocalizedStringIds() {
return localizedStringIds;
}
public int getFirstLocalizedStringId() {
return localizedStringIds[0];
}
public static List<String> getLocalizedStrings(Context context) {
List<String> result = new ArrayList<>();
for (ActivityType activityType : values()) {
List<String> strings = Arrays.stream(activityType.localizedStringIds)
.mapToObj(context::getString)
.collect(Collectors.toList());
result.addAll(strings);
}
return Arrays.stream(values())
.map(ActivityType::getLocalizedStringId)
.map(context::getString)
.collect(Collectors.toList());
}
return result;
@NonNull
public static ActivityType findBy(String iconId) {
if (ActivityIcon.UNKNOWN.iconId.equals(iconId)) {
return ActivityType.UNKNOWN;
}
return Arrays.stream(ActivityType.values()).filter(
it -> it.icon.iconId.equals(iconId)
).findFirst()
.orElse(ActivityType.UNKNOWN);
}
public static ActivityType findByLocalizedString(Context context, String localizedActivityType) {
return findByLocalizedString(context.getResources(), localizedActivityType);
}
public static ActivityType findByLocalizedString(Resources resources, String localizedActivityType) {
return Arrays.stream(ActivityType.values())
.filter(it -> resources.getString(it.getLocalizedStringId()).equals(localizedActivityType))
.findFirst()
.orElse(ActivityType.UNKNOWN);
}
}
@@ -15,9 +15,7 @@ public abstract class Altitude {
public double toM() {
return altitude_m;
}
public abstract Altitude replace(double altitude_m);
public abstract int getLabelId();
public static class WGS84 extends Altitude {
@@ -31,12 +29,7 @@ public abstract class Altitude {
return R.string.wgs84;
}
public static Altitude of(double altitude_m) {
return new WGS84(altitude_m);
}
@Override
public Altitude replace(double altitude_m) {
public static WGS84 of(double altitude_m) {
return new WGS84(altitude_m);
}
}
@@ -52,12 +45,7 @@ public abstract class Altitude {
return R.string.egm2008;
}
public static Altitude of(double altitude_m) {
return new EGM2008(altitude_m);
}
@Override
public Altitude replace(double altitude_m) {
public static EGM2008 of(double altitude_m) {
return new EGM2008(altitude_m);
}
}
@@ -0,0 +1,20 @@
package de.dennisguse.opentracks.data.models;
public record AtmosphericPressure(float value) {
public static AtmosphericPressure ofPA(float value_Pa) {
return new AtmosphericPressure(value_Pa * 100);
}
public static AtmosphericPressure ofHPA(float value_hPa) {
return new AtmosphericPressure(value_hPa);
}
public float getPA() {
return value * 100;
}
public float getHPA() {
return value;
}
}
@@ -0,0 +1,8 @@
package de.dennisguse.opentracks.data.models;
public record BatteryLevel(int percentage) {
public static BatteryLevel of(int percentage) {
return new BatteryLevel(percentage);
}
}
@@ -1,11 +1,8 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull;
import java.time.Duration;
import java.util.Objects;
public class Cadence {
public record Cadence(float value_rpm) {
public static Cadence of(float value, Duration duration) {
if (duration.isZero()) {
@@ -23,34 +20,7 @@ public class Cadence {
return of(0.0f);
}
private final float value_rpm;
private Cadence(float value) {
this.value_rpm = value;
}
public float getRPM() {
return value_rpm;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Cadence cadence = (Cadence) o;
return Float.compare(cadence.value_rpm, value_rpm) == 0;
}
@Override
public int hashCode() {
return Objects.hash(value_rpm);
}
@NonNull
@Override
public String toString() {
return "Cadence{" +
"value=" + value_rpm + " rpm" +
'}';
}
}
@@ -3,16 +3,13 @@ package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import de.dennisguse.opentracks.settings.UnitSystem;
public class Distance {
public record Distance(double distance_m) {
public static Distance of(double distance_m) {
return new Distance(distance_m);
}
public static Distance of(Double distance_m) {
if (distance_m == null) {
return Distance.of(Double.NaN);
@@ -20,11 +17,9 @@ public class Distance {
return Distance.of((double) distance_m);
}
}
public static Distance of(String distance_m) {
return of(Float.parseFloat(distance_m));
}
@Nullable
public static Distance ofOrNull(Double distance_m) {
if (distance_m == null) {
@@ -60,17 +55,11 @@ public class Distance {
public static Distance one(UnitSystem unitSystem) {
return switch (unitSystem) {
case METRIC -> Distance.ofKilometer(1);
case IMPERIAL -> Distance.ofMile(1);
case IMPERIAL_FEET, IMPERIAL_METER -> Distance.ofMile(1);
case NAUTICAL_IMPERIAL -> Distance.ofNauticalMile(1);
};
}
private final double distance_m;
private Distance(double distance_m) {
this.distance_m = distance_m;
}
public Distance plus(@NonNull Distance distance) {
return new Distance(distance_m + distance.distance_m);
}
@@ -130,39 +119,18 @@ public class Distance {
public double toKM_Miles(UnitSystem unitSystem) {
return switch (unitSystem) {
case METRIC -> toKM();
case IMPERIAL -> toMI();
case IMPERIAL_FEET, IMPERIAL_METER -> toMI();
case NAUTICAL_IMPERIAL -> toNauticalMiles();
};
}
public double toM_FT(UnitSystem unitSystem) {
return switch (unitSystem) {
case METRIC -> toM();
case NAUTICAL_IMPERIAL, IMPERIAL -> toFT();
case METRIC, IMPERIAL_METER -> toM();
case NAUTICAL_IMPERIAL, IMPERIAL_FEET -> toFT();
};
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Distance distance = (Distance) o;
return Double.compare(distance.distance_m, distance_m) == 0;
}
@Override
public int hashCode() {
return Objects.hash(distance_m);
}
@NonNull
@Override
public String toString() {
return "Distance{" +
"distance_m=" + distance_m +
'}';
}
// multiplication factors for conversion
private static final double KM_TO_M = 1000.0;
private static final double M_TO_KM = 1 / KM_TO_M;
@@ -47,8 +47,9 @@ public class DistanceFormatter {
if (distance.isInvalid()) {
String valueUnknown = resources.getString(R.string.value_unknown);
return switch (unitSystem) {
case METRIC -> new Pair<>(valueUnknown, resources.getString(R.string.unit_meter));
case IMPERIAL, NAUTICAL_IMPERIAL ->
case METRIC, IMPERIAL_METER ->
new Pair<>(valueUnknown, resources.getString(R.string.unit_meter));
case IMPERIAL_FEET, NAUTICAL_IMPERIAL ->
new Pair<>(valueUnknown, resources.getString(R.string.unit_feet));
};
}
@@ -61,13 +62,20 @@ public class DistanceFormatter {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
}
}
case IMPERIAL -> {
case IMPERIAL_FEET -> {
if (distance.greaterThan(Distance.ofMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
}
case IMPERIAL_METER -> {
if (distance.greaterThan(Distance.ofMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
}
}
case NAUTICAL_IMPERIAL -> {
if (distance.greaterThan(Distance.ofNauticalMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
@@ -1,43 +1,12 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull;
import java.util.Objects;
public class HeartRate {
public record HeartRate(float value) {
public static HeartRate of(float value) {
return new HeartRate(value);
}
private final float value;
private HeartRate(float value) {
this.value = value;
}
public float getBPM() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HeartRate heartRate = (HeartRate) o;
return Float.compare(heartRate.value, value) == 0;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@NonNull
@Override
public String toString() {
return "HeartRate{" +
"value=" + value + " bpm" +
'}';
}
}
@@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -267,31 +266,7 @@ public final class Marker {
return photoUrl != null && !"".equals(photoUrl);
}
public static class Id implements Parcelable {
private final long id;
public Id(long id) {
this.id = id;
}
//TOOD Limit visibility to TrackRecordingService / ContentProvider
public long getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Id id1 = (Id) o;
return id == id1.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public record Id(long id) implements Parcelable {
@NonNull
@Override
@@ -309,7 +284,7 @@ public final class Marker {
parcel.writeLong(id);
}
public static final Parcelable.Creator<Id> CREATOR = new Parcelable.Creator<>() {
public static final Creator<Id> CREATOR = new Creator<>() {
public Id createFromParcel(Parcel in) {
return new Id(in.readLong());
}
@@ -1,43 +1,12 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull;
import java.util.Objects;
public class Power {
public record Power(float value) {
public static Power of(float value) {
return new Power(value);
}
private final float value;
private Power(float value) {
this.value = value;
}
public float getW() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Power power = (Power) o;
return Float.compare(power.value, value) == 0;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@NonNull
@Override
public String toString() {
return "Power{" +
"value=" + value + " W" +
'}';
}
}
@@ -1,14 +1,11 @@
package de.dennisguse.opentracks.data.models;
import androidx.annotation.NonNull;
import java.time.Duration;
import java.util.Objects;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
public class Speed {
public record Speed(double speed_mps) {
public static Speed of(Distance distance, Duration duration) {
if (duration.isZero()) {
@@ -47,12 +44,6 @@ public class Speed {
return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps));
}
private final double speed_mps;
private Speed(double speed_mps) {
this.speed_mps = speed_mps;
}
public Speed mul(double factor) {
return new Speed(factor * speed_mps);
}
@@ -117,29 +108,8 @@ public class Speed {
public double to(UnitSystem unitSystem) {
return switch (unitSystem) {
case METRIC -> toKMH();
case IMPERIAL -> toMPH();
case IMPERIAL_FEET, IMPERIAL_METER -> toMPH();
case NAUTICAL_IMPERIAL -> toKnots();
};
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Speed speed = (Speed) o;
return Double.compare(speed.speed_mps, speed_mps) == 0;
}
@Override
public int hashCode() {
return Objects.hash(speed_mps);
}
@NonNull
@Override
public String toString() {
return "Speed{" +
"speed_mps=" + speed_mps +
'}';
}
}
@@ -42,7 +42,7 @@ public class SpeedFormatter {
int unitId = switch (unitSystem) {
case METRIC ->
reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
case IMPERIAL ->
case IMPERIAL_FEET, IMPERIAL_METER ->
reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
case NAUTICAL_IMPERIAL ->
reportSpeedOrPace ? R.string.unit_knots : R.string.unit_minute_per_nautical_mile;
@@ -44,9 +44,9 @@ public class Track {
private String name = "";
private String description = "";
private String category = "";
private String activityTypeLocalized = "";
private String icon = "";
private ActivityType activityType;
private final ZoneOffset zoneOffset;
@@ -54,7 +54,7 @@ public class Track {
@VisibleForTesting
public Track() {
this.zoneOffset = ZoneOffset.UTC;
this(ZoneOffset.UTC);
}
public Track(@NonNull ZoneOffset zoneOffset) {
@@ -97,20 +97,20 @@ public class Track {
this.description = description;
}
public String getCategory() {
return category;
public String getActivityTypeLocalized() {
return activityTypeLocalized;
}
public void setCategory(String category) {
this.category = category;
public void setActivityTypeLocalized(String activityType) {
this.activityTypeLocalized = activityType;
}
public String getIcon() {
return icon;
public ActivityType getActivityType() {
return activityType;
}
public void setIcon(String icon) {
this.icon = icon;
public void setActivityType(ActivityType activityType) {
this.activityType = activityType;
}
public ZoneOffset getZoneOffset() {
@@ -130,11 +130,11 @@ public class TrackPoint {
return this;
}
public boolean isSegmentStart() {
return type == Type.SEGMENT_START_AUTOMATIC || type == Type.SEGMENT_START_MANUAL;
public boolean isSegmentManualStart() {
return type == Type.SEGMENT_START_MANUAL;
}
public boolean isSegmentEnd() {
public boolean isSegmentManualEnd() {
return type == Type.SEGMENT_END_MANUAL;
}
@@ -39,7 +39,7 @@ public interface TracksColumns extends BaseColumns {
String UUID = "uuid"; // identifier to make tracks globally unique (prevent re-import)
String NAME = "name"; // track name
String DESCRIPTION = "description"; // track description
String CATEGORY = "category"; // track activity type
String ACTIVITY_TYPE_LOCALIZED = "category"; // track activity type
String STARTTIME = "starttime"; // track start time
String STARTTIME_OFFSET = "starttime_offset"; // in plus/minus in seconds
String STOPTIME = "stoptime"; // track stop time
@@ -57,13 +57,13 @@ public interface TracksColumns extends BaseColumns {
String MAX_ALTITUDE = "maxelevation"; // maximum altitude //TODO RENAME column
String ALTITUDE_GAIN = "elevationgain"; // altitude gain //TODO RENAME column
String ALTITUDE_LOSS = "elevationloss"; // altitude loss //TODO RENAME column
String ICON = "icon"; // track activity type icon
String ICON = "icon"; // track activity type icon //TODO DEPRECATED
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ NAME + " TEXT, "
+ DESCRIPTION + " TEXT, "
+ CATEGORY + " TEXT, "
+ ACTIVITY_TYPE_LOCALIZED + " TEXT, "
+ STARTTIME + " INTEGER, "
+ STOPTIME + " INTEGER, "
+ NUMPOINTS + " INTEGER, "
@@ -16,37 +16,62 @@ import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.databinding.ChooseActivityTypeBinding;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* A DialogFragment to choose an activity type.
*/
public class ChooseActivityTypeDialogFragment extends DialogFragment implements AdapterView.OnItemClickListener {
private static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
public static void showDialog(FragmentManager fragmentManager, String preselectedCategory) {
new ChooseActivityTypeDialogFragment(preselectedCategory).show(fragmentManager, ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
@Deprecated
public static void showDialog(FragmentManager fragmentManager, Context context, String activityTypeLocalized) {
ActivityType activityType = ActivityType.findByLocalizedString(context, activityTypeLocalized);
showDialog(fragmentManager, activityType);
}
private static int getPosition(Context context, String category) {
if (category == null) {
public static void showDialog(FragmentManager fragmentManager, ActivityType activityType) {
new ChooseActivityTypeDialogFragment(activityType).show(fragmentManager, ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
private static final List<ActivityType> activityTypes = List.of(
ActivityType.UNKNOWN,
ActivityType.AIRPLANE,
ActivityType.BIKING,
ActivityType.MOUNTAIN_BIKING,
ActivityType.MOTOR_BIKE,
ActivityType.KAYAKING,
ActivityType.BOAT,
ActivityType.SAILING,
ActivityType.DRIVING,
ActivityType.RUNNING,
ActivityType.SNOW_BOARDING,
ActivityType.SKIING,
ActivityType.WALKING,
ActivityType.ESCOOTER,
ActivityType.INLINE_SKATING,
ActivityType.SKATE_BOARDING,
ActivityType.CLIMBING,
ActivityType.SWIMMING,
ActivityType.SWIMMING_OPEN,
ActivityType.WORKOUT
);
private static int getPosition(Context context, ActivityType activityType) {
if (activityType == null) {
return -1;
}
String iconValue = TrackIconUtils.getIconValue(context, category);
return TrackIconUtils.getAllIconValues().indexOf(iconValue);
return activityTypes.indexOf(activityType);
}
private ChooseActivityTypeBinding viewBinding;
private final String preselectedCategory;
private final ActivityType preselectedActivityType;
private ChooseActivityTypeCaller chooseActivityTypeCaller;
private ChooseActivityTypeDialogFragment(String preselectedCategory) {
this.preselectedCategory = preselectedCategory;
private ChooseActivityTypeDialogFragment(ActivityType activityType) {
this.preselectedActivityType = activityType;
}
@NonNull
@@ -61,13 +86,13 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = ChooseActivityTypeBinding.inflate(inflater, container, false);
List<Integer> imageIds = new ArrayList<>();
for (String iconValue : TrackIconUtils.getAllIconValues()) {
imageIds.add(TrackIconUtils.getIconDrawable(iconValue));
List<Integer> iconDrawableIds = new ArrayList<>();
for (ActivityType activityType : activityTypes) {
iconDrawableIds.add(activityType.getIconDrawableId());
}
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(imageIds);
int position = getPosition(getContext(), preselectedCategory);
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(iconDrawableIds);
int position = getPosition(getContext(), preselectedActivityType);
if (position != -1) {
imageAdapter.setSelected(position);
}
@@ -94,7 +119,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
chooseActivityTypeCaller.onChooseActivityTypeDone(TrackIconUtils.getAllIconValues().get(position));
chooseActivityTypeCaller.onChooseActivityTypeDone(activityTypes.get(position));
dismiss();
}
@@ -103,6 +128,6 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
*/
public interface ChooseActivityTypeCaller {
void onChooseActivityTypeDone(String iconValue);
void onChooseActivityTypeDone(ActivityType activityType);
}
}
@@ -78,7 +78,7 @@ class ChooseActivityTypeImageAdapter extends BaseAdapter {
}
if (position == selected) {
imageView.setBackgroundColor(ContextCompat.getColor(parent.getContext(), R.color.opentracks_secondary_color));
imageView.setBackgroundColor(ContextCompat.getColor(parent.getContext(), R.color.opentracks));
} else {
imageView.setBackgroundColor(Color.TRANSPARENT);
}
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.fragments;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
@@ -28,12 +29,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
@@ -43,7 +43,6 @@ import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* A fragment to display track statistics to the user for a recorded {@link Track}.
@@ -89,7 +88,7 @@ public class StatisticsRecordedFragment extends Fragment {
if (PreferencesUtils.isKey(R.string.stats_rate_key, key) && track != null) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
preferenceReportSpeed = PreferencesUtils.isReportSpeed(track);
}
if (key != null && updateUInecessary && isResumed()) {
@@ -113,10 +112,6 @@ public class StatisticsRecordedFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
// sensorsRecyclerView.setAdapter(sensorsAdapter);
return viewBinding.getRoot();
}
@@ -155,7 +150,7 @@ public class StatisticsRecordedFragment extends Fragment {
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
boolean prefsChanged = this.track == null || (!this.track.getCategory().equals(track.getCategory()));
boolean prefsChanged = this.track == null || (!this.track.getActivityTypeLocalized().equals(track.getActivityTypeLocalized()));
this.track = track;
if (prefsChanged) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, getString(R.string.stats_rate_key));
@@ -191,8 +186,12 @@ public class StatisticsRecordedFragment extends Fragment {
// Set activity type
{
String trackIconValue = TrackIconUtils.getIconValue(getContext(), track.getCategory());
viewBinding.statsActivityTypeIcon.setImageDrawable(ContextCompat.getDrawable(getContext(), TrackIconUtils.getIconDrawable(trackIconValue)));
Context context = getContext();
String localizedActivityType = track.getActivityTypeLocalized();
String trackIconValue = ActivityType.findByLocalizedString(context, localizedActivityType)
.getIconId();
viewBinding.statsActivityTypeIcon.setImageDrawable(ContextCompat.getDrawable(getContext(), ActivityType.findBy(trackIconValue)
.getIconDrawableId()));
}
// Set time and start datetime
@@ -253,6 +252,28 @@ public class StatisticsRecordedFragment extends Fragment {
if (sensorStatistics == null) {
return;
}
//TODO
if (sensorStatistics.hasHeartRate()) {
String maxBPM = String.valueOf(Math.round(sensorStatistics.getMaxHeartRate().getBPM()));
String avgBPM = String.valueOf(Math.round(sensorStatistics.getAvgHeartRate().getBPM()));
viewBinding.statsHeartRateGroup.setVisibility(View.VISIBLE);
viewBinding.statsMaxHeartRateValue.setText(maxBPM);
viewBinding.statsAvgHeartRateValue.setText(avgBPM);
}
if (sensorStatistics.hasCadence()) {
String maxRPM = String.valueOf(Math.round(sensorStatistics.getMaxCadence().getRPM()));
String avgRPM = String.valueOf(Math.round(sensorStatistics.getAvgCadence().getRPM()));
viewBinding.statsCadenceGroup.setVisibility(View.VISIBLE);
viewBinding.statsMaxCadenceValue.setText(maxRPM);
viewBinding.statsAvgCadenceValue.setText(avgRPM);
}
if (sensorStatistics.hasPower()) {
String W = String.valueOf(Math.round(sensorStatistics.getAvgPower().getW()));
viewBinding.statsPowerGroup.setVisibility(View.VISIBLE);
viewBinding.statsPowerValue.setText(W);
}
}
}
@@ -122,9 +122,8 @@ public class StatisticsRecordingFragment extends Fragment {
}
recordingLayout = newRecordingLayout;
viewBinding.statsLayout.setColumnCount(recordingLayout.getColumnsPerRow());
viewBinding.statsLayout.removeAllViews(); //Let's start from scratch
viewBinding.statsLayout.setColumnCount(recordingLayout.getColumnsPerRow());
viewHolders.clear();
Map<String, Callable<StatisticViewHolder<?>>> m = Mapping.create(getContext());
@@ -20,15 +20,19 @@ public class TrackFilenameGenerator {
public static final String UUID_KEY = "{uuid}";
public static final String TRACKNAME_KEY = "{name}";
public static final String CATEGORY_KEY = "{category}";
public static final String ACTIVITY_TYPE_KEY = "{category}";
public static final String STARTTIME_TIME_KEY = "{time}";
public static final String STARTTIME_DATE_KEY = "{date}";
public static String getAllOptions() {
return Stream.of(UUID_KEY, TRACKNAME_KEY, CATEGORY_KEY, STARTTIME_TIME_KEY, STARTTIME_DATE_KEY)
return Stream.of(UUID_KEY, TRACKNAME_KEY, ACTIVITY_TYPE_KEY, STARTTIME_TIME_KEY, STARTTIME_DATE_KEY)
.collect(Collectors.joining(", "));
}
public static String format(@NonNull String name, @NonNull TrackFileFormat trackFileFormat) {
return FileUtils.sanitizeFileName(name + "." + trackFileFormat.getExtension());
}
private final String template;
public TrackFilenameGenerator(@NonNull String template) {
@@ -40,11 +44,11 @@ public class TrackFilenameGenerator {
values.put(UUID_KEY, track.getUuid().toString().substring(0, 8));
values.put(TRACKNAME_KEY, track.getName());
values.put(CATEGORY_KEY, track.getCategory());
values.put(ACTIVITY_TYPE_KEY, track.getActivityTypeLocalized());
values.put(STARTTIME_TIME_KEY, track.getStartTime().toLocalTime().toString());
values.put(STARTTIME_DATE_KEY, track.getStartTime().toLocalDate().toString());
return FileUtils.sanitizeFileName(format(template, values)) + "." + trackFileFormat.getExtension();
return format(format(template, values), trackFileFormat);
}
private static String format(String template, Map<String, String> values) {
@@ -77,10 +81,6 @@ public class TrackFilenameGenerator {
return String.format(templateCompiled, valueList.toArray());
}
public String getTemplate() {
return template;
}
public boolean isValid() {
try {
getExample();
@@ -23,8 +23,6 @@ import androidx.annotation.NonNull;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
@@ -68,6 +66,9 @@ public class CSVTrackExporter implements TrackExporter {
SPEED_FORMAT.setMaximumFractionDigits(2);
SPEED_FORMAT.setGroupingUsed(false);
DISTANCE_FORMAT.setMaximumFractionDigits(0);
DISTANCE_FORMAT.setGroupingUsed(false);
HEARTRATE_FORMAT.setMaximumFractionDigits(0);
HEARTRATE_FORMAT.setGroupingUsed(false);
@@ -87,15 +88,23 @@ public class CSVTrackExporter implements TrackExporter {
}
@Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
public boolean writeTrack(@NonNull List<Track> tracks, @NonNull OutputStream outputStream) {
List<Column> columns = List.of(
new Column("time", null),
new Column("trackpoint_type", t -> quote(t.getType().name())),
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLongitude()) : ""),
new Column("altitude", t -> t.hasAltitude() ? COORDINATE_FORMAT.format(t.getAltitude().toM()) : ""),
new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""),
new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""),
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
List<Column> columns = Collections.unmodifiableList(Arrays.asList(new Column("time", null), new Column("trackpoint_type", t -> quote(t.getType().name())), new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""), new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLongitude()) : ""), new Column("altitude", t -> t.hasAltitude() ? COORDINATE_FORMAT.format(t.getAltitude().toM()) : ""), new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""), new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""),
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""), new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""), new Column("altitude_loss", t -> t.hasAltitudeLoss() ? DISTANCE_FORMAT.format(t.getAltitudeLoss()) : ""), new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""), new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""), new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""), new Column("power", t -> t.hasPower() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : "")));
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""),
new Column("altitude_loss", t -> t.hasAltitudeLoss() ? DISTANCE_FORMAT.format(t.getAltitudeLoss()) : ""),
new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""),
new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""),
new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""),
new Column("power", t -> t.hasPower() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : ""));
try {
prepare(outputStream);
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -24,6 +23,7 @@ import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
@@ -31,14 +31,15 @@ import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.ExportActivityBinding;
import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.FileUtils;
@@ -47,12 +48,20 @@ import de.dennisguse.opentracks.util.FileUtils;
* An activity for saving tracks to the external storage.
*
* @author Rodrigo Damazio
* TODO: This class needs some refactoring.
* * It pushes each export job (usually one Track) for export; although export could be run in parallel.
* Also this requires that the ExportActivity stays in foreground, so the user has to activily wait.
* It would be better to let the ExportService handle this and let it report progress / conflicts to ExportActivity
* * File name conflicts are checked in this class instead of the ExportService.
* So, for this check actually a different file name might be used than in the ExportService.
* * Saved state as an object instead of individual values.
*/
public class ExportActivity extends FragmentActivity implements ExportServiceResultReceiver.Receiver {
private static final String TAG = ExportActivity.class.getSimpleName();
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
public static final String EXTRA_ONE_FILE_KEY = "one_file";
public static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
private static final String BUNDLE_AUTO_CONFLICT = "auto_conflict";
@@ -64,9 +73,13 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private static final String BUNDLE_DIRECTORY_FILES = "track_directory_files";
private static final String BUNDLE_TRACK_ERRORS = "track_errors";
private static final int CONFLICT_NONE = 0;
private static final int CONFLICT_OVERWRITE = 1;
private static final int CONFLICT_SKIP = 2;
private static final String BUNDLE_EXPORT_TASKS = "export_tasks";
private enum ConflictResolutionStrategy {
CONFLICT_NONE,
CONFLICT_OVERWRITE,
CONFLICT_SKIP
}
private TrackFileFormat trackFileFormat;
private Uri directoryUri;
@@ -87,12 +100,12 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private ArrayList<String> trackErrors = new ArrayList<>();
private int autoConflict;
private ConflictResolutionStrategy autoConflict;
private ContentProviderUtils contentProviderUtils;
// List of tracks to be exported.
private final ArrayList<Track> tracks = new ArrayList<>();
private ArrayList<ExportTask> exportTasks;
private final LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>();
private final Handler conflictsHandler = new Handler();
@@ -100,34 +113,36 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private final Runnable conflictsRunnable = new Runnable() {
@Override
public void run() {
if (conflictsQueue.size() > 0) {
PendingConflict conflict = conflictsQueue.peek();
if (conflict.resolve()) {
conflictsQueue.remove(conflict);
if (!conflictsQueue.isEmpty()) {
conflictsHandler.post(conflictsRunnable);
}
return;
}
viewBinding.exportProgressLeftButton.setOnClickListener((view) -> {
setConflictVisibility(View.GONE);
conflict.skip();
conflictsQueue.remove(conflict);
if (!conflictsQueue.isEmpty()) {
conflictsHandler.post(conflictsRunnable);
}
});
viewBinding.exportProgressRightButton.setOnClickListener((view) -> {
setConflictVisibility(View.GONE);
conflict.overwrite();
conflictsQueue.remove(conflict);
if (!conflictsQueue.isEmpty()) {
conflictsHandler.post(conflictsRunnable);
}
});
if (conflictsQueue.isEmpty()) {
return;
}
PendingConflict conflict = conflictsQueue.peek();
if (conflict.resolve()) {
conflictsQueue.remove(conflict);
if (!conflictsQueue.isEmpty()) {
conflictsHandler.post(conflictsRunnable);
}
return;
}
viewBinding.exportProgressLeftButton.setOnClickListener((view) -> {
setConflictVisibility(View.GONE);
conflict.skip();
conflictsQueue.remove(conflict);
if (!conflictsQueue.isEmpty()) {
conflictsHandler.post(conflictsRunnable);
}
});
viewBinding.exportProgressRightButton.setOnClickListener((view) -> {
setConflictVisibility(View.GONE);
conflict.overwrite();
conflictsQueue.remove(conflict);
if (!conflictsQueue.isEmpty()) {
conflictsHandler.post(conflictsRunnable);
}
});
}
};
@@ -139,6 +154,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
boolean allInOneFile = getIntent().getBooleanExtra(EXTRA_ONE_FILE_KEY, false);
contentProviderUtils = new ContentProviderUtils(this);
@@ -148,14 +164,17 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
resultReceiver = new ExportServiceResultReceiver(new Handler(), this);
if (savedInstanceState == null) {
autoConflict = CONFLICT_NONE;
autoConflict = ConflictResolutionStrategy.CONFLICT_NONE;
setProgress();
new Thread(() -> {
directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri());
runOnUiThread(() -> initExport(0));
runOnUiThread(() -> {
createExportTasks(allInOneFile);
nextExport(null);
});
}).start();
} else {
autoConflict = savedInstanceState.getInt(BUNDLE_AUTO_CONFLICT);
autoConflict = ConflictResolutionStrategy.valueOf(savedInstanceState.getString(BUNDLE_AUTO_CONFLICT));
trackExportSuccessCount = savedInstanceState.getInt(BUNDLE_SUCCESS_COUNT);
trackExportErrorCount = savedInstanceState.getInt(BUNDLE_ERROR_COUNT);
trackExportOverwrittenCount = savedInstanceState.getInt(BUNDLE_OVERWRITTEN_COUNT);
@@ -163,19 +182,19 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS);
exportTasks = new ArrayList<>(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS));
setProgress();
initExport(getTotalDone());
nextExport(null);
}
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.export_progress_message, directoryDisplayName));
viewBinding.exportActivityToolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
viewBinding.bottomAppBarLayout.bottomAppBar.setNavigationIcon(R.drawable.ic_logo_color_24dp);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(BUNDLE_AUTO_CONFLICT, autoConflict);
outState.putString(BUNDLE_AUTO_CONFLICT, autoConflict.name());
outState.putInt(BUNDLE_SUCCESS_COUNT, trackExportSuccessCount);
outState.putInt(BUNDLE_ERROR_COUNT, trackExportErrorCount);
outState.putInt(BUNDLE_OVERWRITTEN_COUNT, trackExportOverwrittenCount);
@@ -183,13 +202,14 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount);
outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList<String>) directoryFiles);
outState.putStringArrayList(BUNDLE_TRACK_ERRORS, trackErrors);
outState.putParcelableArrayList(BUNDLE_EXPORT_TASKS, exportTasks);
}
@Override
protected void onDestroy() {
super.onDestroy();
conflictsQueue.clear();
tracks.clear();
exportTasks.clear();
}
@Override
@@ -202,57 +222,50 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
this.doubleBackToCancel = true;
Toast.makeText(this, getString(R.string.generic_click_twice_cancel), Toast.LENGTH_SHORT).show();
new Handler().postDelayed(() -> doubleBackToCancel=false, 2000);
new Handler().postDelayed(() -> doubleBackToCancel = false, 2000);
}
private void initExport(int from) {
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
if (cursor == null) {
onExportEnded();
return;
}
trackExportTotalCount = cursor.getCount();
viewBinding.exportProgressTotal.setText("" + trackExportTotalCount);
for (int i = from; i < trackExportTotalCount; i++) {
cursor.moveToPosition(i);
Track track = ContentProviderUtils.createTrack(cursor);
tracks.add(track);
}
if (!tracks.isEmpty()) {
export(tracks.get(0));
} else {
onExportEnded();
}
private void createExportTasks(boolean allInOneFile) {
List<Track> tracks = contentProviderUtils.getTracks();
exportTasks = new ArrayList<>();
if (allInOneFile) {
String filename = "OpenTracks-Backup";
exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::getId).collect(Collectors.toList())));
} else {
exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).collect(Collectors.toList()));
}
trackExportTotalCount = exportTasks.size();
}
/**
* Enqueue track identified by UUID to be exported if not exported already or there is a conflict resolution.
*
* @param track Track object.
* @param conflictResolution conflict resolution to be applied if needed.
*/
private void export(Track track, int conflictResolution) {
boolean fileExists = exportFileExists(track);
private void export(ExportTask exportTask, ConflictResolutionStrategy conflictResolution) {
boolean fileExists = exportFileExists(exportTask);
if (fileExists && conflictResolution == CONFLICT_NONE) {
conflict(track);
} else if (fileExists && conflictResolution == CONFLICT_SKIP) {
if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_NONE) {
conflict(exportTask);
} else if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) {
trackExportSkippedCount++;
onExportCompleted(track);
nextExport(exportTask);
} else {
ExportService.enqueue(this, resultReceiver, track.getId(), trackFileFormat, directoryUri);
ExportService.enqueue(this, resultReceiver, exportTask, directoryUri);
}
}
private void export(Track track) {
export(track, autoConflict);
private void export(ExportTask exportTask) {
export(exportTask, autoConflict);
}
private boolean exportFileExists(Track track) {
String filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
@Deprecated //TODO Check should be done in ExportService
private boolean exportFileExists(ExportTask exportTask) {
String filename;
if (exportTask.isMultiExport()) {
filename = TrackFilenameGenerator.format(exportTask.getFilename(), exportTask.getTrackFileFormat());
} else {
Track track = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0));
filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
}
return directoryFiles.stream().anyMatch(filename::equals);
}
@@ -269,31 +282,29 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
}
private void setProgress() {
int done = getTotalDone();
viewBinding.exportProgressDone.setText("" + done);
viewBinding.exportProgressDone.setText("" + getTotalDone());
viewBinding.exportProgressTotal.setText("" + trackExportTotalCount);
viewBinding.exportProgressBar.setProgress((int) ((float) done / (float) trackExportTotalCount * 100f));
viewBinding.exportProgressSummaryNew.setText(String.valueOf(trackExportSuccessCount));
viewBinding.exportProgressSummaryOverwrite.setText(String.valueOf(trackExportOverwrittenCount));
viewBinding.exportProgressSummarySkip.setText(String.valueOf(trackExportSkippedCount));
viewBinding.exportProgressSummaryErrors.setText(String.valueOf(trackExportErrorCount));
viewBinding.exportProgressBar.setProgress((int) ((float) getTotalDone() / (float) trackExportTotalCount * 100f));
viewBinding.exportProgressSummaryNew.setText("" + trackExportSuccessCount);
viewBinding.exportProgressSummaryOverwrite.setText("" + trackExportOverwrittenCount);
viewBinding.exportProgressSummarySkip.setText("" + trackExportSkippedCount);
viewBinding.exportProgressSummaryErrors.setText("" + trackExportErrorCount);
viewBinding.exportProgressSummaryNewGroup.setVisibility(trackExportSuccessCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummaryOverwriteGroup.setVisibility(trackExportOverwrittenCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummarySkipGroup.setVisibility(trackExportSkippedCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummaryErrorsGroup.setVisibility(trackExportErrorCount > 0 ? View.VISIBLE : View.GONE);
}
private void onExportCompleted(Track track) {
tracks.remove(track);
private void nextExport(@Nullable ExportTask exportTask) {
exportTasks.remove(exportTask);
setProgress();
if (tracks.isEmpty()) {
if (exportTasks.isEmpty()) {
onExportEnded();
return;
}
export(tracks.get(0));
export(exportTasks.get(0));
}
private void onExportEnded() {
@@ -318,30 +329,32 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
}
@Override
public void onExportSuccess(Track.Id trackId) {
Track track = contentProviderUtils.getTrack(trackId);
if (exportFileExists(track)) {
public void onExportSuccess(ExportTask exportTask) {
if (exportFileExists(exportTask)) {
trackExportOverwrittenCount++;
} else {
trackExportSuccessCount++;
}
onExportCompleted(track);
nextExport(exportTask);
}
@Override
public void onExportError(Track.Id trackId) {
Track track = contentProviderUtils.getTrack(trackId);
public void onExportError(ExportTask exportTask) {
trackExportErrorCount++;
trackErrors.add(track.getName());
String name;
if (exportTask.isMultiExport()) {
name = exportTask.getFilename();
} else {
name = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0)).getName();
}
trackErrors.add(name);
onExportCompleted(track);
nextExport(exportTask);
}
private void conflict(Track track) {
PendingConflict newConflict = new PendingConflict(track);
private void conflict(ExportTask exportTask) {
PendingConflict newConflict = new PendingConflict(exportTask);
conflictsQueue.add(newConflict);
if (conflictsQueue.size() == 1) {
@@ -353,10 +366,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* Handle conflicts (exporting file already exists).
*/
private class PendingConflict {
private final Track track;
private final ExportTask exportTask;
public PendingConflict(Track track) {
this.track = track;
public PendingConflict(ExportTask exportTask) {
this.exportTask = exportTask;
}
/**
@@ -366,14 +379,20 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* @return true if it could resolve the conflict or false otherwise.
*/
public boolean resolve() {
if (autoConflict == CONFLICT_NONE) {
if (autoConflict == ConflictResolutionStrategy.CONFLICT_NONE) {
viewBinding.exportProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(ExportActivity.this, R.drawable.ic_report_problem_24));
viewBinding.exportProgressAlertMsg.setText(getString(R.string.export_track_already_exists_msg, track.getName()));
String name;
if (exportTask.isMultiExport()) {
name = exportTask.getFilename();
} else {
name = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0)).getName();
}
viewBinding.exportProgressAlertMsg.setText(getString(R.string.export_track_already_exists_msg, name));
setConflictVisibility(View.VISIBLE);
return false;
}
export(track);
export(exportTask);
return true;
}
@@ -381,10 +400,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* Overwrite the export file and set the autoConflict if user set the "do it for all" switch button.
*/
public void overwrite() {
export(track, CONFLICT_OVERWRITE);
export(exportTask, ConflictResolutionStrategy.CONFLICT_OVERWRITE);
if (viewBinding.exportProgressApplyToAll.isChecked()) {
autoConflict = CONFLICT_OVERWRITE;
autoConflict = ConflictResolutionStrategy.CONFLICT_OVERWRITE;
}
}
@@ -392,10 +411,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* Skip the export file and set the autoConflict if user set the "do it for all" switch button.
*/
public void skip() {
export(track, CONFLICT_SKIP);
export(exportTask, ConflictResolutionStrategy.CONFLICT_SKIP);
if (viewBinding.exportProgressApplyToAll.isChecked()) {
autoConflict = CONFLICT_SKIP;
autoConflict = ConflictResolutionStrategy.CONFLICT_SKIP;
}
}
}
@@ -12,9 +12,6 @@ import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.ExportUtils;
public class ExportService extends JobIntentService {
@@ -22,16 +19,14 @@ public class ExportService extends JobIntentService {
private static final int JOB_ID = 1;
private static final String EXTRA_RECEIVER = "extra_receiver";
private static final String EXTRA_TRACK_ID = "extra_track_id";
private static final String EXTRA_TRACK_FILE_FORMAT = "extra_track_file_format";
private static final String EXTRA_EXPORT_TASK = "export_task";
private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri";
private static final String TAG = ExportService.class.getSimpleName();
public static void enqueue(Context context, ExportServiceResultReceiver receiver, Track.Id trackId, TrackFileFormat trackFileFormat, Uri directoryUri) {
public static void enqueue(Context context, ExportServiceResultReceiver receiver, ExportTask exportTask, Uri directoryUri) {
Intent intent = new Intent(context, JobService.class);
intent.putExtra(EXTRA_RECEIVER, receiver);
intent.putExtra(EXTRA_TRACK_ID, trackId);
intent.putExtra(EXTRA_TRACK_FILE_FORMAT, trackFileFormat);
intent.putExtra(EXTRA_EXPORT_TASK, exportTask);
intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri);
enqueueWork(context, ExportService.class, JOB_ID, intent);
}
@@ -40,13 +35,12 @@ public class ExportService extends JobIntentService {
protected void onHandleWork(@NonNull Intent intent) {
// Get all data.
ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
Track.Id trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
TrackFileFormat trackFileFormat = (TrackFileFormat) intent.getSerializableExtra(EXTRA_TRACK_FILE_FORMAT);
ExportTask exportTask = intent.getParcelableExtra(EXTRA_EXPORT_TASK);
Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI);
// Prepare resultCode and bundle to send to the receiver.
Bundle bundle = new Bundle();
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK, exportTask);
// Build directory file.
DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
@@ -57,9 +51,7 @@ public class ExportService extends JobIntentService {
}
// Export.
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Track track = contentProviderUtils.getTrack(trackId);
boolean success = ExportUtils.exportTrack(this, trackFileFormat, directoryFile, track);
boolean success = ExportUtils.exportTrack(this, directoryFile, exportTask);
// Send result to the receiver.
int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR;
@@ -6,8 +6,6 @@ import android.os.ResultReceiver;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Track;
/**
* Create a new ResultReceive to receive results.
* Your {@link #onReceiveResult} method will be called from the thread running <var>handler</var> if given, or from an arbitrary thread if null.
@@ -17,7 +15,7 @@ public class ExportServiceResultReceiver extends ResultReceiver {
public static final int RESULT_CODE_SUCCESS = 1;
public static final int RESULT_CODE_ERROR = 0;
public static final String RESULT_EXTRA_TRACK_ID = "result_extra_track_id";
public static final String RESULT_EXTRA_EXPORT_TASK = "result_extra_export_task";
private final Receiver receiver;
@@ -28,19 +26,19 @@ public class ExportServiceResultReceiver extends ResultReceiver {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
Track.Id trackId = resultData.getParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID);
ExportTask exportTask = resultData.getParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK);
switch (resultCode) {
case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(trackId);
case RESULT_CODE_ERROR -> receiver.onExportError(trackId);
case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(exportTask);
case RESULT_CODE_ERROR -> receiver.onExportError(exportTask);
default -> throw new RuntimeException("Unknown resultCode.");
}
}
public interface Receiver {
default void onExportSuccess(Track.Id trackId) {
default void onExportSuccess(ExportTask exportTask) {
}
default void onExportError(Track.Id trackId) {
default void onExportError(ExportTask exportTask) {
}
}
}
@@ -0,0 +1,87 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import java.util.Objects;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
public class ExportTask implements Parcelable {
private final String filename; //TODO Name will only be used when exporting multiple tracks
private final TrackFileFormat trackFileFormat;
private final List<Track.Id> trackIds;
public ExportTask(@Nullable String filename, @NonNull TrackFileFormat trackFileFormat, @NonNull List<Track.Id> trackIds) {
this.filename = filename;
this.trackFileFormat = trackFileFormat;
this.trackIds = trackIds;
}
protected ExportTask(Parcel in) {
filename = in.readString();
trackFileFormat = TrackFileFormat.valueOf(in.readString());
trackIds = in.createTypedArrayList(Track.Id.CREATOR);
}
public String getFilename() {
return filename;
}
public TrackFileFormat getTrackFileFormat() {
return trackFileFormat;
}
public List<Track.Id> getTrackIds() {
return trackIds;
}
public boolean isMultiExport() {
return trackIds.size() > 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExportTask that = (ExportTask) o;
return Objects.equals(filename, that.filename) && trackFileFormat == that.trackFileFormat && Objects.equals(trackIds, that.trackIds);
}
@Override
public int hashCode() {
return Objects.hash(filename, trackFileFormat, trackIds);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(filename);
dest.writeString(trackFileFormat.name());
dest.writeTypedList(trackIds);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<ExportTask> CREATOR = new Creator<>() {
@Override
public ExportTask createFromParcel(Parcel in) {
return new ExportTask(in);
}
@Override
public ExportTask[] newArray(int size) {
return new ExportTask[size];
}
};
}
@@ -98,12 +98,7 @@ public class GPXTrackExporter implements TrackExporter {
}
@Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
try {
prepare(outputStream);
writeHeader();
@@ -156,8 +151,12 @@ public class GPXTrackExporter implements TrackExporter {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
wroteSegment = true;
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
if (trackPoint.hasLocation()) {
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
} else {
sensorPoints.add(trackPoint);
}
}
case SENSORPOINT -> sensorPoints.add(trackPoint);
case TRACKPOINT -> {
@@ -273,7 +272,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(track.getCategory()) + "</type>");
printWriter.println("<type>" + StringUtils.formatCData(track.getActivityTypeLocalized()) + "</type>");
printWriter.println("<extensions>");
printWriter.println("<topografix:color>c0c0c0</topografix:color>");
@@ -45,7 +45,7 @@ import de.dennisguse.opentracks.util.StringUtils;
/**
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML version 2.3.
* https://docs.opengeospatial.org/is/12-007r2/12-007r2.html
* <a href="https://docs.opengeospatial.org/is/12-007r2/12-007r2.html">...</a>
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
@@ -59,7 +59,7 @@ public class KMLTrackExporter implements TrackExporter {
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
public static final String EXTENDED_DATA_TYPE_CATEGORY = "type";
public static final String EXTENDED_DATA_TYPE_ACTIVITYTYPE = "type";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
@@ -99,18 +99,14 @@ public class KMLTrackExporter implements TrackExporter {
this.contentProviderUtils = contentProviderUtils;
}
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
try {
prepare(outputStream);
writeHeader(tracks);
for (Track track : tracks) {
writeMarkers(track);
}
boolean hasMultipleTracks = tracks.length > 1;
boolean hasMultipleTracks = tracks.size() > 1;
if (hasMultipleTracks) {
writeMultiTrackBegin();
}
@@ -222,7 +218,7 @@ public class KMLTrackExporter implements TrackExporter {
}
}
private void writeHeader(Track[] tracks) {
private void writeHeader(List<Track> tracks) {
if (printWriter != null) {
printWriter.println(
"""
@@ -241,7 +237,7 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
Track track = tracks[0];
Track track = tracks.get(0);
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<atom:generator>" + StringUtils.formatCData(context.getString(R.string.app_name)) + "</atom:generator>");
@@ -293,7 +289,7 @@ public class KMLTrackExporter implements TrackExporter {
private void writeMultiTrackBegin() {
if (printWriter != null) {
printWriter.println("<Folder id=tour>");
printWriter.println("<Folder id=\"tracks\">");
printWriter.println("<name>" + context.getString(R.string.generic_tracks) + "</name>");
printWriter.println("<open>1</open>");
}
@@ -311,11 +307,11 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<icon>" + StringUtils.formatCData(track.getIcon()) + "</icon>");
printWriter.println("<icon>" + StringUtils.formatCData(track.getActivityType().getIconId()) + "</icon>");
printWriter.println("<opentracks:trackid>" + track.getUuid() + "</opentracks:trackid>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
writeCategory(track.getCategory());
writeActivityType(track.getActivityTypeLocalized());
printWriter.println("<MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<interpolate>1</interpolate>");
@@ -430,19 +426,19 @@ public class KMLTrackExporter implements TrackExporter {
/**
* Writes a placemark.
*
* @param name the name
* @param category the category
* @param description the description
* @param location the location
* @param name the name
* @param activityType the activityType
* @param description the description
* @param location the location
*/
private void writePlacemark(String name, String category, String description, Location location, ZoneOffset zoneOffset) {
private void writePlacemark(String name, String activityType, String description, Location location, ZoneOffset zoneOffset) {
if (location != null) {
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, location) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + KMLTrackExporter.MARKER_STYLE + "</styleUrl>");
writeCategory(category);
writeActivityType(activityType);
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
@@ -463,7 +459,7 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getLocation()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + MARKER_STYLE + "</styleUrl>");
writeCategory(marker.getCategory());
writeActivityType(marker.getCategory());
if (exportPhotos) {
printWriter.println("<Icon><href>" + KmzTrackExporter.buildKmzImageFilePath(marker) + "</href></Icon>");
@@ -518,23 +514,15 @@ public class KMLTrackExporter implements TrackExporter {
return result;
}
/**
* Writes the category.
*
* @param category the category
*/
private void writeCategory(String category) {
if (category == null || category.equals("")) {
private void writeActivityType(String activityTypeLocalized) {
if (activityTypeLocalized == null || activityTypeLocalized.equals("")) {
return;
}
printWriter.println("<ExtendedData>");
printWriter.println("<Data name=\"" + EXTENDED_DATA_TYPE_CATEGORY + "\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("<Data name=\"" + EXTENDED_DATA_TYPE_ACTIVITYTYPE + "\"><value>" + StringUtils.formatCData(activityTypeLocalized) + "</value></Data>");
printWriter.println("</ExtendedData>");
}
/**
* Writes the track style.
*/
private void writeTrackStyle() {
printWriter.println("<Style id=\"" + TRACK_STYLE + "\">");
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
@@ -28,6 +28,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -64,12 +65,7 @@ public class KmzTrackExporter implements TrackExporter {
}
@Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
// Add kml file
ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE);
@@ -91,7 +87,7 @@ public class KmzTrackExporter implements TrackExporter {
}
}
private void addImages(Context context, Track[] tracks, ZipOutputStream zipOutputStream) throws InterruptedException, IOException {
private void addImages(Context context, List<Track> tracks, ZipOutputStream zipOutputStream) throws InterruptedException, IOException {
for (Track track : tracks) {
try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, -1)) {
if (cursor != null && cursor.moveToFirst()) {
@@ -145,6 +141,6 @@ public class KmzTrackExporter implements TrackExporter {
public static String buildKmzImageFilePath(Marker marker) {
String ext = FileUtils.getExtension(marker.getPhotoUrl());
ext = ext == null ? "" : "." + ext;
return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(marker.getId().getId() + ext);
return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(marker.getId().id() + ext);
}
}
@@ -19,6 +19,7 @@ package de.dennisguse.opentracks.io.file.exporter;
import androidx.annotation.NonNull;
import java.io.OutputStream;
import java.util.List;
import de.dennisguse.opentracks.data.models.Track;
@@ -29,7 +30,5 @@ import de.dennisguse.opentracks.data.models.Track;
*/
public interface TrackExporter {
boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream);
boolean writeTrack(Track track, @NonNull OutputStream outputStream);
boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream);
}

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