Compare commits

...

85 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
145 changed files with 1969 additions and 1376 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
+4 -2
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,6 +40,8 @@ _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>
@@ -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).
+18 -12
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.4.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 5141 versionCode 5222
versionName "v4.2.2" 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:2.0.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.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.8.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.5' 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.2' androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 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,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
+6 -4
View File
@@ -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,9 +11,40 @@ 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
@@ -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" />
@@ -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);
case 3:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position); 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);
case 3:
return getString(R.string.settings_chart_by_distance);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position); 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);
case 3:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position); 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);
case 3:
return getString(R.string.settings_chart_by_distance);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position); 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();
if (gpsStatusValue.isGpsStarted()) {
if (snackbarShowing) {
snackbar.dismiss(); 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)),
@@ -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;
case URI_CSV:
return TrackFileFormat.CSV;
default:
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri); 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);
} }
@@ -16,8 +16,6 @@ public abstract class Altitude {
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");
} }
} }
@@ -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,14 +136,9 @@ 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:
case SEGMENT_START_AUTOMATIC:
case SENSORPOINT:
case TRACKPOINT:
writeTrackPoint(columns, trackPoint); writeTrackPoint(columns, 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());
} }
} }
@@ -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,7 +113,10 @@ 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()) {
return;
}
PendingConflict conflict = conflictsQueue.peek(); PendingConflict conflict = conflictsQueue.peek();
if (conflict.resolve()) { if (conflict.resolve()) {
conflictsQueue.remove(conflict); conflictsQueue.remove(conflict);
@@ -128,7 +144,6 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
} }
}); });
} }
}
}; };
@Override @Override
@@ -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
@@ -205,54 +225,47 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
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())));
trackExportTotalCount = cursor.getCount();
viewBinding.exportProgressTotal.setText("" + trackExportTotalCount);
for (int i = from; i < trackExportTotalCount; i++) {
cursor.moveToPosition(i);
Track track = ContentProviderUtils.createTrack(cursor);
tracks.add(track);
}
if (!tracks.isEmpty()) {
export(tracks.get(0));
} else { } else {
onExportEnded(); exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).collect(Collectors.toList()));
}
} }
trackExportTotalCount = exportTasks.size();
} }
/** /**
* Enqueue track identified by UUID to be exported if not exported already or there is a conflict resolution. * 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,36 +140,31 @@ 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,29 +165,27 @@ 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,18 +79,16 @@ 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;
default:
throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode); throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
} }
@@ -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:
case TAG_KML22_SIMPLE_ARRAY_DATA:
dataType = attributes.getValue(ATTRIBUTE_NAME); 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,34 +412,18 @@ 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;
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE:
sensorHeartRateList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE:
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); accuracyHorizontal.add(value);
break; case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL ->
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL:
accuracyVertical.add(value); accuracyVertical.add(value);
break; default ->
default:
Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported."); 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,26 +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.i(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.i(TAG, "Connected to sensor: " + gatt.getDevice() + "; discovering services.");
gatt.discoverServices(); gatt.discoverServices();
break; }
case BluetoothProfile.STATE_DISCONNECTING: case BluetoothProfile.STATE_DISCONNECTING ->
Log.i(TAG, "Disconnecting from sensor: " + gatt.getDevice()); Log.i(TAG, "Disconnecting from sensor: " + gatt.getDevice());
break; case BluetoothProfile.STATE_DISCONNECTED -> {
case BluetoothProfile.STATE_DISCONNECTED:
//This is also triggered, if no connection was established (ca. 30s) //This is also triggered, if no connection was established (ca. 30s)
Log.i(TAG, "Disconnected from sensor: " + gatt.getDevice() + "; trying to reconnect"); Log.i(TAG, "Disconnected from sensor: " + gatt.getDevice() + "; trying to reconnect");
if (gatt.connect()) { if (gatt.connect()) {
Log.e(TAG, "Could not trigger reconnect for sensor: " + gatt.getDevice()); Log.e(TAG, "Could not trigger reconnect for sensor: " + gatt.getDevice());
} }
clearData(); clearData();
break; }
} }
} }
@@ -136,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) {
@@ -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);
}
}
@@ -32,14 +32,9 @@ class PreferencesOpenHelper {
PreferencesUtils.setInt(R.string.prefs_last_version_key, version); PreferencesUtils.setInt(R.string.prefs_last_version_key, version);
for (int i = 1; i <= version; i++) { for (int i = 1; i <= version; i++) {
switch (i) { switch (i) {
case 1: case 1 -> upgradeFrom0to1();
upgradeFrom0to1(); case 2 -> upgradeFrom1to2();
break; default -> throw new RuntimeException("Not implemented: upgrade to " + version);
case 2:
upgradeFrom1to2();
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + version);
} }
} }
} }
@@ -302,6 +302,11 @@ public class PreferencesUtils {
return getBoolean(R.string.stats_fullscreen_while_recording_key, DEFAULT); return getBoolean(R.string.stats_fullscreen_while_recording_key, DEFAULT);
} }
public static boolean shouldVoiceAnnouncementOnDeviceSpeaker() {
final boolean DEFAULT = resources.getBoolean(R.bool.voice_on_device_speaker_default);
return getBoolean(R.string.voice_on_device_speaker_key, DEFAULT);
}
public static Duration getVoiceAnnouncementFrequency() { public static Duration getVoiceAnnouncementFrequency() {
final int DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_frequency_default)); final int DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_frequency_default));
int value = getInt(R.string.voice_announcement_frequency_key, DEFAULT); int value = getInt(R.string.voice_announcement_frequency_key, DEFAULT);
@@ -443,23 +448,21 @@ public class PreferencesUtils {
String displayValue = formatter.formatDistance(distance); String displayValue = formatter.formatDistance(distance);
switch (unitSystem) { switch (unitSystem) {
case METRIC: case METRIC -> {
if (value == recordingDistanceIntervalDefault) { if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value); entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break; }
case IMPERIAL: case IMPERIAL, NAUTICAL_IMPERIAL -> {
case NAUTICAL_IMPERIAL:
if (value == recordingDistanceIntervalDefault) { if (value == recordingDistanceIntervalDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT()); entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break; }
default: default -> throw new RuntimeException("Not implemented");
throw new RuntimeException("Not implemented");
} }
} }
@@ -489,23 +492,21 @@ public class PreferencesUtils {
String displayValue = formatter.formatDistance(distance); String displayValue = formatter.formatDistance(distance);
switch (unitSystem) { switch (unitSystem) {
case METRIC: case METRIC -> {
if (value == maxRecordingDistanceDefault) { if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value); entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break; }
case IMPERIAL: case IMPERIAL, NAUTICAL_IMPERIAL -> {
case NAUTICAL_IMPERIAL:
if (value == maxRecordingDistanceDefault) { if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT()); entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break; }
default: default -> throw new RuntimeException("Not implemented");
throw new RuntimeException("Not implemented");
} }
} }
@@ -564,7 +565,7 @@ public class PreferencesUtils {
String displayValue = formatter.formatDistance(distance); String displayValue = formatter.formatDistance(distance);
switch (unitSystem) { switch (unitSystem) {
case METRIC: case METRIC -> {
if (value == recordingGPSAccuracyDefault) { if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value); entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
} else if (value == recordingGPSAccuracyExcellent) { } else if (value == recordingGPSAccuracyExcellent) {
@@ -574,9 +575,8 @@ public class PreferencesUtils {
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break; }
case IMPERIAL: case IMPERIAL, NAUTICAL_IMPERIAL -> {
case NAUTICAL_IMPERIAL:
if (value == recordingGPSAccuracyDefault) { if (value == recordingGPSAccuracyDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT()); entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
} else if (value == recordingGPSAccuracyExcellent) { } else if (value == recordingGPSAccuracyExcellent) {
@@ -584,9 +584,8 @@ public class PreferencesUtils {
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
break; }
default: default -> throw new RuntimeException("Not implemented");
throw new RuntimeException("Not implemented");
} }
} }
@@ -612,32 +611,30 @@ public class PreferencesUtils {
float value = Float.parseFloat(entryValues[i]); float value = Float.parseFloat(entryValues[i]);
switch (unitSystem) { switch (unitSystem) {
case METRIC: case METRIC -> {
if (value == idleSpeedDefault) { if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value); entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
} else { } else {
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value); entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
} }
break; }
case IMPERIAL: case IMPERIAL -> {
double valueMPH = Speed.ofKMH(value).toMPH(); double valueMPH = Speed.ofKMH(value).toMPH();
if (value == idleSpeedDefault) { if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH); entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH);
} else { } else {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH); entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
} }
break; }
case NAUTICAL_IMPERIAL: case NAUTICAL_IMPERIAL -> {
double valueKnots = Speed.ofKMH(value).toKnots(); double valueKnots = Speed.ofKMH(value).toKnots();
if (value == idleSpeedDefault) { if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_knots_recommended, valueKnots); entries[i] = resources.getString(R.string.value_float_knots_recommended, valueKnots);
} else { } else {
entries[i] = resources.getString(R.string.value_float_knots, valueKnots); entries[i] = resources.getString(R.string.value_float_knots, valueKnots);
} }
break; }
default: default -> throw new RuntimeException("Not implemented");
throw new RuntimeException("Not implemented");
} }
} }
@@ -93,8 +93,6 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
fragment = new ImportExportSettingsFragment(); fragment = new ImportExportSettingsFragment();
} else if (key.equals(getString(R.string.settings_api_key))) { } else if (key.equals(getString(R.string.settings_api_key))) {
fragment = new PublicAPISettingsFragment(); fragment = new PublicAPISettingsFragment();
} else if (key.equals(getString(R.string.settings_open_tracks_key))) {
fragment = new OpenTracksSettingsFragment();
} }
return fragment; return fragment;
@@ -20,7 +20,6 @@ import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout; import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.customRecordingLayout.SettingsCustomLayoutEditAdapter; import de.dennisguse.opentracks.ui.customRecordingLayout.SettingsCustomLayoutEditAdapter;
import de.dennisguse.opentracks.ui.util.ArrayAdapterFilterDisabled; import de.dennisguse.opentracks.ui.util.ArrayAdapterFilterDisabled;
import de.dennisguse.opentracks.util.StatisticsUtils;
public class SettingsCustomLayoutEditActivity extends AbstractActivity implements SettingsCustomLayoutEditAdapter.SettingsCustomLayoutItemClickListener { public class SettingsCustomLayoutEditActivity extends AbstractActivity implements SettingsCustomLayoutEditAdapter.SettingsCustomLayoutItemClickListener {
@@ -41,7 +40,7 @@ public class SettingsCustomLayoutEditActivity extends AbstractActivity implement
// Recycler view with visible stats. // Recycler view with visible stats.
RecordingLayout recordingLayout = getIntent().getParcelableExtra(EXTRA_LAYOUT); RecordingLayout recordingLayout = getIntent().getParcelableExtra(EXTRA_LAYOUT);
profile = recordingLayout.getName(); profile = recordingLayout.getName();
recordingLayoutFieldsVisible = StatisticsUtils.filterVisible(recordingLayout, true); recordingLayoutFieldsVisible = recordingLayout.toRecordingLayout(true);
adapterFieldsVisible = new SettingsCustomLayoutEditAdapter(this, this, recordingLayoutFieldsVisible); adapterFieldsVisible = new SettingsCustomLayoutEditAdapter(this, this, recordingLayoutFieldsVisible);
numColumns = recordingLayout.getColumnsPerRow(); numColumns = recordingLayout.getColumnsPerRow();
@@ -90,7 +89,7 @@ public class SettingsCustomLayoutEditActivity extends AbstractActivity implement
viewBinding.rowsOptions.setText(rowsOptionAdapter.getItem(numColumns - 1).toString(), false); viewBinding.rowsOptions.setText(rowsOptionAdapter.getItem(numColumns - 1).toString(), false);
// Recycler view with not visible stats. // Recycler view with not visible stats.
recordingLayoutFieldsHidden = StatisticsUtils.filterVisible(recordingLayout, false); recordingLayoutFieldsHidden = recordingLayout.toRecordingLayout(false);
adapterFieldsHidden = new SettingsCustomLayoutEditAdapter(this, this, recordingLayoutFieldsHidden); adapterFieldsHidden = new SettingsCustomLayoutEditAdapter(this, this, recordingLayoutFieldsHidden);
RecyclerView recyclerViewNotVisible = viewBinding.recyclerViewNotVisible; RecyclerView recyclerViewNotVisible = viewBinding.recyclerViewNotVisible;
recyclerViewNotVisible.setLayoutManager(new LinearLayoutManager(this)); recyclerViewNotVisible.setLayoutManager(new LinearLayoutManager(this));
@@ -96,8 +96,7 @@ public class BluetoothLeAdapter extends BaseAdapter {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (!(o instanceof Device)) return false; if (!(o instanceof Device device)) return false;
Device device = (Device) o;
return address.equals(device.address); return address.equals(device.address);
} }
@@ -87,4 +87,14 @@ public class DataField implements Parcelable {
public int hashCode() { public int hashCode() {
return Objects.hash(key, isVisible, isPrimary, isWide); return Objects.hash(key, isVisible, isPrimary, isWide);
} }
@Override
public String toString() {
return "DataField{" +
"key='" + key + '\'' +
", isVisible=" + isVisible +
", isPrimary=" + isPrimary +
", isWide=" + isWide +
'}';
}
} }
@@ -66,6 +66,12 @@ public class RecordingLayout implements Parcelable {
return new ArrayList<>(dataFields); return new ArrayList<>(dataFields);
} }
public RecordingLayout toRecordingLayout(boolean visibility) {
RecordingLayout result = new RecordingLayout(this.getName());
result.addFields(dataFields.stream().filter(f -> f.isVisible() == visibility).collect(Collectors.toList()));
return result;
}
public void moveField(int from, int to) { public void moveField(int from, int to) {
DataField dataFieldToMove = dataFields.remove(from); DataField dataFieldToMove = dataFields.remove(from);
dataFields.add(to, dataFieldToMove); dataFields.add(to, dataFieldToMove);
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.ui.markers; package de.dennisguse.opentracks.ui.markers;
import android.content.ActivityNotFoundException;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@@ -244,7 +245,12 @@ public class MarkerEditActivity extends AbstractActivity {
private void createMarkerWithPicture() { private void createMarkerWithPicture() {
Pair<Intent, Uri> intentAndPhotoUri = MarkerUtils.createTakePictureIntent(this, getTrackId()); Pair<Intent, Uri> intentAndPhotoUri = MarkerUtils.createTakePictureIntent(this, getTrackId());
cameraPhotoUri = intentAndPhotoUri.second; cameraPhotoUri = intentAndPhotoUri.second;
try {
takePictureFromCamera.launch(intentAndPhotoUri.first); takePictureFromCamera.launch(intentAndPhotoUri.first);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.no_compatible_camera_installed, Toast.LENGTH_LONG).show();
}
} }
private void createMarkerWithGalleryImage() { private void createMarkerWithGalleryImage() {
@@ -256,6 +262,10 @@ public class MarkerEditActivity extends AbstractActivity {
intent = new Intent(Intent.ACTION_GET_CONTENT); intent = new Intent(Intent.ACTION_GET_CONTENT);
} }
intent.setType("image/*"); intent.setType("image/*");
try {
takePictureFromGallery.launch(intent); takePictureFromGallery.launch(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.no_compatible_gallery_installed, Toast.LENGTH_LONG).show();
}
} }
} }

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