Compare commits

...

108 Commits

Author SHA1 Message Date
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
Peter Storch dae8eaa97a Release: v4.2.2 2023-02-04 21:53:35 +01:00
Peter Storch 565b1ce94d Correct link to F-Droid build monitor 2023-02-04 21:53:24 +01:00
dependabot[bot] 543d5e18bf Bump com.android.tools:desugar_jdk_libs from 2.0.0 to 2.0.1
Bumps [com.android.tools:desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 2.0.0 to 2.0.1.
- [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-03 05:05:29 +01:00
dependabot[bot] 01368df6b1 Bump com.android.tools.build:gradle from 7.4.0 to 7.4.1
Bumps com.android.tools.build:gradle from 7.4.0 to 7.4.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-02 07:03:02 +01:00
Dennis Guse c666d55211 Bug: crash due to color not found.
Introduced in 674d231b7a
2023-01-30 23:11:21 +01:00
Dennis Guse 8cc4fd1d4f Bugfix: not all activityTypes were shown in dropdownlist. 2023-01-29 16:50:04 +01:00
Dennis Guse c241a35861 Refactor: introduced ActivityType. 2023-01-29 16:50:04 +01:00
Dennis Guse 674d231b7a Bug: textcolor for heartrate zone 1 was ignoring theme.
Fixes #1455.
2023-01-27 02:00:38 +01:00
Dennis Guse bebee59eb2 Bug: Track icon wrongly shown in dialog.
Fixes #1453.
2023-01-25 16:37:35 +01:00
dependabot[bot] 24f843b0db Bump material from 1.7.0 to 1.8.0
Bumps [material](https://github.com/material-components/material-components-android) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.7.0...1.8.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-01-25 07:04:21 +01:00
Dennis Guse f0259d13dd Release: v4.2.1 2023-01-21 19:03:07 +01:00
Dennis Guse 9dff965a52 Cycling speed sensor: wheel diameter can now be 100mm until 4000mm
Fixes #1447.
2023-01-17 22:47:59 +01:00
Mario Ruiz b4e6d10fbe Translated using Weblate (Spanish)
Currently translated at 100.0% (503 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/es/
2023-01-16 12:22:26 +01:00
Dennis Guse 1a9d88277f Fix pt-PT translations. 2023-01-13 08:08:49 +01:00
Dennis Guse 4ea956ca0e Merge branch 'translations' 2023-01-13 08:02:27 +01:00
dependabot[bot] 8060c2e1e3 Bump desugar_jdk_libs from 1.2.2 to 2.0.0
Bumps [desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 1.2.2 to 2.0.0.
- [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-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-13 08:01:58 +01:00
dependabot[bot] 8c96b65e59 Bump gradle from 7.3.1 to 7.4.0
Bumps gradle from 7.3.1 to 7.4.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-13 07:47:56 +01:00
Dennis Guse a407e94d3c Upgrade to appcompat:1.6.0 2023-01-11 22:58:23 +01:00
ssantos 310c790324 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (503 of 503 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/pt_PT/
2023-01-11 13:50:39 +01:00
Dennis Guse 12b3d31f5c Bugfix: BluetoothLE requires to trigger reconnect on either disconnect or never connected.
Initial connection timeout seems to be 30s.
Fixes #1442.
2023-01-05 20:52:15 +01:00
dependabot[bot] d43dca61c1 Bump runner from 1.5.1 to 1.5.2
Bumps runner from 1.5.1 to 1.5.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-04 07:28:24 +01:00
dependabot[bot] 20c14a03b2 Bump junit from 1.1.4 to 1.1.5
Bumps junit from 1.1.4 to 1.1.5.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-04 07:27:28 +01:00
dependabot[bot] a6601f4eea Bump espresso-core from 3.5.0 to 3.5.1
Bumps espresso-core from 3.5.0 to 3.5.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-04 07:27:04 +01:00
155 changed files with 2589 additions and 1790 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
with: with:
distribution: 'adopt' distribution: 'adopt'
java-version: '11' java-version: '17'
- name: Assemble APK - name: Assemble APK
shell: bash shell: bash
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '11' java-version: '17'
- run: ./gradlew lint - run: ./gradlew lint
+5 -3
View File
@@ -2,7 +2,7 @@
_OpenTracks_ is a sport tracking application that completely respects your privacy. _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> <table>
<tr> <tr>
@@ -40,8 +40,10 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</tr> </tr>
</table> </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/"> <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> </a>
## Screenshots ## Screenshots
@@ -150,7 +152,7 @@ The developer documentation is in [README_API.md](README_API.md).
## Project history ## Project history
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)). _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. 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). 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). Plonk42's version is available [here](https://github.com/Plonk42/mytracks).
+1 -1
View File
@@ -17,7 +17,7 @@ Application id: de.dennisguse.opentracks
It if it properly tagged F-Droid takes care of building the app and publishing. It if it properly tagged F-Droid takes care of building the app and publishing.
Usually, it takes two days before the update is published (build, signed, and published). Usually, it takes two days before the update is published (build, signed, and published).
The recent build activity can be found [here](https://f-droid.org/wiki/index.php?title=Special:RecentChanges&days=7&from=&hidebots=0&hideanons=1&hideliu=1&limit=500) The recent build activity can be found [here](https://monitor.f-droid.org/builds/build)
## Google Play Store ## Google Play Store
Application id: `de.dennisguse.opentracks.playstore` Application id: `de.dennisguse.opentracks.playstore`
+21 -15
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.3.1' classpath 'com.android.tools.build:gradle:8.1.0-beta03'
} }
} }
@@ -47,20 +47,25 @@ android {
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
} }
compileSdk 33 compileSdk 33
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
}
androidResources {
generateLocaleConfig = true
} }
defaultConfig { defaultConfig {
applicationId "de.dennisguse.opentracks" applicationId "de.dennisguse.opentracks"
versionCode 5119 versionCode 5222
versionName "v4.2.0" versionName "v4.4.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -106,11 +111,11 @@ android {
} }
} }
applicationVariants.all { variant -> applicationVariants.configureEach { variant ->
variant.resValue "string", "applicationId", variant.applicationId variant.resValue "string", "applicationId", variant.applicationId
if (variant.buildType.name == 'nightly') { if (variant.buildType.name == 'nightly') {
variant.outputs.all { variant.outputs.configureEach {
setVersionCodeOverride(getVersionCode()) setVersionCodeOverride(getVersionCode())
setVersionNameOverride(getVersionName()) setVersionNameOverride(getVersionName())
outputFileName = "${applicationId}_${variant.versionCode}.apk" outputFileName = "${applicationId}_${variant.versionCode}.apk"
@@ -120,22 +125,23 @@ android {
} }
dependencies { dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'androidx.appcompat:appcompat:1.6.0-rc01' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'com.google.android.material:material:1.7.0' implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 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:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.1' androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'org.mockito:mockito-android:4.11.0' androidTestImplementation 'org.mockito:mockito-android:5.3.1'
androidTestUtil 'androidx.test:orchestrator:1.4.2' androidTestUtil 'androidx.test:orchestrator:1.4.2'
} }
@@ -0,0 +1,8 @@
v4.2.1: OpenTracks
Changes:
- Speed sensor: acceptable wheel diameter is 100mm-4000mm
Bugfix:
- Reconnect to a sensor if connection is lost
@@ -0,0 +1,9 @@
v4.2.2: OpenTracks
Bugfix:
- Track icon wrongly shown in dialog
- Textcolor for heartrate zone 1 was ignoring theme
- Not all activityTypes were shown in dropdownlist
- Crash due to color not found
@@ -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
@@ -2,13 +2,13 @@ v3.19.0: OpenTracks
Wijzigingen: Wijzigingen:
- Ondersteuning voor Running Speed en Cadence (RCS) sensoren - 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 - 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: Bugfix:
- Berekening van totale tijd mag geen pauze-tijd bevatten (vereist opnieuw importeren) - Berekening totale tijd mag geen pauze-tijd bevatten (vereist opnieuw importeren)
- (opnieuw) Gebruik aangepaste overzichten indien aanwezig - (opnieuw) Gebruik aanwezige aangepaste overzichten
Ontwikkelaar: Ontwikkelaar:
- Upgrade naar SDK31 en AGP7.0.0 (vereist JDK11) - Upgrade naar SDK31 en AGP7.0.0 (vereist JDK11)
@@ -5,4 +5,4 @@ Wijzigingen:
- Gesproken meldingen van hartslag - Gesproken meldingen van hartslag
Bugfix: 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 android.useAndroidX=true
org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=256m org.gradle.jvmargs=-Xmx1024m
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+6
View File
@@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ 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. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+8 -6
View File
@@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal
@@ -13,6 +13,8 @@ import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything; import static org.hamcrest.Matchers.anything;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor; import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.Manifest;
import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
@@ -30,7 +32,11 @@ public class EspressoDeleteTrackTest {
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class); public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule @Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION); public GrantPermissionRule mGrantPermissionRuleLocation = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@Rule
public GrantPermissionRule mGrantPermissionRuleBluetooth = GrantPermissionRule.grant(Manifest.permission.BLUETOOTH_CONNECT);
@Rule
public GrantPermissionRule mGrantPermissionRuleNotifications = GrantPermissionRule.grant(Manifest.permission.POST_NOTIFICATIONS);
@Test @Test
public void espressoDeleteTrackTest() { public void espressoDeleteTrackTest() {
@@ -15,6 +15,8 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor; import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.Manifest;
import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -38,7 +40,11 @@ public class EspressoEditTrackRecordingTest {
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class); public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule @Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION); public GrantPermissionRule mGrantPermissionRuleLocation = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@Rule
public GrantPermissionRule mGrantPermissionRuleBluetooth = GrantPermissionRule.grant(android.Manifest.permission.BLUETOOTH_CONNECT);
@Rule
public GrantPermissionRule mGrantPermissionRuleNotifications = GrantPermissionRule.grant(Manifest.permission.POST_NOTIFICATIONS);
@LargeTest @LargeTest
@Test @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.selectTabAtIndex;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor; import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.Manifest;
import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -31,7 +33,11 @@ public class EspressoUITest {
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class); public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule @Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION); public GrantPermissionRule mGrantPermissionRuleLocation = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@Rule
public GrantPermissionRule mGrantPermissionRuleBluetooth = GrantPermissionRule.grant(android.Manifest.permission.BLUETOOTH_CONNECT);
@Rule
public GrantPermissionRule mGrantPermissionRuleNotifications = GrantPermissionRule.grant(Manifest.permission.POST_NOTIFICATIONS);
@LargeTest @LargeTest
@Test @Test
@@ -27,21 +27,23 @@ public class KmlTrackExporterTest {
*/ */
@Test @Test
public void writeCloseSegment_only_write_sensordata_if_present() { public void writeCloseSegment_only_write_sensordata_if_present() {
String expected = "<when>1970-01-01T00:00:00Z</when>\n" + String expected = """
"<coord/>\n" + <when>1970-01-01T00:00:00Z</when>
"<when>1970-01-01T01:00:00+01:00</when>\n" + <coord/>
"<coord/>\n" + <when>1970-01-01T01:00:00+01:00</when>
"<ExtendedData>\n" + <coord/>
"<SchemaData schemaUrl=\"#schema\">\n" + <ExtendedData>
"</SchemaData>\n" + <SchemaData schemaUrl="#schema">
"</ExtendedData>\n" + </SchemaData>
"</Track>\n"; </ExtendedData>
</Track>
""";
// given // given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)); TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 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.prepare(outputStream);
kmlTrackWriter.writeTrackPoint(ZoneOffset.UTC, trackPoint); kmlTrackWriter.writeTrackPoint(ZoneOffset.UTC, trackPoint);
@@ -166,7 +166,7 @@ public class ExportImportTest {
trackPointCreator.setClock("2020-02-02T02:02:18Z"); trackPointCreator.setClock("2020-02-02T02:02:18Z");
BluetoothRemoteSensorManager mockRemoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class); BluetoothRemoteSensorManager mockRemoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class);
trackPointCreator.setRemoteSensorManager(mockRemoteSensorManager); trackPointCreator.getSensorManager().setBluetoothSensorManager(mockRemoteSensorManager);
service.endCurrentTrack(); service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z"); trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -179,7 +179,7 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0); sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:03:24Z"); trackPointCreator.setClock("2020-02-02T02:03:24Z");
trackPointCreator.setRemoteSensorManager(mockRemoteSensorManager); trackPointCreator.getSensorManager().setBluetoothSensorManager(mockRemoteSensorManager);
service.endCurrentTrack(); service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
@@ -204,11 +204,11 @@ public class ExportImportTest {
// given // given
Track track = contentProviderUtils.getTrack(trackId); 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 // when
// 1. export // 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// 2. import // 2. import
@@ -287,10 +287,10 @@ public class ExportImportTest {
editor.commit(); editor.commit();
Track track = contentProviderUtils.getTrack(trackId); 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 // when
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import // 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
@@ -310,11 +310,11 @@ public class ExportImportTest {
// given // given
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context); TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils);
// when // when
// 1. export // 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// 2. import // 2. import
@@ -428,11 +428,11 @@ public class ExportImportTest {
editor.commit(); editor.commit();
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context); TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils);
// when // when
// 1. export // 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import // 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
@@ -453,11 +453,11 @@ public class ExportImportTest {
// given // given
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.CSV.createTrackExporter(context); TrackExporter trackExporter = TrackFileFormat.CSV.createTrackExporter(context, contentProviderUtils);
// when // when
// 1. export // 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// then // then
@@ -521,12 +521,12 @@ public class ExportImportTest {
sensorDataSet.fillTrackPoint(trackPoint); sensorDataSet.fillTrackPoint(trackPoint);
return sensorDataSet; return sensorDataSet;
}); });
trackPointCreator.setRemoteSensorManager(remoteSensorManager); trackPointCreator.getSensorManager().setBluetoothSensorManager(remoteSensorManager);
trackPointCreator.onChange(new SensorDataSet()); trackPointCreator.onChange(new SensorDataSet());
} }
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) { private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
AltitudeSumManager altitudeSumManager = trackPointCreator.getAltitudeSumManager(); AltitudeSumManager altitudeSumManager = trackPointCreator.getSensorManager().getAltitudeSumManager();
altitudeSumManager.setAltitudeGain_m(altitudeGain); altitudeSumManager.setAltitudeGain_m(altitudeGain);
altitudeSumManager.setAltitudeLoss_m(altitudeGain); altitudeSumManager.setAltitudeLoss_m(altitudeGain);
} }
@@ -231,8 +231,8 @@ public class GPXTrackImporterTest {
importTrackId = importer.importFile(inputStream).get(0); importTrackId = importer.importFile(inputStream).get(0);
Track importedTrack = contentProviderUtils.getTrack(importTrackId); Track importedTrack = contentProviderUtils.getTrack(importTrackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context); TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils);
trackExporter.writeTrack(importedTrack, outputStream); trackExporter.writeTrack(List.of(importedTrack), outputStream);
// then // then
String expected = new BufferedReader( String expected = new BufferedReader(
@@ -11,13 +11,44 @@ public class UintUtilsTest {
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT16_MAX)); assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT16_MAX));
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT16_MAX)); assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT16_MAX));
assertEquals(3, UintUtils.diff(5, 2, 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)); 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 @Test
public void realData() { public void realData() {
assertEquals(1, UintUtils.diff(381616, 381615, UintUtils.UINT32_MAX)); assertEquals(1, UintUtils.diff(381616, 381615, UintUtils.UINT32_MAX));
} }
} }
@@ -45,6 +45,8 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
/** /**
* Tests insert location. * 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) @RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestRecording { public class TrackRecordingServiceTestRecording {
@@ -112,7 +114,7 @@ public class TrackRecordingServiceTestRecording {
String startTime = "2020-02-02T02:02:02Z"; String startTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// then // then
assertEquals(new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null) assertEquals(new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null)
@@ -149,7 +151,7 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
String pauseTime = "2020-02-02T02:02:03Z"; String pauseTime = "2020-02-02T02:02:03Z";
@@ -195,7 +197,7 @@ public class TrackRecordingServiceTestRecording {
String starTime = "2020-02-02T02:02:02Z"; String starTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(starTime); trackPointCreator.setClock(starTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
String pauseTime = "2020-02-02T02:02:03Z"; String pauseTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(pauseTime); trackPointCreator.setClock(pauseTime);
@@ -227,7 +229,7 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
String stopTime = "2020-02-02T02:02:03Z"; String stopTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(stopTime); trackPointCreator.setClock(stopTime);
@@ -237,7 +239,7 @@ public class TrackRecordingServiceTestRecording {
String resumeTime = "2020-02-02T02:02:04Z"; String resumeTime = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(resumeTime); trackPointCreator.setClock(resumeTime);
service.resumeTrack(trackId); service.resumeTrack(trackId);
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// then // then
new TrackPointAssert().assertEquals(List.of( new TrackPointAssert().assertEquals(List.of(
@@ -258,12 +260,13 @@ public class TrackRecordingServiceTestRecording {
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager(); BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager();
// when // when
String sensor1 = "2020-02-02T02:02:03Z"; String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1); trackPointCreator.setClock(sensor1);
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored
String sensor3 = "2020-02-02T02:02:13Z"; String sensor3 = "2020-02-02T02:02:13Z";
@@ -297,7 +300,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
@@ -371,7 +374,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
String gps1 = "2020-02-02T02:02:03Z"; String gps1 = "2020-02-02T02:02:03Z";
@@ -436,7 +439,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
String gps1 = "2020-02-02T02:02:03Z"; String gps1 = "2020-02-02T02:02:03Z";
@@ -500,7 +503,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
String gps1 = "2020-02-02T02:02:03Z"; String gps1 = "2020-02-02T02:02:03Z";
@@ -564,7 +567,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
String gps1 = "2020-02-02T02:02:03Z"; String gps1 = "2020-02-02T02:02:03Z";
@@ -652,7 +655,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
String gps1 = "2020-02-02T02:02:03Z"; String gps1 = "2020-02-02T02:02:03Z";
@@ -702,7 +705,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
String gps1 = "2020-02-02T02:02:03Z"; String gps1 = "2020-02-02T02:02:03Z";
@@ -748,7 +751,7 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
// when // when
String gps1 = "2020-02-02T02:02:03Z"; String gps1 = "2020-02-02T02:02:03Z";
@@ -812,8 +815,8 @@ public class TrackRecordingServiceTestRecording {
TrackPointCreator trackPointCreator = service.getTrackPointCreator(); TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime); trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager); trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager(); BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager();
// when // when
String sensor1 = "2020-02-02T02:02:03Z"; String sensor1 = "2020-02-02T02:02:03Z";
@@ -1,17 +1,12 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.location.Location; import android.location.Location;
import androidx.test.rule.ServiceTestRule; import androidx.test.rule.ServiceTestRule;
import org.mockito.Mockito;
import java.time.Clock;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -24,15 +19,6 @@ public class TrackRecordingServiceTestUtils {
public static void resetService(ServiceTestRule mServiceRule, Context context) throws TimeoutException { public static void resetService(ServiceTestRule mServiceRule, Context context) throws TimeoutException {
// Let's use default values. // Let's use default values.
PreferencesUtils.clear(); 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) { static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
@@ -26,7 +26,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class GPSHandlerTest { public class GPSManagerTest {
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
@@ -34,7 +34,7 @@ public class GPSHandlerTest {
private TrackPointCreator trackPointCreator; private TrackPointCreator trackPointCreator;
@InjectMocks @InjectMocks
private GPSHandler locationHandler; private GPSManager locationHandler;
@BeforeClass @BeforeClass
public static void preSetUp() { public static void preSetUp() {
@@ -50,7 +50,7 @@ public class GPSHandlerTest {
Mockito.when(trackPointCreator.createNow()) Mockito.when(trackPointCreator.createNow())
.thenReturn(Instant.now()); .thenReturn(Instant.now());
locationHandler.onStart(context, new Handler()); locationHandler.start(context, new Handler());
} }
/** /**
@@ -25,7 +25,7 @@ public class TrackPointCreatorTest {
private TrackPointCreator.Callback server; private TrackPointCreator.Callback server;
@Mock @Mock
private GPSHandler locationHandler; private GPSManager locationHandler;
private TrackPointCreator subject; private TrackPointCreator subject;
@@ -34,23 +34,24 @@ public class EGM2008UtilsTest {
int expectedLength = 18671444; int expectedLength = 18671444;
int expectedHeaderLength = 404; int expectedHeaderLength = 404;
String expectedHeader = "P5\n" + String expectedHeader = """
"# Geoid file in PGM format for the GeographicLib::Geoid class\n" + P5
"# Description WGS84 EGM2008, 5-minute grid\n" + # Geoid file in PGM format for the GeographicLib::Geoid class
"# URL http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008\n" + # Description WGS84 EGM2008, 5-minute grid
"# DateTime 2009-08-29 18:45:00\n" + # URL http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008
"# MaxBilinearError 0.478\n" + # DateTime 2009-08-29 18:45:00
"# RMSBilinearError 0.012\n" + # MaxBilinearError 0.478
"# MaxCubicError 0.294\n" + # RMSBilinearError 0.012
"# RMSCubicError 0.005\n" + # MaxCubicError 0.294
"# Offset -108\n" + # RMSCubicError 0.005
"# Scale 0.003\n" + # Offset -108
"# Origin 90N 0E\n" + # Scale 0.003
"# AREA_OR_POINT Point\n" + # Origin 90N 0E
"# Vertical_Datum WGS84\n" + # AREA_OR_POINT Point
"4320 2161\n" + # Vertical_Datum WGS84
"65535" + 4320 2161
"\n"; 65535
""";
// when // when
try (InputStream inputStream = context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA)) { 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()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then // 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 @Test
@@ -93,7 +94,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then // 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 @Test
@@ -108,7 +109,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then // 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 @Test
@@ -123,7 +124,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then // 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 @Test
@@ -138,7 +139,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then // 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 @Test
@@ -153,7 +154,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then // 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 @Test
@@ -173,7 +174,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint1.getLocation()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint1.getLocation());
// then // 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 @Test
@@ -189,7 +190,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then // 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 @Test
@@ -204,7 +205,7 @@ public class EGM2008UtilsTest {
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation()); EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then // 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 static org.junit.Assert.assertEquals;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
/** /**
@@ -37,7 +40,7 @@ public class FileUtilsTest {
*/ */
@Test @Test
public void testBuildUniqueFileName_new() { 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); assertEquals("Filename.ext", filename);
} }
@@ -45,9 +48,14 @@ public class FileUtilsTest {
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file exists already. * Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file exists already.
*/ */
@Test @Test
public void testBuildUniqueFileName_exist() { public void testBuildUniqueFileName_exist() throws IOException {
// Expect "/default.prop" to exist on the phone/emulator // given
String filename = FileUtils.buildUniqueFileName(new File("/"), "default", "prop"); 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); assertEquals("default(1).prop", filename);
} }
@@ -52,8 +52,8 @@ public class StatisticsUtilsTest extends TestCase {
recordingLayout.addField(new DataField("key5", true, true, false)); recordingLayout.addField(new DataField("key5", true, true, false));
// when // when
RecordingLayout resultTrue = StatisticsUtils.filterVisible(recordingLayout, true); RecordingLayout resultTrue = recordingLayout.toRecordingLayout(true);
RecordingLayout resultFalse = StatisticsUtils.filterVisible(recordingLayout, false); RecordingLayout resultFalse = recordingLayout.toRecordingLayout(false);
// then // then
assertEquals(resultTrue.getFields().size(), 3); assertEquals(resultTrue.getFields().size(), 3);
+5 -1
View File
@@ -62,7 +62,9 @@ limitations under the License.
<application <application
android:name=".Startup" 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:hardwareAccelerated="true"
android:hasFragileUserData="false" android:hasFragileUserData="false"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
@@ -87,6 +89,7 @@ limitations under the License.
</intent-filter> </intent-filter>
</activity> </activity>
<!-- NOTE: the name of the MAIN/LAUNCHER is used by icon packs; if this is changed, icon packs break.-->
<activity <activity
android:name=".introduction.IntroductionActivity" android:name=".introduction.IntroductionActivity"
android:exported="true" android:exported="true"
@@ -346,6 +349,7 @@ limitations under the License.
<activity android:name=".ui.aggregatedStatistics.AggregatedStatisticsActivity" /> <activity android:name=".ui.aggregatedStatistics.AggregatedStatisticsActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" /> <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.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity" />
<activity android:name=".io.file.exporter.ExportActivity" /> <activity android:name=".io.file.exporter.ExportActivity" />
@@ -24,6 +24,7 @@ import android.widget.ArrayAdapter;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackEditBinding; import de.dennisguse.opentracks.databinding.TrackEditBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
@@ -75,7 +76,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
viewBinding.fields.trackEditActivityType.setText(track.getCategory()); viewBinding.fields.trackEditActivityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, ActivityType.getLocalizedStrings(this));
viewBinding.fields.trackEditActivityType.setAdapter(adapter); viewBinding.fields.trackEditActivityType.setAdapter(adapter);
viewBinding.fields.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.fields.trackEditActivityType.getAdapter().getItem(position)))); viewBinding.fields.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.fields.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.fields.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> { viewBinding.fields.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
@@ -25,7 +25,6 @@ import android.database.Cursor;
import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.AnimatedVectorDrawable;
import android.location.LocationManager; import android.location.LocationManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Vibrator;
import android.provider.Settings; import android.provider.Settings;
import android.util.Pair; import android.util.Pair;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -246,7 +245,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
} }
// Recording -> Stop // Recording -> Stop
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000); ActivityUtils.vibrate(this, 1000);
updateGpsMenuItem(false, false); updateGpsMenuItem(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this); trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
viewBinding.trackListFabAction.setImageResource(R.drawable.ic_baseline_record_24); viewBinding.trackListFabAction.setImageResource(R.drawable.ic_baseline_record_24);
@@ -311,9 +310,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
searchMenuItem = menu.findItem(R.id.track_list_search); searchMenuItem = menu.findItem(R.id.track_list_search);
SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem); SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem);
searchView.findViewById(R.id.search_edit_frame).setPadding(0, 0, 48, 0); searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setPadding(0, 0, 48, 0);
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(R.id.search_src_text); SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(androidx.appcompat.R.id.search_src_text);
searchAutoComplete.setHintTextColor(ContextCompat.getColor(this, android.R.color.white)); searchAutoComplete.setHintTextColor(ContextCompat.getColor(this, android.R.color.white));
searchAutoComplete.setTextColor(ContextCompat.getColor(this, android.R.color.white)); searchAutoComplete.setTextColor(ContextCompat.getColor(this, android.R.color.white));
@@ -382,6 +381,12 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
SearchView searchView = (SearchView) searchMenuItem.getActionView(); SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setIconified(false); searchView.setIconified(false);
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 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); return super.onOptionsItemSelected(item);
@@ -274,18 +274,14 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
@NonNull @NonNull
@Override @Override
public Fragment createFragment(int position) { public Fragment createFragment(int position) {
switch (position) { return switch (position) {
case 0: case 0 -> StatisticsRecordedFragment.newInstance(trackId);
return StatisticsRecordedFragment.newInstance(trackId); case 1 -> IntervalsFragment.newInstance(trackId, true);
case 1: case 2 -> ChartFragment.newInstance(false);
return IntervalsFragment.newInstance(trackId, true); case 3 -> ChartFragment.newInstance(true);
case 2: default ->
return ChartFragment.newInstance(false); throw new RuntimeException("There isn't Fragment associated with the position: " + position);
case 3: };
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
} }
@Override @Override
@@ -294,18 +290,14 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
} }
public CharSequence getPageTitle(int position) { public CharSequence getPageTitle(int position) {
switch (position) { return switch (position) {
case 0: case 0 -> getString(R.string.track_detail_stats_tab);
return getString(R.string.track_detail_stats_tab); case 1 -> getString(R.string.track_detail_intervals_tab);
case 1: case 2 -> getString(R.string.settings_chart_by_time);
return getString(R.string.track_detail_intervals_tab); case 3 -> getString(R.string.settings_chart_by_distance);
case 2: default ->
return getString(R.string.settings_chart_by_time); throw new RuntimeException("There isn't Fragment associated with the position: " + position);
case 3: };
return 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; package de.dennisguse.opentracks;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Vibrator;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; 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.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity; import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity; 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.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -137,7 +136,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color)); viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setBackgroundColor(ContextCompat.getColor(this, R.color.opentracks_secondary_color)); viewBinding.trackRecordingFabAction.setBackgroundColor(ContextCompat.getColor(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> { viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> {
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000); ActivityUtils.vibrate(this, 1000);
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this); trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackStoppedActivity.class) Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackStoppedActivity.class)
.putExtra(TrackStoppedActivity.EXTRA_TRACK_ID, trackId); .putExtra(TrackStoppedActivity.EXTRA_TRACK_ID, trackId);
@@ -333,18 +332,14 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@NonNull @NonNull
@Override @Override
public Fragment createFragment(int position) { public Fragment createFragment(int position) {
switch (position) { return switch (position) {
case 0: case 0 -> StatisticsRecordingFragment.newInstance();
return StatisticsRecordingFragment.newInstance(); case 1 -> IntervalsFragment.newInstance(trackId, false);
case 1: case 2 -> ChartFragment.newInstance(false);
return IntervalsFragment.newInstance(trackId, false); case 3 -> ChartFragment.newInstance(true);
case 2: default ->
return ChartFragment.newInstance(false); throw new RuntimeException("There isn't Fragment associated with the position: " + position);
case 3: };
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
} }
@Override @Override
@@ -353,18 +348,14 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
} }
public CharSequence getPageTitle(int position) { public CharSequence getPageTitle(int position) {
switch (position) { return switch (position) {
case 0: case 0 -> getString(R.string.track_detail_stats_tab);
return getString(R.string.track_detail_stats_tab); case 1 -> getString(R.string.track_detail_intervals_tab);
case 1: case 2 -> getString(R.string.settings_chart_by_time);
return getString(R.string.track_detail_intervals_tab); case 3 -> getString(R.string.settings_chart_by_distance);
case 2: default ->
return getString(R.string.settings_chart_by_time); throw new RuntimeException("There isn't Fragment associated with the position: " + position);
case 3: };
return 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) { private void onGpsStatusChanged(GpsStatusValue gpsStatusValue) {
if (gpsStatusValue.isGpsStarted() && snackbar != null && snackbar.isShown()) { boolean snackbarShowing = snackbar != null && snackbar.isShown();
snackbar.dismiss(); if (gpsStatusValue.isGpsStarted()) {
if (snackbarShowing) {
snackbar.dismiss();
}
return; return;
} }
if (gpsStatusValue != GpsStatusValue.GPS_DISABLED) {
if (snackbarShowing) {
return; return;
} }
snackbar = Snackbar snackbar = Snackbar
.make(viewBinding.trackRecordingCoordinatorLayout, .make(viewBinding.trackRecordingCoordinatorLayout,
getString(R.string.gps_recording_status, getString(gpsStatusValue.message), getString(R.string.gps_recording_without_signal)), getString(R.string.gps_recording_status, getString(gpsStatusValue.message), getString(R.string.gps_recording_without_signal)),
@@ -8,6 +8,7 @@ import android.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.DistanceFormatter; import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter; import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
@@ -51,7 +52,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
viewBinding.trackEditActivityType.setText(track.getCategory()); viewBinding.trackEditActivityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, ActivityType.getLocalizedStrings(this));
viewBinding.trackEditActivityType.setAdapter(adapter); viewBinding.trackEditActivityType.setAdapter(adapter);
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position)))); viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> { viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
@@ -210,16 +210,11 @@ abstract class ChartValueSeries {
} }
int getTitleId(UnitSystem unitSystem) { int getTitleId(UnitSystem unitSystem) {
switch (unitSystem) { return switch (unitSystem) {
case METRIC: case METRIC -> metricTitleId;
return metricTitleId; case IMPERIAL -> imperialTitleId;
case IMPERIAL: case NAUTICAL_IMPERIAL -> nauticalTitleId;
return imperialTitleId; };
case NAUTICAL_IMPERIAL:
return nauticalTitleId;
default:
throw new RuntimeException("Not implemented");
}
} }
Paint getTitlePaint() { Paint getTitlePaint() {
@@ -733,16 +733,11 @@ public class ChartView extends View {
private String getXAxisLabel() { private String getXAxisLabel() {
Context context = getContext(); Context context = getContext();
if (chartByDistance) { if (chartByDistance) {
switch (unitSystem) { return switch (unitSystem) {
case METRIC: case METRIC -> context.getString(R.string.unit_kilometer);
return context.getString(R.string.unit_kilometer); case IMPERIAL -> context.getString(R.string.unit_mile);
case IMPERIAL: case NAUTICAL_IMPERIAL -> context.getString(R.string.unit_nautical_mile);
return context.getString(R.string.unit_mile); };
case NAUTICAL_IMPERIAL:
return context.getString(R.string.unit_nautical_mile);
default:
throw new RuntimeException("Not implemented");
}
} else { } else {
return context.getString(R.string.description_time); return context.getString(R.string.description_time);
} }
@@ -133,20 +133,12 @@ public class CustomContentProvider extends ContentProvider {
@Override @Override
public int delete(@NonNull Uri url, String where, String[] selectionArgs) { public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
String table; String table = switch (getUrlType(url)) {
switch (getUrlType(url)) { case TRACKPOINTS -> TrackPointsColumns.TABLE_NAME;
case TRACKPOINTS: case TRACKS -> TracksColumns.TABLE_NAME;
table = TrackPointsColumns.TABLE_NAME; case MARKERS -> MarkerColumns.TABLE_NAME;
break; default -> throw new IllegalArgumentException("Unknown URL " + url);
case TRACKS: };
table = TracksColumns.TABLE_NAME;
break;
case MARKERS:
table = MarkerColumns.TABLE_NAME;
break;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
Log.w(TAG, "Deleting from table " + table); Log.w(TAG, "Deleting from table " + table);
int totalChangesBefore = getTotalChanges(); int totalChangesBefore = getTotalChanges();
@@ -186,24 +178,15 @@ public class CustomContentProvider extends ContentProvider {
@Override @Override
public String getType(@NonNull Uri url) { public String getType(@NonNull Uri url) {
switch (getUrlType(url)) { return switch (getUrlType(url)) {
case TRACKPOINTS: case TRACKPOINTS -> TrackPointsColumns.CONTENT_TYPE;
return TrackPointsColumns.CONTENT_TYPE; case TRACKPOINTS_BY_ID, TRACKPOINTS_BY_TRACKID -> TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKPOINTS_BY_ID: case TRACKS -> TracksColumns.CONTENT_TYPE;
case TRACKPOINTS_BY_TRACKID: case TRACKS_BY_ID -> TracksColumns.CONTENT_ITEMTYPE;
return TrackPointsColumns.CONTENT_ITEMTYPE; case MARKERS -> MarkerColumns.CONTENT_TYPE;
case TRACKS: case MARKERS_BY_ID, MARKERS_BY_TRACKID -> MarkerColumns.CONTENT_ITEMTYPE;
return TracksColumns.CONTENT_TYPE; default -> throw new IllegalArgumentException("Unknown URL " + url);
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);
}
} }
@Override @Override
@@ -251,47 +234,47 @@ public class CustomContentProvider extends ContentProvider {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String sortOrder = null; String sortOrder = null;
switch (getUrlType(url)) { switch (getUrlType(url)) {
case TRACKPOINTS: case TRACKPOINTS -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME); queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER; sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
break; }
case TRACKPOINTS_BY_ID: case TRACKPOINTS_BY_ID -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME); queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns._ID + "=" + ContentUris.parseId(url)); queryBuilder.appendWhere(TrackPointsColumns._ID + "=" + ContentUris.parseId(url));
break; }
case TRACKPOINTS_BY_TRACKID: case TRACKPOINTS_BY_TRACKID -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME); queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); 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)) { 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)"); 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 { } else {
queryBuilder.setTables(TracksColumns.TABLE_NAME); queryBuilder.setTables(TracksColumns.TABLE_NAME);
} }
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER; sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
break; }
case TRACKS_BY_ID: case TRACKS_BY_ID -> {
queryBuilder.setTables(TracksColumns.TABLE_NAME); queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); 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); long trackId = ContentUris.parseId(url);
return db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)}); return db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)});
case MARKERS: }
case MARKERS -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME); queryBuilder.setTables(MarkerColumns.TABLE_NAME);
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER; sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
break; }
case MARKERS_BY_ID: case MARKERS_BY_ID -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME); queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns._ID + "=" + ContentUris.parseId(url)); queryBuilder.appendWhere(MarkerColumns._ID + "=" + ContentUris.parseId(url));
break; }
case MARKERS_BY_TRACKID: case MARKERS_BY_TRACKID -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME); queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break; }
default: default -> throw new IllegalArgumentException("Unknown url " + url);
throw new IllegalArgumentException("Unknown url " + url);
} }
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), url); cursor.setNotificationUri(getContext().getContentResolver(), url);
@@ -304,41 +287,40 @@ public class CustomContentProvider extends ContentProvider {
String table; String table;
String whereClause; String whereClause;
switch (getUrlType(url)) { switch (getUrlType(url)) {
case TRACKPOINTS: case TRACKPOINTS -> {
table = TrackPointsColumns.TABLE_NAME; table = TrackPointsColumns.TABLE_NAME;
whereClause = where; whereClause = where;
break; }
case TRACKPOINTS_BY_ID: case TRACKPOINTS_BY_ID -> {
table = TrackPointsColumns.TABLE_NAME; table = TrackPointsColumns.TABLE_NAME;
whereClause = TrackPointsColumns._ID + "=" + ContentUris.parseId(url); whereClause = TrackPointsColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) { if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")"; whereClause += " AND (" + where + ")";
} }
break; }
case TRACKS: case TRACKS -> {
table = TracksColumns.TABLE_NAME; table = TracksColumns.TABLE_NAME;
whereClause = where; whereClause = where;
break; }
case TRACKS_BY_ID: case TRACKS_BY_ID -> {
table = TracksColumns.TABLE_NAME; table = TracksColumns.TABLE_NAME;
whereClause = TracksColumns._ID + "=" + ContentUris.parseId(url); whereClause = TracksColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) { if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")"; whereClause += " AND (" + where + ")";
} }
break; }
case MARKERS: case MARKERS -> {
table = MarkerColumns.TABLE_NAME; table = MarkerColumns.TABLE_NAME;
whereClause = where; whereClause = where;
break; }
case MARKERS_BY_ID: case MARKERS_BY_ID -> {
table = MarkerColumns.TABLE_NAME; table = MarkerColumns.TABLE_NAME;
whereClause = MarkerColumns._ID + "=" + ContentUris.parseId(url); whereClause = MarkerColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) { if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")"; whereClause += " AND (" + where + ")";
} }
break; }
default: default -> throw new IllegalArgumentException("Unknown url " + url);
throw new IllegalArgumentException("Unknown url " + url);
} }
int count; int count;
try { try {
@@ -371,16 +353,12 @@ public class CustomContentProvider extends ContentProvider {
* @param contentValues the content values * @param contentValues the content values
*/ */
private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) { private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) {
switch (urlType) { return switch (urlType) {
case TRACKPOINTS: case TRACKPOINTS -> insertTrackPoint(url, contentValues);
return insertTrackPoint(url, contentValues); case TRACKS -> insertTrack(url, contentValues);
case TRACKS: case MARKERS -> insertMarker(url, contentValues);
return insertTrack(url, contentValues); default -> throw new IllegalArgumentException("Unknown url " + url);
case MARKERS: };
return insertMarker(url, contentValues);
default:
throw new IllegalArgumentException("Unknown url " + url);
}
} }
private Uri insertTrackPoint(Uri url, ContentValues values) { private Uri insertTrackPoint(Uri url, ContentValues values) {
@@ -61,42 +61,18 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
for (int toVersion = oldVersion + 1; toVersion <= newVersion; toVersion++) { for (int toVersion = oldVersion + 1; toVersion <= newVersion; toVersion++) {
Log.i(TAG, "Upgrade from " + oldVersion + " to " + toVersion); Log.i(TAG, "Upgrade from " + oldVersion + " to " + toVersion);
switch (toVersion) { switch (toVersion) {
case 24: case 24 -> upgradeFrom23to24(db);
upgradeFrom23to24(db); case 25 -> upgradeFrom24to25(db);
break; case 26 -> upgradeFrom25to26(db);
case 25: case 27 -> upgradeFrom26to27(db);
upgradeFrom24to25(db); case 28 -> upgradeFrom27to28(db);
break; case 29 -> upgradeFrom28to29(db);
case 26: case 30 -> upgradeFrom29to30(db);
upgradeFrom25to26(db); case 31 -> upgradeFrom30to31(db);
break; case 32 -> upgradeFrom31to32(db);
case 27: case 33 -> upgradeFrom32to33(db);
upgradeFrom26to27(db); case 34 -> upgradeFrom33to34(db);
break; default -> throw new RuntimeException("Not implemented: upgrade to " + toVersion);
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);
} }
} }
} }
@@ -106,41 +82,18 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
for (int toVersion = oldVersion - 1; toVersion >= newVersion; toVersion--) { for (int toVersion = oldVersion - 1; toVersion >= newVersion; toVersion--) {
Log.i(TAG, "Downgrade from " + oldVersion + " to " + toVersion); Log.i(TAG, "Downgrade from " + oldVersion + " to " + toVersion);
switch (toVersion) { switch (toVersion) {
case 23: case 23 -> downgradeFrom24to23(db);
downgradeFrom24to23(db); case 24 -> downgradeFrom25to24(db);
break; case 25 -> downgradeFrom26to25(db);
case 24: case 26 -> downgradeFrom27to26(db);
downgradeFrom25to24(db); case 27 -> downgradeFrom28to27(db);
break; case 28 -> downgradeFrom29to28(db);
case 25: case 29 -> downgradeFrom30to29(db);
downgradeFrom26to25(db); case 30 -> downgradeFrom31to30(db);
break; case 31 -> downgradeFrom32to31(db);
case 26: case 32 -> downgradeFrom33to32(db);
downgradeFrom27to26(db); case 33 -> downgradeFrom34to33(db);
break; default -> throw new RuntimeException("Not implemented: downgrade to " + toVersion);
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);
} }
} }
} }
@@ -129,24 +129,18 @@ public class ShareContentProvider extends CustomContentProvider {
} }
private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) { private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) { return switch (uriMatcher.match(uri)) {
case URI_GPX: case URI_GPX -> TrackFileFormat.GPX;
return TrackFileFormat.GPX; case URI_KML_WITH_TRACKDETAIL_SENSORDATA ->
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KML_WITH_TRACKDETAIL_SENSORDATA: case URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA ->
return TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA; TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES ->
case URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA: TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA; case URI_CSV -> TrackFileFormat.CSV;
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES: default ->
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES; throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
};
case URI_CSV:
return TrackFileFormat.CSV;
default:
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
}
} }
@Nullable @Nullable
@@ -170,14 +164,14 @@ public class ShareContentProvider extends CustomContentProvider {
int i = 0; int i = 0;
for (String col : projection) { for (String col : projection) {
switch (col) { switch (col) {
case OpenableColumns.DISPLAY_NAME: case OpenableColumns.DISPLAY_NAME -> {
cols[i] = OpenableColumns.DISPLAY_NAME; cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = uri.getLastPathSegment(); values[i++] = uri.getLastPathSegment();
break; }
case OpenableColumns.SIZE: case OpenableColumns.SIZE -> {
cols[i] = 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. 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) -> { PipeDataWriter<String> pipeDataWriter = (output, uri1, mimeType, opts, args) -> {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) { try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
// TODO handle failure (i.e., do not export an empty file) // 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) { } catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e); Log.w(TAG, "there occurred an error while sharing a file: " + e);
} }
@@ -0,0 +1,79 @@
package de.dennisguse.opentracks.data.models;
import android.content.Context;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
public enum ActivityType {
AIRPLANE("AIRPLANE", R.drawable.ic_activity_flight_24dp, true, R.string.activity_type_airplane, R.string.activity_type_commercial_airplane, R.string.activity_type_rc_airplane),
BIKE("BIKE", R.drawable.ic_activity_bike_24dp, true, R.string.activity_type_biking, R.string.activity_type_cycling, R.string.activity_type_dirt_bike, R.string.activity_type_road_biking, R.string.activity_type_track_cycling),
MOUNTAIN_BIKE("MOUNTAIN_BIKE", R.drawable.ic_activity_mtb_24dp, true, R.string.activity_type_mountain_biking),
MOTOR_BIKE("MOTOR_BIKE", R.drawable.ic_activity_motorbike_24dp, true, R.string.activity_type_motor_bike),
KAYAK("KAYAK", R.drawable.ic_activity_kayaking_24dp, true, R.string.activity_type_kayaking),
BOAT("BOAT", R.drawable.ic_activity_boat_24dp, true, R.string.activity_type_boat, R.string.activity_type_ferry, R.string.activity_type_motor_boating, R.string.activity_type_rc_boat),
SAILING("SAILING", R.drawable.ic_activity_sailing_24dp, true, R.string.activity_type_sailing),
DRIVE("DRIVE", R.drawable.ic_activity_drive_24dp, true, R.string.activity_type_atv, R.string.activity_type_driving, R.string.activity_type_driving_bus, R.string.activity_type_driving_car),
RUN("RUN", R.drawable.ic_activity_run_24dp, false, R.string.activity_type_running, R.string.activity_type_street_running, R.string.activity_type_track_running, R.string.activity_type_trail_running),
SKI("SKI", R.drawable.ic_activity_skiing_24dp, true, R.string.activity_type_cross_country_skiing, R.string.activity_type_skiing),
SNOW_BOARDING("SNOW_BOARDING", R.drawable.ic_activity_snowboarding_24dp, true, R.string.activity_type_snow_boarding),
UNKNOWN("UNKNOWN", R.drawable.ic_logo_24dp, true, R.string.activity_type_unknown),
WALK("WALK", R.drawable.ic_activity_walk_24dp, false, R.string.activity_type_hiking, R.string.activity_type_off_trail_hiking, R.string.activity_type_speed_walking, R.string.activity_type_trail_hiking, R.string.activity_type_walking),
ESCOOTER("ESCOOTER", R.drawable.ic_activity_escooter_24dp, true, R.string.activity_type_escooter),
INLINE_SKATING("INLINES_SKATING", R.drawable.ic_activity_inline_skating_24dp, true, R.string.activity_type_inline_skating),
SKATE_BOARDING("SKATE_BOARDING", R.drawable.ic_activity_skateboarding_24dp, true, R.string.activity_type_skate_boarding),
CLIMBING("CLIMBING", R.drawable.ic_activity_climbing_24dp, false, R.string.activity_type_climbing),
SWIMMING("SWIMMING", R.drawable.ic_activity_swimming_24dp, false, R.string.activity_type_swimming),
SWIMMING_OPEN("SWIMMING_OPEN", R.drawable.ic_activity_swimming_open_24dp, false, R.string.activity_type_swimming_open),
WORKOUT("WORKOUT", R.drawable.ic_activity_workout_24dp, false, R.string.activity_type_workout);
final String id;
final int iconId;
final boolean showSpeedPreferred;
final int[] localizedStringIds;
//isSpeed?
ActivityType(String id, int iconId, boolean showSpeedPreferred, int... localizedStringIds) {
this.id = id;
this.iconId = iconId;
this.showSpeedPreferred = showSpeedPreferred;
this.localizedStringIds = localizedStringIds;
}
public String getId() {
return id;
}
public int getIconId() {
return iconId;
}
public boolean isShowSpeedPreferred() {
return showSpeedPreferred;
}
public int[] getLocalizedStringIds() {
return localizedStringIds;
}
public int getFirstLocalizedStringId() {
return localizedStringIds[0];
}
public static List<String> getLocalizedStrings(Context context) {
List<String> result = new ArrayList<>();
for (ActivityType activityType : values()) {
List<String> strings = Arrays.stream(activityType.localizedStringIds)
.mapToObj(context::getString)
.collect(Collectors.toList());
result.addAll(strings);
}
return result;
}
}
@@ -15,9 +15,7 @@ public abstract class Altitude {
public double toM() { public double toM() {
return altitude_m; return altitude_m;
} }
public abstract Altitude replace(double altitude_m);
public abstract int getLabelId(); public abstract int getLabelId();
public static class WGS84 extends Altitude { public static class WGS84 extends Altitude {
@@ -31,12 +29,7 @@ public abstract class Altitude {
return R.string.wgs84; return R.string.wgs84;
} }
public static Altitude of(double altitude_m) { public static WGS84 of(double altitude_m) {
return new WGS84(altitude_m);
}
@Override
public Altitude replace(double altitude_m) {
return new WGS84(altitude_m); return new WGS84(altitude_m);
} }
} }
@@ -52,12 +45,7 @@ public abstract class Altitude {
return R.string.egm2008; return R.string.egm2008;
} }
public static Altitude of(double altitude_m) { public static EGM2008 of(double altitude_m) {
return new EGM2008(altitude_m);
}
@Override
public Altitude replace(double altitude_m) {
return new EGM2008(altitude_m); return new EGM2008(altitude_m);
} }
} }
@@ -0,0 +1,18 @@
package de.dennisguse.opentracks.data.models;
public class BatteryLevel {
public static BatteryLevel of(int percentage) {
return new BatteryLevel(percentage);
}
private final int percentage;
private BatteryLevel(int percentage) {
this.percentage = percentage;
}
public int getPercentage() {
return percentage;
}
}
@@ -58,16 +58,11 @@ public class Distance {
} }
public static Distance one(UnitSystem unitSystem) { public static Distance one(UnitSystem unitSystem) {
switch (unitSystem) { return switch (unitSystem) {
case METRIC: case METRIC -> Distance.ofKilometer(1);
return Distance.ofKilometer(1); case IMPERIAL -> Distance.ofMile(1);
case IMPERIAL: case NAUTICAL_IMPERIAL -> Distance.ofNauticalMile(1);
return Distance.ofMile(1); };
case NAUTICAL_IMPERIAL:
return Distance.ofNauticalMile(1);
default:
throw new RuntimeException("Not implemented");
}
} }
private final double distance_m; private final double distance_m;
@@ -133,28 +128,18 @@ public class Distance {
} }
public double toKM_Miles(UnitSystem unitSystem) { public double toKM_Miles(UnitSystem unitSystem) {
switch (unitSystem) { return switch (unitSystem) {
case METRIC: case METRIC -> toKM();
return toKM(); case IMPERIAL -> toMI();
case IMPERIAL: case NAUTICAL_IMPERIAL -> toNauticalMiles();
return toMI(); };
case NAUTICAL_IMPERIAL:
return toNauticalMiles();
default:
throw new RuntimeException("Not implemented");
}
} }
public double toM_FT(UnitSystem unitSystem) { public double toM_FT(UnitSystem unitSystem) {
switch (unitSystem) { return switch (unitSystem) {
case METRIC: case METRIC -> toM();
return toM(); case NAUTICAL_IMPERIAL, IMPERIAL -> toFT();
case NAUTICAL_IMPERIAL: };
case IMPERIAL:
return toFT();
default:
throw new RuntimeException("Not implemented");
}
} }
@Override @Override
@@ -46,38 +46,36 @@ public class DistanceFormatter {
public Pair<String, String> getDistanceParts(Distance distance) { public Pair<String, String> getDistanceParts(Distance distance) {
if (distance.isInvalid()) { if (distance.isInvalid()) {
String valueUnknown = resources.getString(R.string.value_unknown); String valueUnknown = resources.getString(R.string.value_unknown);
switch (unitSystem) { return switch (unitSystem) {
case METRIC: case METRIC -> new Pair<>(valueUnknown, resources.getString(R.string.unit_meter));
return new Pair<>(valueUnknown, resources.getString(R.string.unit_meter)); case IMPERIAL, NAUTICAL_IMPERIAL ->
case IMPERIAL: new Pair<>(valueUnknown, resources.getString(R.string.unit_feet));
case NAUTICAL_IMPERIAL: };
return new Pair<>(valueUnknown, resources.getString(R.string.unit_feet));
default:
throw new RuntimeException("Not implemented");
}
} }
switch (unitSystem) { switch (unitSystem) {
case METRIC: case METRIC -> {
if (distance.greaterThan(Distance.ofKilometer(threshold))) { if (distance.greaterThan(Distance.ofKilometer(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer)); return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
} else { } else {
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter)); return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
} }
case IMPERIAL: }
case IMPERIAL -> {
if (distance.greaterThan(Distance.ofMile(threshold))) { if (distance.greaterThan(Distance.ofMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile)); return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
} else { } else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet)); return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
} }
case NAUTICAL_IMPERIAL: }
case NAUTICAL_IMPERIAL -> {
if (distance.greaterThan(Distance.ofNauticalMile(threshold))) { if (distance.greaterThan(Distance.ofNauticalMile(threshold))) {
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile)); return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
} else { } else {
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet)); 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");
} }
} }
@@ -1,8 +1,12 @@
package de.dennisguse.opentracks.data.models; package de.dennisguse.opentracks.data.models;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.ui.util.ThemeUtils;
public class HeartRateZones { public class HeartRateZones {
@@ -12,22 +16,23 @@ public class HeartRateZones {
this.max = max; this.max = max;
} }
public int getColorForZone(HeartRate current) { public int getTextColorForZone(Context context, HeartRate current) {
if (current != null) { if (current != null) {
if (current.getBPM() >= max.getBPM() * 0.9) { if (current.getBPM() >= max.getBPM() * 0.9) {
return R.color.heart_rate_zone_textcolor_5; return ContextCompat.getColor(context, R.color.heart_rate_zone_textcolor_5);
} }
if (current.getBPM() >= max.getBPM() * 0.8) { if (current.getBPM() >= max.getBPM() * 0.8) {
return R.color.heart_rate_zone_textcolor_4; return ContextCompat.getColor(context, R.color.heart_rate_zone_textcolor_4);
} }
if (current.getBPM() >= max.getBPM() * 0.7) { if (current.getBPM() >= max.getBPM() * 0.7) {
return R.color.heart_rate_zone_textcolor_3; return ContextCompat.getColor(context, R.color.heart_rate_zone_textcolor_3);
} }
if (current.getBPM() >= max.getBPM() * 0.6) { if (current.getBPM() >= max.getBPM() * 0.6) {
return R.color.heart_rate_zone_textcolor_2; return ContextCompat.getColor(context, R.color.heart_rate_zone_textcolor_2);
} }
} }
return R.color.heart_rate_zone_textcolor_1; // Zone 1
return ThemeUtils.getTextColorPrimary(context);
} }
} }
@@ -115,16 +115,11 @@ public class Speed {
} }
public double to(UnitSystem unitSystem) { public double to(UnitSystem unitSystem) {
switch (unitSystem) { return switch (unitSystem) {
case METRIC: case METRIC -> toKMH();
return toKMH(); case IMPERIAL -> toMPH();
case IMPERIAL: case NAUTICAL_IMPERIAL -> toKnots();
return toMPH(); };
case NAUTICAL_IMPERIAL:
return toKnots();
default:
throw new RuntimeException("Not implemented");
}
} }
@Override @Override
@@ -39,20 +39,14 @@ public class SpeedFormatter {
* @return the formatted speed (or null) and it's unit as {@link Pair} * @return the formatted speed (or null) and it's unit as {@link Pair}
*/ */
public Pair<String, String> getSpeedParts(Speed speed) { public Pair<String, String> getSpeedParts(Speed speed) {
int unitId; int unitId = switch (unitSystem) {
switch (unitSystem) { case METRIC ->
case METRIC: reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
unitId = reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer; case IMPERIAL ->
break; reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
case IMPERIAL: case NAUTICAL_IMPERIAL ->
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile; reportSpeedOrPace ? R.string.unit_knots : R.string.unit_minute_per_nautical_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");
}
String unitString = resources.getString(unitId); String unitString = resources.getString(unitId);
@@ -122,17 +122,15 @@ public class StatisticsRecordingFragment extends Fragment {
} }
recordingLayout = newRecordingLayout; recordingLayout = newRecordingLayout;
viewBinding.statsLayout.setColumnCount(recordingLayout.getColumnsPerRow());
viewBinding.statsLayout.removeAllViews(); //Let's start from scratch viewBinding.statsLayout.removeAllViews(); //Let's start from scratch
viewBinding.statsLayout.setColumnCount(recordingLayout.getColumnsPerRow());
viewHolders.clear(); viewHolders.clear();
Map<String, Callable<StatisticViewHolder<?>>> m = Mapping.create(getContext()); Map<String, Callable<StatisticViewHolder<?>>> m = Mapping.create(getContext());
int rowIndex = 0; int rowIndex = 0;
int columnIndex = 0; int columnIndex = 0;
for (int i = 0; i < recordingLayout.getFields().size(); i++) { for (DataField dataField : recordingLayout.toRecordingLayout(true).getFields()) {
DataField dataField = recordingLayout.getFields().get(i);
GridLayout.LayoutParams param = new GridLayout.LayoutParams(); GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.setGravity(Gravity.FILL_HORIZONTAL); param.setGravity(Gravity.FILL_HORIZONTAL);
param.width = 0; param.width = 0;
@@ -162,7 +160,7 @@ public class StatisticsRecordingFragment extends Fragment {
viewBinding.statsLayout.addView(viewHolder.getView(), param); viewBinding.statsLayout.addView(viewHolder.getView(), param);
} catch (Exception e) { } 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.Context;
import android.content.res.Resources; import android.content.res.Resources;
import androidx.annotation.NonNull;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Locale; import java.util.Locale;
@@ -25,8 +27,8 @@ public enum TrackFileFormat {
KML_WITH_TRACKDETAIL_AND_SENSORDATA("KML_WITH_TRACKDETAIL_AND_SENSORDATA") { KML_WITH_TRACKDETAIL_AND_SENSORDATA("KML_WITH_TRACKDETAIL_AND_SENSORDATA") {
@Override @Override
public TrackExporter createTrackExporter(Context context) { public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
return new KMLTrackExporter(context, false); return new KMLTrackExporter(context, contentProviderUtils, false);
} }
@Override @Override
@@ -45,9 +47,9 @@ public enum TrackFileFormat {
private static final boolean exportPhotos = false; private static final boolean exportPhotos = false;
@Override @Override
public TrackExporter createTrackExporter(Context context) { public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
KMLTrackExporter exporter = new KMLTrackExporter(context, exportPhotos); KMLTrackExporter exporter = new KMLTrackExporter(context, contentProviderUtils, exportPhotos);
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos); return new KmzTrackExporter(context, contentProviderUtils, exporter, exportPhotos);
} }
@Override @Override
@@ -70,9 +72,9 @@ public enum TrackFileFormat {
private static final boolean exportPhotos = true; private static final boolean exportPhotos = true;
@Override @Override
public TrackExporter createTrackExporter(Context context) { public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
KMLTrackExporter exporter = new KMLTrackExporter(context, exportPhotos); KMLTrackExporter exporter = new KMLTrackExporter(context, contentProviderUtils, exportPhotos);
return new KmzTrackExporter(context, new ContentProviderUtils(context), exporter, exportPhotos); return new KmzTrackExporter(context, contentProviderUtils, exporter, exportPhotos);
} }
@Override @Override
@@ -93,8 +95,8 @@ public enum TrackFileFormat {
GPX("GPX") { GPX("GPX") {
@Override @Override
public TrackExporter createTrackExporter(Context context) { public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
return new GPXTrackExporter(new ContentProviderUtils(context), context.getString(R.string.app_name)); return new GPXTrackExporter(contentProviderUtils, context.getString(R.string.app_name));
} }
@Override @Override
@@ -109,8 +111,8 @@ public enum TrackFileFormat {
CSV("CSV") { CSV("CSV") {
@Override @Override
public TrackExporter createTrackExporter(Context context) { public TrackExporter createTrackExporter(@NonNull Context context, @NonNull ContentProviderUtils contentProviderUtils) {
return new CSVTrackExporter(new ContentProviderUtils(context)); return new CSVTrackExporter(contentProviderUtils);
} }
@Override @Override
@@ -161,7 +163,7 @@ public enum TrackFileFormat {
* *
* @param context the context * @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. * Returns the file extension for each format.
@@ -29,6 +29,10 @@ public class TrackFilenameGenerator {
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
} }
public static String format(@NonNull String name, @NonNull TrackFileFormat trackFileFormat) {
return FileUtils.sanitizeFileName(name + "." + trackFileFormat.getExtension());
}
private final String template; private final String template;
public TrackFilenameGenerator(@NonNull String template) { public TrackFilenameGenerator(@NonNull String template) {
@@ -44,7 +48,7 @@ public class TrackFilenameGenerator {
values.put(STARTTIME_TIME_KEY, track.getStartTime().toLocalTime().toString()); values.put(STARTTIME_TIME_KEY, track.getStartTime().toLocalTime().toString());
values.put(STARTTIME_DATE_KEY, track.getStartTime().toLocalDate().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) { private static String format(String template, Map<String, String> values) {
@@ -77,10 +81,6 @@ public class TrackFilenameGenerator {
return String.format(templateCompiled, valueList.toArray()); return String.format(templateCompiled, valueList.toArray());
} }
public String getTemplate() {
return template;
}
public boolean isValid() { public boolean isValid() {
try { try {
getExample(); getExample();
@@ -23,8 +23,6 @@ import androidx.annotation.NonNull;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.function.Function; import java.util.function.Function;
@@ -87,13 +85,8 @@ public class CSVTrackExporter implements TrackExporter {
} }
@Override @Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { public boolean writeTrack(@NonNull List<Track> tracks, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream); List<Column> columns = List.of(
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
List<Column> columns = Collections.unmodifiableList(Arrays.asList(
new Column("time", null), new Column("time", null),
new Column("trackpoint_type", t -> quote(t.getType().name())), new Column("trackpoint_type", t -> quote(t.getType().name())),
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""), 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("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("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""),
new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""), 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 { try {
prepare(outputStream); prepare(outputStream);
@@ -144,15 +136,10 @@ public class CSVTrackExporter implements TrackExporter {
TrackPoint trackPoint = trackPointIterator.next(); TrackPoint trackPoint = trackPointIterator.next();
switch (trackPoint.getType()) { switch (trackPoint.getType()) {
case SEGMENT_START_MANUAL: case SEGMENT_START_MANUAL, SEGMENT_END_MANUAL, SEGMENT_START_AUTOMATIC, SENSORPOINT, TRACKPOINT ->
case SEGMENT_END_MANUAL: writeTrackPoint(columns, trackPoint);
case SEGMENT_START_AUTOMATIC: default ->
case SENSORPOINT: throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
case TRACKPOINT:
writeTrackPoint(columns, trackPoint);
break;
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) { public void writeHeader(List<Column> columns) {
if (printWriter != null) { if (printWriter != null) {
String columnNames = columns.stream().map(c -> c.columnName) String columnNames = columns.stream().map(c -> c.columnName).reduce((s, s2) -> s + "," + s2).orElseThrow(() -> new RuntimeException("No columns defined"));
.reduce((s, s2) -> s + "," + s2)
.orElseThrow(() -> new RuntimeException("No columns defined"));
printWriter.println("#" + columnNames); printWriter.println("#" + columnNames);
} }
} }
public void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) { public void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
if (printWriter != null) { if (printWriter != null) {
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint)) String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint)).reduce((s, s2) -> s + "," + s2).orElseThrow(() -> new RuntimeException("No columns defined"));
.reduce((s, s2) -> s + "," + s2)
.orElseThrow(() -> new RuntimeException("No columns defined"));
printWriter.println(columnNames); printWriter.println(columnNames);
} }
} }
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.io.file.exporter; package de.dennisguse.opentracks.io.file.exporter;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@@ -24,6 +23,7 @@ import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
@@ -31,14 +31,15 @@ import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.ExportActivityBinding; import de.dennisguse.opentracks.databinding.ExportActivityBinding;
import de.dennisguse.opentracks.io.file.ErrorListDialog; import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExportUtils; import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.FileUtils; 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. * An activity for saving tracks to the external storage.
* *
* @author Rodrigo Damazio * @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 { public class ExportActivity extends FragmentActivity implements ExportServiceResultReceiver.Receiver {
private static final String TAG = ExportActivity.class.getSimpleName(); private static final String TAG = ExportActivity.class.getSimpleName();
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri"; 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"; public static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
private static final String BUNDLE_AUTO_CONFLICT = "auto_conflict"; 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_DIRECTORY_FILES = "track_directory_files";
private static final String BUNDLE_TRACK_ERRORS = "track_errors"; private static final String BUNDLE_TRACK_ERRORS = "track_errors";
private static final int CONFLICT_NONE = 0; private static final String BUNDLE_EXPORT_TASKS = "export_tasks";
private static final int CONFLICT_OVERWRITE = 1;
private static final int CONFLICT_SKIP = 2; private enum ConflictResolutionStrategy {
CONFLICT_NONE,
CONFLICT_OVERWRITE,
CONFLICT_SKIP;
}
private TrackFileFormat trackFileFormat; private TrackFileFormat trackFileFormat;
private Uri directoryUri; private Uri directoryUri;
@@ -87,12 +100,12 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private ArrayList<String> trackErrors = new ArrayList<>(); private ArrayList<String> trackErrors = new ArrayList<>();
private int autoConflict; private ConflictResolutionStrategy autoConflict;
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
// List of tracks to be exported. // 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 LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>();
private final Handler conflictsHandler = new Handler(); private final Handler conflictsHandler = new Handler();
@@ -100,34 +113,36 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private final Runnable conflictsRunnable = new Runnable() { private final Runnable conflictsRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
if (conflictsQueue.size() > 0) { if (conflictsQueue.isEmpty()) {
PendingConflict conflict = conflictsQueue.peek(); return;
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);
}
});
} }
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); directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY); trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
boolean allInOneFile = getIntent().getBooleanExtra(EXTRA_ONE_FILE_KEY, false);
contentProviderUtils = new ContentProviderUtils(this); contentProviderUtils = new ContentProviderUtils(this);
@@ -148,14 +164,17 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
resultReceiver = new ExportServiceResultReceiver(new Handler(), this); resultReceiver = new ExportServiceResultReceiver(new Handler(), this);
if (savedInstanceState == null) { if (savedInstanceState == null) {
autoConflict = CONFLICT_NONE; autoConflict = ConflictResolutionStrategy.CONFLICT_NONE;
setProgress(); setProgress();
new Thread(() -> { new Thread(() -> {
directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri()); directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri());
runOnUiThread(() -> initExport(0)); runOnUiThread(() -> {
createExportTasks(allInOneFile);
nextExport(null);
});
}).start(); }).start();
} else { } else {
autoConflict = savedInstanceState.getInt(BUNDLE_AUTO_CONFLICT); autoConflict = ConflictResolutionStrategy.valueOf(savedInstanceState.getString(BUNDLE_AUTO_CONFLICT));
trackExportSuccessCount = savedInstanceState.getInt(BUNDLE_SUCCESS_COUNT); trackExportSuccessCount = savedInstanceState.getInt(BUNDLE_SUCCESS_COUNT);
trackExportErrorCount = savedInstanceState.getInt(BUNDLE_ERROR_COUNT); trackExportErrorCount = savedInstanceState.getInt(BUNDLE_ERROR_COUNT);
trackExportOverwrittenCount = savedInstanceState.getInt(BUNDLE_OVERWRITTEN_COUNT); trackExportOverwrittenCount = savedInstanceState.getInt(BUNDLE_OVERWRITTEN_COUNT);
@@ -163,9 +182,9 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT); trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES); directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS); trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS);
exportTasks = new ArrayList<>(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS));
setProgress(); setProgress();
initExport(getTotalDone()); nextExport(null);
} }
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.export_progress_message, directoryDisplayName)); viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.export_progress_message, directoryDisplayName));
@@ -175,7 +194,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
@Override @Override
protected void onSaveInstanceState(@NonNull Bundle outState) { protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(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_SUCCESS_COUNT, trackExportSuccessCount);
outState.putInt(BUNDLE_ERROR_COUNT, trackExportErrorCount); outState.putInt(BUNDLE_ERROR_COUNT, trackExportErrorCount);
outState.putInt(BUNDLE_OVERWRITTEN_COUNT, trackExportOverwrittenCount); outState.putInt(BUNDLE_OVERWRITTEN_COUNT, trackExportOverwrittenCount);
@@ -183,13 +202,14 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount); outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount);
outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList<String>) directoryFiles); outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList<String>) directoryFiles);
outState.putStringArrayList(BUNDLE_TRACK_ERRORS, trackErrors); outState.putStringArrayList(BUNDLE_TRACK_ERRORS, trackErrors);
outState.putParcelableArrayList(BUNDLE_EXPORT_TASKS, exportTasks);
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
conflictsQueue.clear(); conflictsQueue.clear();
tracks.clear(); exportTasks.clear();
} }
@Override @Override
@@ -202,57 +222,50 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
this.doubleBackToCancel = true; this.doubleBackToCancel = true;
Toast.makeText(this, getString(R.string.generic_click_twice_cancel), Toast.LENGTH_SHORT).show(); 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) { private void createExportTasks(boolean allInOneFile) {
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) { List<Track> tracks = contentProviderUtils.getTracks();
if (cursor == null) { exportTasks = new ArrayList<>();
onExportEnded(); if (allInOneFile) {
return; String filename = "OpenTracks-Backup";
} exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::getId).collect(Collectors.toList())));
} else {
trackExportTotalCount = cursor.getCount(); exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).collect(Collectors.toList()));
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();
}
} }
trackExportTotalCount = exportTasks.size();
} }
/** /**
* Enqueue track identified by UUID to be exported if not exported already or there is a conflict resolution. * 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) { private void export(ExportTask exportTask, ConflictResolutionStrategy conflictResolution) {
boolean fileExists = exportFileExists(track); boolean fileExists = exportFileExists(exportTask);
if (fileExists && conflictResolution == CONFLICT_NONE) { if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_NONE) {
conflict(track); conflict(exportTask);
} else if (fileExists && conflictResolution == CONFLICT_SKIP) { } else if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) {
trackExportSkippedCount++; trackExportSkippedCount++;
onExportCompleted(track); nextExport(exportTask);
} else { } else {
ExportService.enqueue(this, resultReceiver, track.getId(), trackFileFormat, directoryUri); ExportService.enqueue(this, resultReceiver, exportTask, directoryUri);
} }
} }
private void export(Track track) { private void export(ExportTask exportTask) {
export(track, autoConflict); export(exportTask, autoConflict);
} }
private boolean exportFileExists(Track track) { @Deprecated //TODO Check should be done in ExportService
String filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat); 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); return directoryFiles.stream().anyMatch(filename::equals);
} }
@@ -269,31 +282,29 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
} }
private void setProgress() { private void setProgress() {
int done = getTotalDone(); viewBinding.exportProgressDone.setText("" + getTotalDone());
viewBinding.exportProgressDone.setText("" + done);
viewBinding.exportProgressTotal.setText("" + trackExportTotalCount); viewBinding.exportProgressTotal.setText("" + trackExportTotalCount);
viewBinding.exportProgressBar.setProgress((int) ((float) done / (float) trackExportTotalCount * 100f)); viewBinding.exportProgressBar.setProgress((int) ((float) getTotalDone() / (float) trackExportTotalCount * 100f));
viewBinding.exportProgressSummaryNew.setText(String.valueOf(trackExportSuccessCount)); viewBinding.exportProgressSummaryNew.setText("" + trackExportSuccessCount);
viewBinding.exportProgressSummaryOverwrite.setText(String.valueOf(trackExportOverwrittenCount)); viewBinding.exportProgressSummaryOverwrite.setText("" + trackExportOverwrittenCount);
viewBinding.exportProgressSummarySkip.setText(String.valueOf(trackExportSkippedCount)); viewBinding.exportProgressSummarySkip.setText("" + trackExportSkippedCount);
viewBinding.exportProgressSummaryErrors.setText(String.valueOf(trackExportErrorCount)); viewBinding.exportProgressSummaryErrors.setText("" + trackExportErrorCount);
viewBinding.exportProgressSummaryNewGroup.setVisibility(trackExportSuccessCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummaryNewGroup.setVisibility(trackExportSuccessCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummaryOverwriteGroup.setVisibility(trackExportOverwrittenCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummaryOverwriteGroup.setVisibility(trackExportOverwrittenCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummarySkipGroup.setVisibility(trackExportSkippedCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummarySkipGroup.setVisibility(trackExportSkippedCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummaryErrorsGroup.setVisibility(trackExportErrorCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummaryErrorsGroup.setVisibility(trackExportErrorCount > 0 ? View.VISIBLE : View.GONE);
} }
private void onExportCompleted(Track track) { private void nextExport(@Nullable ExportTask exportTask) {
tracks.remove(track); exportTasks.remove(exportTask);
setProgress(); setProgress();
if (tracks.isEmpty()) { if (exportTasks.isEmpty()) {
onExportEnded(); onExportEnded();
return; return;
} }
export(tracks.get(0)); export(exportTasks.get(0));
} }
private void onExportEnded() { private void onExportEnded() {
@@ -318,30 +329,32 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
} }
@Override @Override
public void onExportSuccess(Track.Id trackId) { public void onExportSuccess(ExportTask exportTask) {
Track track = contentProviderUtils.getTrack(trackId); if (exportFileExists(exportTask)) {
if (exportFileExists(track)) {
trackExportOverwrittenCount++; trackExportOverwrittenCount++;
} else { } else {
trackExportSuccessCount++; trackExportSuccessCount++;
} }
onExportCompleted(track); nextExport(exportTask);
} }
@Override @Override
public void onExportError(Track.Id trackId) { public void onExportError(ExportTask exportTask) {
Track track = contentProviderUtils.getTrack(trackId);
trackExportErrorCount++; 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) { private void conflict(ExportTask exportTask) {
PendingConflict newConflict = new PendingConflict(track); PendingConflict newConflict = new PendingConflict(exportTask);
conflictsQueue.add(newConflict); conflictsQueue.add(newConflict);
if (conflictsQueue.size() == 1) { if (conflictsQueue.size() == 1) {
@@ -353,10 +366,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* Handle conflicts (exporting file already exists). * Handle conflicts (exporting file already exists).
*/ */
private class PendingConflict { private class PendingConflict {
private final Track track; private final ExportTask exportTask;
public PendingConflict(Track track) { public PendingConflict(ExportTask exportTask) {
this.track = track; 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. * @return true if it could resolve the conflict or false otherwise.
*/ */
public boolean resolve() { 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.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); setConflictVisibility(View.VISIBLE);
return false; return false;
} }
export(track); export(exportTask);
return true; 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. * Overwrite the export file and set the autoConflict if user set the "do it for all" switch button.
*/ */
public void overwrite() { public void overwrite() {
export(track, CONFLICT_OVERWRITE); export(exportTask, ConflictResolutionStrategy.CONFLICT_OVERWRITE);
if (viewBinding.exportProgressApplyToAll.isChecked()) { 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. * Skip the export file and set the autoConflict if user set the "do it for all" switch button.
*/ */
public void skip() { public void skip() {
export(track, CONFLICT_SKIP); export(exportTask, ConflictResolutionStrategy.CONFLICT_SKIP);
if (viewBinding.exportProgressApplyToAll.isChecked()) { if (viewBinding.exportProgressApplyToAll.isChecked()) {
autoConflict = CONFLICT_SKIP; autoConflict = ConflictResolutionStrategy.CONFLICT_SKIP;
} }
} }
} }
@@ -12,6 +12,9 @@ import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService; import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -22,16 +25,14 @@ public class ExportService extends JobIntentService {
private static final int JOB_ID = 1; private static final int JOB_ID = 1;
private static final String EXTRA_RECEIVER = "extra_receiver"; private static final String EXTRA_RECEIVER = "extra_receiver";
private static final String EXTRA_TRACK_ID = "extra_track_id"; private static final String EXTRA_EXPORT_TASK = "export_task";
private static final String EXTRA_TRACK_FILE_FORMAT = "extra_track_file_format";
private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri"; private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri";
private static final String TAG = ExportService.class.getSimpleName(); 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 intent = new Intent(context, JobService.class);
intent.putExtra(EXTRA_RECEIVER, receiver); intent.putExtra(EXTRA_RECEIVER, receiver);
intent.putExtra(EXTRA_TRACK_ID, trackId); intent.putExtra(EXTRA_EXPORT_TASK, exportTask);
intent.putExtra(EXTRA_TRACK_FILE_FORMAT, trackFileFormat);
intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri); intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri);
enqueueWork(context, ExportService.class, JOB_ID, intent); enqueueWork(context, ExportService.class, JOB_ID, intent);
} }
@@ -40,13 +41,12 @@ public class ExportService extends JobIntentService {
protected void onHandleWork(@NonNull Intent intent) { protected void onHandleWork(@NonNull Intent intent) {
// Get all data. // Get all data.
ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER); ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
Track.Id trackId = intent.getParcelableExtra(EXTRA_TRACK_ID); ExportTask exportTask = intent.getParcelableExtra(EXTRA_EXPORT_TASK);
TrackFileFormat trackFileFormat = (TrackFileFormat) intent.getSerializableExtra(EXTRA_TRACK_FILE_FORMAT);
Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI); Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI);
// Prepare resultCode and bundle to send to the receiver. // Prepare resultCode and bundle to send to the receiver.
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId); bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK, exportTask);
// Build directory file. // Build directory file.
DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri); DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
@@ -57,9 +57,7 @@ public class ExportService extends JobIntentService {
} }
// Export. // Export.
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this); boolean success = ExportUtils.exportTrack(this, directoryFile, exportTask);
Track track = contentProviderUtils.getTrack(trackId);
boolean success = ExportUtils.exportTrack(this, trackFileFormat, directoryFile, track);
// Send result to the receiver. // Send result to the receiver.
int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR; int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR;
@@ -17,7 +17,7 @@ public class ExportServiceResultReceiver extends ResultReceiver {
public static final int RESULT_CODE_SUCCESS = 1; public static final int RESULT_CODE_SUCCESS = 1;
public static final int RESULT_CODE_ERROR = 0; 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; private final Receiver receiver;
@@ -28,24 +28,19 @@ public class ExportServiceResultReceiver extends ResultReceiver {
@Override @Override
protected void onReceiveResult(int resultCode, Bundle resultData) { 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) { switch (resultCode) {
case RESULT_CODE_SUCCESS: case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(exportTask);
receiver.onExportSuccess(trackId); case RESULT_CODE_ERROR -> receiver.onExportError(exportTask);
break; default -> throw new RuntimeException("Unknown resultCode.");
case RESULT_CODE_ERROR:
receiver.onExportError(trackId);
break;
default:
throw new RuntimeException("Unknown resultCode.");
} }
} }
public interface Receiver { 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 @Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
try { try {
prepare(outputStream); prepare(outputStream);
writeHeader(); writeHeader();
@@ -145,37 +140,32 @@ public class GPXTrackExporter implements TrackExporter {
} }
switch (trackPoint.getType()) { switch (trackPoint.getType()) {
case SEGMENT_START_MANUAL: case SEGMENT_START_MANUAL ->
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported."); Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
break; case SEGMENT_END_MANUAL -> {
case SEGMENT_END_MANUAL:
if (wroteSegment) writeCloseSegment(); if (wroteSegment) writeCloseSegment();
wroteSegment = false; wroteSegment = false;
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_END_MANUAL.name() + " is not supported."); 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(); if (wroteSegment) writeCloseSegment();
writeOpenSegment(); writeOpenSegment();
wroteSegment = true; wroteSegment = true;
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance)); trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear(); sensorPoints.clear();
break; }
case SENSORPOINT: case SENSORPOINT -> sensorPoints.add(trackPoint);
sensorPoints.add(trackPoint); case TRACKPOINT -> {
break;
case TRACKPOINT:
if (!wroteSegment) { if (!wroteSegment) {
// Might happen for older data (pre v3.15.0) // Might happen for older data (pre v3.15.0)
writeOpenSegment(); writeOpenSegment();
wroteSegment = true; wroteSegment = true;
} }
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance)); trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
sensorPoints.clear(); sensorPoints.clear();
break; }
default: default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType()); throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
} }
} }
@@ -93,24 +93,20 @@ public class KMLTrackExporter implements TrackExporter {
private final List<Float> accuracyHorizontal = new ArrayList<>(); private final List<Float> accuracyHorizontal = new ArrayList<>();
private final List<Float> accuracyVertical = 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.context = context;
this.exportPhotos = exportPhotos; this.exportPhotos = exportPhotos;
this.contentProviderUtils = new ContentProviderUtils(context); this.contentProviderUtils = contentProviderUtils;
} }
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
try { try {
prepare(outputStream); prepare(outputStream);
writeHeader(tracks); writeHeader(tracks);
for (Track track : tracks) { for (Track track : tracks) {
writeMarkers(track); writeMarkers(track);
} }
boolean hasMultipleTracks = tracks.length > 1; boolean hasMultipleTracks = tracks.size() > 1;
if (hasMultipleTracks) { if (hasMultipleTracks) {
writeMultiTrackBegin(); writeMultiTrackBegin();
} }
@@ -169,30 +165,28 @@ public class KMLTrackExporter implements TrackExporter {
} }
switch (trackPoint.getType()) { switch (trackPoint.getType()) {
case SEGMENT_START_MANUAL: case SEGMENT_START_MANUAL, SEGMENT_START_AUTOMATIC -> {
case SEGMENT_START_AUTOMATIC:
if (wroteSegment) writeCloseSegment(); if (wroteSegment) writeCloseSegment();
writeOpenSegment(); writeOpenSegment();
writeTrackPoint(track.getZoneOffset(), trackPoint); writeTrackPoint(track.getZoneOffset(), trackPoint);
wroteSegment = true; wroteSegment = true;
break; }
case SEGMENT_END_MANUAL: case SEGMENT_END_MANUAL -> {
if (!wroteSegment) writeOpenSegment(); if (!wroteSegment) writeOpenSegment();
writeTrackPoint(track.getZoneOffset(), trackPoint); writeTrackPoint(track.getZoneOffset(), trackPoint);
writeCloseSegment(); writeCloseSegment();
wroteSegment = false; wroteSegment = false;
break; }
case SENSORPOINT: case SENSORPOINT, TRACKPOINT -> {
case TRACKPOINT:
if (!wroteSegment) { if (!wroteSegment) {
// Might happen for older data (pre v3.15.0) // Might happen for older data (pre v3.15.0)
writeOpenSegment(); writeOpenSegment();
wroteSegment = true; wroteSegment = true;
} }
writeTrackPoint(track.getZoneOffset(), trackPoint); writeTrackPoint(track.getZoneOffset(), trackPoint);
break; }
default: default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType()); 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) { if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); printWriter.println(
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.3\""); """
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\""); <?xml version="1.0" encoding="UTF-8"?>
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\">"); """);
//TODO ADD xsi:schemaLocation for atom printWriter.println(
printWriter.println("xsi:schemaLocation=" + """
"\"http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd" <kml xmlns="http://www.opengis.net/kml/2.3"
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">"); 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("<Document>");
printWriter.println("<open>1</open>"); printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>"); printWriter.println("<visibility>1</visibility>");
Track track = tracks[0]; Track track = tracks.get(0);
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>"); printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<atom:generator>" + StringUtils.formatCData(context.getString(R.string.app_name)) + "</atom:generator>"); 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() { private void writeMultiTrackBegin() {
if (printWriter != null) { 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("<name>" + context.getString(R.string.generic_tracks) + "</name>");
printWriter.println("<open>1</open>"); printWriter.println("<open>1</open>");
} }
@@ -28,6 +28,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -64,12 +65,7 @@ public class KmzTrackExporter implements TrackExporter {
} }
@Override @Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
// Add kml file // Add kml file
ZipEntry zipEntry = new ZipEntry(KMZ_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) { for (Track track : tracks) {
try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, -1)) { try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, -1)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
@@ -19,6 +19,7 @@ package de.dennisguse.opentracks.io.file.exporter;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
@@ -29,7 +30,5 @@ import de.dennisguse.opentracks.data.models.Track;
*/ */
public interface TrackExporter { public interface TrackExporter {
boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream); boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream);
boolean writeTrack(Track track, @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 { public static class DefaultTrackExportDirectoryChooserActivity extends DirectoryChooserActivity {
@Override @Override
@@ -156,103 +156,94 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
@Override @Override
public void endElement(String uri, String localName, String tag) { public void endElement(String uri, String localName, String tag) {
switch (tag) { switch (tag) {
case TAG_GPX: case TAG_GPX -> onFileEnd();
onFileEnd(); case TAG_MARKER -> onMarkerEnd();
break; case TAG_TRACK -> {
case TAG_MARKER:
onMarkerEnd();
break;
case TAG_TRACK:
trackImporter.setTrack(context, name, uuid, description, category, null, zoneOffset); trackImporter.setTrack(context, name, uuid, description, category, null, zoneOffset);
zoneOffset = null; zoneOffset = null;
break; }
case TAG_TRACK_SEGMENT: case TAG_TRACK_SEGMENT -> onTrackSegmentEnd();
onTrackSegmentEnd(); case TAG_TRACK_POINT -> currentSegment.add(createTrackPoint());
break; case TAG_NAME -> {
case TAG_TRACK_POINT:
currentSegment.add(createTrackPoint());
break;
case TAG_NAME:
if (content != null) { if (content != null) {
name = content.trim(); name = content.trim();
} }
break; }
case TAG_DESCRIPTION: case TAG_DESCRIPTION -> {
if (content != null) { if (content != null) {
description = content.trim(); description = content.trim();
} }
break; }
case TAG_TYPE: case TAG_TYPE -> {
if (content != null) { if (content != null) {
category = content.trim(); category = content.trim();
} }
break; }
case TAG_TIME: case TAG_TIME -> {
if (content != null) { if (content != null) {
time = content.trim(); time = content.trim();
} }
break; }
case TAG_ALTITUDE: case TAG_ALTITUDE -> {
if (content != null) { if (content != null) {
altitude = content.trim(); altitude = content.trim();
} }
break; }
case TAG_COMMENT: case TAG_COMMENT -> {
if (content != null) { if (content != null) {
markerType = content.trim(); markerType = content.trim();
} }
break; }
case TAG_EXTENSION_SPEED: case TAG_EXTENSION_SPEED, TAG_EXTENSION_SPEED_COMPAT -> {
case TAG_EXTENSION_SPEED_COMPAT:
if (content != null) { if (content != null) {
speed = content.trim(); speed = content.trim();
} }
break; }
case TAG_EXTENSION_HEARTRATE: case TAG_EXTENSION_HEARTRATE -> {
if (content != null) { if (content != null) {
heartrate = content.trim(); heartrate = content.trim();
} }
break; }
case TAG_EXTENSION_CADENCE: case TAG_EXTENSION_CADENCE -> {
if (content != null) { if (content != null) {
cadence = content.trim(); cadence = content.trim();
} }
break; }
case TAG_EXTENSION_POWER: case TAG_EXTENSION_POWER -> {
if (content != null) { if (content != null) {
power = content.trim(); power = content.trim();
} }
break; }
case TAG_ID: case TAG_ID -> {
if (content != null) { if (content != null) {
uuid = content.trim(); uuid = content.trim();
} }
break; }
case TAG_EXTENSION_GAIN: case TAG_EXTENSION_GAIN -> {
if (content != null) { if (content != null) {
gain = content.trim(); gain = content.trim();
} }
break; }
case TAG_EXTENSION_LOSS: case TAG_EXTENSION_LOSS -> {
if (content != null) { if (content != null) {
loss = content.trim(); loss = content.trim();
} }
break; }
case TAG_EXTENSION_DISTANCE: case TAG_EXTENSION_DISTANCE -> {
if (content != null) { if (content != null) {
sensorDistance = content.trim(); sensorDistance = content.trim();
} }
break; }
case TAG_EXTENSION_ACCURACY_HORIZONTAL: case TAG_EXTENSION_ACCURACY_HORIZONTAL -> {
if (content != null) { if (content != null) {
accuracyHorizontal = content.trim(); accuracyHorizontal = content.trim();
} }
break; }
case TAG_EXTENSION_ACCURACY_VERTICAL: case TAG_EXTENSION_ACCURACY_VERTICAL -> {
if (content != null) { if (content != null) {
accuracyVertical = content.trim(); accuracyVertical = content.trim();
} }
break; }
} }
content = ""; content = "";
@@ -79,19 +79,17 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
String message = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE); String message = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE);
switch (resultCode) { switch (resultCode) {
case ImportServiceResultReceiver.RESULT_CODE_ERROR: case ImportServiceResultReceiver.RESULT_CODE_ERROR -> {
summary.errorCount++; summary.errorCount++;
summary.fileErrors.add(getApplication().getString(R.string.import_error_info, fileName, message)); 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.importedTrackIds.addAll(trackIds);
summary.successCount++; summary.successCount++;
break; }
case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS: case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS -> summary.existsCount++;
summary.existsCount++; default ->
break; throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
default:
throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
} }
importData.postValue(summary); importData.postValue(summary);
@@ -136,27 +136,18 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
@Override @Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException { public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) { switch (tag) {
case TAG_PLACEMARK: case TAG_PLACEMARK, TAG_PHOTO_OVERLAY ->
case TAG_PHOTO_OVERLAY:
// Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, category, and description. // Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, category, and description.
onMarkerStart(); onMarkerStart();
break; case TAG_MULTI_TRACK, TAG_KML22_MULTI_TRACK -> trackImporter.newTrack();
case TAG_MULTI_TRACK: case TAG_TRACK, TAG_KML22_TRACK -> {
case TAG_KML22_MULTI_TRACK:
trackImporter.newTrack();
break;
case TAG_TRACK:
case TAG_KML22_TRACK:
if (trackImporter == null) { if (trackImporter == null) {
throw new SAXException("Missing " + TAG_MULTI_TRACK); throw new SAXException("Missing " + TAG_MULTI_TRACK);
} }
onTrackSegmentStart(); onTrackSegmentStart();
break; }
case TAG_DATA_CATEGORY: case TAG_DATA_CATEGORY, TAG_SIMPLE_ARRAY_DATA, TAG_KML22_SIMPLE_ARRAY_DATA ->
case TAG_SIMPLE_ARRAY_DATA: dataType = attributes.getValue(ATTRIBUTE_NAME);
case TAG_KML22_SIMPLE_ARRAY_DATA:
dataType = attributes.getValue(ATTRIBUTE_NAME);
break;
} }
} }
@@ -168,32 +159,18 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
@Override @Override
public void endElement(String uri, String localName, String tag) throws SAXException { public void endElement(String uri, String localName, String tag) throws SAXException {
switch (tag) { switch (tag) {
case TAG_KML: case TAG_KML -> onFileEnd();
onFileEnd(); case TAG_PLACEMARK, TAG_PHOTO_OVERLAY ->
break;
case TAG_PLACEMARK:
case TAG_PHOTO_OVERLAY:
// Note that a track is contained in a Placemark, calling onMarkerEnd is save since markerType is not set for a track. // Note that a track is contained in a Placemark, calling onMarkerEnd is save since markerType is not set for a track.
onMarkerEnd(); onMarkerEnd();
break; case TAG_COORDINATES -> onMarkerLocationEnd();
case TAG_COORDINATES: case TAG_MULTI_TRACK, TAG_KML22_MULTI_TRACK -> {
onMarkerLocationEnd();
break;
case TAG_MULTI_TRACK:
case TAG_KML22_MULTI_TRACK:
trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset); trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset);
zoneOffset = null; zoneOffset = null;
break; }
case TAG_TRACK: case TAG_TRACK, TAG_KML22_TRACK -> onTrackSegmentEnd();
case TAG_KML22_TRACK: case TAG_COORD, TAG_KML22_COORD -> onCoordEnded();
onTrackSegmentEnd(); case TAG_VALUE, TAG_KML22_VALUE -> {
break;
case TAG_COORD:
case TAG_KML22_COORD:
onCoordEnded();
break;
case TAG_VALUE:
case TAG_KML22_VALUE:
if (KMLTrackExporter.EXTENDED_DATA_TYPE_CATEGORY.equals(dataType)) { if (KMLTrackExporter.EXTENDED_DATA_TYPE_CATEGORY.equals(dataType)) {
if (content != null) { if (content != null) {
category = content.trim(); category = content.trim();
@@ -201,28 +178,28 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
} else { } else {
onExtendedDataValueEnd(); onExtendedDataValueEnd();
} }
break; }
case TAG_NAME: case TAG_NAME -> {
if (content != null) { if (content != null) {
name = content.trim(); name = content.trim();
} }
break; }
case TAG_UUID: case TAG_UUID -> {
if (content != null) { if (content != null) {
uuid = content.trim(); uuid = content.trim();
} }
break; }
case TAG_DESCRIPTION: case TAG_DESCRIPTION -> {
if (content != null) { if (content != null) {
description = content.trim(); description = content.trim();
} }
break; }
case TAG_ICON: case TAG_ICON -> {
if (content != null) { if (content != null) {
icon = content.trim(); icon = content.trim();
} }
break; }
case TAG_WHEN: case TAG_WHEN -> {
if (content != null) { if (content != null) {
try { try {
OffsetDateTime time = StringUtils.parseTime(content.trim()); OffsetDateTime time = StringUtils.parseTime(content.trim());
@@ -234,18 +211,17 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", content.trim())), e); throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", content.trim())), e);
} }
} }
}
break; case TAG_STYLE_URL -> {
case TAG_STYLE_URL:
if (content != null) { if (content != null) {
markerType = content.trim(); markerType = content.trim();
} }
break; }
case TAG_HREF: case TAG_HREF -> {
if (content != null) { if (content != null) {
photoUrl = content.trim(); photoUrl = content.trim();
} }
break; }
} }
// Reset element content // Reset element content
@@ -436,35 +412,19 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
} }
} }
switch (dataType) { switch (dataType) {
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED: case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED -> sensorSpeedList.add(value);
sensorSpeedList.add(value); case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE -> sensorDistanceList.add(value);
break; case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER -> sensorPowerList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE: case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE -> sensorHeartRateList.add(value);
sensorDistanceList.add(value); case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE -> sensorCadenceList.add(value);
break; case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN -> altitudeGainList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER: case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS -> altitudeLossList.add(value);
sensorPowerList.add(value); case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL ->
break; accuracyHorizontal.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE: case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL ->
sensorHeartRateList.add(value); accuracyVertical.add(value);
break; default ->
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE: Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
sensorCadenceList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN:
altitudeGainList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS:
altitudeLossList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL:
accuracyHorizontal.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL:
accuracyVertical.add(value);
break;
default:
Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
} }
} }
@@ -40,9 +40,9 @@ import de.dennisguse.opentracks.sensors.sensorData.SensorData;
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications. * Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
* Also parses the transferred data into {@link SensorDataObserver}. * Also parses the transferred data into {@link SensorDataObserver}.
*/ */
public abstract class BluetoothConnectionManager<DataType> { public abstract class AbstractBluetoothConnectionManager<DataType> {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); private static final String TAG = AbstractBluetoothConnectionManager.class.getSimpleName();
private final SensorDataObserver observer; private final SensorDataObserver observer;
@@ -53,23 +53,22 @@ public abstract class BluetoothConnectionManager<DataType> {
@Override @Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
switch (newState) { switch (newState) {
case BluetoothProfile.STATE_CONNECTING: case BluetoothProfile.STATE_CONNECTING ->
Log.d(TAG, "Connecting to sensor: " + gatt.getDevice()); Log.i(TAG, "Connecting to sensor: " + gatt.getDevice());
break; case BluetoothProfile.STATE_CONNECTED -> {
case BluetoothProfile.STATE_CONNECTED: Log.i(TAG, "Connected to sensor: " + gatt.getDevice() + "; discovering services.");
Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
gatt.discoverServices(); gatt.discoverServices();
break; }
case BluetoothProfile.STATE_DISCONNECTING: case BluetoothProfile.STATE_DISCONNECTING ->
Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice()); Log.i(TAG, "Disconnecting from sensor: " + gatt.getDevice());
break; case BluetoothProfile.STATE_DISCONNECTED -> {
//This is also triggered, if no connection was established (ca. 30s)
case BluetoothProfile.STATE_DISCONNECTED: Log.i(TAG, "Disconnected from sensor: " + gatt.getDevice() + "; trying to reconnect");
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice()); if (gatt.connect()) {
Log.e(TAG, "Could not trigger reconnect for sensor: " + gatt.getDevice());
}
clearData(); clearData();
break; }
} }
} }
@@ -133,12 +132,12 @@ public abstract class BluetoothConnectionManager<DataType> {
} }
}; };
BluetoothConnectionManager(ServiceMeasurementUUID serviceUUUID, SensorDataObserver observer) { AbstractBluetoothConnectionManager(ServiceMeasurementUUID serviceUUUID, SensorDataObserver observer) {
this.serviceMeasurementUUIDs = List.of(serviceUUUID); this.serviceMeasurementUUIDs = List.of(serviceUUUID);
this.observer = observer; this.observer = observer;
} }
BluetoothConnectionManager(List<ServiceMeasurementUUID> serviceUUUID, SensorDataObserver observer) { AbstractBluetoothConnectionManager(List<ServiceMeasurementUUID> serviceUUUID, SensorDataObserver observer) {
this.serviceMeasurementUUIDs = serviceUUUID; this.serviceMeasurementUUIDs = serviceUUUID;
this.observer = observer; this.observer = observer;
} }
@@ -19,7 +19,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
/** /**
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer). * Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/ */
public class AltitudeSumManager implements SensorEventListener { public class AltitudeSumManager implements SensorConnector, SensorEventListener {
private static final String TAG = AltitudeSumManager.class.getSimpleName(); private static final String TAG = AltitudeSumManager.class.getSimpleName();
@@ -8,11 +8,11 @@ import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
public class BluetoothRemoteSensorManagerCyclingCadence extends BluetoothConnectionManager<Cadence> { public class BluetoothConnectionManagerCyclingCadence extends AbstractBluetoothConnectionManager<Cadence> {
private static final String TAG = BluetoothRemoteSensorManagerCyclingCadence.class.getSimpleName(); private static final String TAG = BluetoothConnectionManagerCyclingCadence.class.getSimpleName();
BluetoothRemoteSensorManagerCyclingCadence(SensorDataObserver observer) { BluetoothConnectionManagerCyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_CADENCE, observer); super(BluetoothUtils.CYCLING_CADENCE, observer);
} }
@@ -5,9 +5,9 @@ import android.bluetooth.BluetoothGattCharacteristic;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
public class BluetoothRemoteSensorManagerCyclingDistanceSpeed extends BluetoothConnectionManager<SensorDataCyclingDistanceSpeed.Data> { public class BluetoothConnectionManagerCyclingDistanceSpeed extends AbstractBluetoothConnectionManager<SensorDataCyclingDistanceSpeed.Data> {
BluetoothRemoteSensorManagerCyclingDistanceSpeed(SensorDataObserver observer) { BluetoothConnectionManagerCyclingDistanceSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE, observer); super(BluetoothUtils.CYCLING_SPEED_CADENCE, observer);
} }
@@ -7,9 +7,9 @@ import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Power; import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
public class BluetoothRemoteSensorManagerCyclingPower extends BluetoothConnectionManager<Power> { public class BluetoothConnectionManagerCyclingPower extends AbstractBluetoothConnectionManager<Power> {
BluetoothRemoteSensorManagerCyclingPower(@NonNull SensorDataObserver observer) { BluetoothConnectionManagerCyclingPower(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_POWER, observer); super(BluetoothUtils.CYCLING_POWER, observer);
} }
@@ -7,9 +7,9 @@ import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate; import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
public class BluetoothRemoteSensorManagerHeartRate extends BluetoothConnectionManager<HeartRate> { public class BluetoothConnectionManagerHeartRate extends AbstractBluetoothConnectionManager<HeartRate> {
BluetoothRemoteSensorManagerHeartRate(@NonNull SensorDataObserver observer) { BluetoothConnectionManagerHeartRate(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.HEARTRATE, observer); super(BluetoothUtils.HEARTRATE, observer);
} }
@@ -6,9 +6,9 @@ import androidx.annotation.NonNull;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning; import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
public class BluetoothRemoteSensorManagerRunningSpeedAndCadence extends BluetoothConnectionManager<SensorDataRunning.Data> { public class BluetoothConnectionRunningSpeedAndCadence extends AbstractBluetoothConnectionManager<SensorDataRunning.Data> {
BluetoothRemoteSensorManagerRunningSpeedAndCadence(@NonNull SensorDataObserver observer) { BluetoothConnectionRunningSpeedAndCadence(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.RUNNING_SPEED_CADENCE, observer); super(BluetoothUtils.RUNNING_SPEED_CADENCE, observer);
} }
@@ -52,7 +52,7 @@ import de.dennisguse.opentracks.util.PermissionRequester;
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.SensorDataObserver { public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBluetoothConnectionManager.SensorDataObserver {
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName(); private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
@@ -65,15 +65,15 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private Distance preferenceWheelCircumference; private Distance preferenceWheelCircumference;
private final BluetoothRemoteSensorManagerHeartRate heartRate = new BluetoothRemoteSensorManagerHeartRate(this); private final BluetoothConnectionManagerHeartRate heartRate = new BluetoothConnectionManagerHeartRate(this);
private final BluetoothRemoteSensorManagerCyclingCadence cyclingCadence = new BluetoothRemoteSensorManagerCyclingCadence(this); private final BluetoothConnectionManagerCyclingCadence cyclingCadence = new BluetoothConnectionManagerCyclingCadence(this);
private final BluetoothRemoteSensorManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothRemoteSensorManagerCyclingDistanceSpeed(this); private final BluetoothConnectionManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManagerCyclingDistanceSpeed(this);
private final BluetoothRemoteSensorManagerCyclingPower cyclingPower = new BluetoothRemoteSensorManagerCyclingPower(this); private final BluetoothConnectionManagerCyclingPower cyclingPower = new BluetoothConnectionManagerCyclingPower(this);
private final BluetoothRemoteSensorManagerRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothRemoteSensorManagerRunningSpeedAndCadence(this); private final BluetoothConnectionRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionRunningSpeedAndCadence(this);
private final SensorDataSet sensorDataSet = new SensorDataSet(); private final SensorDataSet sensorDataSet = new SensorDataSet();
private final SensorDataSetChangeObserver observer; private final SensorManager.SensorDataSetChangeObserver observer;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override @Override
@@ -114,21 +114,23 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
} }
}; };
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @NonNull SensorDataSetChangeObserver observer) { public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @NonNull SensorManager.SensorDataSetChangeObserver observer) {
this.context = context; this.context = context;
this.handler = handler; this.handler = handler;
this.observer = observer; this.observer = observer;
bluetoothAdapter = BluetoothUtils.getAdapter(context); bluetoothAdapter = BluetoothUtils.getAdapter(context);
} }
public void start() { @Override
public void start(Context context, Handler handler) {
started = true; started = true;
//Registering triggers connection startup //Registering triggers connection startup
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
} }
public synchronized void stop() { @Override
public synchronized void stop(Context context) {
heartRate.disconnect(); heartRate.disconnect();
cyclingCadence.disconnect(); cyclingCadence.disconnect();
cyclingSpeed.disconnect(); cyclingSpeed.disconnect();
@@ -145,7 +147,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
} }
private synchronized void connect(BluetoothConnectionManager<?> connectionManager, String address) { private synchronized void connect(AbstractBluetoothConnectionManager<?> connectionManager, String address) {
if (!isEnabled()) { if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled."); Log.w(TAG, "Bluetooth not enabled.");
return; return;
@@ -230,8 +232,4 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
public Handler getHandler() { public Handler getHandler() {
return handler; return handler;
} }
public interface SensorDataSetChangeObserver {
void onChange(SensorDataSet sensorDataSet);
}
} }
@@ -28,6 +28,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import de.dennisguse.opentracks.data.models.BatteryLevel;
import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.HeartRate;
@@ -48,6 +49,11 @@ public class BluetoothUtils {
public static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL); public static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID BATTERY = new ServiceMeasurementUUID(
new UUID(0x180F00001000L, 0x800000805f9b34fbL),
new UUID(0x2A1900001000L, 0x800000805f9b34fbL)
);
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID( public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
new UUID(0x180D00001000L, 0x800000805f9b34fbL), new UUID(0x180D00001000L, 0x800000805f9b34fbL),
new UUID(0x2A3700001000L, 0x800000805f9b34fbL) new UUID(0x2A3700001000L, 0x800000805f9b34fbL)
@@ -102,6 +108,17 @@ public class BluetoothUtils {
return BluetoothUtils.getAdapter(context) != null; return BluetoothUtils.getAdapter(context) != null;
} }
public static BatteryLevel parseBatteryLevel(BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION org.bluetooth.characteristic.battery_level.xml
byte[] raw = characteristic.getValue();
if (raw.length == 0) {
return null;
}
final int batteryLevel = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
return BatteryLevel.of(batteryLevel);
}
public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) { public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml //DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
byte[] raw = characteristic.getValue(); byte[] raw = characteristic.getValue();
@@ -0,0 +1,11 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.os.Handler;
public interface SensorConnector {
void start(Context context, Handler handler);
void stop(Context context);
}
@@ -0,0 +1,85 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
public class SensorManager {
private static final String TAG = SensorManager.class.getSimpleName();
private BluetoothRemoteSensorManager bluetoothSensorManager;
private AltitudeSumManager altitudeSumManager;
public SensorManager(Context context, Handler handler, SensorDataSetChangeObserver observer) {
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, observer);
altitudeSumManager = new AltitudeSumManager();
}
public void start(Context context, Handler handler) {
bluetoothSensorManager.start(context, handler);
altitudeSumManager.start(context, handler);
}
public void stop(Context context) {
if (bluetoothSensorManager != null) {
bluetoothSensorManager.stop(context);
bluetoothSensorManager = null;
}
if (altitudeSumManager != null) {
altitudeSumManager.stop(context);
altitudeSumManager = null;
}
}
public SensorDataSet fill(TrackPoint trackPoint) {
altitudeSumManager.fill(trackPoint);
return bluetoothSensorManager.fill(trackPoint);
}
public void reset() {
if (bluetoothSensorManager == null || altitudeSumManager == null) {
Log.d(TAG, "No recording running and no reset necessary.");
return;
}
bluetoothSensorManager.reset();
altitudeSumManager.reset();
}
@Deprecated
@VisibleForTesting
public BluetoothRemoteSensorManager getBluetoothSensorManager() {
return bluetoothSensorManager;
}
@Deprecated
@VisibleForTesting
public void setBluetoothSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
this.bluetoothSensorManager = remoteSensorManager;
}
@Deprecated
@VisibleForTesting
public AltitudeSumManager getAltitudeSumManager() {
return altitudeSumManager;
}
@Deprecated
@VisibleForTesting
public void setAltitudeSumManager(AltitudeSumManager altitudeSumManager) {
this.altitudeSumManager = altitudeSumManager;
}
@Deprecated
public interface SensorDataSetChangeObserver {
void onChange(SensorDataSet sensorDataSet);
}
}
@@ -14,6 +14,7 @@ public class UintUtils {
* @return diff * @return diff
*/ */
public static long diff(long a, long b, final long UINT_MAX) { public static long diff(long a, long b, final long UINT_MAX) {
if (a < 0 || b < 0) { if (a < 0 || b < 0) {
throw new RuntimeException("a or b cannot be less than zero."); throw new RuntimeException("a or b cannot be less than zero.");
} }
@@ -25,6 +26,6 @@ public class UintUtils {
return a - b; return a - b;
} }
return (UINT_MAX - b) + a; return (UINT_MAX + 1 - b) + a;
} }
} }
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks.sensors.sensorData; package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import java.time.Instant; import java.time.Instant;
@@ -32,16 +31,6 @@ public abstract class SensorData<T> {
this.time = time; this.time = time;
} }
@NonNull
public String getSensorAddress() {
return sensorAddress;
}
@Nullable
public String getSensorName() {
return sensorName;
}
public String getSensorNameOrAddress() { public String getSensorNameOrAddress() {
return sensorName != null ? sensorName : sensorAddress; return sensorName != null ? sensorName : sensorAddress;
} }
@@ -76,9 +76,8 @@ public class SensorDataCyclingCadence extends SensorData<Cadence> {
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SensorDataCyclingCadence)) return false; if (!(obj instanceof SensorDataCyclingCadence comp)) return false;
SensorDataCyclingCadence comp = (SensorDataCyclingCadence) obj;
if (hasData() && comp.hasData() == hasData()) { if (hasData() && comp.hasData() == hasData()) {
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime(); return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
} else { } else {
@@ -93,9 +93,8 @@ public class SensorDataCyclingDistanceSpeed extends SensorData<SensorDataCycling
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (!(obj instanceof SensorDataCyclingDistanceSpeed)) return false; if (!(obj instanceof SensorDataCyclingDistanceSpeed comp)) return false;
SensorDataCyclingDistanceSpeed comp = (SensorDataCyclingDistanceSpeed) obj;
if (!(hasData() && comp.hasData())) { if (!(hasData() && comp.hasData())) {
return false; return false;
} }
@@ -111,7 +111,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
egm2008CorrectionManager = new EGM2008CorrectionManager(); egm2008CorrectionManager = new EGM2008CorrectionManager();
trackRecordingManager = new TrackRecordingManager(this); trackRecordingManager = new TrackRecordingManager(this);
trackRecordingManager.start(); trackRecordingManager.start();
trackPointCreator = new TrackPointCreator(this); trackPointCreator = new TrackPointCreator(this, this, handler);
voiceAnnouncementManager = new VoiceAnnouncementManager(this); voiceAnnouncementManager = new VoiceAnnouncementManager(this);
notificationManager = new TrackRecordingServiceNotificationManager(this); notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -177,7 +177,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
public void resumeTrack(Track.Id trackId) { public void resumeTrack(Track.Id trackId) {
trackPointCreator.reset();
if (!trackRecordingManager.resumeExistingTrack(trackId, trackPointCreator)) { if (!trackRecordingManager.resumeExistingTrack(trackId, trackPointCreator)) {
Log.w(TAG, "Cannot resume a non-existing track."); Log.w(TAG, "Cannot resume a non-existing track.");
return; return;
@@ -328,7 +327,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
TrackPoint trackPoint = data.second.first; TrackPoint trackPoint = data.second.first;
egm2008CorrectionManager.correctAltitude(this, trackPoint); egm2008CorrectionManager.correctAltitude(this, trackPoint);
localVoiceAnnouncementManager.update(data.first); localVoiceAnnouncementManager.update(this, data.first);
recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second)); recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second));
} }
@@ -28,6 +28,7 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -67,17 +68,8 @@ public class VoiceAnnouncement {
public void onAudioFocusChange(int focusChange) { public void onAudioFocusChange(int focusChange) {
Log.d(TAG, "Audio focus changed to " + focusChange); Log.d(TAG, "Audio focus changed to " + focusChange);
boolean stop = false; boolean stop = List.of(AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)
switch (focusChange) { .contains(focusChange);
case AudioManager.AUDIOFOCUS_GAIN:
stop = false;
break;
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
stop = true;
break;
}
if (stop && tts != null && tts.isSpeaking()) { if (stop && tts != null && tts.isSpeaking()) {
tts.stop(); tts.stop();
@@ -15,12 +15,14 @@
*/ */
package de.dennisguse.opentracks.services.announcement; package de.dennisguse.opentracks.services.announcement;
import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.mediarouter.media.MediaRouter;
import java.time.Duration; import java.time.Duration;
@@ -73,12 +75,20 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
updateNextTaskDistance(); updateNextTaskDistance();
} }
public void update(@NonNull Track track) { public void update(@NonNull Context context, @NonNull Track track) {
if (voiceAnnouncement == null) { if (voiceAnnouncement == null) {
Log.e(TAG, "Cannot update when in status shutdown."); Log.e(TAG, "Cannot update when in status shutdown.");
return; return;
} }
if (!PreferencesUtils.shouldVoiceAnnouncementOnDeviceSpeaker()
&& MediaRouter.getInstance(context)
.getSelectedRoute()
.isDeviceSpeaker()) {
Log.i(TAG, "No voice announcement on device speaker.");
return;
}
boolean announce = false; boolean announce = false;
this.trackStatistics = track.getTrackStatistics(); this.trackStatistics = track.getTrackStatistics();
if (trackStatistics.getTotalDistance().greaterThan(nextTotalDistance)) { if (trackStatistics.getTotalDistance().greaterThan(nextTotalDistance)) {
@@ -43,29 +43,28 @@ class VoiceAnnouncementUtils {
String unitDistanceTTS; String unitDistanceTTS;
String unitSpeedTTS; String unitSpeedTTS;
switch (unitSystem) { switch (unitSystem) {
case METRIC: case METRIC -> {
perUnitStringId = R.string.voice_per_kilometer; perUnitStringId = R.string.voice_per_kilometer;
distanceId = R.plurals.voiceDistanceKilometers; distanceId = R.plurals.voiceDistanceKilometers;
speedId = R.plurals.voiceSpeedKilometersPerHour; speedId = R.plurals.voiceSpeedKilometersPerHour;
unitDistanceTTS = "kilometer"; unitDistanceTTS = "kilometer";
unitSpeedTTS = "kilometer per hour"; unitSpeedTTS = "kilometer per hour";
break; }
case IMPERIAL: case IMPERIAL -> {
perUnitStringId = R.string.voice_per_mile; perUnitStringId = R.string.voice_per_mile;
distanceId = R.plurals.voiceDistanceMiles; distanceId = R.plurals.voiceDistanceMiles;
speedId = R.plurals.voiceSpeedMilesPerHour; speedId = R.plurals.voiceSpeedMilesPerHour;
unitDistanceTTS = "mile"; unitDistanceTTS = "mile";
unitSpeedTTS = "mile per hour"; unitSpeedTTS = "mile per hour";
break; }
case NAUTICAL_IMPERIAL: case NAUTICAL_IMPERIAL -> {
perUnitStringId = R.string.voice_per_nautical_mile; perUnitStringId = R.string.voice_per_nautical_mile;
distanceId = R.plurals.voiceDistanceNauticalMiles; distanceId = R.plurals.voiceDistanceNauticalMiles;
speedId = R.plurals.voiceSpeedMKnots; speedId = R.plurals.voiceSpeedMKnots;
unitDistanceTTS = "nautical mile"; unitDistanceTTS = "nautical mile";
unitSpeedTTS = "knots"; unitSpeedTTS = "knots";
break; }
default: default -> throw new RuntimeException("Not implemented");
throw new RuntimeException("Not implemented");
} }
double distanceInUnit = totalDistance.toKM_Miles(unitSystem); double distanceInUnit = totalDistance.toKM_Miles(unitSystem);
@@ -5,7 +5,6 @@ import android.util.Log;
import java.io.IOException; import java.io.IOException;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.util.EGM2008Utils; import de.dennisguse.opentracks.util.EGM2008Utils;
@@ -30,6 +29,6 @@ public class EGM2008CorrectionManager {
} }
} }
trackPoint.setAltitude(Altitude.EGM2008.of(egm2008Correction.correctAltitude(trackPoint.getLocation()))); trackPoint.setAltitude(egm2008Correction.correctAltitude(trackPoint.getLocation()));
} }
} }
@@ -20,14 +20,15 @@ import java.time.Instant;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.SensorConnector;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PermissionRequester; import de.dennisguse.opentracks.util.PermissionRequester;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GPSHandler implements LocationListenerCompat, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener { public class GPSManager implements SensorConnector, LocationListenerCompat, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
private final String TAG = GPSHandler.class.getSimpleName(); private final String TAG = GPSManager.class.getSimpleName();
public static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER; public static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
@@ -40,11 +41,11 @@ public class GPSHandler implements LocationListenerCompat, GpsStatus.GpsStatusLi
private Duration gpsInterval; private Duration gpsInterval;
private Distance thresholdHorizontalAccuracy; private Distance thresholdHorizontalAccuracy;
public GPSHandler(TrackPointCreator trackPointCreator) { public GPSManager(TrackPointCreator trackPointCreator) {
this.trackPointCreator = trackPointCreator; this.trackPointCreator = trackPointCreator;
} }
public void onStart(@NonNull Context context, @NonNull Handler handler) { public void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context; this.context = context;
this.handler = handler; this.handler = handler;
@@ -61,7 +62,7 @@ public class GPSHandler implements LocationListenerCompat, GpsStatus.GpsStatusLi
@SuppressWarnings({"MissingPermission"}) @SuppressWarnings({"MissingPermission"})
//TODO upgrade to AGP7.0.0/API31 started complaining about removeUpdates. //TODO upgrade to AGP7.0.0/API31 started complaining about removeUpdates.
public void onStop() { public void stop(Context context) {
if (locationManager != null && context != null) { if (locationManager != null && context != null) {
if (PermissionRequester.GPS.hasPermission(context)) { if (PermissionRequester.GPS.hasPermission(context)) {
LocationManagerCompat.removeUpdates(locationManager, this); LocationManagerCompat.removeUpdates(locationManager, this);
@@ -16,14 +16,13 @@ import java.time.ZoneId;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.AltitudeSumManager; import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
/** /**
* Creates TrackPoints while recording by fusing data from different sensors (e.g., GNSS, barometer, BLE sensors). * Creates TrackPoints while recording by fusing data from different sensors (e.g., GNSS, barometer, BLE sensors).
*/ */
public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDataSetChangeObserver { public class TrackPointCreator implements SensorManager.SensorDataSetChangeObserver {
private static final String TAG = TrackPointCreator.class.getSimpleName(); private static final String TAG = TrackPointCreator.class.getSimpleName();
@@ -34,45 +33,34 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
@NonNull @NonNull
private Clock clock = new MonotonicClock(); private Clock clock = new MonotonicClock();
private final GPSHandler gpsHandler; private final GPSManager gpsManager;
private BluetoothRemoteSensorManager remoteSensorManager; private SensorManager sensorManager;
private AltitudeSumManager altitudeSumManager;
public TrackPointCreator(Callback service) { public TrackPointCreator(Callback service, Context context, Handler handler) {
this.service = service; this.service = service;
this.gpsHandler = new GPSHandler(this); this.gpsManager = new GPSManager(this);
this.sensorManager = new SensorManager(context, handler, this);
} }
@VisibleForTesting @VisibleForTesting
TrackPointCreator(GPSHandler gpsHandler, Callback service) { TrackPointCreator(GPSManager gpsManager, Callback service) {
this.service = service; this.service = service;
this.gpsHandler = gpsHandler; this.gpsManager = gpsManager;
} }
public synchronized void start(@NonNull Context context, @NonNull Handler handler) { public synchronized void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context; this.context = context;
gpsHandler.onStart(context, handler); gpsManager.start(context, handler);
sensorManager.start(context, handler);
remoteSensorManager = new BluetoothRemoteSensorManager(context, handler, this);
altitudeSumManager = new AltitudeSumManager();
remoteSensorManager.start();
altitudeSumManager.start(context, handler);
} }
private boolean isStarted() { private boolean isStarted() {
return context != null; return context != null;
} }
public synchronized void reset() { private synchronized void reset() {
if (remoteSensorManager == null || altitudeSumManager == null) { sensorManager.reset();
Log.d(TAG, "No recording running and no reset necessary.");
return;
}
remoteSensorManager.reset();
altitudeSumManager.reset();
} }
private SensorDataSet addSensorData(TrackPoint trackPoint) { private SensorDataSet addSensorData(TrackPoint trackPoint) {
@@ -80,31 +68,12 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
Log.w(TAG, "Not started, should not be called."); Log.w(TAG, "Not started, should not be called.");
return null; return null;
} }
SensorDataSet sensorDataSet = null;
BluetoothRemoteSensorManager localRemoteSensorManager = remoteSensorManager;
if (localRemoteSensorManager != null) {
sensorDataSet = localRemoteSensorManager.fill(trackPoint);
}
AltitudeSumManager localAltitudeSumManager = altitudeSumManager;
if (localAltitudeSumManager != null) {
localAltitudeSumManager.fill(trackPoint);
}
return sensorDataSet; return sensorManager.fill(trackPoint);
} }
public synchronized void stop() { public synchronized void stop() {
gpsHandler.onStop(); gpsManager.stop(context);
if (remoteSensorManager != null) {
remoteSensorManager.stop();
remoteSensorManager = null;
}
if (altitudeSumManager != null) {
altitudeSumManager.stop(context);
altitudeSumManager = null;
}
this.context = null; this.context = null;
} }
@@ -125,7 +94,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
public void onNewTrackPoint(@NonNull TrackPoint trackPoint) { public void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
addSensorData(trackPoint); addSensorData(trackPoint);
boolean stored = service.newTrackPoint(trackPoint, gpsHandler.getThresholdHorizontalAccuracy()); boolean stored = service.newTrackPoint(trackPoint, gpsManager.getThresholdHorizontalAccuracy());
if (stored) { if (stored) {
reset(); reset();
} }
@@ -169,26 +138,8 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
} }
@VisibleForTesting @VisibleForTesting
public AltitudeSumManager getAltitudeSumManager() { public SensorManager getSensorManager() {
return altitudeSumManager; return sensorManager;
}
@Deprecated
@VisibleForTesting
public void setAltitudeSumManager(AltitudeSumManager altitudeSumManager) {
this.altitudeSumManager = altitudeSumManager;
}
@Deprecated
@VisibleForTesting
public BluetoothRemoteSensorManager getRemoteSensorManager() {
return remoteSensorManager;
}
@Deprecated
@VisibleForTesting
public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
this.remoteSensorManager = remoteSensorManager;
} }
@VisibleForTesting @VisibleForTesting
@@ -202,8 +153,8 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
} }
@VisibleForTesting @VisibleForTesting
public GPSHandler getGpsHandler() { public GPSManager getGpsHandler() {
return gpsHandler; return gpsManager;
} }
void sendGpsStatus(GpsStatusValue gpsStatusValue) { void sendGpsStatus(GpsStatusValue gpsStatusValue) {
@@ -28,6 +28,7 @@ import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat; import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -79,7 +80,7 @@ public class ActivityTypePreference extends DialogPreference {
textView = view.findViewById(R.id.activity_type_preference_text_view); textView = view.findViewById(R.id.activity_type_preference_text_view);
String category = PreferencesUtils.getDefaultActivity(); String category = PreferencesUtils.getDefaultActivity();
textView.setText(category); textView.setText(category);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line); ArrayAdapter<String> adapter = new ArrayAdapter<>(context, android.R.layout.simple_dropdown_item_1line, ActivityType.getLocalizedStrings(context));
textView.setAdapter(adapter); textView.setAdapter(adapter);
textView.setOnItemClickListener((parent, v, position, id) -> { textView.setOnItemClickListener((parent, v, position, id) -> {
String iconValue = TrackIconUtils.getIconValue(context, (String) textView.getAdapter().getItem(position)); String iconValue = TrackIconUtils.getIconValue(context, (String) textView.getAdapter().getItem(position));
@@ -68,18 +68,10 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key)); ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key));
int entriesId; int entriesId = switch (unitSystem) {
switch (unitSystem) { case METRIC -> R.array.stats_rate_metric_options;
case METRIC: case IMPERIAL, NAUTICAL_IMPERIAL -> R.array.stats_rate_imperial_options;
entriesId = R.array.stats_rate_metric_options; };
break;
case IMPERIAL:
case NAUTICAL_IMPERIAL:
entriesId = R.array.stats_rate_imperial_options;
break;
default:
throw new RuntimeException("Not implemented");
}
String[] entries = getResources().getStringArray(entriesId); String[] entries = getResources().getStringArray(entriesId);
statsRatePreferences.setEntries(entries); statsRatePreferences.setEntries(entries);
@@ -64,7 +64,7 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
DocumentFile directory = IntentUtils.toDocumentFile(getContext(), directoryUri); DocumentFile directory = IntentUtils.toDocumentFile(getContext(), directoryUri);
//Use same value for not set as Androidx ListPreference and EditTextPreference //Use same value for not set as Androidx ListPreference and EditTextPreference
if (directory == null) { if (directory == null) {
return getString(R.string.not_set); return getString(androidx.preference.R.string.not_set);
} }
return directoryUri + (directory.canWrite() ? "" : getString(R.string.export_dir_not_writable)); return directoryUri + (directory.canWrite() ? "" : getString(R.string.export_dir_not_writable));
@@ -49,11 +49,6 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
((SettingsActivity) getActivity()).openScreen(getString(R.string.settings_api_key)); ((SettingsActivity) getActivity()).openScreen(getString(R.string.settings_api_key));
return true; return true;
}); });
findPreference(getString(R.string.settings_open_tracks_key)).setOnPreferenceClickListener(preference -> {
((SettingsActivity) getActivity()).openScreen(getString(R.string.settings_open_tracks_key));
return true;
});
} }
@Override @Override
@@ -1,21 +0,0 @@
package de.dennisguse.opentracks.settings;
import android.os.Bundle;
import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.R;
public class OpenTracksSettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_open_tracks);
}
@Override
public void onStart() {
super.onStart();
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_open_tracks_title);
}
}

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