Compare commits

..

120 Commits

Author SHA1 Message Date
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
Dennis Guse 9225d90e45 Release: v4.3.3 2023-04-27 18:49:23 +02:00
Dennis Guse 32dc6fef82 Bugfix: CustomLayout - title for pace was speed/GPS.
Introduced in d70e7b8788
2023-04-26 07:19:11 +02:00
Dennis Guse 96fb65b6b0 Bugfix: hidden DataFields should be hidden.
Introduced in d70e7b8788
2023-04-26 07:19:11 +02:00
dependabot[bot] 936bbf9264 Bump org.mockito:mockito-android from 5.3.0 to 5.3.1
Bumps [org.mockito:mockito-android](https://github.com/mockito/mockito) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.3.0...v5.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-24 07:01:14 +02:00
Dennis Guse 241a52ee7c Release: v4.3.2 2023-04-23 09:57:41 +02:00
Dennis Guse c66e285bf5 README: move human tech badge to codeberg. 2023-04-23 09:06:15 +02:00
Dennis Guse 8c1ddc077d Merge remote-tracking branch 'weblate/main' 2023-04-22 13:29:01 +02:00
dependabot[bot] 61c18af639 Bump com.android.tools.build:gradle from 8.1.0-alpha11 to 8.1.0-beta01
Bumps com.android.tools.build:gradle from 8.1.0-alpha11 to 8.1.0-beta01.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-21 10:19:35 +02:00
dependabot[bot] b83df3ac4b Bump androidx.core:core-splashscreen from 1.0.0 to 1.0.1
Bumps androidx.core:core-splashscreen from 1.0.0 to 1.0.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-20 07:29:34 +02:00
plum7x 53f1c4b7f3 Translated using Weblate (Chinese (Traditional))
Currently translated at 2.9% (3 of 103 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/zh_Hant/
2023-04-19 11:50:26 +02:00
plum7x 6290bdbab0 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (506 of 506 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-04-19 11:50:26 +02:00
josé m bd0d85cd37 Translated using Weblate (Galician)
Currently translated at 100.0% (506 of 506 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-04-19 11:50:25 +02:00
Dennis Guse ba08417236 Cleanup: moved non-translatable string to correct file.
Part of #1527.
2023-04-18 12:55:21 +02:00
Dennis Guse 63281a3ca8 README: add dedicated link ot weblate. 2023-04-16 20:02:09 +02:00
Dennis Guse fae0cf8454 Merge pull request #1525 from OpenTracksApp/java17cleanup
Java 17 cleanup
2023-04-13 15:38:18 +02:00
Dennis Guse efddb31391 Cleanup: use Java17 switch statements.
AndroidStudio code cleanup.
2023-04-13 15:10:17 +02:00
Dennis Guse 3a8e547e10 Gradle: use configureEach instead of all. 2023-04-13 14:52:41 +02:00
Dennis Guse fe09d18197 Merge pull request #1523 from OpenTracksApp/bugKML#1522
Bug kml#1522
2023-04-13 14:34:48 +02:00
Dennis Guse 93bcf8d494 Bugfix: KML header malformed.
Fixes #1522.
Introduced in 82fe859c84
2023-04-12 23:09:04 +02:00
Dennis Guse 819f454bd8 Refactored TrackFileFormat interface. 2023-04-12 22:41:56 +02:00
Dennis Guse 7d58014b15 Cleanup/Java 17: use multi-line text blocks. 2023-04-12 22:39:30 +02:00
Dennis Guse 9dac95569c AGP: use Java 17. 2023-04-12 22:31:41 +02:00
Dennis Guse 96c98ad275 Merge pull request #1521 from OpenTracksApp/agp81
Upgrade to AGP 8.1.0-alpha
2023-04-12 19:37:15 +02:00
Dennis Guse 7dcc0ec8bc AGP: generate local_config.xml 2023-04-12 19:30:57 +02:00
Dennis Guse 09ecc03be5 AGP: update 8.0.0 (incl. Java 17). 2023-04-12 19:30:57 +02:00
dependabot[bot] d7a5ffb938 Bump org.mockito:mockito-android from 5.2.0 to 5.3.0
Bumps [org.mockito:mockito-android](https://github.com/mockito/mockito) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.2.0...v5.3.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-04-12 09:32:03 +02:00
Dennis Guse 0605dbb0ab Release: v4.3.1 2023-04-07 18:11:52 +02:00
Dennis Guse bf03de6be7 Bugfix: GPS disabled warning was showing (repeatedly but incorrectly).
Fixes #1429.
2023-04-07 11:13:57 +02:00
Dennis Guse d65ce361f7 Upgrade gradlew to 8.0.2. 2023-04-06 07:37:43 +02:00
Dennis Guse 3f600f6be7 Voice announcements: added setting to skip announcements on the device's internal speaker.
Fixes #1392.
2023-04-05 18:26:29 +02:00
dependabot[bot] 9aab3c216c Bump com.android.tools:desugar_jdk_libs from 2.0.2 to 2.0.3
Bumps [com.android.tools:desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/google/desugar_jdk_libs/releases)
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 19:25:16 +02:00
Dennis Guse 7935185812 UI: split stats_item between generic and sensor (some bugfixes).
Part of #24.
2023-03-29 21:50:42 +02:00
Dennis Guse 4c1b1be96c UI: split stats_item between generic and sensor.
Part of #24.
2023-03-28 21:08:46 +02:00
Dennis Guse 5762830655 Merge pull request #1461 from OpenTracksApp/refactorSensor
Refactor: unify the internal sensor handling.
2023-03-28 21:08:08 +02:00
Dennis Guse 6d61206782 Renamed GPSHandler to GPSManager. 2023-03-28 20:58:23 +02:00
Dennis Guse b9dedef292 Refactor: introduced SensorManager. 2023-03-28 20:58:23 +02:00
Dennis Guse f3caecd049 Refactor: rename BluetoothConnectionManager. 2023-03-27 22:59:31 +02:00
Dennis Guse e16780277f Refactor: introduced SensorConnector interface. 2023-03-27 22:59:31 +02:00
Dennis Guse 13484685a3 Merge remote-tracking branch 'weblate/main' 2023-03-25 09:06:27 +01:00
plum7x 7b745d9837 Update strings.xml 2023-03-17 07:41:32 +01:00
plum7x 52742064f6 Updated traditional Chinese translation (#1498) 2023-03-14 20:50:51 +01:00
josé m 2edf5582aa Translated using Weblate (Galician)
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/gl/
2023-03-11 18:37:57 +01:00
dependabot[bot] 808eb92bf2 Bump org.mockito:mockito-android from 4.11.0 to 5.2.0
Bumps [org.mockito:mockito-android](https://github.com/mockito/mockito) from 4.11.0 to 5.2.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.11.0...v5.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-10 06:13:23 +01:00
Dennis Guse c0b84c3d72 Release: v4.3.0 2023-03-09 05:20:22 +01:00
Miguel Almeida e275d3e5c0 Enable backups if client side encryption is enabled 2023-03-08 23:12:26 +01:00
Dennis Guse b4ebad2c3c Merge remote-tracking branch 'weblate/main' 2023-03-07 21:25:56 +01:00
dependabot[bot] 1ab76e1317 Bump com.android.tools.build:gradle from 7.4.1 to 7.4.2
Bumps com.android.tools.build:gradle from 7.4.1 to 7.4.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-28 06:04:48 -05:00
nobkd 3d1104dd0b remove typo 2023-02-17 10:44:41 -05:00
dependabot[bot] 2df45d31de Bump androidx.appcompat:appcompat from 1.6.0 to 1.6.1
Bumps androidx.appcompat:appcompat from 1.6.0 to 1.6.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-12 14:06:47 +01:00
Allan Nordhøy 5436981e0d Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.4% (492 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-02-10 07:38:30 +01:00
dependabot[bot] fd2b1bec7d Bump com.android.tools:desugar_jdk_libs from 2.0.1 to 2.0.2
Bumps [com.android.tools:desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/google/desugar_jdk_libs/releases)
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-06 12:18:53 +01:00
elastic rock bb1eaa5b56 Android system per-app language preferences 2023-02-05 00:58:21 +01:00
264 changed files with 3311 additions and 2875 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
java-version: '17'
- name: Assemble APK
shell: bash
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
java-version: '17'
- run: ./gradlew lint
+5 -3
View File
@@ -2,7 +2,7 @@
_OpenTracks_ is a sport tracking application that completely respects your privacy.
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
[![Awesome Humane Tech](https://codeberg.org/teaserbot-labs/delightful-humane-design/raw/branch/main/humane-tech-badge.svg)](https://delightful.club/delightful-humane-design/#health)
<table>
<tr>
@@ -40,8 +40,10 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</tr>
</table>
Translations are hosted on <a href="https://hosted.weblate.org/engage/opentracks/">
hosted.weblate.org</a>.
<a href="https://hosted.weblate.org/engage/opentracks/">
<img src="https://hosted.weblate.org/widgets/opentracks/-/horizontal-auto.svg" alt="Translation status" />
<img src="https://hosted.weblate.org/widgets/opentracks/-/horizontal-auto.svg" alt="Translation status" />
</a>
## Screenshots
@@ -150,7 +152,7 @@ The developer documentation is in [README_API.md](README_API.md).
## Project history
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
Initially, _My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html).
_My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html).
In 2016, [Google decided to discontinue](https://support.google.com/maps/answer/6333516) _My Tracks_ and stopped distributing it via the Google Play store in April 2016.
Then [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
Plonk42's version is available [here](https://github.com/Plonk42/mytracks).
+20 -14
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.1'
classpath 'com.android.tools.build:gradle:8.1.0-beta04'
}
}
@@ -47,25 +47,30 @@ android {
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
compileSdk 33
compileSdk 34
buildFeatures {
viewBinding true
buildConfig true
}
androidResources {
generateLocaleConfig = true
}
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 5141
versionName "v4.2.2"
versionCode 5262
versionName "v4.5.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdk 21
targetSdk 33
targetSdk 34
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
@@ -106,11 +111,11 @@ android {
}
}
applicationVariants.all { variant ->
applicationVariants.configureEach { variant ->
variant.resValue "string", "applicationId", variant.applicationId
if (variant.buildType.name == 'nightly') {
variant.outputs.all {
variant.outputs.configureEach {
setVersionCodeOverride(getVersionCode())
setVersionNameOverride(getVersionName())
outputFileName = "${applicationId}_${variant.versionCode}.apk"
@@ -120,22 +125,23 @@ android {
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.preference:preference:1.2.0'
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.0'
implementation 'androidx.core:core-splashscreen:1.0.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:4.11.0'
androidTestImplementation 'org.mockito:mockito-android:5.4.0'
androidTestUtil 'androidx.test:orchestrator:1.4.2'
}
@@ -0,0 +1,5 @@
v4.3.0: OpenTracks
Changes:
- Allow backups (incl. encrypted to the Google)
- Android 13: allow app-specific language settings
@@ -0,0 +1,8 @@
v4.3.1: OpenTracks
Changes:
- Setting to skip voice announcements on device speaker
Bugfix:
- GPS disabled warning was falsely shown
@@ -0,0 +1,9 @@
v4.3.2: OpenTracks
Bugfix:
- KML header was misformed (#1522)
Developer:
- Use Java 17 (multi-line text, switch)
- API33: locale config is generated
@@ -0,0 +1,5 @@
v4.3.3: OpenTracks
Bugfixes:
- Hidden fields were shown (introduced in v4.2.0)
- Speed was shown instead of pace (introduced in v4.2.0)
@@ -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
@@ -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
@@ -0,0 +1,22 @@
一個尊重您的隱私的運動蹤跡紀錄器。
運動與戶外活動賦予幸福、技能與自信。
由保持追蹤您的訓練來重視您的健康。
紀錄您的跑步或健行,並且在您騎自行車時給您一台具有大螢幕的自行車電腦。
用照片標記您一路上有趣的地點。
保持紀錄非常詳細的統計資料以供分析。
僅分享資料給您想要分享的其他人。
* 語音播報。
* 支援藍芽 LE 感測器:心律、速度與距離 (騎自行車)、踏頻 (騎自行車)、功率計 (騎自行車)。
* 海拔上升與下降:通過氣壓感測器。
* EGM2008 海拔顯示 (平均海平面以上);匯出為 WGS84。
* 匯出蹤跡資料為 KMZ (包含照片)、KML、GPX。
* 沒有網路存取或額外的權限需求。
* 深色與淺色主題,取決於系統設定。
* 沒有廣告。
此為 自由軟體 / 免費軟體 / 開放原始碼
這意味您可以使用、學習、修改與分享此專案的原始碼。
授權條款:Apache 2.0
@@ -0,0 +1 @@
一個尊重您的隱私的運動蹤跡紀錄器。
@@ -0,0 +1 @@
OpenTracks
+1 -1
View File
@@ -1,2 +1,2 @@
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=256m
org.gradle.jvmargs=-Xmx1024m
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-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+14 -4
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,10 +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##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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"'
@@ -143,12 +143,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
@@ -205,6 +209,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+9 -6
View File
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
@@ -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());
@@ -15,6 +15,8 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.Manifest;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -38,7 +40,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
@@ -13,6 +13,8 @@ import static org.hamcrest.Matchers.anything;
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.Manifest;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -31,8 +33,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,18 @@
package de.dennisguse.opentracks;
import android.Manifest;
import android.os.Build;
import androidx.test.rule.GrantPermissionRule;
import de.dennisguse.opentracks.test.BuildConfig;
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);
@@ -27,21 +27,23 @@ public class KmlTrackExporterTest {
*/
@Test
public void writeCloseSegment_only_write_sensordata_if_present() {
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
"<coord/>\n" +
"<when>1970-01-01T01:00:00+01:00</when>\n" +
"<coord/>\n" +
"<ExtendedData>\n" +
"<SchemaData schemaUrl=\"#schema\">\n" +
"</SchemaData>\n" +
"</ExtendedData>\n" +
"</Track>\n";
String expected = """
<when>1970-01-01T00:00:00Z</when>
<coord/>
<when>1970-01-01T01:00:00+01:00</when>
<coord/>
<ExtendedData>
<SchemaData schemaUrl="#schema">
</SchemaData>
</ExtendedData>
</Track>
""";
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
KMLTrackExporter kmlTrackWriter = (KMLTrackExporter) TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
KMLTrackExporter kmlTrackWriter = (KMLTrackExporter) TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context, null);
kmlTrackWriter.prepare(outputStream);
kmlTrackWriter.writeTrackPoint(ZoneOffset.UTC, trackPoint);
@@ -57,7 +57,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;
@@ -97,7 +96,7 @@ 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 String TRACK_ACTIVITY_TYPE = "the activity type";
private static final String TRACK_DESCRIPTION = "the description";
private File tmpFile;
@@ -165,8 +164,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.setRemoteSensorManager(mockRemoteSensorManager);
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -179,12 +177,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.setRemoteSensorManager(mockRemoteSensorManager);
service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId);
track.setIcon(TRACK_ICON);
track.setCategory(TRACK_CATEGORY);
track.setActivityType(TRACK_ACTIVITY_TYPE);
track.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.updateTrack(track);
@@ -204,11 +201,11 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.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,7 +216,7 @@ public class ExportImportTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getActivityType(), importedTrack.getActivityType());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon());
@@ -287,10 +284,10 @@ public class ExportImportTest {
editor.commit();
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
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);
@@ -310,11 +307,11 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.GPX.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
@@ -326,7 +323,7 @@ public class ExportImportTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getActivityType(), importedTrack.getActivityType());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
@@ -428,11 +425,11 @@ public class ExportImportTest {
editor.commit();
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils);
// 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);
@@ -453,11 +450,11 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.CSV.createTrackExporter(context);
TrackExporter trackExporter = TrackFileFormat.CSV.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);
// then
@@ -495,9 +492,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,25 +502,24 @@ 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.setRemoteSensorManager(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) {
AltitudeSumManager altitudeSumManager = trackPointCreator.getAltitudeSumManager();
AltitudeSumManager altitudeSumManager = trackPointCreator.getSensorManager().getAltitudeSumManager();
altitudeSumManager.setAltitudeGain_m(altitudeGain);
altitudeSumManager.setAltitudeLoss_m(altitudeGain);
}
@@ -545,6 +538,6 @@ public class ExportImportTest {
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(time);
trackPointCreator.getGpsHandler().onLocationChanged(location);
trackPointCreator.onChange(location);
}
}
@@ -79,7 +79,7 @@ public class GPXTrackImporterTest {
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("the category", importedTrack.getCategory());
assertEquals("the category", importedTrack.getActivityType());
assertEquals("the description", importedTrack.getDescription());
assertEquals("2021-01-07 22:51", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
@@ -133,7 +133,7 @@ public class GPXTrackImporterTest {
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getActivityType());
assertEquals("", importedTrack.getDescription());
assertEquals("20210907_213924.gpx", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
@@ -181,7 +181,7 @@ public class GPXTrackImporterTest {
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getActivityType());
assertEquals("", importedTrack.getDescription());
assertEquals("20210907_213924.gpx", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
@@ -231,8 +231,8 @@ public class GPXTrackImporterTest {
importTrackId = importer.importFile(inputStream).get(0);
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
trackExporter.writeTrack(importedTrack, outputStream);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils);
trackExporter.writeTrack(List.of(importedTrack), outputStream);
// then
String expected = new BufferedReader(
@@ -70,7 +70,7 @@ public class KMLTrackImporterTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getActivityType());
assertEquals("", importedTrack.getDescription());
assertEquals("", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
@@ -119,7 +119,7 @@ public class KMLTrackImporterTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.getCategory());
assertEquals("", importedTrack.getActivityType());
assertEquals("", importedTrack.getDescription());
assertEquals("", importedTrack.getName());
assertEquals("", importedTrack.getIcon());
@@ -159,7 +159,7 @@ public class KMLTrackImporterTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("unknown", importedTrack.getCategory());
assertEquals("unknown", importedTrack.getActivityType());
assertEquals("Test Track", importedTrack.getDescription());
assertEquals("2020-11-28 18:06", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
@@ -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));
}
}
}
@@ -45,6 +45,8 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests insert location.
* Note: on API23, the TrackRecordingService may not be stopped properly before the next test.
* So, if something fails, subsequent tests may be affected.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestRecording {
@@ -112,7 +114,7 @@ public class TrackRecordingServiceTestRecording {
String startTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// then
assertEquals(new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null)
@@ -149,7 +151,7 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
String pauseTime = "2020-02-02T02:02:03Z";
@@ -195,7 +197,7 @@ public class TrackRecordingServiceTestRecording {
String starTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(starTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
String pauseTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(pauseTime);
@@ -227,7 +229,7 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
String stopTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(stopTime);
@@ -237,7 +239,7 @@ public class TrackRecordingServiceTestRecording {
String resumeTime = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(resumeTime);
service.resumeTrack(trackId);
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// then
new TrackPointAssert().assertEquals(List.of(
@@ -258,12 +260,13 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager();
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored
String sensor3 = "2020-02-02T02:02:13Z";
@@ -297,7 +300,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
@@ -371,7 +374,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -436,7 +439,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -500,7 +503,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -564,7 +567,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -652,7 +655,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -702,7 +705,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -748,7 +751,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
@@ -812,8 +815,8 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager();
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager();
// when
String sensor1 = "2020-02-02T02:02:03Z";
@@ -1,17 +1,12 @@
package de.dennisguse.opentracks.services;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import androidx.test.rule.ServiceTestRule;
import org.mockito.Mockito;
import java.time.Clock;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -24,15 +19,6 @@ public class TrackRecordingServiceTestUtils {
public static void resetService(ServiceTestRule mServiceRule, Context context) throws TimeoutException {
// Let's use default values.
PreferencesUtils.clear();
// Reset service (if some previous test failed)
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
BluetoothRemoteSensorManager remoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class);
service.getTrackPointCreator().setRemoteSensorManager(remoteSensorManager);
service.getTrackPointCreator().setClock(Clock.systemUTC());
service.endCurrentTrack();
}
static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
@@ -44,6 +30,6 @@ public class TrackRecordingServiceTestUtils {
location.setSpeed(speed);
trackPointCreator.setClock(time);
trackPointCreator.getGpsHandler().onLocationChanged(location);
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
}
}
@@ -185,7 +185,7 @@ 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);
@@ -207,7 +207,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 +223,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 +245,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);
@@ -26,7 +26,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(MockitoJUnitRunner.class)
public class GPSHandlerTest {
public class GPSManagerTest {
private final Context context = ApplicationProvider.getApplicationContext();
@@ -34,7 +34,7 @@ public class GPSHandlerTest {
private TrackPointCreator trackPointCreator;
@InjectMocks
private GPSHandler locationHandler;
private GPSManager locationHandler;
@BeforeClass
public static void preSetUp() {
@@ -50,7 +50,7 @@ public class GPSHandlerTest {
Mockito.when(trackPointCreator.createNow())
.thenReturn(Instant.now());
locationHandler.onStart(context, new Handler());
locationHandler.start(context, new Handler());
}
/**
@@ -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 GPSHandler 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.setActivityType("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.setActivityType("hiking");
String expected = //"Created by"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks (Debug)</a><p>"
+ "Name: -<br>"
@@ -78,7 +78,7 @@ public class TrackStatisticsUpdaterTest {
// when
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
data.trackPoints.forEach(subject::addTrackPoint);
data.trackPoints().forEach(subject::addTrackPoint);
// then
TrackStatistics statistics = subject.getTrackStatistics();
@@ -41,7 +41,7 @@ public class AggregatedStatisticsTest {
Track track = new Track();
track.setIcon(TrackIconUtils.getIconValue(context, category));
track.setCategory(category);
track.setActivityType(category);
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).getActivityType());
assertEquals(driving, aggregatedStatistics.getItem(3).getActivityType());
}
}
}
@@ -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.setActivityType(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()));
@@ -34,23 +34,24 @@ public class EGM2008UtilsTest {
int expectedLength = 18671444;
int expectedHeaderLength = 404;
String expectedHeader = "P5\n" +
"# Geoid file in PGM format for the GeographicLib::Geoid class\n" +
"# Description WGS84 EGM2008, 5-minute grid\n" +
"# URL http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008\n" +
"# DateTime 2009-08-29 18:45:00\n" +
"# MaxBilinearError 0.478\n" +
"# RMSBilinearError 0.012\n" +
"# MaxCubicError 0.294\n" +
"# RMSCubicError 0.005\n" +
"# Offset -108\n" +
"# Scale 0.003\n" +
"# Origin 90N 0E\n" +
"# AREA_OR_POINT Point\n" +
"# Vertical_Datum WGS84\n" +
"4320 2161\n" +
"65535" +
"\n";
String expectedHeader = """
P5
# Geoid file in PGM format for the GeographicLib::Geoid class
# Description WGS84 EGM2008, 5-minute grid
# URL http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008
# DateTime 2009-08-29 18:45:00
# MaxBilinearError 0.478
# RMSBilinearError 0.012
# MaxCubicError 0.294
# RMSCubicError 0.005
# Offset -108
# Scale 0.003
# Origin 90N 0E
# AREA_OR_POINT Point
# Vertical_Datum WGS84
4320 2161
65535
""";
// when
try (InputStream inputStream = context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA)) {
@@ -78,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
@@ -93,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
@@ -108,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
@@ -123,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
@@ -138,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
@@ -153,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
@@ -173,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
@@ -189,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
@@ -204,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);
}
@@ -52,8 +52,8 @@ public class StatisticsUtilsTest extends TestCase {
recordingLayout.addField(new DataField("key5", true, true, false));
// when
RecordingLayout resultTrue = StatisticsUtils.filterVisible(recordingLayout, true);
RecordingLayout resultFalse = StatisticsUtils.filterVisible(recordingLayout, false);
RecordingLayout resultTrue = recordingLayout.toRecordingLayout(true);
RecordingLayout resultFalse = recordingLayout.toRecordingLayout(false);
// then
assertEquals(resultTrue.getFields().size(), 3);
+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>
+10 -1
View File
@@ -62,7 +62,9 @@ limitations under the License.
<application
android:name=".Startup"
android:allowBackup="false"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules_30_lower"
android:dataExtractionRules="@xml/backup_rules_31_higher"
android:hardwareAccelerated="true"
android:hasFragileUserData="false"
android:icon="@drawable/ic_launcher"
@@ -87,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"
@@ -96,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" />
@@ -346,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);
}
@@ -74,7 +74,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
viewBinding.fields.trackEditName.setText(track.getName());
viewBinding.fields.trackEditActivityType.setText(track.getCategory());
viewBinding.fields.trackEditActivityType.setText(track.getActivityType());
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, ActivityType.getLocalizedStrings(this));
viewBinding.fields.trackEditActivityType.setAdapter(adapter);
@@ -109,7 +109,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);
}
@@ -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,12 +43,12 @@ 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;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
@@ -83,7 +83,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 +137,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 +164,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,7 +212,7 @@ 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);
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
@@ -210,13 +228,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 +264,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 +272,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
return true;
});
setSupportActionBar(viewBinding.bottomAppBar);
setSupportActionBar(viewBinding.trackListToolbar);
loadData(getIntent());
}
@@ -309,21 +327,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(R.id.search_edit_frame).setPadding(0, 0, 48, 0);
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(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 +344,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 +358,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 +437,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();
}
}
}
@@ -504,13 +484,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
return true;
}
if (itemId == R.id.list_context_menu_aggregated_stats) {
Intent intent = IntentUtils.newIntent(this, AggregatedStatisticsActivity.class)
.putParcelableArrayListExtra(AggregatedStatisticsActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
startActivity(intent);
return true;
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
viewBinding.trackList.setItemChecked(i, true);
@@ -532,11 +505,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 +516,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, TracksColumns.STARTTIME, TracksColumns.STARTTIME_OFFSET,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT};
final String sortOrder = TracksColumns.STARTTIME + " DESC";
@@ -557,7 +526,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 + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, sortOrder);
}
@@ -581,7 +550,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);
@@ -274,18 +273,14 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return StatisticsRecordedFragment.newInstance(trackId);
case 1:
return IntervalsFragment.newInstance(trackId, true);
case 2:
return ChartFragment.newInstance(false);
case 3:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
return switch (position) {
case 0 -> StatisticsRecordedFragment.newInstance(trackId);
case 1 -> IntervalsFragment.newInstance(trackId, true);
case 2 -> ChartFragment.newInstance(false);
case 3 -> ChartFragment.newInstance(true);
default ->
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
};
}
@Override
@@ -294,18 +289,14 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.track_detail_stats_tab);
case 1:
return getString(R.string.track_detail_intervals_tab);
case 2:
return getString(R.string.settings_chart_by_time);
case 3:
return getString(R.string.settings_chart_by_distance);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
return switch (position) {
case 0 -> getString(R.string.track_detail_stats_tab);
case 1 -> getString(R.string.track_detail_intervals_tab);
case 2 -> getString(R.string.settings_chart_by_time);
case 3 -> getString(R.string.settings_chart_by_distance);
default ->
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
};
}
}
@@ -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;
@@ -45,6 +43,7 @@ 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;
@@ -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);
@@ -320,8 +319,8 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public void onChooseActivityTypeDone(String iconValue) {
Track track = contentProviderUtils.getTrack(trackId);
String category = getString(TrackIconUtils.getIconActivityType(iconValue));
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
String activityType = getString(TrackIconUtils.getIconActivityType(iconValue));
TrackUtils.updateTrack(this, track, null, activityType, null, contentProviderUtils);
}
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
@@ -333,18 +332,14 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return StatisticsRecordingFragment.newInstance();
case 1:
return IntervalsFragment.newInstance(trackId, false);
case 2:
return ChartFragment.newInstance(false);
case 3:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
return switch (position) {
case 0 -> StatisticsRecordingFragment.newInstance();
case 1 -> IntervalsFragment.newInstance(trackId, false);
case 2 -> ChartFragment.newInstance(false);
case 3 -> ChartFragment.newInstance(true);
default ->
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
};
}
@Override
@@ -353,18 +348,14 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.track_detail_stats_tab);
case 1:
return getString(R.string.track_detail_intervals_tab);
case 2:
return getString(R.string.settings_chart_by_time);
case 3:
return getString(R.string.settings_chart_by_distance);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
return switch (position) {
case 0 -> getString(R.string.track_detail_stats_tab);
case 1 -> getString(R.string.track_detail_intervals_tab);
case 2 -> getString(R.string.settings_chart_by_time);
case 3 -> getString(R.string.settings_chart_by_distance);
default ->
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
};
}
}
@@ -381,13 +372,18 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
private void onGpsStatusChanged(GpsStatusValue gpsStatusValue) {
if (gpsStatusValue.isGpsStarted() && snackbar != null && snackbar.isShown()) {
snackbar.dismiss();
boolean snackbarShowing = snackbar != null && snackbar.isShown();
if (gpsStatusValue.isGpsStarted()) {
if (snackbarShowing) {
snackbar.dismiss();
}
return;
}
if (gpsStatusValue != GpsStatusValue.GPS_DISABLED) {
if (snackbarShowing) {
return;
}
snackbar = Snackbar
.make(viewBinding.trackRecordingCoordinatorLayout,
getString(R.string.gps_recording_status, getString(gpsStatusValue.message), getString(R.string.gps_recording_without_signal)),
@@ -50,7 +50,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
viewBinding.trackEditName.setText(track.getName());
viewBinding.trackEditActivityType.setText(track.getCategory());
viewBinding.trackEditActivityType.setText(track.getActivityType());
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, ActivityType.getLocalizedStrings(this));
viewBinding.trackEditActivityType.setAdapter(adapter);
@@ -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.getActivityType()))
.build(this)
.getSpeedParts(track.getTrackStatistics().getAverageMovingSpeed());
viewBinding.speed.setText(parts.first);
@@ -164,7 +164,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
return;
}
category = track.getCategory();
category = track.getActivityType();
boolean reportSpeed = PreferencesUtils.isReportSpeed(category);
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
viewBinding.chartView.setReportSpeed(reportSpeed);
@@ -210,16 +210,11 @@ abstract class ChartValueSeries {
}
int getTitleId(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return metricTitleId;
case IMPERIAL:
return imperialTitleId;
case NAUTICAL_IMPERIAL:
return nauticalTitleId;
default:
throw new RuntimeException("Not implemented");
}
return switch (unitSystem) {
case METRIC -> metricTitleId;
case IMPERIAL_FEET, IMPERIAL_METER -> imperialTitleId;
case NAUTICAL_IMPERIAL -> nauticalTitleId;
};
}
Paint getTitlePaint() {
@@ -733,16 +733,11 @@ public class ChartView extends View {
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
switch (unitSystem) {
case METRIC:
return context.getString(R.string.unit_kilometer);
case IMPERIAL:
return context.getString(R.string.unit_mile);
case NAUTICAL_IMPERIAL:
return context.getString(R.string.unit_nautical_mile);
default:
throw new RuntimeException("Not implemented");
}
return switch (unitSystem) {
case METRIC -> context.getString(R.string.unit_kilometer);
case IMPERIAL_FEET, IMPERIAL_METER -> context.getString(R.string.unit_mile);
case NAUTICAL_IMPERIAL -> context.getString(R.string.unit_nautical_mile);
};
} else {
return context.getString(R.string.description_time);
}
@@ -100,7 +100,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 categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int startTimeOffsetIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET);
int stopTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME);
@@ -129,7 +129,7 @@ public class ContentProviderUtils {
track.setDescription(cursor.getString(descriptionIndex));
}
if (!cursor.isNull(categoryIndex)) {
track.setCategory(cursor.getString(categoryIndex));
track.setActivityType(cursor.getString(categoryIndex));
}
if (!cursor.isNull(startTimeIndex)) {
@@ -290,7 +290,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, track.getActivityType());
values.put(TracksColumns.STARTTIME_OFFSET, track.getZoneOffset().getTotalSeconds());
if (trackStatistics.getStartTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
@@ -403,7 +403,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 +422,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 +442,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 +495,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 +503,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());
@@ -133,20 +133,12 @@ public class CustomContentProvider extends ContentProvider {
@Override
public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
String table;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
break;
case MARKERS:
table = MarkerColumns.TABLE_NAME;
break;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
String table = switch (getUrlType(url)) {
case TRACKPOINTS -> TrackPointsColumns.TABLE_NAME;
case TRACKS -> TracksColumns.TABLE_NAME;
case MARKERS -> MarkerColumns.TABLE_NAME;
default -> throw new IllegalArgumentException("Unknown URL " + url);
};
Log.w(TAG, "Deleting from table " + table);
int totalChangesBefore = getTotalChanges();
@@ -186,24 +178,15 @@ public class CustomContentProvider extends ContentProvider {
@Override
public String getType(@NonNull Uri url) {
switch (getUrlType(url)) {
case TRACKPOINTS:
return TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_BY_ID:
case TRACKPOINTS_BY_TRACKID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS:
return TracksColumns.CONTENT_TYPE;
case TRACKS_BY_ID:
return TracksColumns.CONTENT_ITEMTYPE;
case MARKERS:
return MarkerColumns.CONTENT_TYPE;
case MARKERS_BY_ID:
case MARKERS_BY_TRACKID:
return MarkerColumns.CONTENT_ITEMTYPE;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
return switch (getUrlType(url)) {
case TRACKPOINTS -> TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_BY_ID, TRACKPOINTS_BY_TRACKID -> TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS -> TracksColumns.CONTENT_TYPE;
case TRACKS_BY_ID -> TracksColumns.CONTENT_ITEMTYPE;
case MARKERS -> MarkerColumns.CONTENT_TYPE;
case MARKERS_BY_ID, MARKERS_BY_TRACKID -> MarkerColumns.CONTENT_ITEMTYPE;
default -> throw new IllegalArgumentException("Unknown URL " + url);
};
}
@Override
@@ -251,47 +234,47 @@ public class CustomContentProvider extends ContentProvider {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String sortOrder = null;
switch (getUrlType(url)) {
case TRACKPOINTS:
case TRACKPOINTS -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
break;
case TRACKPOINTS_BY_ID:
}
case TRACKPOINTS_BY_ID -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns._ID + "=" + ContentUris.parseId(url));
break;
case TRACKPOINTS_BY_TRACKID:
}
case TRACKPOINTS_BY_TRACKID -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
case TRACKS:
}
case TRACKS -> {
if (projection != null && Arrays.asList(projection).contains(TracksColumns.MARKER_COUNT)) {
queryBuilder.setTables(TracksColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT " + MarkerColumns.TRACKID + " AS markerTrackId, COUNT(*) AS " + TracksColumns.MARKER_COUNT + " FROM " + MarkerColumns.TABLE_NAME + " GROUP BY " + MarkerColumns.TRACKID + ") ON (" + TracksColumns.TABLE_NAME + "." + TracksColumns._ID + "= markerTrackId)");
} else {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
}
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
break;
case TRACKS_BY_ID:
}
case TRACKS_BY_ID -> {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
case TRACKS_SENSOR_STATS:
}
case TRACKS_SENSOR_STATS -> {
long trackId = ContentUris.parseId(url);
return db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)});
case MARKERS:
}
case MARKERS -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
break;
case MARKERS_BY_ID:
}
case MARKERS_BY_ID -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns._ID + "=" + ContentUris.parseId(url));
break;
case MARKERS_BY_TRACKID:
}
case MARKERS_BY_TRACKID -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
default -> throw new IllegalArgumentException("Unknown url " + url);
}
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), url);
@@ -304,41 +287,40 @@ public class CustomContentProvider extends ContentProvider {
String table;
String whereClause;
switch (getUrlType(url)) {
case TRACKPOINTS:
case TRACKPOINTS -> {
table = TrackPointsColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKPOINTS_BY_ID:
}
case TRACKPOINTS_BY_ID -> {
table = TrackPointsColumns.TABLE_NAME;
whereClause = TrackPointsColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
case TRACKS:
}
case TRACKS -> {
table = TracksColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKS_BY_ID:
}
case TRACKS_BY_ID -> {
table = TracksColumns.TABLE_NAME;
whereClause = TracksColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
case MARKERS:
}
case MARKERS -> {
table = MarkerColumns.TABLE_NAME;
whereClause = where;
break;
case MARKERS_BY_ID:
}
case MARKERS_BY_ID -> {
table = MarkerColumns.TABLE_NAME;
whereClause = MarkerColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
default -> throw new IllegalArgumentException("Unknown url " + url);
}
int count;
try {
@@ -371,16 +353,12 @@ public class CustomContentProvider extends ContentProvider {
* @param contentValues the content values
*/
private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) {
switch (urlType) {
case TRACKPOINTS:
return insertTrackPoint(url, contentValues);
case TRACKS:
return insertTrack(url, contentValues);
case MARKERS:
return insertMarker(url, contentValues);
default:
throw new IllegalArgumentException("Unknown url " + url);
}
return switch (urlType) {
case TRACKPOINTS -> insertTrackPoint(url, contentValues);
case TRACKS -> insertTrack(url, contentValues);
case MARKERS -> insertMarker(url, contentValues);
default -> throw new IllegalArgumentException("Unknown url " + url);
};
}
private Uri insertTrackPoint(Uri url, ContentValues values) {
@@ -61,42 +61,18 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
for (int toVersion = oldVersion + 1; toVersion <= newVersion; toVersion++) {
Log.i(TAG, "Upgrade from " + oldVersion + " to " + toVersion);
switch (toVersion) {
case 24:
upgradeFrom23to24(db);
break;
case 25:
upgradeFrom24to25(db);
break;
case 26:
upgradeFrom25to26(db);
break;
case 27:
upgradeFrom26to27(db);
break;
case 28:
upgradeFrom27to28(db);
break;
case 29:
upgradeFrom28to29(db);
break;
case 30:
upgradeFrom29to30(db);
break;
case 31:
upgradeFrom30to31(db);
break;
case 32:
upgradeFrom31to32(db);
break;
case 33:
upgradeFrom32to33(db);
break;
case 34:
upgradeFrom33to34(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
case 24 -> upgradeFrom23to24(db);
case 25 -> upgradeFrom24to25(db);
case 26 -> upgradeFrom25to26(db);
case 27 -> upgradeFrom26to27(db);
case 28 -> upgradeFrom27to28(db);
case 29 -> upgradeFrom28to29(db);
case 30 -> upgradeFrom29to30(db);
case 31 -> upgradeFrom30to31(db);
case 32 -> upgradeFrom31to32(db);
case 33 -> upgradeFrom32to33(db);
case 34 -> upgradeFrom33to34(db);
default -> throw new RuntimeException("Not implemented: upgrade to " + toVersion);
}
}
}
@@ -106,41 +82,18 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
for (int toVersion = oldVersion - 1; toVersion >= newVersion; toVersion--) {
Log.i(TAG, "Downgrade from " + oldVersion + " to " + toVersion);
switch (toVersion) {
case 23:
downgradeFrom24to23(db);
break;
case 24:
downgradeFrom25to24(db);
break;
case 25:
downgradeFrom26to25(db);
break;
case 26:
downgradeFrom27to26(db);
break;
case 27:
downgradeFrom28to27(db);
break;
case 28:
downgradeFrom29to28(db);
break;
case 29:
downgradeFrom30to29(db);
break;
case 30:
downgradeFrom31to30(db);
break;
case 31:
downgradeFrom32to31(db);
break;
case 32:
downgradeFrom33to32(db);
break;
case 33:
downgradeFrom34to33(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
case 23 -> downgradeFrom24to23(db);
case 24 -> downgradeFrom25to24(db);
case 25 -> downgradeFrom26to25(db);
case 26 -> downgradeFrom27to26(db);
case 27 -> downgradeFrom28to27(db);
case 28 -> downgradeFrom29to28(db);
case 29 -> downgradeFrom30to29(db);
case 30 -> downgradeFrom31to30(db);
case 31 -> downgradeFrom32to31(db);
case 32 -> downgradeFrom33to32(db);
case 33 -> downgradeFrom34to33(db);
default -> throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
}
}
@@ -129,24 +129,18 @@ public class ShareContentProvider extends CustomContentProvider {
}
private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case URI_GPX:
return TrackFileFormat.GPX;
case URI_KML_WITH_TRACKDETAIL_SENSORDATA:
return TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA:
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
case URI_CSV:
return TrackFileFormat.CSV;
default:
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
}
return switch (uriMatcher.match(uri)) {
case URI_GPX -> TrackFileFormat.GPX;
case URI_KML_WITH_TRACKDETAIL_SENSORDATA ->
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA ->
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES ->
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
case URI_CSV -> TrackFileFormat.CSV;
default ->
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
};
}
@Nullable
@@ -170,14 +164,14 @@ public class ShareContentProvider extends CustomContentProvider {
int i = 0;
for (String col : projection) {
switch (col) {
case OpenableColumns.DISPLAY_NAME:
case OpenableColumns.DISPLAY_NAME -> {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = uri.getLastPathSegment();
break;
case OpenableColumns.SIZE:
}
case OpenableColumns.SIZE -> {
cols[i] = OpenableColumns.SIZE;
values[i++] = -1; //Report unknown size; if applications need to know, one need to generate the file here also (count bytes that are written to OutputStream.
break;
}
}
}
@@ -214,12 +208,12 @@ public class ShareContentProvider extends CustomContentProvider {
}
}
final TrackExporter trackExporter = getTrackFileFormat(uri).createTrackExporter(getContext());
final TrackExporter trackExporter = getTrackFileFormat(uri).createTrackExporter(getContext(), new ContentProviderUtils(getContext()));
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);
}
@@ -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 + " IN (%s)", TextUtils.join(",", Collections.nCopies(categories.size(), "?")));
}
if (from != null && to != null) {
selection += selection.isEmpty() ? "" : " AND ";
@@ -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,22 @@
package de.dennisguse.opentracks.data.models;
import java.util.Objects;
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);
}
}
@@ -5,7 +5,7 @@ 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 +23,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) {
@@ -58,22 +53,11 @@ public class Distance {
}
public static Distance one(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return Distance.ofKilometer(1);
case IMPERIAL:
return Distance.ofMile(1);
case NAUTICAL_IMPERIAL:
return Distance.ofNauticalMile(1);
default:
throw new RuntimeException("Not implemented");
}
}
private final double distance_m;
private Distance(double distance_m) {
this.distance_m = distance_m;
return switch (unitSystem) {
case METRIC -> Distance.ofKilometer(1);
case IMPERIAL_FEET, IMPERIAL_METER -> Distance.ofMile(1);
case NAUTICAL_IMPERIAL -> Distance.ofNauticalMile(1);
};
}
public Distance plus(@NonNull Distance distance) {
@@ -133,49 +117,18 @@ public class Distance {
}
public double toKM_Miles(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toKM();
case IMPERIAL:
return toMI();
case NAUTICAL_IMPERIAL:
return toNauticalMiles();
default:
throw new RuntimeException("Not implemented");
}
return switch (unitSystem) {
case METRIC -> toKM();
case IMPERIAL_FEET, IMPERIAL_METER -> toMI();
case NAUTICAL_IMPERIAL -> toNauticalMiles();
};
}
public double toM_FT(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toM();
case NAUTICAL_IMPERIAL:
case IMPERIAL:
return toFT();
default:
throw new RuntimeException("Not implemented");
}
}
@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 +
'}';
return switch (unitSystem) {
case METRIC, IMPERIAL_METER -> toM();
case NAUTICAL_IMPERIAL, IMPERIAL_FEET -> toFT();
};
}
// multiplication factors for conversion
@@ -46,38 +46,44 @@ public class DistanceFormatter {
public Pair<String, String> getDistanceParts(Distance distance) {
if (distance.isInvalid()) {
String valueUnknown = resources.getString(R.string.value_unknown);
switch (unitSystem) {
case METRIC:
return new Pair<>(valueUnknown, resources.getString(R.string.unit_meter));
case IMPERIAL:
case NAUTICAL_IMPERIAL:
return new Pair<>(valueUnknown, resources.getString(R.string.unit_feet));
default:
throw new RuntimeException("Not implemented");
}
return switch (unitSystem) {
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));
};
}
switch (unitSystem) {
case METRIC:
case METRIC -> {
if (distance.greaterThan(Distance.ofKilometer(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
} else {
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 NAUTICAL_IMPERIAL:
}
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));
} else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
}
default:
throw new RuntimeException("Not implemented");
}
default -> throw new RuntimeException("Not implemented");
}
}
@@ -4,40 +4,13 @@ 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());
}
@@ -4,40 +4,13 @@ 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);
}
@@ -115,36 +106,10 @@ public class Speed {
}
public double to(UnitSystem unitSystem) {
switch (unitSystem) {
case METRIC:
return toKMH();
case IMPERIAL:
return toMPH();
case NAUTICAL_IMPERIAL:
return toKnots();
default:
throw new RuntimeException("Not implemented");
}
}
@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 +
'}';
return switch (unitSystem) {
case METRIC -> toKMH();
case IMPERIAL_FEET, IMPERIAL_METER -> toMPH();
case NAUTICAL_IMPERIAL -> toKnots();
};
}
}
@@ -39,20 +39,14 @@ public class SpeedFormatter {
* @return the formatted speed (or null) and it's unit as {@link Pair}
*/
public Pair<String, String> getSpeedParts(Speed speed) {
int unitId;
switch (unitSystem) {
case METRIC:
unitId = reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
break;
case IMPERIAL:
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
break;
case NAUTICAL_IMPERIAL:
unitId = reportSpeedOrPace ? R.string.unit_knots : R.string.unit_minute_per_nautical_mile;
break;
default:
throw new RuntimeException("Not implemented");
}
int unitId = switch (unitSystem) {
case METRIC ->
reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
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;
};
String unitString = resources.getString(unitId);
@@ -44,7 +44,7 @@ public class Track {
private String name = "";
private String description = "";
private String category = "";
private String activityType = "";
private String icon = "";
@@ -97,12 +97,12 @@ public class Track {
this.description = description;
}
public String getCategory() {
return category;
public String getActivityType() {
return activityType;
}
public void setCategory(String category) {
this.category = category;
public void setActivityType(String activityType) {
this.activityType = activityType;
}
public String getIcon() {
@@ -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 = "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
@@ -63,7 +63,7 @@ public interface TracksColumns extends BaseColumns {
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ NAME + " TEXT, "
+ DESCRIPTION + " TEXT, "
+ CATEGORY + " TEXT, "
+ ACTIVITY_TYPE + " TEXT, "
+ STARTTIME + " INTEGER, "
+ STOPTIME + " INTEGER, "
+ NUMPOINTS + " INTEGER, "
@@ -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);
}
@@ -89,7 +89,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.getActivityType());
}
if (key != null && updateUInecessary && isResumed()) {
@@ -155,7 +155,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.getActivityType().equals(track.getActivityType()));
this.track = track;
if (prefsChanged) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, getString(R.string.stats_rate_key));
@@ -191,7 +191,7 @@ public class StatisticsRecordedFragment extends Fragment {
// Set activity type
{
String trackIconValue = TrackIconUtils.getIconValue(getContext(), track.getCategory());
String trackIconValue = TrackIconUtils.getIconValue(getContext(), track.getActivityType());
viewBinding.statsActivityTypeIcon.setImageDrawable(ContextCompat.getDrawable(getContext(), TrackIconUtils.getIconDrawable(trackIconValue)));
}
@@ -122,17 +122,15 @@ 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());
int rowIndex = 0;
int columnIndex = 0;
for (int i = 0; i < recordingLayout.getFields().size(); i++) {
DataField dataField = recordingLayout.getFields().get(i);
for (DataField dataField : recordingLayout.toRecordingLayout(true).getFields()) {
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.setGravity(Gravity.FILL_HORIZONTAL);
param.width = 0;
@@ -162,7 +160,7 @@ public class StatisticsRecordingFragment extends Fragment {
viewBinding.statsLayout.addView(viewHolder.getView(), param);
} catch (Exception e) {
throw new RuntimeException(e);
throw new RuntimeException("Could not add " + dataField.getKey(), e);
}
}
}
@@ -3,6 +3,8 @@ package de.dennisguse.opentracks.io.file;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Locale;
@@ -25,8 +27,8 @@ public enum TrackFileFormat {
KML_WITH_TRACKDETAIL_AND_SENSORDATA("KML_WITH_TRACKDETAIL_AND_SENSORDATA") {
@Override
public TrackExporter createTrackExporter(Context context) {
return new KMLTrackExporter(context, false);
public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
return new KMLTrackExporter(context, contentProviderUtils, false);
}
@Override
@@ -45,9 +47,9 @@ public enum TrackFileFormat {
private static final boolean exportPhotos = false;
@Override
public TrackExporter createTrackExporter(Context context) {
KMLTrackExporter exporter = new KMLTrackExporter(context, exportPhotos);
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
KMLTrackExporter exporter = new KMLTrackExporter(context, contentProviderUtils, exportPhotos);
return new KmzTrackExporter(context, contentProviderUtils, exporter, exportPhotos);
}
@Override
@@ -70,9 +72,9 @@ public enum TrackFileFormat {
private static final boolean exportPhotos = true;
@Override
public TrackExporter createTrackExporter(Context context) {
KMLTrackExporter exporter = new KMLTrackExporter(context, exportPhotos);
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos);
public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
KMLTrackExporter exporter = new KMLTrackExporter(context, contentProviderUtils, exportPhotos);
return new KmzTrackExporter(context, contentProviderUtils, exporter, exportPhotos);
}
@Override
@@ -93,8 +95,8 @@ public enum TrackFileFormat {
GPX("GPX") {
@Override
public TrackExporter createTrackExporter(Context context) {
return new GPXTrackExporter(new ContentProviderUtils(context), context.getString(R.string.app_name));
public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
return new GPXTrackExporter(contentProviderUtils, context.getString(R.string.app_name));
}
@Override
@@ -109,8 +111,8 @@ public enum TrackFileFormat {
CSV("CSV") {
@Override
public TrackExporter createTrackExporter(Context context) {
return new CSVTrackExporter(new ContentProviderUtils(context));
public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
return new CSVTrackExporter(contentProviderUtils);
}
@Override
@@ -161,7 +163,7 @@ public enum TrackFileFormat {
*
* @param context the context
*/
public abstract TrackExporter createTrackExporter(Context context);
public abstract TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils);
/**
* Returns the file extension for each format.
@@ -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.getActivityType());
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;
@@ -87,13 +85,8 @@ public class CSVTrackExporter 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) {
List<Column> columns = Collections.unmodifiableList(Arrays.asList(
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()) : ""),
@@ -108,8 +101,7 @@ public class CSVTrackExporter implements TrackExporter {
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("power", t -> t.hasPower() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : ""));
try {
prepare(outputStream);
@@ -144,15 +136,10 @@ public class CSVTrackExporter implements TrackExporter {
TrackPoint trackPoint = trackPointIterator.next();
switch (trackPoint.getType()) {
case SEGMENT_START_MANUAL:
case SEGMENT_END_MANUAL:
case SEGMENT_START_AUTOMATIC:
case SENSORPOINT:
case TRACKPOINT:
writeTrackPoint(columns, trackPoint);
break;
default:
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
case SEGMENT_START_MANUAL, SEGMENT_END_MANUAL, SEGMENT_START_AUTOMATIC, SENSORPOINT, TRACKPOINT ->
writeTrackPoint(columns, trackPoint);
default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
}
}
@@ -171,18 +158,14 @@ public class CSVTrackExporter implements TrackExporter {
public void writeHeader(List<Column> columns) {
if (printWriter != null) {
String columnNames = columns.stream().map(c -> c.columnName)
.reduce((s, s2) -> s + "," + s2)
.orElseThrow(() -> new RuntimeException("No columns defined"));
String columnNames = columns.stream().map(c -> c.columnName).reduce((s, s2) -> s + "," + s2).orElseThrow(() -> new RuntimeException("No columns defined"));
printWriter.println("#" + columnNames);
}
}
public void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
if (printWriter != null) {
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint))
.reduce((s, s2) -> s + "," + s2)
.orElseThrow(() -> new RuntimeException("No columns defined"));
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint)).reduce((s, s2) -> s + "," + s2).orElseThrow(() -> new RuntimeException("No columns defined"));
printWriter.println(columnNames);
}
}
@@ -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,24 +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);
break;
case RESULT_CODE_ERROR:
receiver.onExportError(trackId);
break;
default:
throw new RuntimeException("Unknown resultCode.");
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();
@@ -145,37 +140,32 @@ public class GPXTrackExporter implements TrackExporter {
}
switch (trackPoint.getType()) {
case SEGMENT_START_MANUAL:
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
break;
case SEGMENT_END_MANUAL:
case SEGMENT_START_MANUAL ->
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
case SEGMENT_END_MANUAL -> {
if (wroteSegment) writeCloseSegment();
wroteSegment = false;
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_END_MANUAL.name() + " is not supported.");
break;
case SEGMENT_START_AUTOMATIC:
}
case SEGMENT_START_AUTOMATIC -> {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
wroteSegment = true;
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
break;
case SENSORPOINT:
sensorPoints.add(trackPoint);
break;
case TRACKPOINT:
}
case SENSORPOINT -> sensorPoints.add(trackPoint);
case TRACKPOINT -> {
if (!wroteSegment) {
// Might happen for older data (pre v3.15.0)
writeOpenSegment();
wroteSegment = true;
}
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear();
break;
default:
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
}
@@ -278,7 +268,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.getActivityType()) + "</type>");
printWriter.println("<extensions>");
printWriter.println("<topografix:color>c0c0c0</topografix:color>");
@@ -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";
@@ -93,24 +93,20 @@ public class KMLTrackExporter implements TrackExporter {
private final List<Float> accuracyHorizontal = new ArrayList<>();
private final List<Float> accuracyVertical = new ArrayList<>();
public KMLTrackExporter(Context context, boolean exportPhotos) {
public KMLTrackExporter(Context context, ContentProviderUtils contentProviderUtils, boolean exportPhotos) {
this.context = context;
this.exportPhotos = exportPhotos;
this.contentProviderUtils = new ContentProviderUtils(context);
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();
}
@@ -169,30 +165,28 @@ public class KMLTrackExporter implements TrackExporter {
}
switch (trackPoint.getType()) {
case SEGMENT_START_MANUAL:
case SEGMENT_START_AUTOMATIC:
case SEGMENT_START_MANUAL, SEGMENT_START_AUTOMATIC -> {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
writeTrackPoint(track.getZoneOffset(), trackPoint);
wroteSegment = true;
break;
case SEGMENT_END_MANUAL:
}
case SEGMENT_END_MANUAL -> {
if (!wroteSegment) writeOpenSegment();
writeTrackPoint(track.getZoneOffset(), trackPoint);
writeCloseSegment();
wroteSegment = false;
break;
case SENSORPOINT:
case TRACKPOINT:
}
case SENSORPOINT, TRACKPOINT -> {
if (!wroteSegment) {
// Might happen for older data (pre v3.15.0)
writeOpenSegment();
wroteSegment = true;
}
writeTrackPoint(track.getZoneOffset(), trackPoint);
break;
default:
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
}
@@ -224,22 +218,26 @@ public class KMLTrackExporter implements TrackExporter {
}
}
private void writeHeader(Track[] tracks) {
private void writeHeader(List<Track> tracks) {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.3\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\">");
//TODO ADD xsi:schemaLocation for atom
printWriter.println("xsi:schemaLocation=" +
"\"http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd"
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
printWriter.println(
"""
<?xml version="1.0" encoding="UTF-8"?>
""");
printWriter.println(
"""
<kml xmlns="http://www.opengis.net/kml/2.3"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd
http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
"""); //TODO ADD xsi:schemaLocation for atom
printWriter.println("<Document>");
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>");
@@ -291,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>");
}
@@ -313,7 +311,7 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<opentracks:trackid>" + track.getUuid() + "</opentracks:trackid>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
writeCategory(track.getCategory());
writeActivityType(track.getActivityType());
printWriter.println("<MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<interpolate>1</interpolate>");
@@ -428,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>");
@@ -461,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>");
@@ -516,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 actiivtyType) {
if (actiivtyType == null || actiivtyType.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(actiivtyType) + "</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);
}
@@ -101,6 +101,30 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
}
}
public static class ExportDirectoryChooserOneFileActivity extends DirectoryChooserActivity {
@Override
protected DocumentFile configureDirectoryChooserIntent(Intent intent) {
super.configureDirectoryChooserIntent(intent);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
return IntentUtils.toDocumentFile(this, PreferencesUtils.getDefaultExportDirectoryUri());
}
@Override
protected boolean isDirectoryValid(final DocumentFile directoryUri) {
return super.isDirectoryValid(directoryUri) && directoryUri.canWrite();
}
@Override
protected Intent createNextActivityIntent(Uri directoryUri) {
Intent intent = IntentUtils.newIntent(this, ExportActivity.class);
intent.putExtra(ExportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri);
intent.putExtra(ExportActivity.EXTRA_ONE_FILE_KEY, true);
intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat());
return intent;
}
}
public static class DefaultTrackExportDirectoryChooserActivity extends DirectoryChooserActivity {
@Override
@@ -98,7 +98,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private String name;
private String description;
private String category;
private String activityType;
private String latitude;
private String longitude;
private String altitude;
@@ -156,103 +156,94 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
@Override
public void endElement(String uri, String localName, String tag) {
switch (tag) {
case TAG_GPX:
onFileEnd();
break;
case TAG_MARKER:
onMarkerEnd();
break;
case TAG_TRACK:
trackImporter.setTrack(context, name, uuid, description, category, null, zoneOffset);
case TAG_GPX -> onFileEnd();
case TAG_MARKER -> onMarkerEnd();
case TAG_TRACK -> {
trackImporter.setTrack(context, name, uuid, description, activityType, null, zoneOffset);
zoneOffset = null;
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentEnd();
break;
case TAG_TRACK_POINT:
currentSegment.add(createTrackPoint());
break;
case TAG_NAME:
}
case TAG_TRACK_SEGMENT -> onTrackSegmentEnd();
case TAG_TRACK_POINT -> currentSegment.add(createTrackPoint());
case TAG_NAME -> {
if (content != null) {
name = content.trim();
}
break;
case TAG_DESCRIPTION:
}
case TAG_DESCRIPTION -> {
if (content != null) {
description = content.trim();
}
break;
case TAG_TYPE:
}
case TAG_TYPE -> {
if (content != null) {
category = content.trim();
activityType = content.trim();
}
break;
case TAG_TIME:
}
case TAG_TIME -> {
if (content != null) {
time = content.trim();
}
break;
case TAG_ALTITUDE:
}
case TAG_ALTITUDE -> {
if (content != null) {
altitude = content.trim();
}
break;
case TAG_COMMENT:
}
case TAG_COMMENT -> {
if (content != null) {
markerType = content.trim();
}
break;
case TAG_EXTENSION_SPEED:
case TAG_EXTENSION_SPEED_COMPAT:
}
case TAG_EXTENSION_SPEED, TAG_EXTENSION_SPEED_COMPAT -> {
if (content != null) {
speed = content.trim();
}
break;
case TAG_EXTENSION_HEARTRATE:
}
case TAG_EXTENSION_HEARTRATE -> {
if (content != null) {
heartrate = content.trim();
}
break;
case TAG_EXTENSION_CADENCE:
}
case TAG_EXTENSION_CADENCE -> {
if (content != null) {
cadence = content.trim();
}
break;
case TAG_EXTENSION_POWER:
}
case TAG_EXTENSION_POWER -> {
if (content != null) {
power = content.trim();
}
break;
case TAG_ID:
}
case TAG_ID -> {
if (content != null) {
uuid = content.trim();
}
break;
case TAG_EXTENSION_GAIN:
}
case TAG_EXTENSION_GAIN -> {
if (content != null) {
gain = content.trim();
}
break;
case TAG_EXTENSION_LOSS:
}
case TAG_EXTENSION_LOSS -> {
if (content != null) {
loss = content.trim();
}
break;
case TAG_EXTENSION_DISTANCE:
}
case TAG_EXTENSION_DISTANCE -> {
if (content != null) {
sensorDistance = content.trim();
}
break;
case TAG_EXTENSION_ACCURACY_HORIZONTAL:
}
case TAG_EXTENSION_ACCURACY_HORIZONTAL -> {
if (content != null) {
accuracyHorizontal = content.trim();
}
break;
case TAG_EXTENSION_ACCURACY_VERTICAL:
}
case TAG_EXTENSION_ACCURACY_VERTICAL -> {
if (content != null) {
accuracyVertical = content.trim();
}
break;
}
}
content = "";
@@ -394,7 +385,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private void onMarkerStart(Attributes attributes) {
name = null;
description = null;
category = null;
activityType = null;
photoUrl = null;
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
@@ -424,8 +415,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
if (description != null) {
marker.setDescription(description);
}
if (category != null) {
marker.setCategory(category);
if (activityType != null) {
marker.setCategory(activityType);
}
if (photoUrl != null) {
@@ -112,7 +112,7 @@ public class ImportActivity extends FragmentActivity {
});
//Works for a directory, but we might have received multiple files via SEND_MULTIPLE.
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.import_progress_message, documentFiles.get(0).getName()));
viewBinding.importActivityToolbar.setTitle(getString(R.string.import_progress_message, documentFiles.get(0).getName()));
viewBinding.bottomAppBarLayout.bottomAppBar.setNavigationIcon(R.drawable.ic_logo_color_24dp);
}
@@ -79,19 +79,17 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
String message = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE);
switch (resultCode) {
case ImportServiceResultReceiver.RESULT_CODE_ERROR:
case ImportServiceResultReceiver.RESULT_CODE_ERROR -> {
summary.errorCount++;
summary.fileErrors.add(getApplication().getString(R.string.import_error_info, fileName, message));
break;
case ImportServiceResultReceiver.RESULT_CODE_IMPORTED:
}
case ImportServiceResultReceiver.RESULT_CODE_IMPORTED -> {
summary.importedTrackIds.addAll(trackIds);
summary.successCount++;
break;
case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS:
summary.existsCount++;
break;
default:
throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
}
case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS -> summary.existsCount++;
default ->
throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
}
importData.postValue(summary);

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