Compare commits

...

86 Commits

Author SHA1 Message Date
Dennis Guse 0b30ecabb1 Release: v3.16.0 2021-03-24 22:35:33 +01:00
mondstern 107cc14a2d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-03-24 17:23:33 +01:00
mondstern 6d3d8bb83c Translated using Weblate (Basque)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-03-24 17:23:33 +01:00
Michael Munzert 9c8e0335ef Translated using Weblate (German)
Currently translated at 90.1% (477 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-03-24 17:23:33 +01:00
FTno 8c648c69e8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-03-24 17:23:33 +01:00
Valentine A 4422464f8d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-03-24 17:23:33 +01:00
mondstern d094531779 Translated using Weblate (Polish)
Currently translated at 88.2% (464 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-03-24 17:23:33 +01:00
mondstern a01341d235 Translated using Weblate (Basque)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-03-24 17:23:33 +01:00
mondstern 83065dad39 Translated using Weblate (Estonian)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-03-24 17:23:33 +01:00
Dennis Guse 68ba3e0c2a Bugfix: announcements should consider hours when computing minutes.
Fixes #674.
2021-03-24 17:23:27 +01:00
Dennis Guse 3f21edc44c Cleanup. 2021-03-21 20:01:57 +01:00
Dennis Guse aefd1a6a35 Fixed test. 2021-03-21 19:58:18 +01:00
Dennis Guse 74fa73e4a4 TrackList: show aggregated stats icon in the first slot.
Fixes #657.
2021-03-20 22:21:18 +01:00
Dennis Guse 0a006d64f9 AnnouncementPeriodicTask: acquire sharedprefences on start (so it can be restarted).
Introduced in 15fbd90ca4.
Fixes #665.
2021-03-20 15:31:57 +01:00
dependabot[bot] 997ab6060e Bump gradle from 4.1.2 to 4.1.3
Bumps gradle from 4.1.2 to 4.1.3.

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-19 08:19:53 +01:00
Román Martínez 6d926ca7a8 Notification manager shows GPS accuracy, distance and speed. Fixes #584 and #483. 2021-03-18 21:49:55 +01:00
dependabot[bot] 3c1f1a9ba5 Bump espresso-core from 3.4.0-alpha04 to 3.4.0-alpha05
Bumps espresso-core from 3.4.0-alpha04 to 3.4.0-alpha05.

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-16 17:56:23 +01:00
Dennis Guse ed5da55fba Hack: Garmin Speed 2 might count backwards. 2021-03-15 17:27:41 +01:00
Dennis Guse d1e9649505 Garmin Speed 2: added workaround if cadence instead of speed is provided. 2021-03-15 17:27:41 +01:00
Dennis Guse 2431d60171 Cleanup. 2021-03-15 17:27:41 +01:00
Dennis Guse e08f5e7273 Cleanup: AnnouncementPeriodicTask uses TrackPointsIterator. 2021-03-15 17:27:41 +01:00
Dennis Guse 7d2a6fff59 TrackPointIterator: removed manual batching as it should not be necessary. 2021-03-15 17:27:41 +01:00
Dennis Guse a5158a55d9 Use distance from cycling speed sensors.
Fixes #505.
2021-03-15 17:27:41 +01:00
Dennis Guse 15fbd90ca4 Performance: Keep reference to sharedPreferences.
So far, we opened the SharedPreferences very often, which is very slow; also happened in the UI thread a lot.

Fixes #642.
2021-03-15 12:01:32 +01:00
mondstern eebc33f60c Translated using Weblate (Dutch)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-03-14 19:22:32 +01:00
mondstern ba2a755fb7 Translated using Weblate (Italian)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-03-14 19:22:32 +01:00
mondstern 344c50a20c Translated using Weblate (Finnish)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-03-14 19:22:32 +01:00
mondstern 9a9a610321 Translated using Weblate (Spanish)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-03-14 19:22:32 +01:00
mondstern 8af427ea48 Translated using Weblate (Danish)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-03-14 19:22:32 +01:00
Dennis Guse 7b7d7c8061 GPX: export and import power data.
Fixes #638.
2021-03-14 19:22:29 +01:00
Román Martínez 7d6919d3f9 Bug fixed: ImportService send a list of Track.Id. 2021-03-14 19:22:05 +01:00
Román Martínez 7eceb979c3 Merge pull request #646 from OpenTracksApp/exportTrackName#512
Track exported names: UUID, track's name and extension
2021-03-14 18:43:28 +01:00
Román Martínez bd48944ad5 Track export name: first UUID, name and extension (<First 32bits UUID>_<track's name>.<extension>). Fixes #512. 2021-03-14 18:42:27 +01:00
Dennis Guse 503cba1315 Import: allow import multiple select files.
Fixes #559.
2021-03-14 12:48:27 +01:00
Dennis Guse a313cb6244 Enabling StrictMode for debugging simplified. 2021-03-12 20:15:50 +01:00
Román Martínez 7f0c6309c0 Merge pull request #634 from OpenTracksApp/ui#566
Recorded fragment shows name, desc, start datetime.
2021-03-12 19:55:12 +01:00
Román Martínez 02c578327d Shared element transition (from List to Recorded). 2021-03-12 19:37:35 +01:00
Román Martínez 7fec0dcff4 StatisticsRecordedFragment does not need to check null for track/trackStatistics. Fixes #635. 2021-03-12 19:37:35 +01:00
Román Martínez 34eb7a833a Statistics recorded fragment shows activity's information (name, description, start datetime).
Re-arrange stats putting the focus on moving stats.
Fixes #566.
2021-03-12 19:37:35 +01:00
Román Martínez 5db3aa7ac1 Lowercase units for sensors and unit next to value. 2021-03-12 19:37:35 +01:00
mondstern 47fd7c51a9 Translated using Weblate (Finnish)
Currently translated at 98.6% (519 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-03-11 21:16:40 +01:00
mondstern 2f6f0b4e17 Translated using Weblate (German)
Currently translated at 90.1% (474 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-03-11 21:16:40 +01:00
mondstern 41b794f66b Translated using Weblate (Finnish)
Currently translated at 97.9% (515 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-03-11 21:16:40 +01:00
paulle e2c592367a Translated using Weblate (German)
Currently translated at 88.9% (468 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-03-11 21:16:40 +01:00
Hatsune Miku 7a24c56474 Translated using Weblate (Sinhala)
Currently translated at 0.3% (2 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/si/
2021-03-11 21:16:40 +01:00
Hatsune Miku 3f6bd6926a Added translation using Weblate (Sinhala) 2021-03-11 21:16:40 +01:00
Dennis Guse 508af45449 Refactored TrackImporter interface. 2021-03-11 21:16:36 +01:00
Dennis Guse 0f89cd7875 GPX: support multiple tracks.
Fixes #617.
2021-03-11 21:16:36 +01:00
Dennis Guse 29b522b8a2 Cleanup. 2021-03-11 21:16:24 +01:00
Dennis Guse 14e63a1a54 Release: v3.15.4 2021-03-09 21:44:02 +01:00
Román Martínez e559583d23 Voice announcement in seconds instead of minutes. Fixes #640. 2021-03-09 20:14:54 +01:00
Dennis Guse 3b9711d82e Release: v3.15.3 2021-03-08 18:16:18 +01:00
Dennis Guse 114fc8255f KML export: remove #start (was broken due to #316) and #end (included track statistics) placemarks.
Fixes #629.
2021-03-08 18:11:36 +01:00
Dennis Guse 26e655d58f Cleanup. 2021-03-08 18:11:36 +01:00
Dennis Guse 815b6a3cf1 StatisticsRecordingFragment: set speed=0 when TrackPoint is outdated. 2021-03-07 22:28:53 +01:00
Dennis Guse 3dc8a724d8 Bug: StatisticsRecordingFragment: speed and location where sometimes 0.
Fixes #628.
2021-03-07 22:28:53 +01:00
Dennis Guse 70f571cd09 CI: avoid OutOfMemoryError.
Fixes #599.
2021-03-07 19:46:37 +01:00
Dennis Guse 3f5c3a5c08 Bugfix: Close cursor. 2021-03-06 18:27:24 +01:00
Dennis Guse a13465391f Import: replaced SAXParserException as it only shows the message of it's cause. 2021-03-06 18:13:06 +01:00
Dennis Guse 97454b5369 Statistics*Fragment now updates the units on track category change. 2021-03-04 19:18:31 +01:00
Dennis Guse f6a47c4594 Statistics*Fragement now uses SharedPreferenceListener instead of accessing preferences repeatedly. 2021-03-04 19:18:31 +01:00
Dennis Guse fce76230e1 TrackDataListener now passes most recent TrackPoint to onNewTrackPointsDone(). 2021-03-04 19:18:31 +01:00
Dennis Guse fdf4336a9b Cleanup. 2021-03-04 19:18:31 +01:00
Dennis Guse 844c6e9609 TrackPoint: moved methods into class. 2021-03-04 19:18:31 +01:00
Manuel Zangl 784942ffd2 Fixed longitudeText in StatisticsRecordingFragment 2021-03-04 17:38:42 +01:00
Román Martínez edf4ccc941 It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence. 2021-03-02 19:55:18 +01:00
Dennis Guse d90258294f Release: v3.15.2 2021-03-01 19:42:16 +01:00
mondstern 01f4c13307 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-02-28 20:56:11 +01:00
FTno 873956426d Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-02-28 20:56:11 +01:00
Xosé M 4128c54960 Translated using Weblate (Galician)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-02-28 20:56:11 +01:00
mondstern 9978bddf21 Translated using Weblate (Czech)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-02-28 20:56:11 +01:00
Nikita Epifanov a9726b9a94 Translated using Weblate (Russian)
Currently translated at 81.5% (429 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-02-28 20:56:11 +01:00
Crystal-RainSlide 05ba52bab5 Translated using Weblate (Chinese (Simplified))
Currently translated at 80.7% (425 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/zh_Hans/
2021-02-28 20:56:11 +01:00
FTno ca95f40e77 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (526 of 526 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-02-28 20:56:11 +01:00
mondstern 5cb85c4383 Translated using Weblate (Italian)
Currently translated at 98.4% (513 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-02-28 20:56:11 +01:00
mondstern d0a477b112 Translated using Weblate (Finnish)
Currently translated at 94.4% (492 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-02-28 20:56:11 +01:00
mondstern ec365a18ca Translated using Weblate (Danish)
Currently translated at 88.2% (460 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-02-28 20:56:11 +01:00
mondstern 0ff735e559 Translated using Weblate (Czech)
Currently translated at 100.0% (521 of 521 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-02-28 20:56:11 +01:00
Dennis Guse 8a35835791 GPX export: re-implemented basic support for multiple tracks.
Fixes #614.
2021-02-28 20:56:05 +01:00
Dennis Guse e3569dab56 Release: v3.15.1 2021-02-27 18:40:05 +01:00
Dennis Guse 4040e3f45b Bugfix: crash on recording start due to show latitude/longitude option.
Fixes #613.
2021-02-27 18:37:42 +01:00
Dennis Guse eac7efba64 Upgrade desugar lib. 2021-02-26 21:30:08 +01:00
dependabot[bot] 21a3971a56 Bump mockito-android from 3.7.7 to 3.8.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.7.7 to 3.8.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.7.7...v3.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-23 08:07:43 +01:00
Dennis Guse 4e4a9abf6f Localization: fix/remove translation for wheel circumference (instead of wheel size).
Fixes #605.
2021-02-22 23:11:06 +01:00
Román Martínez a9624f72e7 Track recorded stats shows sensor data from trackpoints table.
Heart rate: maximum and average.
Cadence: maximum and average.
Power: average.
Fixes #525 and #502.
2021-02-14 17:34:19 +01:00
Dennis Guse bb38d274fb Voice announcements: cleanup. 2021-02-13 14:27:54 +01:00
126 changed files with 3606 additions and 1976 deletions
+2
View File
@@ -18,4 +18,6 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
profile: Nexus 6
script: ./gradlew connectedCheck
+6
View File
@@ -19,6 +19,12 @@ However, often only one value is provided.
### Speed
* Garmin Speed Sensor 2
* Has updatable firmware that requires an account for garmin.com
* This sensor reports speed data as cadence.
A workaround is in place.
* This sensor sometimes counts backwards (unknown software version).
A workaround is in place.
* Wahoo Speed (Model: WFRPMSPD)
### Cadence
+6 -6
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.android.tools.build:gradle:4.1.3'
}
}
@@ -51,8 +51,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3855
versionName "v3.15.0"
versionCode 3941
versionName "v3.16.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -86,8 +86,8 @@ android {
}
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha04'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha05'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
@@ -99,5 +99,5 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'org.mockito:mockito-android:3.7.7'
androidTestImplementation 'org.mockito:mockito-android:3.8.0'
}
+1 -11
View File
@@ -2,17 +2,7 @@
interface TrackExporter {
}
class KmzTrackExporter implements TrackExporter
KmzTrackExporter o-- FileTrackExporter
class FileTrackExporter implements TrackExporter {
/' One track only '/
}
FileTrackExporter o-- TrackWriter
interface TrackWriter {
}
class GpxTrackWriter implements TrackWriter
class KmlTrackWriter implements TrackWriter
class KmlTrackExporter implements TrackExporter
interface TrackImporter {
}
+28 -4
View File
@@ -1,8 +1,32 @@
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://opentracksapp.com/xmlschemas/v1">
<xs:element name="trackuuid"
type="xs:string" />
<xsd:element name="trackuuid"
type="xsd:string">
<xsd:annotation>
<xsd:documentation xml:lang="en">
A unique track identifier stored as UUID.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xs:schema>
<xsd:element name="loss"
type="xsd:float">
<xsd:annotation>
<xsd:documentation xml:lang="en">
The lost elevation in meters from the previous to the current TrackPoint.
Only used in GPX.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="gain"
type="xsd:string">
<xsd:annotation>
<xsd:documentation xml:lang="en">
The gained elevation in meters from the previous to the current TrackPoint.
Only used in GPX.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:schema>
@@ -0,0 +1,7 @@
v3.15.1: OpenTracks
Changes:
- After recording show sensor statistics (e.g., avg HR)
Bugfix:
- Crash on recording start with show latitude/longitude enabled
@@ -0,0 +1,7 @@
v3.15.2: OpenTracks
Changes:
- Updated translations
Bugfix:
- Crash on GPX export
@@ -0,0 +1,8 @@
v3.15.3: OpenTracks
Changes:
- Computation avg/max: heart rate, cadence, and power
Bugfix:
- KML/KMZ do not export #start/#end placemarks anymore
- While recording: Speed/location was sometimes falsly shown as 0.
@@ -0,0 +1,4 @@
v3.15.4: OpenTracks
Bugfix:
- Voice annoucements interval should be in minutes (introduced in v3.15.0)
@@ -0,0 +1,13 @@
v3.16.0: OpenTracks
Changes:
- Cycling: get distance from BLE speed sensor
- Reworked UI presented while recording
- UI performance optimizations
- Exported filenames: <First 32bits UUID>_<track's name>.<extension>
- Import multiple files via Intent (e.g., from Gadgetbridge)
- GPX: export power data
- Updated translations
Bugfix:
- Voice announcement use minutes correctly
@@ -11,6 +11,7 @@ import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.FileUtils;
public class TestDataUtil {
@@ -150,4 +151,13 @@ public class TestDataUtil {
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, trackPoint, photoUrl);
}
public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null);
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
while (trackPointIterator.hasNext()) {
trackPoints.add(trackPointIterator.next());
}
return trackPoints;
}
}
@@ -0,0 +1,92 @@
package de.dennisguse.opentracks.content.data;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class TestSensorDataUtil {
private final List<TrackPoint> trackPointList = new ArrayList<>();
private final List<SensorData> sensorDataList = new ArrayList<>();
public void add(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
TrackPoint tp = new TrackPoint(type);
int i = trackPointList.size() + 1;
tp.setLatitude(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0);
tp.setLongitude(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0);
tp.setHeartRate_bpm(hr);
tp.setCyclingCadence_rpm(cadence);
tp.setPower(power);
tp.setAccuracy(1f);
tp.setAltitude(1f);
tp.setTime(time);
tp.setSpeed(5f + (i / 10f));
tp.setElevationGain(3f);
tp.setElevationLoss(3f);
trackPointList.add(tp);
}
public List<TrackPoint> getTrackPointList() {
return this.trackPointList;
}
public SensorDataStats computeStats() {
if (sensorDataList.size() <= 1) {
return null;
}
SensorDataStats stats = new SensorDataStats();
long timeElapsed;
long movingTime = 0;
SensorData dataPrev = sensorDataList.get(0);
stats.maxHr = dataPrev.hr;
stats.maxCadence = dataPrev.cadence;
SensorData dataCurrent;
for (int i = 1; i < sensorDataList.size(); i++) {
dataCurrent = sensorDataList.get(i);
if (dataPrev.type != TrackPoint.Type.SEGMENT_START_MANUAL) {
timeElapsed = dataCurrent.type != TrackPoint.Type.SEGMENT_START_MANUAL ? dataCurrent.time.getEpochSecond() - dataPrev.time.getEpochSecond() : 0;
stats.avgHr += (dataPrev.hr * timeElapsed);
stats.maxHr = Math.max(dataPrev.hr, stats.maxHr);
stats.avgCadence += (dataPrev.cadence * timeElapsed);
stats.maxCadence = Math.max(dataPrev.cadence, stats.maxCadence);
stats.avgPower += (dataPrev.power * timeElapsed);
movingTime += timeElapsed;
}
dataPrev = dataCurrent;
}
stats.avgHr /= movingTime;
stats.avgCadence /= movingTime;
stats.avgPower /= movingTime;
return stats;
}
private static class SensorData {
final Instant time;
final float hr;
final float cadence;
final float power;
final TrackPoint.Type type;
public SensorData(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
this.time = time;
this.hr = hr == null ? 0 : hr;
this.cadence = cadence == null ? 0 : cadence;
this.power = power == null ? 0 : power;
this.type = type;
}
}
public static class SensorDataStats {
public float avgHr = 0f;
public float maxHr = 0f;
public float avgCadence = 0f;
public float maxCadence = 0f;
public float avgPower = 0f;
}
}
@@ -0,0 +1,28 @@
package de.dennisguse.opentracks.content.data;
import org.junit.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TrackPointTest {
@Test
public void isRecent_true() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
tp.setTime(Instant.now());
assertTrue(tp.isRecent());
}
@Test
public void isRecent_false() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
tp.setTime(Instant.now().minus(2, ChronoUnit.MINUTES));
assertFalse(tp.isRecent());
}
}
@@ -35,18 +35,22 @@ import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
@@ -90,29 +94,22 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_noPoints() {
testIterator(new Track.Id(1), 0, 1);
testIterator(new Track.Id(1), 0);
}
@Test
public void testLocationIterator_noBatchAscending() {
testIterator(new Track.Id(1), 50, 100);
testIterator(new Track.Id(2), 50, 50);
}
@Test
public void testLocationIterator_batchAscending() {
testIterator(new Track.Id(1), 50, 11);
testIterator(new Track.Id(2), 50, 25);
public void testLocationIterator_noAscending() {
testIterator(new Track.Id(1), 50);
testIterator(new Track.Id(2), 50);
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(new Track.Id(1), 20000, 2000);
testIterator(new Track.Id(1), 20000);
}
private void testIterator(Track.Id trackId, int numPoints, int batchSize) {
long lastPointId = initializeTrack(trackId, numPoints);
contentProviderUtils.setDefaultCursorBatchSize(batchSize);
private void testIterator(Track.Id trackId, int numPoints) {
TrackPoint.Id lastPointId = initializeTrack(trackId, numPoints);
List<TrackPoint> locations = new ArrayList<>(numPoints);
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
while (it.hasNext()) {
@@ -120,13 +117,13 @@ public class CustomContentProviderUtilsTest {
assertNotNull(trackPoint);
locations.add(trackPoint);
// Make sure the IDs are returned in the right order.
assertEquals(lastPointId - numPoints + locations.size(), trackPoint.getId().getId());
assertEquals(lastPointId.getId() - numPoints + locations.size(), trackPoint.getId().getId());
}
assertEquals(numPoints, locations.size());
}
}
private long initializeTrack(Track.Id id, int numPoints) {
private TrackPoint.Id initializeTrack(Track.Id id, int numPoints) {
Track track = new Track();
track.setId(id);
track.setName("Test: " + id.getId());
@@ -146,17 +143,17 @@ public class CustomContentProviderUtilsTest {
contentProviderUtils.bulkInsertTrackPoint(trackPoints, id);
// Load all inserted trackPoints.
long lastPointId = -1;
TrackPoint.Id lastPointId = null;
int counter = 0;
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(id, null)) {
while (it.hasNext()) {
TrackPoint trackPoint = it.next();
lastPointId = trackPoint.getId().getId();
lastPointId = trackPoint.getId();
counter++;
}
}
assertTrue(numPoints == 0 || lastPointId > 0);
assertTrue(numPoints == 0 || lastPointId.getId() > 0);
assertEquals(numPoints, counter);
return lastPointId;
@@ -766,9 +763,9 @@ public class CustomContentProviderUtilsTest {
// when / then
contentProviderUtils.bulkInsertTrackPoint(track.second, trackId);
assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, null, 1000).getCount());
assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
contentProviderUtils.bulkInsertTrackPoint(track.second.subList(0, 8), trackId);
assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, null, 1000).getCount());
assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
}
/**
@@ -825,7 +822,7 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
contentProviderUtils.insertTrackPoint(TestDataUtil.createTrackPoint(22), trackId);
assertEquals(11, contentProviderUtils.getTrackPoints(trackId).size());
assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
}
@Test
@@ -843,7 +840,7 @@ public class CustomContentProviderUtilsTest {
contentProviderUtils.insertTrackPoint(trackPoint, trackId);
// then
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertTrue(trackPoints.get(10).hasHeartRate());
assertEquals(trackPoint.getHeartRate_bpm(), trackPoints.get(10).getHeartRate_bpm(), 0.01);
assertEquals(trackPoint.getCyclingCadence_rpm(), trackPoints.get(10).getCyclingCadence_rpm(), 0.01);
@@ -875,7 +872,7 @@ public class CustomContentProviderUtilsTest {
.map(TrackPoint.Id::new).collect(Collectors.toList());
// when
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds.get(8), 5);
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds.get(8));
// then
assertEquals(2, cursor.getCount());
@@ -927,4 +924,337 @@ public class CustomContentProviderUtilsTest {
assertEquals("12", ContentProviderUtils.formatIdListForUri(new Track.Id(12)));
assertEquals("42,43,44", ContentProviderUtils.formatIdListForUri(new Track.Id(42), new Track.Id(43), new Track.Id(44)));
}
@Test
public void testGetSensorStats_noSensorData() {
// given
List<TrackPoint> trackPointList = new ArrayList<>();
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
trackPoint.setPower(null);
trackPoint.setCyclingCadence_rpm(null);
trackPoint.setHeartRate_bpm(null);
trackPointList.add(trackPoint);
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, trackPointList);
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_needAtLeastTwoTrackPointsFalse() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 90 300 0
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.TRACKPOINT);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_needAtLeastTwoTrackPointsTrue() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 90 300 -1
* 1 140 90 300 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, 90f, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertTrue(sensorStatistics.hasHeartRate());
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertTrue(sensorStatistics.hasCadence());
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertTrue(sensorStatistics.hasPower());
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_onlyHr() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 NULL NULL -1
* 1 140 NULL NULL 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, null, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, null, null, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertTrue(sensorStatistics.hasHeartRate());
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertFalse(sensorStatistics.hasCadence());
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_onlyCadence() {
// given
/*
* time elapsed hr cadence power track type
* 0 NULL 90 NULL -1
* 1 NULL 90 NULL 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, null, 90f, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, 90f, null, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertFalse(sensorStatistics.hasHeartRate());
assertTrue(sensorStatistics.hasCadence());
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertFalse(sensorStatistics.hasPower());
}
@Test
public void testGetSensorStats_onlyPower() {
// given
/*
* time elapsed hr cadence power track type
* 0 NULL NULL 300 -1
* 1 NULL NULL 300 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, null, null, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, null, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertFalse(sensorStatistics.hasHeartRate());
assertFalse(sensorStatistics.hasCadence());
assertTrue(sensorStatistics.hasPower());
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 7 160 90 275 0
* 4 155 85 280 0
* 1 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(10, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(14, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(15, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_withManualResume() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 3 174 88 400 0
* 20 127 54 175 -2
* 3 160 90 275 0
* 7 155 85 280 0
* 3 150 90 267 0
* 3 170 90 240 0
* 2 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_MANUAL);
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
@Test
public void testGetSensorStats_withStartAutomatic() {
// given
/*
* time elapsed hr cadence power track type
* 0 140 75 250 -1
* 2 148 80 300 0
* 1 150 82 325 0
* 3 174 88 400 0
* 20 127 54 175 -1
* 3 160 90 275 0
* 7 155 85 280 0
* 3 150 90 267 0
* 3 170 90 240 0
* 2 155 84 295 1
*/
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
}
private void testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
// given
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
Random random = new Random();
for (int i = 0; i < totalPoints; i++) {
int randomNum = withStartSegments ? random.nextInt(50) - 2 : 0;
TrackPoint.Type type = randomNum >= 0 ? TrackPoint.Type.TRACKPOINT : TrackPoint.Type.getById(randomNum);
float randomHr = random.nextFloat() * (200f - 90f) + 90f;
float randomCadence = random.nextFloat() * (110f - 40f) + 40f;
float randomPower = random.nextFloat() * (500f - 100f) + 100f;
sensorDataUtil.add(start.plus(i, ChronoUnit.SECONDS), randomHr, randomCadence, randomPower, type);
}
sensorDataUtil.add(start.plus(totalPoints, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
Track.Id trackId = new Track.Id(start.toEpochMilli());
Track track = TestDataUtil.createTrack(trackId);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
// when
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
// then
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0.01f);
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0.01f);
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0.01f);
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0.01f);
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0.01f);
}
@Test
public void testGetSensorStats_veryLongActivity12h() {
testGetSensorStats_randomData(43200, false);
}
@Test
public void testGetSensorStats_withSeveralRandomStartSegments() {
testGetSensorStats_randomData(5000, true);
}
}
@@ -24,7 +24,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
assertEquals(60, current.getValue(), 0.01);
}
@Test
@@ -37,7 +37,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(33.53, current.getCadence_rpm(), 0.01);
assertEquals(33.53, current.getValue(), 0.01);
}
@Test
@@ -50,7 +50,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(0, current.getCadence_rpm(), 0.01);
assertEquals(0, current.getValue(), 0.01);
}
@@ -64,7 +64,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertFalse(current.hasCadence_rpm());
assertFalse(current.hasValue());
}
@Test
@@ -77,7 +77,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
assertEquals(60, current.getValue(), 0.01);
}
@Test
@@ -90,40 +90,42 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
assertEquals(60, current.getValue(), 0.01);
}
@Test
public void compute_speed() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress", "sensorName", 1, 6184);
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 2, 8016);
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 1, 6184);
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
// when
current.compute(previous, 2150);
// then
assertEquals(1.20, current.getSpeed_mps(), 0.01);
assertEquals(1.20, current.getValue().distance_m, 2150);
assertEquals(1.20, current.getValue().speed_mps, 0.01);
}
@Test
public void compute_speed_rollOverCount() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress", "sensorName", UintUtils.UINT16_MAX - 1, 1024);
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT16_MAX - 1, 1024);
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous, 2000);
// then
assertEquals(2, current.getSpeed_mps(), 0.01);
assertEquals(1.20, current.getValue().distance_m, 2000);
assertEquals(2, current.getValue().speed_mps, 0.01);
}
@Test
public void equals_speed_with_no_data() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress");
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress");
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
@@ -2,7 +2,7 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.content.SharedPreferences;
import android.os.Looper;
import android.util.Log;
@@ -23,6 +23,7 @@ import org.junit.runners.JUnit4;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -31,6 +32,7 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -69,6 +71,7 @@ public class ExportImportTest {
}
private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -89,17 +92,17 @@ public class ExportImportTest {
trackId = service.startNewTrack();
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
service.pauseCurrentTrack();
service.resumeCurrentTrack();
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.endCurrentTrack();
track = contentProviderUtils.getTrack(trackId);
@@ -109,7 +112,7 @@ public class ExportImportTest {
contentProviderUtils.updateTrack(track);
track = contentProviderUtils.getTrack(trackId);
trackPoints = contentProviderUtils.getTrackPoints(trackId);
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
markers = contentProviderUtils.getMarkers(trackId);
assertEquals(10, trackPoints.size());
assertEquals(2, markers.size());
@@ -148,7 +151,7 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
importTrackId = trackImporter.importFile(inputStream);
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
// 1. track
@@ -161,10 +164,10 @@ public class ExportImportTest {
assertEquals(track.getUuid(), importedTrack.getUuid());
// 2. trackpoints
assertTrackpoints(trackPoints, false, false, false, false, false);
assertTrackpoints(trackPoints, false, false, false, false, false, false);
// 3. trackstatistics
assertTrackStatistics(false, false);
assertTrackStatistics(false, false, false);
// 4. markers
assertMarkers();
@@ -187,7 +190,7 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
importTrackId = trackImporter.importFile(inputStream);
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
// 1. track
@@ -199,10 +202,10 @@ public class ExportImportTest {
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. trackpoints
assertTrackpoints(trackPoints, true, true, true, true, true);
assertTrackpoints(trackPoints, true, true, true, true, true, true);
// 2. trackstatistics
assertTrackStatistics(false, true);
assertTrackStatistics(false, true, true);
// 4. markers
assertMarkers();
@@ -212,7 +215,7 @@ public class ExportImportTest {
@Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() {
// given
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.import_prevent_reimport_key, true);
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
@@ -225,45 +228,13 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
importTrackId = trackImporter.importFile(inputStream);
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNull(importedTrack);
}
@Ignore
@LargeTest
@Test
public void kmz_only_track() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata_and_pictures() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@LargeTest
@Test
public void gpx() {
@@ -281,7 +252,7 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
importTrackId = trackImporter.importFile(inputStream);
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
// 1. track
@@ -301,10 +272,10 @@ public class ExportImportTest {
trackPointsWithCoordinates.get(0).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackPointsWithCoordinates.get(3).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
assertTrackpoints(trackPointsWithCoordinates, false, true, true, true, true);
assertTrackpoints(trackPointsWithCoordinates, true, true, true, true, true, false);
// 3. trackstatistics
assertTrackStatistics(true, true);
assertTrackStatistics(true, true, false);
// 4. markers
assertMarkers();
@@ -314,7 +285,7 @@ public class ExportImportTest {
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() {
// given
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.import_prevent_reimport_key, true);
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
@@ -327,7 +298,7 @@ public class ExportImportTest {
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
importTrackId = trackImporter.importFile(inputStream);
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
// 1. track
@@ -354,9 +325,8 @@ public class ExportImportTest {
}
}
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss) {
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss, boolean verifyDistance) {
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(trackPoints.size(), importedTrackPoints.size());
for (int i = 0; i < trackPoints.size(); i++) {
@@ -391,13 +361,13 @@ public class ExportImportTest {
}
if (verifyHeartrate) {
assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
assertEquals("" + trackPoint, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
}
if (verifyCadence) {
assertEquals(trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
assertEquals("" + trackPoint, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
}
if (verifyPower) {
assertEquals(trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
assertEquals("" + trackPoint, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
}
if (verifyElevationGain) {
assertEquals(trackPoint.getElevationGain(), importedTrackPoint.getElevationGain(), 0.01);
@@ -405,10 +375,13 @@ public class ExportImportTest {
if (verifyElevationLoss) {
assertEquals(trackPoint.getElevationLoss(), importedTrackPoint.getElevationLoss(), 0.01);
}
if (verifyDistance) {
assertEquals(trackPoint.getSensorDistance(), importedTrackPoint.getSensorDistance(), 0.01);
}
}
}
private void assertTrackStatistics(boolean isGpx, boolean verifyElevationGainAndLoss) {
private void assertTrackStatistics(boolean isGpx, boolean verifyElevationGainAndLoss, boolean verifyDistance) {
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack.getTrackStatistics());
@@ -426,7 +399,9 @@ public class ExportImportTest {
assertEquals(trackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01);
if (verifyDistance) {
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01);
}
// Speed
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed(), 0.01);
@@ -446,20 +421,15 @@ public class ExportImportTest {
}
}
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, long speed, long altitude, float elevationGain, float heartRate, float cyclingCadence, float power) {
Location location = new Location("");
location.setTime(time);
location.setLongitude(longitude);
location.setLatitude(latitude);
location.setAccuracy(accuracy);
location.setAltitude(altitude);
location.setSpeed(speed);
TrackPoint tp = new TrackPoint(location);
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float elevationGain, float heartRate, float cyclingCadence, float power, float distance) {
TrackPoint tp = new TrackPoint(latitude, longitude, (double) altitude, Instant.ofEpochMilli(time));
tp.setAccuracy(accuracy);
tp.setSpeed(speed);
tp.setHeartRate_bpm(heartRate);
tp.setCyclingCadence_rpm(cyclingCadence);
tp.setPower(power);
tp.setElevationGain(elevationGain);
tp.setSensorDistance(distance);
return tp;
}
}
@@ -14,6 +14,7 @@ import org.junit.runners.JUnit4;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -54,7 +55,7 @@ public class LegacyImportTest {
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream);
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
// 1. track
@@ -69,7 +70,7 @@ public class LegacyImportTest {
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(6, importedTrackPoints.size());
// first 3 trackpoints
@@ -95,7 +96,7 @@ public class LegacyImportTest {
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty);
// when
importTrackId = trackImporter.importFile(inputStream);
importTrackId = trackImporter.importFile(inputStream).get(0);
}
/**
@@ -110,7 +111,7 @@ public class LegacyImportTest {
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream);
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
// 1. track
@@ -122,7 +123,7 @@ public class LegacyImportTest {
assertEquals("UNKNOWN", importedTrack.getIcon());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(6, importedTrackPoints.size());
// first segment
@@ -12,6 +12,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
@@ -25,7 +26,10 @@ public class TrackRecordingServiceNotificationManagerTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Mock
private TrackPoint locationMock;
private TrackPoint trackPointMock;
@Mock
private TrackStatistics trackStatisticsMock;
@Mock
private NotificationCompat.Builder notificationCompatBuilder;
@@ -35,18 +39,20 @@ public class TrackRecordingServiceNotificationManagerTest {
@Test
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
when(locationMock.hasAccuracy()).thenReturn(true);
when(locationMock.getAccuracy()).thenReturn(999f);
when(trackPointMock.hasAccuracy()).thenReturn(true);
when(trackPointMock.getAccuracy()).thenReturn(999f);
when(trackStatisticsMock.getTotalDistance()).thenReturn(0d);
when(notificationCompatBuilder.setContentText(anyString())).thenReturn(notificationCompatBuilder);
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean())).thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
subject.setMetricUnits(true);
// when
subject.updateTrackPoint(context, locationMock, 100);
subject.updateTrackPoint(context, locationMock, 100);
subject.updateTrackPoint(context, locationMock, 1000);
subject.updateTrackPoint(context, locationMock, 100);
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, 100);
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, 100);
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, 1000);
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, 100);
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
@@ -45,6 +45,7 @@ import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -84,6 +85,7 @@ public class TrackRecordingServiceTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
private ContentProviderUtils contentProviderUtils;
private final Track.Id trackId = new Track.Id(Math.abs(new Random().nextLong()));
@@ -102,7 +104,6 @@ public class TrackRecordingServiceTest {
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().commit();
// Ensure that the database is empty before every test
@@ -190,7 +191,7 @@ public class TrackRecordingServiceTest {
Track.Id trackId = service.startNewTrack();
// then
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(1, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
@@ -207,7 +208,7 @@ public class TrackRecordingServiceTest {
service.endCurrentTrack();
// then
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(2, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
@@ -225,7 +226,7 @@ public class TrackRecordingServiceTest {
service.pauseCurrentTrack();
// then
assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
//when
service.resumeTrack(trackId);
@@ -234,7 +235,7 @@ public class TrackRecordingServiceTest {
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(3, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
@@ -250,7 +251,7 @@ public class TrackRecordingServiceTest {
assertTrue(service.isRecording());
service.endCurrentTrack();
assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
// when
service.resumeTrack(trackId);
@@ -260,7 +261,7 @@ public class TrackRecordingServiceTest {
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(4, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
@@ -277,7 +278,7 @@ public class TrackRecordingServiceTest {
assertTrue(service.isRecording());
service.pauseCurrentTrack();
assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
// when
service.endCurrentTrack();
@@ -286,7 +287,7 @@ public class TrackRecordingServiceTest {
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(2, trackPoints.size());
assertEquals(TrackPoint.Type.SEGMENT_START_MANUAL, trackPoints.get(0).getType());
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
@@ -302,7 +303,7 @@ public class TrackRecordingServiceTest {
// when
// Just set recording track to a bogus value.
// Make sure that the service will not start recording and will clear the bogus track.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L);
PreferencesUtils.setLong(sharedPreferences, context, R.string.recording_track_id_key, 123L);
// then
assertFalse(service.isRecording());
@@ -325,7 +326,7 @@ public class TrackRecordingServiceTest {
// then
assertNull(newTrackId);
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(sharedPreferences, context));
assertEquals(trackId, service.getRecordingTrackId());
}
@@ -341,7 +342,7 @@ public class TrackRecordingServiceTest {
service.endCurrentTrack();
// then
assertFalse(PreferencesUtils.isRecording(context));
assertFalse(PreferencesUtils.isRecording(sharedPreferences, context));
assertNull(service.getRecordingTrackId());
}
@@ -388,8 +389,8 @@ public class TrackRecordingServiceTest {
assertNotNull(track.getId());
contentProviderUtils.insertTrack(track);
assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId().getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
PreferencesUtils.setLong(sharedPreferences, context, R.string.recording_track_id_key, isRecording ? track.getId().getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.recording_track_paused_key, !isRecording);
}
// NOTE: Do not use to create a track that is currently recording.
@@ -422,7 +423,6 @@ public class TrackRecordingServiceTest {
location.setTime(time);
location.setBearing(3.0f);
TrackPoint trackPoint = new TrackPoint(location);
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(ApplicationProvider.getApplicationContext());
trackRecordingService.newTrackPoint(trackPoint, prefAccuracy);
trackRecordingService.newTrackPoint(trackPoint, 50);
}
}
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -104,7 +105,7 @@ public class TrackRecordingServiceTestLocation {
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(8, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
@@ -138,7 +139,7 @@ public class TrackRecordingServiceTestLocation {
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(4, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
@@ -185,7 +186,7 @@ public class TrackRecordingServiceTestLocation {
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new Pair<>(TrackPoint.Type.TRACKPOINT, 1),
@@ -214,7 +215,7 @@ public class TrackRecordingServiceTestLocation {
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(6, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
@@ -260,7 +261,7 @@ public class TrackRecordingServiceTestLocation {
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(8, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
@@ -292,7 +293,7 @@ public class TrackRecordingServiceTestLocation {
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(7, trackPoints.size());
assertTrackPoints(List.of(
new Pair<>(TrackPoint.Type.SEGMENT_START_MANUAL, null),
@@ -53,6 +53,7 @@ public class TrackRecordingServiceTestLooper {
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
private ContentProviderUtils contentProviderUtils;
@@ -77,7 +78,6 @@ public class TrackRecordingServiceTestLooper {
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
// Ensure that the database is empty before every test
@@ -99,28 +99,28 @@ public class TrackRecordingServiceTestLooper {
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracy() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, 500);
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_gps_accuracy_key, 500);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default)));
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default)));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, -1);
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, -1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByTime() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, 1);
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, 1);
fullRecordingSession();
}
@@ -128,14 +128,14 @@ public class TrackRecordingServiceTestLooper {
@MediumTest
@Test
public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default)));
PreferencesUtils.setInt(sharedPreferences, context, R.string.max_recording_distance_key, Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default)));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_maxRecordingDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, 50);
PreferencesUtils.setInt(sharedPreferences, context, R.string.max_recording_distance_key, 50);
fullRecordingSession();
}
@@ -143,42 +143,42 @@ public class TrackRecordingServiceTestLooper {
@Test
public void testWithProperties_minRecordingDistanceDefault() throws TimeoutException {
int minRecordingIntervalDefault = Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, minRecordingIntervalDefault);
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_distance_interval_key, minRecordingIntervalDefault);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, 2);
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_distance_interval_key, 2);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDefault() throws TimeoutException {
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_default));
PreferencesUtils.setString(sharedPreferences, context, R.string.stats_units_key, context.getString(R.string.stats_units_default));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDisabled() throws TimeoutException {
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_imperial));
PreferencesUtils.setString(sharedPreferences, context, R.string.stats_units_key, context.getString(R.string.stats_units_imperial));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingIntervalDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
PreferencesUtils.setInt(sharedPreferences, context, R.string.min_recording_interval_key, Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingInterval() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, 2);
PreferencesUtils.setInt(sharedPreferences, context, R.string.min_recording_interval_key, 2);
fullRecordingSession();
}
@@ -186,7 +186,7 @@ public class TrackRecordingServiceTestLooper {
@Test
public void testWithProperties_minRequiredAccuracyDefault() throws TimeoutException {
int recordingGPSAccuracyDefault = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, recordingGPSAccuracyDefault);
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_gps_accuracy_key, recordingGPSAccuracyDefault);
fullRecordingSession();
}
@@ -209,7 +209,7 @@ public class TrackRecordingServiceTestLooper {
Track track = contentProviderUtils.getTrack(trackId);
assertNotNull(track);
assertEquals(trackId, track.getId());
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(sharedPreferences, context));
assertEquals(trackId, service.getRecordingTrackId());
// Insert a few points, markers and statistics.
@@ -223,7 +223,7 @@ public class TrackRecordingServiceTestLooper {
location.setTime(startTime + i * 10000);
location.setBearing(3.0f);
TrackPoint trackPoint = new TrackPoint(location);
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(sharedPreferences, context);
service.newTrackPoint(trackPoint, prefAccuracy);
if (i % 7 == 0) {
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import org.junit.After;
import org.junit.Before;
@@ -25,6 +26,9 @@ public class HandlerServerTest {
@Mock
private LocationHandler locationHandler;
@Mock
private SharedPreferences sharedPreferences;
private HandlerServer subject;
@Before
@@ -41,10 +45,10 @@ public class HandlerServerTest {
@Test
public void onSharedPreferenceChanged() {
// when
subject.onSharedPreferenceChanged(null, null, null);
subject.onSharedPreferenceChanged(context, sharedPreferences, null);
// then
verify(locationHandler).onSharedPreferenceChanged(null, null, null);
verify(locationHandler).onSharedPreferenceChanged(context, sharedPreferences, null);
}
@Test
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify;
public class LocationHandlerTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@Mock
private HandlerServer handlerServer;
@@ -43,11 +44,10 @@ public class LocationHandlerTest {
@Before
public void setUp() {
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().commit();
locationHandler.onSharedPreferenceChanged(context, PreferencesUtils.getSharedPreferences(context), context.getString(R.string.recording_gps_accuracy_key));
locationHandler.onSharedPreferenceChanged(context, PreferencesUtils.getSharedPreferences(context), context.getString(R.string.min_recording_interval_key));
locationHandler.onSharedPreferenceChanged(context, sharedPreferences, context.getString(R.string.recording_gps_accuracy_key));
locationHandler.onSharedPreferenceChanged(context, sharedPreferences, context.getString(R.string.min_recording_interval_key));
locationHandler.onStart(context);
}
@@ -85,7 +85,7 @@ public class LocationHandlerTest {
@Test
public void testOnLocationChanged_poorAccuracy() {
// given
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(sharedPreferences, context);
// when
locationHandler.onLocationChanged(createLocation(45f, 35f, prefAccuracy + 1, 5, System.currentTimeMillis()));
@@ -1,27 +1,36 @@
package de.dennisguse.opentracks.stats;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class TrackStatisticsUpdaterTest {
private static final int GPS_DISTANCE = 50;
@Test
public void addTrackPoint() {
public void addTrackPoint_TestingTrack() {
// given
TestDataUtil.TrackData data = TestDataUtil.createTestingTrack(new Track.Id(1));
// when
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
data.trackPoints.forEach(it -> updater.addTrackPoint(it, 50));
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
data.trackPoints.forEach(it -> subject.addTrackPoint(it, GPS_DISTANCE));
// then
TrackStatistics statistics = updater.getTrackStatistics();
TrackStatistics statistics = subject.getTrackStatistics();
assertEquals(85.35, statistics.getTotalDistance(), 0.01);
assertEquals(Duration.ofMillis(13999), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(6), statistics.getMovingTime());
@@ -35,4 +44,122 @@ public class TrackStatisticsUpdaterTest {
assertEquals(14.226, statistics.getAverageMovingSpeed(), 0.01);
assertEquals(6.566, statistics.getAverageSpeed(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_not_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
tp3.setSpeed(5f);
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(1.10, subject.getTrackStatistics().getTotalDistance(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_moving_and_sensor_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
tp2.setSpeed(5f);
TrackPoint tp3 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(3000));
tp2.setSpeed(5f);
TrackPoint tp4 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(4000));
tp2.setSpeed(5f);
tp4.setSensorDistance(5f);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(10f);
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(110.57, subject.getTrackStatistics().getTotalDistance(), 0.01);
// when
subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE);
// then
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_not_moving_and_sensor_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000));
tp4.setSensorDistance(5f);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(10f);
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01);
// when
subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE);
// then
assertEquals(15, subject.getTrackStatistics().getTotalDistance(), 0.01);
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_not_moving() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_moving() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_not_moving_and_sensor_speed() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_moving_and_sensor_speed() {
}
}
@@ -11,7 +11,6 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -26,27 +25,27 @@ public class AnnouncementUtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void getAnnouncement() {
public void getAnnouncement_metric() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000f);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", null);
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, null);
// then
assertEquals("OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour", announcement);
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 18.4 kilometers per hour", announcement);
}
@Test
public void getAnnouncement_withInterval() {
public void getAnnouncement_withInterval_metric() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000f);
@@ -54,30 +53,10 @@ public class AnnouncementUtilsTest {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000);
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
int speedId = R.plurals.voiceSpeedKilometersPerHour;
double kmPerHour = lastInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH;
String firstPartMsg = "OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour";
String rateMsg = " Lap speed of " + context.getResources().getQuantityString(speedId, getQuantityCount(kmPerHour), kmPerHour);
String msg = firstPartMsg + rateMsg;
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", lastInterval);
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval);
// then
assertEquals(msg, announcement);
}
private int getQuantityCount(double d) {
if (d == 0) {
return 0;
} else if (d == 1) {
return 1;
} else if (d == 2) {
return 2;
} else {
int count = (int) d;
return Math.max(count, 3);
}
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 18.4 kilometers per hour Lap speed of 51.2 kilometers per hour", announcement);
}
}
@@ -47,7 +47,7 @@ public class BluetoothUtilsTest {
// then
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertNull(sensor.getSpeed());
assertNull(sensor.getDistanceSpeed());
}
@Test
@@ -60,7 +60,7 @@ public class BluetoothUtilsTest {
// then
assertNull(sensor.getCadence());
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
assertEquals(225, sensor.getDistanceSpeed().getWheelRevolutionsCount());
}
@Test
@@ -73,7 +73,7 @@ public class BluetoothUtilsTest {
// then
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
assertEquals(225, sensor.getDistanceSpeed().getWheelRevolutionsCount());
}
@Test
@@ -0,0 +1,60 @@
package de.dennisguse.opentracks.util;
import junit.framework.TestCase;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ExportUtilsTest extends TestCase {
@Test
public void testIsExportFileExists_exists() {
// given
UUID uuid = UUID.randomUUID();
String format = "kmz";
List<String> fileNames = new ArrayList<>();
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
fileNames.add(uuid.toString().substring(0, 8) + "_other name name.kmz");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertTrue(exists);
}
@Test
public void testIsExportFileExists_not_exists1() {
// given
UUID uuid = UUID.randomUUID();
String format = "gpx";
List<String> fileNames = new ArrayList<>();
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_name.gpx");
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertFalse(exists);
}
@Test
public void testIsExportFileExists_not_exists2() {
// given
UUID uuid = UUID.randomUUID();
String format = "kmz";
List<String> fileNames = new ArrayList<>();
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
// when
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
// then
assertFalse(exists);
}
}
@@ -22,12 +22,14 @@ public class PreferencesUtilsTest {
@Test
public void ExportTrackFileFormat_ok() {
// given
SharedPreferences.Editor editor = PreferencesUtils.getSharedPreferences(context).edit();
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.export_trackfileformat_key), TrackFileFormat.KMZ_WITH_TRACKDETAIL.name());
editor.commit();
// when
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(sharedPreferences, context);
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL, trackFileFormat);
@@ -36,12 +38,13 @@ public class PreferencesUtilsTest {
@Test
public void ExportTrackFileFormat_invalid() {
// given
SharedPreferences.Editor editor = PreferencesUtils.getSharedPreferences(context).edit();
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.export_trackfileformat_key), "invalid");
editor.commit();
// when
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(sharedPreferences, context);
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
@@ -50,12 +53,13 @@ public class PreferencesUtilsTest {
@Test
public void ExportTrackFileFormat_noValue() {
// given
SharedPreferences.Editor editor = PreferencesUtils.getSharedPreferences(context).edit();
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear();
editor.commit();
// when
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(sharedPreferences, context);
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
@@ -94,18 +94,12 @@ public class StringUtilsTest {
assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there"));
}
/**
* Tests {@link StringUtils#parseTime(String)} with fractional seconds.
*/
@Test
public void testGetTime_fractional() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 352);
}
/**
* Tests {@link StringUtils#parseTime(String)} with time zone.
*/
@Test
public void testGetTime_timezone() {
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
@@ -118,9 +112,6 @@ public class StringUtilsTest {
assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0);
}
/**
* Tests {@link StringUtils#parseTime(String)} with fractional seconds and time zone.
*/
@Test
public void testGetTime_fractionalAndTimezone() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -46,12 +47,14 @@ public class TrackNameUtilsTest {
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
private static final SharedPreferences SHARED_PREFERENCES = PreferencesUtils.getSharedPreferences(CONTEXT);
/**
* Tests when the track_name_key is settings_recording_track_name_date_local_value.
*/
@Test
public void testTrackName_date_local() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value));
PreferencesUtils.setString(SHARED_PREFERENCES, CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value));
assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
@@ -60,7 +63,7 @@ public class TrackNameUtilsTest {
*/
@Test
public void testTrackName_date_iso_8601() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
PreferencesUtils.setString(SHARED_PREFERENCES, CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US);
assertEquals(simpleDateFormat.format(START_TIME.toEpochMilli()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
@@ -70,7 +73,7 @@ public class TrackNameUtilsTest {
*/
@Test
public void testTrackName_number() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value));
PreferencesUtils.setString(SHARED_PREFERENCES, CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value));
assertEquals("Track " + TRACK_ID.getId(), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
}
+5
View File
@@ -73,11 +73,13 @@ limitations under the License.
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx+xml" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx" />
</intent-filter>
@@ -135,6 +137,7 @@ limitations under the License.
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kmz" />
</intent-filter>
@@ -192,6 +195,8 @@ limitations under the License.
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kml+xml" />
</intent-filter>
@@ -66,7 +66,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
public void onConfirmDeleteDone(Track.Id... trackIds) {
boolean stopRecording = false;
Track.Id recordingTrackId = PreferencesUtils.getRecordingTrackId(this);
Track.Id recordingTrackId = PreferencesUtils.getRecordingTrackId(PreferencesUtils.getSharedPreferences(this), this);
for (Track.Id trackId : trackIds) {
if (trackId.equals(recordingTrackId)) {
stopRecording = true;
@@ -84,11 +84,9 @@ public class MarkerEditActivity extends AbstractActivity {
// Setup UI elements
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.markerEditMarkerType.setAdapter(adapter);
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> {
viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
});
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString()));
viewBinding.markerEditCancel.setOnClickListener(v -> {
viewModel.onCancel();
@@ -66,12 +66,12 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(MarkerListActivity.this);
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, MarkerListActivity.this);
}
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(MarkerListActivity.this);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, MarkerListActivity.this);
}
if (key != null) {
runOnUiThread(MarkerListActivity.this::invalidateOptionsMenu);
@@ -138,7 +138,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
}
@Override
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks;
import android.app.Application;
import android.content.SharedPreferences;
import android.os.StrictMode;
import android.util.Log;
@@ -27,31 +28,24 @@ public class Startup extends Application {
Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE);
//Set default values of preferences on first start.
PreferencesUtils.resetPreferences(this, false);
if (PreferencesUtils.getString(this, R.string.stats_units_key, "").equals("")) {
SharedPreferences sharedPreferences = PreferencesUtils.resetPreferences(this, false);
if (PreferencesUtils.getString(sharedPreferences, this, R.string.stats_units_key, "").equals("")) {
String statsUnits = getString(Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial : R.string.stats_units_metric);
PreferencesUtils.setString(this, R.string.stats_units_key, statsUnits);
PreferencesUtils.setString(sharedPreferences, this, R.string.stats_units_key, statsUnits);
}
ActivityUtils.applyNightMode(this);
ActivityUtils.applyNightMode(sharedPreferences, this);
//TODO Workaround to reset recordingTrackId on app startup as the TrackRecordingService (likely) crashed.
if (PreferencesUtils.isRecording(this)) {
if (PreferencesUtils.isRecording(sharedPreferences, this)) {
Log.e(TAG, "Reset recordingTrackId; likely the TrackRecordingService crashed.");
PreferencesUtils.setLong(this, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setLong(sharedPreferences, this, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
}
//In debug builds: show thread and VM warnings.
if (BuildConfig.DEBUG) {
Log.d(TAG, "Enabling strict mode");
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
StrictMode.enableDefaults();
}
}
}
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks;
import android.Manifest;
import android.app.ActivityOptions;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
@@ -29,6 +30,7 @@ import android.location.LocationManager;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
@@ -107,18 +109,18 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(TrackListActivity.this);
metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, TrackListActivity.this);
}
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackListActivity.this);
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, TrackListActivity.this);
if (key != null && PreferencesUtils.isRecording(recordingTrackId)) {
trackRecordingServiceConnection.startAndBind(TrackListActivity.this);
}
}
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackListActivity.this);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, TrackListActivity.this);
}
if (key != null) {
runOnUiThread(() -> {
@@ -195,17 +197,20 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
Intent newIntent;
if (trackId == recordingTrackId.getId()) {
// Is recording -> open record activity.
newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId));
startActivity(newIntent);
} else {
// Not recording -> open detail activity.
newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordedActivity.class)
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId));
ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation(
this,
new Pair<>(view.findViewById(R.id.list_item_icon), TrackRecordedActivity.VIEW_TRACK_ICON));
startActivity(newIntent, activityOptions.toBundle());
}
startActivity(newIntent);
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@@ -257,7 +262,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(this);
}
@@ -24,6 +24,7 @@ import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
@@ -55,6 +56,8 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
public static final String VIEW_TRACK_ICON = "track_icon";
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
@@ -84,6 +87,8 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
if (savedInstanceState != null) {
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
postponeEnterTransition();
}
@Override
@@ -138,7 +143,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.findItem(R.id.track_detail_resume_track).setVisible(!PreferencesUtils.isRecording(this));
menu.findItem(R.id.track_detail_resume_track).setVisible(!PreferencesUtils.isRecording(PreferencesUtils.getSharedPreferences(this), this));
Track track = contentProviderUtils.getTrack(trackId);
setTitle(track != null ? track.getName() : "");
return super.onPrepareOptionsMenu(menu);
@@ -275,4 +280,9 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
}
}
}
public void startPostponedEnterTransitionWith(View viewIcon, View viewName) {
ViewCompat.setTransitionName(viewIcon, TrackRecordedActivity.VIEW_TRACK_ICON);
startPostponedEnterTransition();
}
}
@@ -97,9 +97,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackRecordingActivity.this);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, TrackRecordingActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
}
@@ -170,7 +170,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
private void setLockscreenPolicy() {
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackRecordingActivity.this);
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(sharedPreferences, TrackRecordingActivity.this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen);
@@ -182,7 +182,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
private void setScreenOnPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldKeepScreenOn(TrackRecordingActivity.this);
boolean keepScreenOn = PreferencesUtils.shouldKeepScreenOn(sharedPreferences, TrackRecordingActivity.this);
if (keepScreenOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -192,7 +192,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
private void setFullscreenPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldUseFullscreen(TrackRecordingActivity.this);
boolean keepScreenOn = PreferencesUtils.shouldUseFullscreen(sharedPreferences, TrackRecordingActivity.this);
if (keepScreenOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
@@ -206,7 +206,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(this);
trackDataHub.start();
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
@@ -134,11 +135,13 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
}
}
//TODO Check preference handling.
private void setCommonValues(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
String category = aggregatedStatistic.getCategory();
reportSpeed = PreferencesUtils.isReportSpeed(context, category);
metricsUnits = PreferencesUtils.isMetricUnits(context);
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category);
metricsUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
sportIcon.setImageResource(getIcon(aggregatedStatistic));
typeLabel.setText(category);
@@ -14,8 +14,8 @@ import java.util.concurrent.Future;
*/
class ExecutorListViewService<T> {
private List<T> enqueueObjects = new ArrayList<>();
private ExecutorService executorService;
private final List<T> enqueueObjects = new ArrayList<>();
private final ExecutorService executorService;
public ExecutorListViewService(int numThreads) {
executorService = Executors.newFixedThreadPool(numThreads);
@@ -23,7 +23,6 @@ class ExecutorListViewService<T> {
public void shutdown() {
enqueueObjects.clear();
enqueueObjects = null;
executorService.shutdown();
}
@@ -25,11 +25,12 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, String category, StackMode stackMode) {
super(context, R.layout.interval_stats_list_item, intervalList);
metricUnits = PreferencesUtils.isMetricUnits(context);
metricUnits = PreferencesUtils.isMetricUnits(PreferencesUtils.getSharedPreferences(context), context);
this.category = category;
this.stackMode = stackMode;
}
//TODO Check preference handling! Should not be accessed in getView()
@NonNull
@Override
public View getView(int position, @Nullable View intervalView, @NonNull ViewGroup parent) {
@@ -60,7 +61,7 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
}
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits));
if (PreferencesUtils.isReportSpeed(getContext(), category)) {
if (PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category)) {
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, true));
} else {
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, false));
@@ -33,7 +33,8 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
private final Activity activity;
private ExecutorListViewService executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
//TODO Should be Marker.Id
private final ExecutorListViewService<Long> executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
private boolean scroll = false;
@@ -111,8 +112,8 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
return;
}
long id = cursor.getLong(cursor.getColumnIndex(MarkerColumns._ID));
String photoUrl = cursor.getString(cursor.getColumnIndex(MarkerColumns.PHOTOURL));
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MarkerColumns._ID));
String photoUrl = cursor.getString(cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL));
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
if (hasPhoto) {
@@ -12,7 +12,7 @@ public class ScrollVisibleViews implements AbsListView.OnScrollListener {
private int from = -1;
private int to = -1;
private VisibleViewsListener visibleViewsListener;
private final VisibleViewsListener visibleViewsListener;
public ScrollVisibleViews(@NonNull VisibleViewsListener visibleViewsListener) {
this.visibleViewsListener = visibleViewsListener;
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -13,19 +12,11 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final int HEART_RATE_TYPE = 0;
public static final int CADENCE_TYPE = 1;
public static final int POWER_TYPE = 2;
private List<Pair<Integer, SensorData>> sensorDataList;
private List<SensorDataModel> sensorDataList;
private final Context context;
public SensorsAdapter(Context context) {
@@ -42,9 +33,8 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
int type = sensorDataList.get(position).first;
SensorData sensorData = sensorDataList.get(position).second;
viewHolder.setData(sensorData, type);
SensorDataModel sensorDataModel = sensorDataList.get(position);
viewHolder.setData(sensorDataModel);
}
@Override
@@ -56,12 +46,7 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
}
}
@Override
public int getItemViewType(int position) {
return sensorDataList.get(position).first;
}
public List<Pair<Integer, SensorData>> swapData(List<Pair<Integer, SensorData>> data) {
public List<SensorDataModel> swapData(List<SensorDataModel> data) {
if (sensorDataList == data) {
return null;
}
@@ -89,70 +74,18 @@ public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
unit = itemView.findViewById(R.id.stats_sensor_unit);
}
public void setData(SensorData sensorData, int type) {
switch (type) {
case HEART_RATE_TYPE:
setHeartRateSensorData((SensorDataHeartRate) sensorData);
break;
case CADENCE_TYPE:
setCadenceSensorData((SensorDataCycling.Cadence) sensorData);
break;
case POWER_TYPE:
setPowerSensorData((SensorDataCyclingPower) sensorData);
break;
default:
throw new RuntimeException("Unknown sensor type");
}
}
public void setData(SensorDataModel sensorDataModel) {
String sensorName = sensorDataModel.getSensorName();
String sensorValue = sensorDataModel.hasValue() ? sensorDataModel.getSensorValue() : context.getString(R.string.value_unknown);
private void setHeartRateSensorData(SensorDataHeartRate data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasHeartRate_bpm() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
}
this.label.setText(context.getString(sensorDataModel.getLabelId()));
if (sensorName == null) {
this.sensorValue.setVisibility(View.GONE);
} else {
this.sensorValue.setText(sensorName);
}
this.label.setText(context.getString(R.string.sensor_state_heart_rate));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_beats_per_minute);
}
private void setCadenceSensorData(SensorDataCycling.Cadence data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasCadence_rpm() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
}
}
this.label.setText(context.getString(R.string.sensor_state_cadence));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_rounds_per_minute);
}
private void setPowerSensorData(SensorDataCyclingPower data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasPower_w() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getPower_w(), 0);
}
}
this.label.setText(context.getString(R.string.sensor_state_power));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_power);
this.unit.setText(context.getString(sensorDataModel.getUnitId()));
}
}
}
@@ -71,6 +71,8 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
private final ContentProviderUtils contentProviderUtils;
private final int targetNumPoints;
private SharedPreferences sharedPreferences;
private boolean started;
private HandlerThread handlerThread;
private Handler handler;
@@ -140,8 +142,10 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
PreferencesUtils.register(context, this);
onSharedPreferenceChanged(null, null);
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
onSharedPreferenceChanged(sharedPreferences, null);
handler.post(() -> {
if (started) {
loadDataForAll();
@@ -155,7 +159,8 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
return;
}
PreferencesUtils.unregister(context, this);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
sharedPreferences = null;
started = false;
@@ -224,10 +229,10 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
handler.post(() -> {
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, context);
}
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, context);
}
});
}
@@ -365,10 +370,11 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
}
TrackPoint trackPoint = null;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
while (trackPointIterator.hasNext()) {
TrackPoint trackPoint = trackPointIterator.next();
trackPoint = trackPointIterator.next();
TrackPoint.Id trackPointId = trackPoint.getId();
// Stop if past the last wanted point
@@ -398,18 +404,23 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
}
localNumLoadedTrackPoints++;
localLastSeenTrackPointIdId = trackPointId;
}
}
if (trackPoint != null) {
localLastSeenTrackPointIdId = trackPoint.getId();
}
if (updateSamplingState) {
numLoadedPoints = localNumLoadedTrackPoints;
firstSeenTrackPointId = localFirstSeenTrackPointId;
lastSeenTrackPointId = localLastSeenTrackPointIdId;
}
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone();
if (trackPoint != null) {
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone(trackPoint);
}
}
}
@@ -16,6 +16,8 @@
package de.dennisguse.opentracks.content;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -32,6 +34,7 @@ public interface TrackDataListener {
*
* @param track the track
*/
//TODO Could be @NonNull
void onTrackUpdated(Track track);
/**
@@ -44,20 +47,20 @@ public interface TrackDataListener {
*
* @param trackPoint the trackPoint
*/
void onSampledInTrackPoint(TrackPoint trackPoint);
void onSampledInTrackPoint(@NonNull TrackPoint trackPoint);
/**
* Called when a sampled out track point is read.
*
* @param trackPoint the trackPoint
*/
void onSampledOutTrackPoint(TrackPoint trackPoint);
void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint);
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint)} and {@link #onSampledOutTrackPoint(TrackPoint)}.
*/
void onNewTrackPointsDone();
void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint);
/**
* Called to clear previously sent markers.
@@ -109,6 +109,19 @@ public class Track {
this.trackStatistics = trackStatistics;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Track track = (Track) o;
return id.equals(track.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public static class Id implements Parcelable {
private final long id;
@@ -21,6 +21,7 @@ import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
@@ -42,6 +43,11 @@ import java.util.Objects;
*/
public class TrackPoint {
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private TrackPoint.Id id;
private Instant time;
@@ -51,6 +57,7 @@ public class TrackPoint {
private Double altitude_m;
private Float speed_mps;
private Float bearing;
private Float sensorDistance_m;
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
@@ -174,7 +181,23 @@ public class TrackPoint {
}
public boolean hasLocation() {
return latitude != null || longitude != null;
return latitude != null && longitude != null;
}
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
@NonNull
@@ -202,7 +225,7 @@ public class TrackPoint {
return elevationGain != null;
}
public Float getElevationGain() {
public float getElevationGain() {
return elevationGain;
}
@@ -222,22 +245,6 @@ public class TrackPoint {
this.elevationLoss = elevationLoss;
}
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
public Instant getTime() {
return time;
}
@@ -246,6 +253,12 @@ public class TrackPoint {
this.time = time;
}
public boolean isRecent() {
return Instant.now()
.isBefore(time.plus(MAX_LOCATION_AGE));
}
public boolean hasAltitude() {
return altitude_m != null;
}
@@ -270,6 +283,10 @@ public class TrackPoint {
this.speed_mps = speed;
}
public boolean isMoving() {
return hasSpeed() && getSpeed() >= MAX_NO_MOVEMENT_SPEED;
}
public boolean hasBearing() {
return bearing != null;
}
@@ -294,8 +311,16 @@ public class TrackPoint {
this.accuracy = horizontalAccuracy;
}
public float distanceTo(@NonNull TrackPoint dest) {
return getLocation().distanceTo(dest.getLocation());
public float distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
return getLocation().distanceTo(previous.getLocation());
}
public boolean fulfillsAccuracy(int poorAccuracy) {
return hasAccuracy() && accuracy < poorAccuracy;
}
public float bearingTo(@NonNull TrackPoint dest) {
@@ -307,6 +332,18 @@ public class TrackPoint {
}
// Sensor data
public boolean hasSensorDistance() {
return sensorDistance_m != null;
}
public Float getSensorDistance() {
return sensorDistance_m;
}
public void setSensorDistance(Float distance_m) {
this.sensorDistance_m = distance_m;
}
public boolean hasSensorData() {
return hasHeartRate() || hasCyclingCadence() || hasPower();
}
@@ -327,7 +364,7 @@ public class TrackPoint {
return cyclingCadence_rpm != null;
}
public Float getCyclingCadence_rpm() {
public float getCyclingCadence_rpm() {
return cyclingCadence_rpm;
}
@@ -339,7 +376,7 @@ public class TrackPoint {
return power != null;
}
public Float getPower() {
public float getPower() {
return power;
}
@@ -50,10 +50,18 @@ public interface TrackPointsColumns extends BaseColumns {
String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate";
String SENSOR_CADENCE = "sensor_cadence";
String SENSOR_DISTANCE = "sensor_distance"; //DISTANCE from previous TrackPoint
String SENSOR_POWER = "sensor_power";
String ELEVATION_GAIN = "elevation_gain";
String ELEVATION_LOSS = "elevation_loss";
// Alias for sensor statistics
String ALIAS_AVG_HR = "avg_hr";
String ALIAS_MAX_HR = "max_hr";
String ALIAS_AVG_CADENCE = "avg_cadence";
String ALIAS_MAX_CADENCE = "max_cadence";
String ALIAS_AVG_POWER = "avg_power";
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TRACKID + " INTEGER NOT NULL, "
@@ -70,6 +78,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ ELEVATION_GAIN + " FLOAT, "
+ ELEVATION_LOSS + " FLOAT, "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1)), "
+ SENSOR_DISTANCE + " FLOAT, "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
+ ")";
@@ -30,6 +30,7 @@ public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
Uri CONTENT_URI_SENSOR_STATS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
String DEFAULT_SORT_ORDER = _ID;
@@ -19,6 +19,7 @@ class CachedTrackPointsIndexes {
final int bearingIndex;
final int sensorHeartRateIndex;
final int sensorCadenceIndex;
final int sensorDistanceIndex;
final int sensorPowerIndex;
final int elevationGainIndex;
final int elevationLossIndex;
@@ -35,6 +36,7 @@ class CachedTrackPointsIndexes {
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
sensorDistanceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_DISTANCE);
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
elevationGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_GAIN);
elevationLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_LOSS);
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.content.provider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
@@ -44,6 +45,7 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
@@ -68,7 +70,6 @@ public class ContentProviderUtils {
private static final String ID_SEPARATOR = ",";
private final ContentResolver contentResolver;
private int defaultCursorBatchSize = 2000;
public ContentProviderUtils(Context context) {
contentResolver = context.getContentResolver();
@@ -174,7 +175,7 @@ public class ContentProviderUtils {
// Delete track last since it triggers a database vacuum call
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id->Long.toString(id.getId())).toArray(String[]::new));
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new));
}
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
@@ -554,6 +555,9 @@ public class ContentProviderUtils {
if (!cursor.isNull(indexes.sensorCadenceIndex)) {
trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex));
}
if (!cursor.isNull(indexes.sensorDistanceIndex)) {
trackPoint.setSensorDistance(cursor.getFloat(indexes.sensorDistanceIndex));
}
if (!cursor.isNull(indexes.sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex));
}
@@ -577,24 +581,6 @@ public class ContentProviderUtils {
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
/**
* Gets the first location id for a track.
* Returns null if it doesn't exist.
*
* @param trackId the track id
*/
@Deprecated
public Track.Id getFirstTrackPointId(Track.Id trackId) {
String selection = TrackPointsColumns._ID + "=(SELECT MIN(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
return new Track.Id(cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)));
}
}
return null;
}
/**
* Gets the last location id for a track.
* Returns -1L if it doesn't exist.
@@ -645,9 +631,9 @@ public class ContentProviderUtils {
*
* @param trackId the track id
* @param startTrackPointId the starting trackPoint id. `null` to ignore
* @param maxLocations maximum number of locations to return. `null` for no limit
*/
public Cursor getTrackPointCursor(Track.Id trackId, TrackPoint.Id startTrackPointId, Integer maxLocations) {
@NonNull
public Cursor getTrackPointCursor(@NonNull Track.Id trackId, TrackPoint.Id startTrackPointId) {
String selection;
String[] selectionArgs;
if (startTrackPointId != null) {
@@ -658,11 +644,7 @@ public class ContentProviderUtils {
selectionArgs = new String[]{Long.toString(trackId.getId())};
}
String sortOrder = TrackPointsColumns.DEFAULT_SORT_ORDER;
if (maxLocations != null) {
sortOrder += " LIMIT " + maxLocations;
}
return getTrackPointCursor(null, selection, selectionArgs, sortOrder);
return getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns.DEFAULT_SORT_ORDER);
}
/**
@@ -724,6 +706,9 @@ public class ContentProviderUtils {
if (trackPoint.hasCyclingCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm());
}
if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance());
}
if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower());
}
@@ -773,39 +758,6 @@ public class ContentProviderUtils {
return contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, projection, selection, selectionArgs, sortOrder);
}
@VisibleForTesting
public List<TrackPoint> getTrackPoints(Track.Id trackId) {
List<TrackPoint> trackPoints = null;
try (Cursor trackPointCursor = getTrackPointCursor(trackId, null, null)) {
if (trackPointCursor != null) {
trackPointCursor.moveToFirst();
trackPoints = new ArrayList<>(trackPointCursor.getCount());
for (int i = 0; i < trackPointCursor.getCount(); i++) {
trackPoints.add(createTrackPoint(trackPointCursor));
trackPointCursor.moveToNext();
}
}
}
return trackPoints;
}
int getDefaultCursorBatchSize() {
return defaultCursorBatchSize;
}
/**
* Sets the default cursor batch size. For testing purpose.
*
* @param defaultCursorBatchSize the default cursor batch size
*/
@VisibleForTesting
void setDefaultCursorBatchSize(int defaultCursorBatchSize) {
this.defaultCursorBatchSize = defaultCursorBatchSize;
}
public static String formatIdListForUri(Track.Id... trackIds) {
long[] ids = new long[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
@@ -835,4 +787,21 @@ public class ContentProviderUtils {
public static String[] parseTrackIdsFromUri(Uri url) {
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
}
public SensorStatistics getSensorStats(@NonNull Track.Id trackId) {
SensorStatistics sensorStatistics = null;
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.getId()), null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
sensorStatistics = new SensorStatistics(
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_HR)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_HR)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_CADENCE)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_CADENCE)) : null,
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_POWER)) : null
);
}
}
return sensorStatistics;
}
}
@@ -36,6 +36,7 @@ import androidx.annotation.VisibleForTesting;
import java.util.Arrays;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
@@ -56,6 +57,38 @@ public class CustomContentProvider extends ContentProvider {
private SQLiteDatabase db;
/**
* The string representing the query that compute sensor stats from trackpoints table.
* It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence.
* Finally, it ignores manual pause (SEGMENT_START_MANUAL).
*/
private final String SENSOR_STATS_QUERY =
"WITH time_select as " +
"(SELECT t1." + TrackPointsColumns.TIME + " * (t1." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")) time_value " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t1 " +
"WHERE t1." + TrackPointsColumns._ID + " > t." + TrackPointsColumns._ID + " AND t1." + TrackPointsColumns.TRACKID + " = ? ORDER BY _id LIMIT 1) " +
"SELECT " +
"SUM(t." + TrackPointsColumns.SENSOR_HEARTRATE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_HR + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_HEARTRATE + ") " + TrackPointsColumns.ALIAS_MAX_HR + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_CADENCE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_CADENCE + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_CADENCE + ") " + TrackPointsColumns.ALIAS_MAX_CADENCE + ", " +
"SUM(t." + TrackPointsColumns.SENSOR_POWER + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + " " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
@@ -63,6 +96,7 @@ public class CustomContentProvider extends ContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
@@ -223,6 +257,9 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
case TRACKS_SENSOR_STATS:
long trackId = ContentUris.parseId(url);
return db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)});
case MARKERS:
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
@@ -363,6 +400,7 @@ public class CustomContentProvider extends ContentProvider {
TRACKPOINTS_BY_TRACKID,
TRACKS,
TRACKS_BY_ID,
TRACKS_SENSOR_STATS,
MARKERS,
MARKERS_BY_ID,
MARKERS_BY_TRACKID
@@ -25,7 +25,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 30;
private static final int DATABASE_VERSION = 31;
@VisibleForTesting
public static final String DATABASE_NAME = "database.db";
@@ -82,6 +82,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 30:
upgradeFrom29to30(db);
break;
case 31:
upgradeFrom30to31(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -115,7 +118,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 29:
downgradeFrom30to29(db);
break;
case 30:
downgradeFrom31to30(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
@@ -394,4 +399,32 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.setTransactionSuccessful();
db.endTransaction();
}
/**
* Add distance column to TrackPoint.
*/
private void upgradeFrom30to31(SQLiteDatabase db) {
db.beginTransaction();
// TrackPoints
db.execSQL("ALTER TABLE trackpoints ADD COLUMN sensor_distance FLOAT");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom31to30(SQLiteDatabase db) {
db.beginTransaction();
// TrackPoints; identical to upgradeFrom27to28()
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1)), FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
}
@@ -1,7 +1,9 @@
package de.dennisguse.opentracks.content.provider;
import android.database.Cursor;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.Iterator;
import java.util.NoSuchElementException;
@@ -10,9 +12,8 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
* A lightweight wrapper around the original {@link Cursor}.
*/
//TODO Remove batching; that should be handled by the database/contentprovider (i.e., already in place as we use a cursor)!
public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private static final String TAG = TrackPointIterator.class.getSimpleName();
@@ -20,7 +21,6 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private final ContentProviderUtils contentProviderUtils;
private final Track.Id trackId;
private final CachedTrackPointsIndexes indexes;
private TrackPoint.Id lastTrackPointId = null;
private Cursor cursor;
public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) {
@@ -28,28 +28,11 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
this.trackId = trackId;
cursor = getCursor(startTrackPointId);
indexes = cursor != null ? new CachedTrackPointsIndexes(cursor)
: null;
indexes = new CachedTrackPointsIndexes(cursor);
}
/**
* Gets the track point cursor.
*
* @param trackPointId the starting track point id
*/
private Cursor getCursor(TrackPoint.Id trackPointId) {
return contentProviderUtils.getTrackPointCursor(trackId, trackPointId, contentProviderUtils.getDefaultCursorBatchSize());
}
/**
* Advances the cursor to the next batch. Returns true if successful.
*/
private boolean advanceCursorToNextBatch() {
TrackPoint.Id trackPointId = lastTrackPointId == null ? null : new TrackPoint.Id(lastTrackPointId.getId() + 1);
Log.d(TAG, "Advancing track point id: " + trackPointId);
cursor.close();
cursor = getCursor(trackPointId);
return cursor != null;
return contentProviderUtils.getTrackPointCursor(trackId, trackPointId);
}
@Override
@@ -57,32 +40,23 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
if (cursor == null) {
return false;
}
if (cursor.isAfterLast()) {
return false;
}
if (cursor.isLast()) {
if (cursor.getCount() != contentProviderUtils.getDefaultCursorBatchSize()) {
return false;
}
return advanceCursorToNextBatch() && !cursor.isAfterLast();
}
return true;
return !cursor.isLast() && !cursor.isAfterLast();
}
@Override
@NonNull
public TrackPoint next() {
if (cursor == null) {
if (cursor == null || !cursor.moveToNext()) {
throw new NoSuchElementException();
}
if (!cursor.moveToNext()) {
if (!advanceCursorToNextBatch() || !cursor.moveToNext()) {
throw new NoSuchElementException();
}
}
lastTrackPointId = new TrackPoint.Id(cursor.getLong(indexes.idIndex));
return ContentProviderUtils.fillTrackPoint(cursor, indexes);
}
@VisibleForTesting
public int getCount() {
return cursor.getCount();
}
@Override
public void close() {
if (cursor != null) {
@@ -8,12 +8,14 @@ import java.time.Instant;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
public class SensorData {
public abstract class SensorData<T> {
protected T value;
private final String sensorAddress;
private final String sensorName;
private final Instant timestamp_ms;
private final Instant time;
SensorData(String sensorAddress) {
this(sensorAddress, null);
@@ -24,10 +26,10 @@ public class SensorData {
}
@VisibleForTesting
SensorData(String sensorAddress, String sensorName, Instant timestamp_ms) {
SensorData(String sensorAddress, String sensorName, Instant time) {
this.sensorAddress = sensorAddress;
this.sensorName = sensorName;
this.timestamp_ms = timestamp_ms;
this.time = time;
}
@NonNull
@@ -44,7 +46,31 @@ public class SensorData {
return sensorName != null ? sensorName : sensorAddress;
}
public boolean hasValue() {
return value != null;
}
public T getValue() {
return value;
}
/**
* Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance.
*/
public void reset() {
}
/**
* Is the data recent considering the current time.
*/
public boolean isRecent() {
return Instant.now().isBefore(timestamp_ms.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
return Instant.now()
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
}
@NonNull
@Override
public String toString() {
return "sensorAddress='" + sensorAddress;
}
}
@@ -4,6 +4,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import de.dennisguse.opentracks.util.UintUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -20,11 +21,10 @@ public final class SensorDataCycling {
private SensorDataCycling() {
}
public static class Cadence extends SensorData {
public static class Cadence extends SensorData<Float> {
private final Long crankRevolutionsCount; // UINT32
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
private Float cadence_rpm;
public Cadence(String sensorAddress) {
super(sensorAddress);
@@ -41,7 +41,7 @@ public final class SensorDataCycling {
/**
* Workaround for Wahoo CADENCE: provides speed instead of cadence
*/
public Cadence(@NonNull SensorDataCycling.Speed speed) {
public Cadence(@NonNull DistanceSpeed speed) {
this(speed.getSensorAddress(), speed.getSensorName(), speed.wheelRevolutionsCount, speed.wheelRevolutionsTime);
}
@@ -57,24 +57,16 @@ public final class SensorDataCycling {
return crankRevolutionsTime;
}
public boolean hasCadence_rpm() {
return cadence_rpm != null;
}
public float getCadence_rpm() {
return cadence_rpm;
}
public void compute(Cadence previous) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
cadence_rpm = null;
value = null;
} else {
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
float cadence_ms = crankDiff / timeDiff_ms;
cadence_rpm = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
value = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
}
}
}
@@ -82,7 +74,7 @@ public final class SensorDataCycling {
@NonNull
@Override
public String toString() {
return "cadence=" + cadence_rpm + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
return super.toString() + " cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
}
@Override
@@ -98,19 +90,18 @@ public final class SensorDataCycling {
}
}
public static class Speed extends SensorData {
public static class DistanceSpeed extends SensorData<DistanceSpeed.Data> {
private final Integer wheelRevolutionsCount; // UINT16
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
private Float speed_mps;
public Speed(String sensorAddress) {
public DistanceSpeed(String sensorAddress) {
super(sensorAddress);
this.wheelRevolutionsCount = null;
this.wheelRevolutionsTime = null;
}
public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
public DistanceSpeed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
super(sensorAddress, sensorName);
this.wheelRevolutionsCount = wheelRevolutionsCount;
this.wheelRevolutionsTime = wheelRevolutionsTime;
@@ -128,64 +119,88 @@ public final class SensorDataCycling {
return wheelRevolutionsTime;
}
public boolean hasSpeed_mps() {
return speed_mps != null;
}
public float getSpeed_mps() {
return speed_mps;
}
public void compute(Speed previous, int wheel_circumference_mm) {
public void compute(DistanceSpeed previous, int wheel_circumference_mm) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
speed_mps = null;
value = null;
} else {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as it some of those seem to count backwards
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
speed_mps = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
float distance_m = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
float distance_overall_m = distance_m;
if (previous.hasValue()) {
distance_overall_m += previous.getValue().distance_overall_m;
}
float speed_mps = (float) (distance_m / timeDiff_s);
value = new Data(distance_m, distance_overall_m, speed_mps);
}
}
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.distance_m, 0, value.speed_mps);
}
}
@NonNull
@Override
public String toString() {
return "speed=" + speed_mps + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
return super.toString() + " data=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Speed)) return false;
if (!(obj instanceof DistanceSpeed)) return false;
Speed comp = (Speed) obj;
DistanceSpeed comp = (DistanceSpeed) obj;
if (hasData() && comp.hasData() == hasData()) {
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
} else {
return false;
}
}
public static class Data {
public final float distance_m;
public final float distance_overall_m;
public final float speed_mps;
private Data(float distance_m, float distance_overall_m, float speed_mps) {
this.distance_m = distance_m;
this.distance_overall_m = distance_overall_m;
this.speed_mps = speed_mps;
}
@Override
public String toString() {
return "Data{" +
"distance_m=" + distance_m +
", distance_overall_m=" + distance_overall_m +
", speed_mps=" + speed_mps +
'}';
}
}
}
public static class CadenceAndSpeed extends SensorData {
public static class CadenceAndSpeed extends SensorData<Pair<Cadence, DistanceSpeed>> {
private final Cadence cadence;
private final Speed speed;
public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) {
public CadenceAndSpeed(String sensorAddress, String sensorName, @Nullable Cadence cadence, @Nullable DistanceSpeed distanceSpeed) {
super(sensorAddress, sensorName);
this.cadence = cadence;
this.speed = speed;
this.value = new Pair<>(cadence, distanceSpeed);
}
public Cadence getCadence() {
return cadence;
return this.value != null ? this.value.first : null;
}
public Speed getSpeed() {
return speed;
public DistanceSpeed getDistanceSpeed() {
return this.value != null ? this.value.second : null;
}
}
}
@@ -2,31 +2,20 @@ package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
public class SensorDataCyclingPower extends SensorData {
private final Float power_w;
public class SensorDataCyclingPower extends SensorData<Float> {
public SensorDataCyclingPower(String address) {
super(address);
this.power_w = null;
}
public SensorDataCyclingPower(String name, String address, float power_w) {
super(name, address);
this.power_w = power_w;
}
public boolean hasPower_w() {
return power_w != null;
}
public float getPower_w() {
return power_w;
this.value = power_w;
}
@NonNull
@Override
public String toString() {
return "power=" + power_w;
return super.toString() + " power=" + value;
}
}
@@ -2,31 +2,20 @@ package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
public class SensorDataHeartRate extends SensorData {
private final Float heartRate_bpm;
public class SensorDataHeartRate extends SensorData<Float> {
public SensorDataHeartRate(String address) {
super(address);
heartRate_bpm = null;
}
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
super(name, address);
this.heartRate_bpm = heartRate_bpm;
}
public boolean hasHeartRate_bpm() {
return heartRate_bpm != null;
}
public Float getHeartRate_bpm() {
return heartRate_bpm;
this.value = heartRate_bpm;
}
@NonNull
@Override
public String toString() {
return "heart=" + heartRate_bpm;
return super.toString() + " heart=" + value;
}
}
@@ -10,7 +10,7 @@ public final class SensorDataSet {
private SensorDataCycling.Cadence cyclingCadence;
private SensorDataCycling.Speed cyclingSpeed;
private SensorDataCycling.DistanceSpeed cyclingDistanceSpeed;
private SensorDataCyclingPower cyclingPower;
@@ -25,57 +25,65 @@ public final class SensorDataSet {
return cyclingCadence;
}
public SensorDataCycling.Speed getCyclingSpeed() {
return cyclingSpeed;
public SensorDataCycling.DistanceSpeed getCyclingDistanceSpeed() {
return cyclingDistanceSpeed;
}
public SensorDataCyclingPower getCyclingPower() {
return cyclingPower;
}
public void set(SensorData data) {
public void set(SensorData<?> data) {
set(data, data);
}
public void remove(SensorData type) {
public void remove(SensorData<?> type) {
set(type, null);
}
public void clear() {
this.heartRate = null;
this.cyclingCadence = null;
this.cyclingSpeed = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
}
public void fillTrackPoint(TrackPoint trackPoint) {
if (heartRate != null) {
trackPoint.setHeartRate_bpm(heartRate.getHeartRate_bpm());
trackPoint.setHeartRate_bpm(heartRate.getValue());
}
if (cyclingCadence != null && cyclingCadence.hasCadence_rpm()) {
trackPoint.setCyclingCadence_rpm(cyclingCadence.getCadence_rpm());
if (cyclingCadence != null && cyclingCadence.hasValue()) {
trackPoint.setCyclingCadence_rpm(cyclingCadence.getValue());
}
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distance_overall_m);
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed_mps);
}
if (cyclingPower != null && cyclingPower.hasPower_w()) {
trackPoint.setPower(cyclingPower.getPower_w());
if (cyclingPower != null && cyclingPower.hasValue()) {
trackPoint.setPower(cyclingPower.getValue());
}
}
public void reset() {
if (heartRate != null) heartRate.reset();
if (cyclingCadence != null) cyclingCadence.reset();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset();
if (cyclingPower != null) cyclingPower.reset();
}
@NonNull
@Override
public String toString() {
return (getHeartRate() != null ? "" + getHeartRate() : "")
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "")
+ (getCyclingDistanceSpeed() != null ? " " + getCyclingDistanceSpeed() : "")
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
}
private void set(@NonNull SensorData type, SensorData data) {
private void set(@NonNull SensorData<?> type, SensorData<?> data) {
if (type instanceof SensorDataHeartRate) {
this.heartRate = (SensorDataHeartRate) data;
return;
@@ -85,8 +93,8 @@ public final class SensorDataSet {
this.cyclingCadence = (SensorDataCycling.Cadence) data;
return;
}
if (type instanceof SensorDataCycling.Speed) {
this.cyclingSpeed = (SensorDataCycling.Speed) data;
if (type instanceof SensorDataCycling.DistanceSpeed) {
this.cyclingDistanceSpeed = (SensorDataCycling.DistanceSpeed) data;
return;
}
@@ -65,31 +65,28 @@ public class ChartFragment extends Fragment implements TrackDataListener {
return chartFragment;
}
private final List<ChartPoint> pendingPoints = new ArrayList<>();
private SharedPreferences sharedPreferences;
private int recordingDistanceInterval;
private TrackDataHub trackDataHub;
// Stats gathered from the received data
private TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
private ChartBinding viewBinding;
//TODO REMOVE: Used to restore the TrackStatistics via TrackStatisticsUpdater; for the diagrams we should consider not this setting and just use all data that is in the database
@Deprecated
private int recordingDistanceInterval;
private final List<ChartPoint> pendingPoints = new ArrayList<>();
private String category = "";
// Modes of operation
private boolean chartByDistance;
private ChartBinding viewBinding;
// UI elements
private ChartView chartView;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
if (metricUnits != chartView.getMetricUnits()) {
chartView.setMetricUnits(metricUnits);
runOnUiThread(() -> {
@@ -100,7 +97,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
if (reportSpeed != chartView.getReportSpeed()) {
chartView.setReportSpeed(reportSpeed);
chartView.applyReportSpeed();
@@ -114,7 +111,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
if (PreferencesUtils.isKey(getContext(), R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(getContext());
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, getContext());
}
}
};
@@ -165,8 +162,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void onResume() {
super.onResume();
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
checkChartSettings();
getActivity().runOnUiThread(updateChart);
@@ -176,7 +174,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void onPause() {
super.onPause();
pauseTrackDataHub();
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
@@ -192,6 +190,12 @@ public class ChartFragment extends Fragment implements TrackDataListener {
viewBinding = null;
}
@Override
public void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
@Override
public void onTrackUpdated(Track track) {
if (isResumed()) {
@@ -201,7 +205,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
category = track.getCategory();
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
boolean reportSpeed = PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category);
if (reportSpeed != chartView.getReportSpeed()) {
chartView.setReportSpeed(reportSpeed);
chartView.applyReportSpeed();
@@ -224,14 +228,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
pendingPoints.add(createPendingPoint(trackPoint));
}
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
if (trackStatisticsUpdater != null) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
@@ -240,7 +244,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
if (isResumed()) {
chartView.addChartPoints(pendingPoints);
pendingPoints.clear();
@@ -44,14 +44,16 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
private IntervalStatisticsAdapter adapter;
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter;
private SharedPreferences sharedPreferences;
private TrackDataHub trackDataHub;
private String category;
private IntervalListViewBinding viewBinding;
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(getContext());
metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
if (adapter != null) {
adapter.notifyDataSetChanged();
spinnerAdapter.notifyDataSetChanged();
@@ -73,8 +75,9 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
metricUnits = PreferencesUtils.isMetricUnits(getContext());
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
@@ -125,6 +128,8 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
public void onPause() {
super.onPause();
pauseTrackDataHub();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
@@ -137,7 +142,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
public void onDestroy() {
super.onDestroy();
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
sharedPreferences = null;
adapter = null;
viewModel = null;
@@ -183,7 +188,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
category = track != null ? track.getCategory() : "";
// Set rate label.
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category); //TODO Handle sharedPreferenceChangeListener
viewBinding.intervalRate.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
}
});
@@ -198,21 +203,21 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
viewModel.add(trackPoint);
}
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
viewModel.add(trackPoint);
}
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
if (isResumed()) {
runOnUiThread(this::loadIntervals);
}
@@ -27,15 +27,24 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.adapters.SensorsAdapter;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
/**
* A fragment to display track statistics to the user for a recorded {@link Track}.
@@ -43,36 +52,10 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
public class StatisticsRecordedFragment extends Fragment {
private static final String TRACK_ID_KEY = "trackId";
private TrackStatistics trackStatistics;
private String category = "";
private Track.Id trackId;
private ContentProviderUtils contentProviderUtils;
private StatisticsRecordedBinding viewBinding;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
Bundle bundle = new Bundle();
bundle.putParcelable(TRACK_ID_KEY, trackId);
@@ -82,18 +65,70 @@ public class StatisticsRecordedFragment extends Fragment {
return fragment;
}
private TrackStatistics trackStatistics;
private SensorStatistics sensorStatistics;
private SensorsAdapter sensorsAdapter;
private String category = "";
private Track.Id trackId;
private ContentProviderUtils contentProviderUtils;
private StatisticsRecordedBinding viewBinding;
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
}
if (key != null && updateUInecessary && isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trackId = getArguments().getParcelable(TRACK_ID_KEY);
contentProviderUtils = new ContentProviderUtils(getContext());
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sensorsAdapter = new SensorsAdapter(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
loadStatistics();
}
@@ -103,7 +138,13 @@ public class StatisticsRecordedFragment extends Fragment {
super.onDestroyView();
viewBinding = null;
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
public void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
public void loadStatistics() {
@@ -111,22 +152,36 @@ public class StatisticsRecordedFragment extends Fragment {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
Track track = contentProviderUtils.getTrack(trackId);
trackStatistics = track != null ? track.getTrackStatistics() : null;
category = track != null ? track.getCategory() : "";
trackStatistics = track.getTrackStatistics();
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
String newCategory = track.getCategory();
if (!category.equals(newCategory)) {
category = newCategory;
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
loadTrackDescription(track);
updateUI();
updateSensorUI();
((TrackRecordedActivity) getActivity()).startPostponedEnterTransitionWith(viewBinding.statsActivityTypeIcon, viewBinding.statsNameValue);
}
});
}
}
private void updateUI() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
private void loadTrackDescription(@NonNull Track track) {
viewBinding.statsNameValue.setText(track.getName());
viewBinding.statsDescriptionValue.setText(track.getDescription());
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTime(getContext(), trackStatistics.getStartTime()));
}
private void updateUI() {
// Set total distance
{
double totalDistance = trackStatistics == null ? Double.NaN : trackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
double totalDistance = trackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -139,39 +194,39 @@ public class StatisticsRecordedFragment extends Fragment {
}
// Set time and start datetime
if (trackStatistics != null) {
{
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getMovingTime()));
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
}
// Set average speed/pace
{
double speed = trackStatistics != null ? trackStatistics.getAverageSpeed() : Double.NaN;
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
double speed = trackStatistics.getAverageSpeed();
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
// Set max speed/pace
{
double speed = trackStatistics == null ? Double.NaN : trackStatistics.getMaxSpeed();
double speed = trackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
// Set moving speed/pace
{
double speed = trackStatistics != null ? trackStatistics.getAverageMovingSpeed() : Double.NaN;
double speed = trackStatistics.getAverageMovingSpeed();
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
@@ -179,21 +234,43 @@ public class StatisticsRecordedFragment extends Fragment {
// Set elevation gain and loss
{
// Make elevation visible?
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
boolean showElevation = PreferencesUtils.isShowStatsElevation(sharedPreferences, getContext());
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
Float elevationGain_m = trackStatistics != null ? trackStatistics.getTotalElevationGain() : null;
Float elevationLoss_m = trackStatistics != null ? trackStatistics.getTotalElevationLoss() : null;
Float elevationGain_m = trackStatistics.getTotalElevationGain();
Float elevationLoss_m = trackStatistics.getTotalElevationLoss();
Pair<String, String> parts;
parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits);
parts = StringUtils.formatElevation(getContext(), elevationGain_m, preferenceMetricUnits);
viewBinding.statsElevationGainValue.setText(parts.first);
viewBinding.statsElevationGainUnit.setText(parts.second);
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, metricUnits);
parts = StringUtils.formatElevation(getContext(), elevationLoss_m, preferenceMetricUnits);
viewBinding.statsElevationLossValue.setText(parts.first);
viewBinding.statsElevationLossUnit.setText(parts.second);
}
}
private void updateSensorUI() {
if (sensorStatistics == null) {
return;
}
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorStatistics.hasHeartRate()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_max, R.string.sensor_unit_beats_per_minute, sensorStatistics.getMaxHeartRate()));
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_avg, R.string.sensor_unit_beats_per_minute, sensorStatistics.getAvgHeartRate()));
}
if (sensorStatistics.hasCadence()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_max, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getMaxCadence()));
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_avg, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getAvgCadence()));
}
if (sensorStatistics.hasPower()) {
sensorDataList.add(new SensorDataModel(R.string.sensor_state_power_avg, R.string.sensor_unit_power, sensorStatistics.getAvgPower()));
}
if (sensorDataList.size() > 0) {
sensorsAdapter.swapData(sensorDataList);
}
}
}
@@ -10,6 +10,7 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -26,18 +27,17 @@ import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
/**
* A fragment to display track statistics to the user for a currently recording {@link Track}.
@@ -45,54 +45,79 @@ import de.dennisguse.opentracks.util.UnitConversions;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
//TODO isRecording should not be relevant anymore as we now have StatisticRecordedFragment.
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
public class StatisticsRecordingFragment extends Fragment implements TrackDataListener {
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND_MS;
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private TrackDataHub trackDataHub;
private Handler handlerUpdateUI;
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private TrackPoint lastTrackPoint;
//TODO Should not be nullable
private TrackStatistics lastTrackStatistics;
private String category = "";
private StatisticsRecordingBinding viewBinding;
private SensorsAdapter sensorsAdapter;
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
private SharedPreferences sharedPreferences;
private boolean preferenceMetricUnits;
private boolean preferenceReportSpeed;
private boolean preferenceShowElevation;
private boolean preferenceShowCoordinate;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
boolean updateUInecessary = false;
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
updateUInecessary = true;
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
updateUInecessary = true;
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_elevation_key, key)) {
updateUInecessary = true;
preferenceShowElevation = PreferencesUtils.isShowStatsElevation(sharedPreferences, getContext());
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_coordinate_key, key)) {
updateUInecessary = true;
preferenceShowCoordinate = PreferencesUtils.isStatsShowCoordinate(sharedPreferences, getContext());
}
if (key != null && updateUInecessary && isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTrackStatistics != null) {
synchronized (this) {
if (lastTrackPoint != null && lastTrackPoint.hasLocation() && !lastTrackPoint.isRecent()) {
lastTrackPoint = null;
setLocationValues();
}
}
updateTotalTime();
updateSensorDataUI();
}
@@ -103,27 +128,38 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
};
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handlerUpdateUI = new Handler();
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sensorsAdapter = new SensorsAdapter(getContext());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection.startConnection(getContext());
updateUIeachSecond.run();
handlerUpdateUI.post(updateUIeachSecond);
}
@@ -131,7 +167,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
public void onPause() {
super.onPause();
pauseTrackDataHub();
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
}
@@ -152,6 +189,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection = null;
sharedPreferences = null;
}
@Override
@@ -160,7 +198,11 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
getActivity().runOnUiThread(() -> {
if (isResumed()) {
lastTrackStatistics = track != null ? track.getTrackStatistics() : null;
category = track != null ? track.getCategory() : "";
String newCategory = track != null ? track.getCategory() : "";
if (!category.equals(newCategory)) {
category = newCategory;
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
updateUI();
}
});
@@ -173,33 +215,32 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
@Override
public void onSampledInTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
// We don't care.
}
@Override
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
public void onNewTrackPointsDone(@NonNull TrackPoint newLastTrackPoint) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastTrackPoint = null;
}
synchronized (this) {
this.lastTrackPoint = newLastTrackPoint;
TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now.
if (trackPoint != null && trackPoint.hasLocation()) {
boolean hasFix = !LocationUtils.isTrackPointOld(trackPoint);
if (!hasFix) {
lastTrackPoint = null;
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
this.lastTrackPoint = null;
}
if (this.lastTrackPoint != null && this.lastTrackPoint.hasLocation() && !this.lastTrackPoint.isRecent()) {
this.lastTrackPoint = null;
}
setLocationValues();
}
setLocationValues();
}
});
}
@@ -272,15 +313,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
} else {
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
if (sensorDataSet != null) {
List<Pair<Integer, SensorData>> sensorDataList = new ArrayList<>();
List<SensorDataModel> sensorDataList = new ArrayList<>();
if (sensorDataSet.getHeartRate() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.HEART_RATE_TYPE, sensorDataSet.getHeartRate()));
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
}
if (sensorDataSet.getCyclingCadence() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.CADENCE_TYPE, sensorDataSet.getCyclingCadence()));
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
}
if(sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.POWER_TYPE, sensorDataSet.getCyclingPower()));
if (sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
}
sensorsAdapter.swapData(sensorDataList);
setSpeedSensorData(sensorDataSet);
@@ -293,8 +334,6 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set elevation gain
private void setTotalElevationGain(Float elevationGain_m) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
Float totalElevationGain = elevationGain_m;
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationGain()) {
@@ -305,15 +344,13 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, preferenceMetricUnits);
viewBinding.statsElevationGainValue.setText(parts.first);
viewBinding.statsElevationGainUnit.setText(parts.second);
}
// Set elevation loss
private void setTotalElevationLoss(Float elevationLoss_m) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
Float totalElevationLoss = elevationLoss_m;
if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationLoss()) {
@@ -324,40 +361,33 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationLoss, preferenceMetricUnits);
viewBinding.statsElevationLossValue.setText(parts.first);
viewBinding.statsElevationLossUnit.setText(parts.second);
}
private void setSpeedSensorData(SensorDataSet sensorDataSet) {
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null) {
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
if (data.hasSpeed_mps() && data.isRecent()) {
setSpeed(data.getSpeed_mps());
if (data.hasValue() && data.isRecent()) {
setTotalDistance(data.getValue().distance_overall_m);
setSpeed(data.getValue().speed_mps);
}
if (data.hasValue() && data.isRecent()) {
setSpeed(data.getValue().speed_mps);
}
}
}
private void updateStats() {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
boolean isRecording = isSelectedTrackRecording();
// Set total distance
{
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
setTotalDistance(0);
// Set activity type
{
viewBinding.statsActivityTypeIcon.setEnabled(isRecording);
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
}
@@ -370,9 +400,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set average speed/pace
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
@@ -381,9 +411,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
{
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
@@ -392,18 +422,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
// Set elevation gain and loss
{
// Make elevation visible?
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
}
}
@@ -420,46 +448,47 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
private void setLocationValues() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
// Set speed/pace
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
setSpeed(speed);
// Set elevation
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
viewBinding.statsElevationGroup.setVisibility(preferenceShowElevation ? View.VISIBLE : View.GONE);
if (showElevation) {
if (preferenceShowElevation) {
// Current elevation
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsElevationCurrentValue.setText(parts.first);
viewBinding.statsElevationCurrentUnit.setText(parts.second);
}
// Set coordinate
boolean showCoordinate = PreferencesUtils.isStatsShowCoordinate(getContext());
viewBinding.statsCoordinateGroup.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
if (showCoordinate) {
double latitude = lastTrackPoint != null ? lastTrackPoint.getLatitude() : Double.NaN;
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
viewBinding.statsCoordinateGroup.setVisibility(preferenceShowCoordinate ? View.VISIBLE : View.GONE);
if (preferenceShowCoordinate) {
String latitudeText = getContext().getString(R.string.value_unknown);
String longitudeText = getContext().getString(R.string.value_unknown);
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLongitude());
}
viewBinding.statsLatitudeValue.setText(latitudeText);
double longitude = lastTrackPoint != null ? lastTrackPoint.getLongitude() : Double.NaN;
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
viewBinding.statsLongitudeValue.setText(longitudeText);
}
}
private void setTotalDistance(double sensorDistanceSinceLastTrackpoint) {
double totalDistance = lastTrackStatistics != null ? (lastTrackStatistics.getTotalDistance() + sensorDistanceSinceLastTrackpoint) : Double.NaN;
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
private void setSpeed(double speed) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
viewBinding.statsSpeedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsSpeedValue.setText(parts.first);
viewBinding.statsSpeedUnit.setText(parts.second);
}
@@ -91,7 +91,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private ContentProviderUtils contentProviderUtils;
// List of tracks to be exported.
private final ArrayList<Track.Id> trackIds = new ArrayList<>();
private final ArrayList<Track> tracks = new ArrayList<>();
private final LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>();
private final Handler conflictsHandler = new Handler();
@@ -187,7 +187,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
protected void onDestroy() {
super.onDestroy();
conflictsQueue.clear();
trackIds.clear();
tracks.clear();
}
@Override
@@ -215,11 +215,11 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
for (int i = from; i < trackExportTotalCount; i++) {
cursor.moveToPosition(i);
Track track = ContentProviderUtils.createTrack(cursor);
trackIds.add(track.getId());
tracks.add(track);
}
if (!trackIds.isEmpty()) {
export(trackIds.get(0));
if (!tracks.isEmpty()) {
export(tracks.get(0));
} else {
onExportEnded();
}
@@ -227,26 +227,26 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
}
/**
* Enqueue track identified by trackId 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 trackId the track's id.
* @param track Track object.
* @param conflictResolution conflict resolution to be applied if needed.
*/
private void export(Track.Id trackId, int conflictResolution) {
boolean fileExists = ExportUtils.isExportFileExists(trackId, trackFileFormat.getExtension(), directoryFiles);
private void export(Track track, int conflictResolution) {
boolean fileExists = ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles);
if (fileExists && conflictResolution == CONFLICT_NONE) {
conflict(trackId);
conflict(track);
} else if (fileExists && conflictResolution == CONFLICT_SKIP) {
trackExportSkippedCount++;
onExportCompleted(trackId);
onExportCompleted(track);
} else {
ExportService.enqueue(this, resultReceiver, trackId, trackFileFormat, directoryUri);
ExportService.enqueue(this, resultReceiver, track.getId(), trackFileFormat, directoryUri);
}
}
private void export(Track.Id trackId) {
export(trackId, autoConflict);
private void export(Track track) {
export(track, autoConflict);
}
private void setConflictVisibility(int visibility) {
@@ -278,14 +278,15 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
viewBinding.exportProgressSummaryErrorsGroup.setVisibility(trackExportErrorCount > 0 ? View.VISIBLE : View.GONE);
}
private void onExportCompleted(Track.Id trackId) {
trackIds.remove(trackId);
private void onExportCompleted(Track track) {
tracks.remove(track);
setProgress();
if (trackIds.isEmpty()) {
if (tracks.isEmpty()) {
onExportEnded();
return;
}
export(trackIds.get(0));
export(tracks.get(0));
}
private void onExportEnded() {
@@ -317,15 +318,15 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
}
Track.Id trackId = resultData.getParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID);
Track track = contentProviderUtils.getTrack(trackId);
switch (resultCode) {
case ExportServiceResultReceiver.RESULT_CODE_ERROR:
trackExportErrorCount++;
Track track = contentProviderUtils.getTrack(trackId);
trackErrors.add(track.getName());
break;
case ExportServiceResultReceiver.RESULT_CODE_SUCCESS:
if (ExportUtils.isExportFileExists(trackId, trackFileFormat.getExtension(), directoryFiles)) {
if (ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles)) {
trackExportOverwrittenCount++;
} else {
trackExportSuccessCount++;
@@ -335,11 +336,11 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
throw new RuntimeException(TAG + ": export service result code invalid: " + resultCode);
}
onExportCompleted(trackId);
onExportCompleted(track);
}
private void conflict(Track.Id trackId) {
PendingConflict newConflict = new PendingConflict(trackId);
private void conflict(Track track) {
PendingConflict newConflict = new PendingConflict(track);
conflictsQueue.add(newConflict);
if (conflictsQueue.size() == 1) {
@@ -351,10 +352,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* Handle conflicts (exporting file already exists).
*/
private class PendingConflict {
private final Track.Id trackId;
private final Track track;
public PendingConflict(Track.Id trackId) {
this.trackId = trackId;
public PendingConflict(Track track) {
this.track = track;
}
/**
@@ -365,14 +366,13 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
*/
public boolean resolve() {
if (autoConflict == CONFLICT_NONE) {
Track track = contentProviderUtils.getTrack(trackId);
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()));
setConflictVisibility(View.VISIBLE);
return false;
}
export(trackId);
export(track);
return true;
}
@@ -380,7 +380,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* Overwrite the export file and set the autoConflict if user set the "do it for all" switch button.
*/
public void overwrite() {
export(trackId, CONFLICT_OVERWRITE);
export(track, CONFLICT_OVERWRITE);
if (viewBinding.exportProgressApplyToAll.isChecked()) {
autoConflict = CONFLICT_OVERWRITE;
@@ -391,7 +391,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* Skip the export file and set the autoConflict if user set the "do it for all" switch button.
*/
public void skip() {
export(trackId, CONFLICT_SKIP);
export(track, CONFLICT_SKIP);
if (viewBinding.exportProgressApplyToAll.isChecked()) {
autoConflict = CONFLICT_SKIP;
@@ -34,9 +34,10 @@ import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML.
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to GPX.
* NOTE:
* * does not export {@link TrackPoint} without a latitude/longitude (not supported by GPX 1.1).
* * cannot export multiple {@link Track}s - all {@link TrackPoint}s are exported as if they would belong to the first track.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
@@ -48,8 +49,10 @@ public class GPXTrackExporter implements TrackExporter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat POWER_FORMAT = NumberFormat.getInstance(Locale.US);
static {
/*
@@ -71,6 +74,9 @@ public class GPXTrackExporter implements TrackExporter {
CADENCE_FORMAT.setMaximumFractionDigits(0);
CADENCE_FORMAT.setGroupingUsed(false);
POWER_FORMAT.setMaximumFractionDigits(0);
POWER_FORMAT.setGroupingUsed(false);
}
private final ContentProviderUtils contentProviderUtils;
@@ -85,13 +91,22 @@ public class GPXTrackExporter implements TrackExporter {
@Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
try {
prepare(outputStream);
writeHeader(track);
writeHeader();
writeMarkers(track);
for (Track track : tracks) {
writeMarkers(track);
}
writeLocations(track);
for (Track track : tracks) {
writeTrackPoints(track);
}
writeFooter();
close();
@@ -103,12 +118,7 @@ public class GPXTrackExporter implements TrackExporter {
}
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
throw new UnsupportedOperationException();
}
private void writeLocations(Track track) throws InterruptedException {
private void writeTrackPoints(Track track) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
@@ -119,7 +129,7 @@ public class GPXTrackExporter implements TrackExporter {
TrackPoint trackPoint = trackPointIterator.next();
if (!wroteTrack) {
writeBeginTrack(track, trackPoint);
writeBeginTrack(track);
wroteTrack = true;
}
@@ -158,14 +168,12 @@ public class GPXTrackExporter implements TrackExporter {
writeCloseSegment();
}
if (wroteTrack) {
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
writeEndTrack(track, lastValidTrackPoint);
} else {
if (!wroteTrack) {
// Write an empty track
writeBeginTrack(track, null);
writeEndTrack(track, null);
writeBeginTrack(track);
}
writeEndTrack();
}
}
@@ -181,7 +189,7 @@ public class GPXTrackExporter implements TrackExporter {
}
public void writeHeader(Track track) {
public void writeHeader() {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<gpx");
@@ -193,17 +201,13 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemes/TrackPointExtension/v2\"");
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
printWriter.println("xsi:schemaLocation=" +
"\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd"
+ " http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd"
+ " http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd"
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
printWriter.println("<metadata>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("</metadata>");
}
}
@@ -243,7 +247,7 @@ public class GPXTrackExporter implements TrackExporter {
}
}
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
public void writeBeginTrack(Track track) {
if (printWriter != null) {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
@@ -257,7 +261,7 @@ public class GPXTrackExporter implements TrackExporter {
}
}
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
public void writeEndTrack() {
if (printWriter != null) {
printWriter.println("</trk>");
}
@@ -297,6 +301,10 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCyclingCadence_rpm()) + "</gpxtpx:cad>");
}
if (trackPoint.hasPower()) {
printWriter.println("<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower()) + "</pwr:PowerInWatts>");
}
if (trackPoint.hasElevationGain()) {
printWriter.println("<opentracks:gain>" + ELEVATION_FORMAT.format(trackPoint.getElevationGain()) + "</opentracks:gain>");
}
@@ -305,6 +313,10 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<opentracks:loss>" + ELEVATION_FORMAT.format(trackPoint.getElevationLoss()) + "</opentracks:loss>");
}
if (trackPoint.hasSensorDistance()) {
printWriter.println("<opentracks:distance>" + DISTANCE_FORMAT.format(trackPoint.getElevationLoss()) + "</opentracks:distance>");
}
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
}
@@ -33,7 +33,6 @@ import java.util.List;
import java.util.Objects;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.DescriptionGenerator;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -54,12 +53,11 @@ public class KMLTrackExporter implements TrackExporter {
private static final String TAG = KMLTrackExporter.class.getSimpleName();
public static final String MARKER_STYLE = "waypoint";
private static final String START_STYLE = "start";
private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_HEART_RATE = "heart_rate";
public static final String EXTENDED_DATA_TYPE_POWER = "power";
@@ -67,25 +65,24 @@ public class KMLTrackExporter implements TrackExporter {
public static final String EXTENDED_DATA_TYPE_ELEVATION_LOSS = "elevation_loss";
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context;
private final boolean exportPhotos;
private final boolean exportTrackDetail;
private final boolean exportSensorData;
private final DescriptionGenerator descriptionGenerator;
private final ContentProviderUtils contentProviderUtils;
private PrintWriter printWriter;
private final List<Float> speedList = new ArrayList<>();
private final List<Float> distanceList = new ArrayList<>();
private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
private final List<Float> elevationGainList = new ArrayList<>();
private final List<Float> elevationLossList = new ArrayList<>();
@Deprecated // Figure out a better way to do this! (if needed)
private TrackPoint startTrackPoint;
public KMLTrackExporter(Context context, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) {
@@ -93,7 +90,6 @@ public class KMLTrackExporter implements TrackExporter {
this.exportTrackDetail = exportTrackDetail;
this.exportSensorData = exportSensorData;
this.exportPhotos = exportPhotos;
this.descriptionGenerator = new DescriptionGenerator(context);
this.contentProviderUtils = new ContentProviderUtils(context);
}
@@ -165,9 +161,12 @@ public class KMLTrackExporter implements TrackExporter {
TrackPoint trackPoint = trackPointIterator.next();
setLocationTime(trackPoint, offset);
if (startTrackPoint == null) {
startTrackPoint = trackPoint;
}
if (!wroteTrack) {
writeBeginTrack(track, trackPoint);
writeBeginTrack(track);
wroteTrack = true;
}
@@ -202,15 +201,14 @@ public class KMLTrackExporter implements TrackExporter {
writeCloseSegment();
}
if (wroteTrack) {
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
writeEndTrack(track, lastValidTrackPoint);
} else {
if (!wroteTrack) {
// Write an empty track
writeBeginTrack(track, null);
writeEndTrack(track, null);
writeBeginTrack(track);
}
writeEndTrack();
startTrackPoint = null;
}
}
@@ -247,8 +245,6 @@ public class KMLTrackExporter implements TrackExporter {
}
writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(MARKER_STYLE, MARKER_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
@@ -312,12 +308,8 @@ public class KMLTrackExporter implements TrackExporter {
}
}
private void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
this.startTrackPoint = startTrackPoint;
private void writeBeginTrack(Track track) {
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
Location location = startTrackPoint != null ? startTrackPoint.getLocation() : null;
writePlacemark(name, "", "", START_STYLE, location);
printWriter.println("<Placemark>");
if (exportTrackDetail) {
@@ -336,17 +328,10 @@ public class KMLTrackExporter implements TrackExporter {
}
private void writeEndTrack(Track track, TrackPoint endTrackPoint) {
private void writeEndTrack() {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
if (exportTrackDetail) {
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, false);
Location location = endTrackPoint != null ? endTrackPoint.getLocation() : null;
writePlacemark(name, "", description, END_STYLE, location);
}
}
}
@@ -355,6 +340,7 @@ public class KMLTrackExporter implements TrackExporter {
if (printWriter != null) {
printWriter.println("<gx:Track>");
speedList.clear();
distanceList.clear();
powerList.clear();
cadenceList.clear();
heartRateList.clear();
@@ -372,6 +358,9 @@ public class KMLTrackExporter implements TrackExporter {
writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED);
}
if (exportSensorData) {
if (distanceList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(distanceList, EXTENDED_DATA_TYPE_DISTANCE);
}
if (powerList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER);
}
@@ -409,6 +398,7 @@ public class KMLTrackExporter implements TrackExporter {
speedList.add(trackPoint.hasSpeed() ? trackPoint.getSpeed() : null);
if (exportSensorData) {
distanceList.add(trackPoint.hasSensorDistance() ? trackPoint.getSensorDistance() : null);
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null);
cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null);
@@ -17,10 +17,13 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteConstraintException;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
@@ -88,6 +91,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected String uuid;
protected String gain;
protected String loss;
protected String distance;
// The current track data
private TrackData trackData;
@@ -95,10 +99,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// The SAX locator to get the current line information
private Locator locator;
private final SharedPreferences sharedPreferences;
AbstractFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
this.context = context;
this.contentProviderUtils = contentProviderUtils;
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
}
@Override
@@ -118,11 +125,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
@Override
public Track.Id importFile(InputStream inputStream) {
@NonNull
public List<Track.Id> importFile(InputStream inputStream) {
try {
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, this);
return trackIds.get(0);
} catch (IOException | SAXException | ParserConfigurationException e) {
return trackIds;
} catch (IOException | SAXException | ParserConfigurationException | ParsingException e) {
Log.e(TAG, "Unable to import file", e);
if (trackIds.size() > 0) {
cleanImport();
@@ -134,9 +142,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
}
/**
* On file end.
*/
protected void onFileEnd() {
// Add markers to the last imported track
int size = trackIds.size();
@@ -259,7 +264,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
Track track = contentProviderUtils.getTrack(trackData.track.getUuid());
if (track != null) {
if (PreferencesUtils.getPreventReimportTracks(context)) {
if (PreferencesUtils.getPreventReimportTracks(sharedPreferences, context)) {
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
}
@@ -304,7 +309,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
}
protected void addMarker() throws SAXException {
protected void addMarker() throws ParsingException {
// Markers must have a time, else cannot match to the track points
if (time == null) {
Log.w(TAG, "Marker without time ignored.");
@@ -335,7 +340,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
markers.add(marker);
}
protected TrackPoint getTrackPoint() throws SAXException {
protected TrackPoint getTrackPoint() throws ParsingException {
TrackPoint trackPoint = createTrackPoint();
if (trackPoint.hasLocation()) {
@@ -350,7 +355,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackPoint.setTime(time);
} else if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
throw new ParsingException(createErrorMessage("Invalid location detected: " + trackPoint));
}
}
@@ -369,7 +374,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
*/
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
float speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / timeDifference.toMillis();
float speed = trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint) / timeDifference.toMillis();
trackPoint.setSpeed(speed);
}
}
@@ -377,8 +382,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
long maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(context);
double distanceToLastTrackLocation = trackPoint.distanceTo(trackData.lastLocationInCurrentSegment);
long maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation > maxRecordingDistance) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
@@ -424,16 +429,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return null;
}
/**
* Creates a location.
*/
private TrackPoint createTrackPoint() throws SAXException {
private TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
try {
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
if (latitude == null || longitude == null) {
@@ -444,7 +446,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackPoint.setLatitude(Double.parseDouble(latitude));
trackPoint.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
@@ -452,7 +454,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
trackPoint.setAltitude(Double.parseDouble(altitude));
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
@@ -460,14 +462,14 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
trackPoint.setSpeed(Float.parseFloat(speed));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
}
if (heartrate != null) {
try {
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
@@ -475,7 +477,15 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
@@ -483,14 +493,21 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try {
trackPoint.setElevationGain(Float.parseFloat(gain));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation gain: %s", gain)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation gain: %s", gain)), e);
}
}
if (loss != null) {
try {
trackPoint.setElevationLoss(Float.parseFloat(loss));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation loss: %s", loss)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation loss: %s", loss)), e);
}
}
if (distance != null) {
try {
trackPoint.setSensorDistance(Float.parseFloat(distance));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
}
@@ -546,4 +563,21 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// The buffered locations
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
}
public static class ParsingException extends RuntimeException {
private ParsingException(@NonNull String message) {
super(message);
}
private ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@NonNull
@Override
public String toString() {
return "" + getMessage();
}
}
}
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.io.file.importer;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
@@ -18,10 +19,14 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
private static final int DIRECTORY_PICKER_REQUEST_CODE = 6;
protected SharedPreferences sharedPreferences;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
DocumentFile directoryUri = configureDirectoryChooserIntent(intent);
if (directoryUri == null) {
@@ -38,6 +43,12 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
onActivityResultCustom(requestCode, resultCode, resultData);
}
@Override
protected void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
protected void onActivityResultCustom(int requestCode, int resultCode, @Nullable Intent resultData) {
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
@@ -80,14 +91,14 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
super.configureDirectoryChooserIntent(intent);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
return PreferencesUtils.getDefaultExportDirectoryUri(this);
return PreferencesUtils.getDefaultExportDirectoryUri(sharedPreferences, this);
}
@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_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat(this));
intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat(sharedPreferences, this));
return intent;
}
}
@@ -101,11 +112,11 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
case RESULT_OK:
Uri directoryUri = resultData.getData();
PreferencesUtils.setDefaultExportDirectoryUri(this, directoryUri);
PreferencesUtils.setDefaultExportDirectoryUri(sharedPreferences, this, directoryUri);
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), resultData, directoryUri);
break;
case RESULT_CANCELED:
PreferencesUtils.setDefaultExportDirectoryUri(this, null);
PreferencesUtils.setDefaultExportDirectoryUri(sharedPreferences, this, null);
//TODO Remove stored permission contentResolver.releasePersistableUriPermission
break;
}
@@ -118,8 +129,8 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
protected DocumentFile configureDirectoryChooserIntent(Intent intent) {
super.configureDirectoryChooserIntent(intent);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (PreferencesUtils.isDefaultExportDirectoryUri(this)) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri(this).getUri());
if (PreferencesUtils.isDefaultExportDirectoryUri(sharedPreferences, this)) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri(sharedPreferences, this).getUri());
}
return null;
}
@@ -28,7 +28,9 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Imports a GPX file.
* Uses https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd
* Uses:
* * https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd
* * https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd
* <p>
* {@link de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter} does not export information if a segment was started automatic or manually.
* Therefore, all segments starts are marked as SEGMENT_START_AUTOMATIC.
@@ -57,9 +59,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
private static final String TAG_EXTENSION_GAIN = "opentracks:gain";
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
/**
* Constructor.
@@ -156,6 +160,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
cadence = content.trim();
}
break;
case TAG_EXTENSION_POWER:
if (content != null) {
power = content.trim();
}
break;
case TAG_ID:
if (content != null) {
uuid = content.trim();
@@ -171,6 +180,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
loss = content.trim();
}
break;
case TAG_EXTENSION_DISTANCE:
if (content != null) {
distance = content.trim();
}
break;
}
// Reset element content
@@ -196,11 +210,12 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
altitude = null;
time = null;
speed = null;
power = null;
gain = null;
loss = null;
}
private void onTrackPointEnd() throws SAXException {
private void onTrackPointEnd() {
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
TrackPoint trackPoint = getTrackPoint();
if (isFirstTrackPointInSegment) {
@@ -221,7 +236,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
markerType = null;
}
private void onMarkerEnd() throws SAXException {
private void onMarkerEnd() {
addMarker();
}
}
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -30,6 +31,10 @@ import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.databinding.ImportActivityBinding;
@@ -47,17 +52,15 @@ public class ImportActivity extends FragmentActivity {
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
private static final String BUNDLE_TOOLBAR_TITLE = "toolbar_title";
private static final String BUNDLE_DOCUMENT_URI = "document_uri";
private static final String BUNDLE_DOCUMENT_URIS = "document_uris";
private static final String BUNDLE_IS_DIRECTORY = "is_directory";
private ImportActivityBinding viewBinding;
boolean doubleBackToCancel = false;
private Uri documentUri;
private ArrayList<Uri> documentUris = new ArrayList<>();
private boolean isDirectory;
private String toolbarTitle;
private ImportViewModel viewModel;
private ImportViewModel.Summary summary;
@@ -70,35 +73,46 @@ public class ImportActivity extends FragmentActivity {
Toolbar toolbar = findViewById(R.id.toolbar);
final DocumentFile documentFile;
final Intent intent = getIntent();
final ClipData intentClipData = intent.getClipData();
if (savedInstanceState == null) {
if (getIntent().getData() != null) {
documentUri = getIntent().getData();
isDirectory = false;
} else if (getIntent().getClipData() != null && getIntent().getClipData().getItemCount() > 0) {
documentUri = getIntent().getClipData().getItemAt(0).getUri();
if (intent.getData() != null) {
documentUris.add(intent.getData());
isDirectory = false;
} else {
// Started from DirectoryChooserActivity
documentUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
isDirectory = true;
if (intentClipData != null && intentClipData.getItemCount() > 0) {
for (int i = 0; i < intentClipData.getItemCount(); i++) {
documentUris.add(intentClipData.getItemAt(i).getUri());
}
isDirectory = false;
} else {
// Started from DirectoryChooserActivity
documentUris.add(intent.getParcelableExtra(EXTRA_DIRECTORY_URI_KEY));
isDirectory = true;
}
}
documentFile = isDirectory ? DocumentFile.fromTreeUri(this, documentUri) : DocumentFile.fromSingleUri(this, documentUri);
toolbarTitle = getString(R.string.import_progress_message, documentFile.getName());
} else {
documentUri = savedInstanceState.getParcelable(BUNDLE_DOCUMENT_URI);
toolbarTitle = savedInstanceState.getString(BUNDLE_TOOLBAR_TITLE);
documentUris = savedInstanceState.getParcelable(BUNDLE_DOCUMENT_URIS);
isDirectory = savedInstanceState.getBoolean(BUNDLE_IS_DIRECTORY);
documentFile = isDirectory ? DocumentFile.fromTreeUri(this, documentUri) : DocumentFile.fromSingleUri(this, documentUri);
}
toolbar.setTitle(toolbarTitle);
final List<DocumentFile> documentFiles;
if (isDirectory) {
documentFiles = new ArrayList<>();
documentFiles.add(DocumentFile.fromTreeUri(this, documentUris.get(0)));
} else {
documentFiles = documentUris.stream().map(it -> DocumentFile.fromSingleUri(this, it)).collect(Collectors.toList());
}
//Works for a directory, but we might have received multiple files via SEND_MULTIPLE.
toolbar.setTitle(getString(R.string.import_progress_message, documentFiles.get(0).getName()));
initViews();
viewModel = new ViewModelProvider(this).get(ImportViewModel.class);
viewModel.getImportData(documentFile).observe(this, data -> {
viewModel.getImportData(documentFiles).observe(this, data -> {
summary = data;
setProgress();
});
@@ -107,9 +121,8 @@ public class ImportActivity extends FragmentActivity {
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(BUNDLE_DOCUMENT_URI, documentUri);
outState.putParcelableArrayList(BUNDLE_DOCUMENT_URIS, documentUris);
outState.putBoolean(BUNDLE_IS_DIRECTORY, isDirectory);
outState.putString(BUNDLE_TOOLBAR_TITLE, toolbarTitle);
}
@Override
@@ -14,6 +14,7 @@ import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
@@ -57,33 +58,34 @@ public class ImportService extends JobIntentService {
trackImporter = new KmzTrackImporter(this, file.getUri());
} else {
Log.d(TAG, "Unsupported file format.");
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unsupported_format));
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
return;
}
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri())) {
Track.Id trackId = trackImporter.importFile(inputStream);
if (trackId != null) {
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackId, file.getName(), getString(R.string.import_file_imported, file.getName()));
ArrayList<Track.Id> trackIds = new ArrayList<>(trackImporter.importFile(inputStream));
if (!trackIds.isEmpty()) {
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackIds, file, getString(R.string.import_file_imported, file.getName()));
} else {
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackId, file.getName(), getString(R.string.import_unable_to_import_file, file.getName()));
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackIds, file, getString(R.string.import_unable_to_import_file, file.getName()));
}
} catch (IOException e) {
Log.d(TAG, "Unable to import file", e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unable_to_import_file, e.getMessage()));
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unable_to_import_file, e.getMessage()));
} catch (ImportParserException e) {
Log.d(TAG, "Parser error: " + e.getMessage(), e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_parser_error, e.getMessage()));
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_parser_error, e.getMessage()));
} catch (ImportAlreadyExistsException e) {
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file.getName(), e.getMessage());
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file, e.getMessage());
}
}
private void sendResult(int resultCode, Track.Id trackId, String fileName, String message) {
private void sendResult(int resultCode, ArrayList<Track.Id> trackId, DocumentFile file, String message) {
Bundle bundle = new Bundle();
bundle.putParcelable(ImportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, fileName);
bundle.putParcelableArrayList(ImportServiceResultReceiver.RESULT_EXTRA_LIST_TRACK_ID, trackId);
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, file.getName());
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE, message);
resultReceiver.send(resultCode, bundle);
}
@@ -16,7 +16,7 @@ public class ImportServiceResultReceiver extends ResultReceiver {
public static final int RESULT_CODE_IMPORTED = 1;
public static final int RESULT_CODE_ALREADY_EXISTS = 2;
public static final String RESULT_EXTRA_TRACK_ID = "result_track_id";
public static final String RESULT_EXTRA_LIST_TRACK_ID = "result_track_id";
public static final String RESULT_EXTRA_FILENAME = "result_extra_filename";
public static final String RESULT_EXTRA_MESSAGE = "result_extra_message";
@@ -11,7 +11,9 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
@@ -33,20 +35,20 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
summary = new Summary();
}
public LiveData<Summary> getImportData(DocumentFile documentFile) {
LiveData<Summary> getImportData(List<DocumentFile> documentFiles) {
if (importData == null) {
importData = new MutableLiveData<>();
loadData(documentFile);
loadData(documentFiles);
}
return importData;
}
public void cancel() {
void cancel() {
cancel = true;
}
private void loadData(DocumentFile documentFile) {
List<DocumentFile> fileList = FileUtils.getFiles(documentFile);
private void loadData(List<DocumentFile> documentFiles) {
List<DocumentFile> fileList = documentFiles.stream().map(FileUtils::getFiles).flatMap(Collection::stream).collect(Collectors.toList());
summary.totalCount = fileList.size();
filesToImport.addAll(fileList);
importNextFile();
@@ -66,7 +68,7 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
throw new RuntimeException(TAG + ": onReceiveResult resultData NULL");
}
Track.Id trackId = resultData.getParcelable(ImportServiceResultReceiver.RESULT_EXTRA_TRACK_ID);
ArrayList<Track.Id> trackIds = resultData.getParcelableArrayList(ImportServiceResultReceiver.RESULT_EXTRA_LIST_TRACK_ID);
String fileName = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME);
String message = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE);
@@ -76,7 +78,7 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
summary.fileErrors.add(getApplication().getString(R.string.import_error_info, fileName, message));
break;
case ImportServiceResultReceiver.RESULT_CODE_IMPORTED:
summary.importedTrackIds.add(trackId);
summary.importedTrackIds.addAll(trackIds);
summary.successCount++;
break;
case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS:
@@ -90,12 +92,12 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
importNextFile();
}
public static class Summary {
static class Summary {
private int totalCount;
private int successCount;
private int existsCount;
private int errorCount;
private ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
private final ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
private final ArrayList<String> fileErrors = new ArrayList<>();
public int getTotalCount() {
@@ -35,6 +35,7 @@ import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
*
* @author Jimmy Shih
*/
//TODO Do not use AbstractFileTrackImporter as TrackPoint sensor generation is done differently here.
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
@@ -65,6 +66,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private String extendedDataType;
private final ArrayList<TrackPoint> trackPoints = new ArrayList<>();
private final ArrayList<Float> speedList = new ArrayList<>();
private final ArrayList<Float> distanceList = new ArrayList<>();
private final ArrayList<Float> cadenceList = new ArrayList<>();
private final ArrayList<Float> heartRateList = new ArrayList<>();
private final ArrayList<Float> powerList = new ArrayList<>();
@@ -215,6 +217,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
super.onTrackSegmentStart();
trackPoints.clear();
speedList.clear();
distanceList.clear();
heartRateList.clear();
cadenceList.clear();
powerList.clear();
@@ -231,6 +234,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (i < speedList.size()) {
trackPoint.setSpeed(speedList.get(i));
}
if (i < distanceList.size()) {
trackPoint.setSensorDistance(distanceList.get(i));
}
if (i < heartRateList.size()) {
trackPoint.setHeartRate_bpm(heartRateList.get(i));
}
@@ -309,6 +315,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
speedList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE:
distanceList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER:
powerList.add(value);
break;
@@ -20,6 +20,8 @@ import android.content.Context;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -66,17 +68,23 @@ public class KmzTrackImporter implements TrackImporter {
}
@Override
public Track.Id importFile(InputStream inputStream) {
Track.Id trackId = findAndParseKmlFile(inputStream);
public @NonNull
List<Track.Id> importFile(InputStream inputStream) {
List<Track.Id> trackIds = findAndParseKmlFile(inputStream);
if (!copyKmzImages(trackId)) {
cleanImport(context, trackId);
return null;
ArrayList<Track.Id> trackIdsWithImages = new ArrayList<>();
for (Track.Id trackId : trackIds) {
if (copyKmzImages(trackId)) {
trackIdsWithImages.add(trackId);
deleteOrphanImages(context, trackId);
} else {
cleanImport(context, trackId);
return new ArrayList<>();
}
}
deleteOrphanImages(context, trackId);
return trackId;
return trackIdsWithImages;
}
/**
@@ -150,14 +158,13 @@ public class KmzTrackImporter implements TrackImporter {
/**
* Finds KmzTrackExporter.KMZ_KML_FILE file inside kmz file (inputStream) and it parses it.
* TODO: May load multiple tracks, but only returns the last Track.Id.
*
* @param inputStream kmz input stream.
*/
private Track.Id findAndParseKmlFile(InputStream inputStream) {
private List<Track.Id> findAndParseKmlFile(InputStream inputStream) {
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
Track.Id trackId = null;
ArrayList<Track.Id> trackIds = new ArrayList<>();
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
@@ -167,20 +174,21 @@ public class KmzTrackImporter implements TrackImporter {
String fileName = zipEntry.getName();
if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
trackId = parseKml(zipInputStream);
if (trackId == null) {
List<Track.Id> trackId = parseKml(zipInputStream);
if (trackId.isEmpty()) {
Log.d(TAG, "Unable to parse kml in kmz");
throw new ImportParserException(context.getString(R.string.import_unable_to_import_file, fileName));
}
trackIds.addAll(trackId);
}
zipInputStream.closeEntry();
}
if (trackId == null) {
if (trackIds.isEmpty()) {
Log.d(TAG, "Unable to find doc.kml in kmz");
throw new ImportParserException(context.getString(R.string.import_no_kml_file_found));
}
return trackId;
return trackIds;
} catch (ImportParserException | ImportAlreadyExistsException e) {
Log.e(TAG, "Unable to import file", e);
throw e;
@@ -234,13 +242,7 @@ public class KmzTrackImporter implements TrackImporter {
}
}
/**
* Parses kml
*
* @param zipInputStream the zip input stream
* @return the imported track id or -1L
*/
private Track.Id parseKml(ZipInputStream zipInputStream) {
private List<Track.Id> parseKml(ZipInputStream zipInputStream) {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context);
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
@@ -15,7 +15,10 @@
*/
package de.dennisguse.opentracks.io.file.importer;
import androidx.annotation.NonNull;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
@@ -30,8 +33,7 @@ public interface TrackImporter {
* Import a file.
*
* @param inputStream the file's input stream
* @return the imported track id or null.
*/
//TODO Figure out how can make the import an atomic operation (incl. database transaction rollback).
Track.Id importFile(InputStream inputStream);
@NonNull
List<Track.Id> importFile(InputStream inputStream) throws ImportParserException;
}
@@ -66,7 +66,6 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackNameUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
/**
* A background service that registers a location listener and records track points.
@@ -83,39 +82,40 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private PeriodicTaskExecutor voiceExecutor;
private TrackRecordingServiceNotificationManager notificationManager;
private SharedPreferences sharedPreferences;
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private int recordingDistanceInterval;
private int maxRecordingDistance;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Context context = TrackRecordingService.this;
if (PreferencesUtils.isKey(TrackRecordingService.this, R.string.recording_track_id_key, key)) {
// Only through the TrackRecordingService can one stop a recording and set the recordingTrackId to -1L.
if (PreferencesUtils.isRecording(TrackRecordingService.this)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackRecordingService.this);
if (PreferencesUtils.isRecording(sharedPreferences, TrackRecordingService.this)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, TrackRecordingService.this);
}
}
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, context);
}
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
voiceExecutor.setMetricUnits(metricUnits);
notificationManager.setMetricUnits(metricUnits);
}
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(context));
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(sharedPreferences, context));
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
}
if (PreferencesUtils.isKey(context, R.string.max_recording_distance_key, key)) {
maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(context);
maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context);
}
handlerServer.onSharedPreferenceChanged(context, preferences, key);
handlerServer.onSharedPreferenceChanged(context, sharedPreferences, key);
}
};
@@ -148,8 +148,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// onSharedPreferenceChanged might not set recordingTrackId.
recordingTrackId = null;
PreferencesUtils.register(this, sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
restartTrackAfterServiceRestart();
}
@@ -190,8 +191,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// Reverse order from onCreate
showNotification(false); //TODO Why?
PreferencesUtils.unregister(this, sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferences = null;
try {
voiceExecutor.shutdown();
@@ -306,7 +307,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
//TODO Pass TrackPoint
track.setName(TrackNameUtils.getTrackName(this, trackId, segmentStartTrackPoint.getTime()));
String category = PreferencesUtils.getDefaultActivity(this);
String category = PreferencesUtils.getDefaultActivity(sharedPreferences, this);
track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(this, category));
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
@@ -537,9 +538,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private void updateRecordingState(Track.Id trackId, boolean paused) {
recordingTrackId = trackId;
long currentTrackId = trackId != null ? trackId.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
PreferencesUtils.setLong(this, R.string.recording_track_id_key, currentTrackId);
PreferencesUtils.setLong(sharedPreferences, this, R.string.recording_track_id_key, currentTrackId);
recordingTrackPaused = paused;
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused);
PreferencesUtils.setBoolean(sharedPreferences, this, R.string.recording_track_paused_key, recordingTrackPaused);
}
@Override
@@ -557,7 +558,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
fillWithSensorDataSet(trackPoint);
notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy);
notificationManager.updateTrackPoint(this, track.getTrackStatistics(), trackPoint, recordingGpsAccuracy);
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
@@ -571,7 +572,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
double distanceToLastTrackLocation = trackPoint.distanceTo(lastValidTrackPoint);
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) {
insertTrackPointIfNewer(track, lastTrackPoint);
@@ -594,7 +595,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
if (!isIdle && !TrackPointUtils.isMoving(trackPoint)) {
if (!isIdle && !trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
@@ -605,7 +606,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
if (isIdle && TrackPointUtils.isMoving(trackPoint)) {
if (isIdle && trackPoint.isMoving()) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
@@ -664,6 +665,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
trackPoint.setElevationLoss(elevationSumManager.getElevationLoss_m());
elevationSumManager.reset();
}
if (remoteSensorManager != null) {
fillWithSensorDataSet(trackPoint);
remoteSensorManager.reset();
}
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
@@ -20,6 +20,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
@@ -209,9 +210,10 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
try {
if (showEditor) {
// Need to remember the recordingTrackId before calling endCurrentTrack() as endCurrentTrack() sets the value to -1L.
Track.Id recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
Track.Id recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, context);
trackRecordingService.endCurrentTrack();
if (PreferencesUtils.isRecording(context)) {
if (PreferencesUtils.isRecording(sharedPreferences, context)) {
Intent intent = IntentUtils.newIntent(context, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, recordingTrackId)
.putExtra(TrackEditActivity.EXTRA_NEW_TRACK, true);
@@ -12,7 +12,7 @@ import androidx.core.app.NotificationCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -30,6 +30,8 @@ class TrackRecordingServiceNotificationManager {
private boolean previousLocationWasAccurate = true;
private Boolean metricUnits = null;
TrackRecordingServiceNotificationManager(Context context) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -59,14 +61,14 @@ class TrackRecordingServiceNotificationManager {
}
void updateContent(String content) {
notificationBuilder.setContentText(content);
notificationBuilder.setSubText(content);
updateNotification();
}
void updateTrackPoint(Context context, TrackPoint trackPoint, int recordingGpsAccuracy) {
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, int recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
if (trackPoint.hasAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getAccuracy(), PreferencesUtils.isMetricUnits(context));
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getAccuracy(), metricUnits);
boolean currentLocationWasAccurate = trackPoint.getAccuracy() < recordingGpsAccuracy;
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
@@ -74,7 +76,9 @@ class TrackRecordingServiceNotificationManager {
previousLocationWasAccurate = currentLocationWasAccurate;
}
notificationBuilder.setContentText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, StringUtils.formatDistance(context, trackStatistics.getTotalDistance(), metricUnits)));
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, StringUtils.formatSpeed(context, trackPoint.getSpeed(), metricUnits, true)));
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
notificationBuilder.setOnlyAlertOnce(true);
@@ -93,6 +97,10 @@ class TrackRecordingServiceNotificationManager {
return notificationBuilder.build();
}
void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
}
private void updateNotification() {
notificationManager.notify(NOTIFICATION_ID, getNotification());
}
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.LocationManager;
import android.os.Handler;
@@ -15,6 +16,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* This class handle GPS status according to received locations and some thresholds.
*/
//TODO should handle sharedpreference changes
class GpsStatus {
private static final String TAG = GpsStatus.class.getSimpleName();
@@ -60,16 +62,16 @@ class GpsStatus {
private final Handler gpsStatusHandler;
private GpsStatusRunner gpsStatusRunner = null;
/**
* @param context The context object.
* @param client The client.
* @param minRecordingInterval Value of min recording interval preference.
*/
public GpsStatus(Context context, GpsStatusListener client, Duration minRecordingInterval) {
public GpsStatus(Context context, GpsStatusListener client) {
this.client = client;
this.context = context;
signalBadThreshold = PreferencesUtils.getRecordingDistanceInterval(context);
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
signalBadThreshold = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
Duration minRecordingInterval = Duration.ofSeconds(PreferencesUtils.getMinRecordingInterval(sharedPreferences, context));
signalLostThreshold = !minRecordingInterval.isNegative() ? SIGNAL_LOST_THRESHOLD.plus(minRecordingInterval) : SIGNAL_LOST_THRESHOLD;
gpsStatusHandler = new Handler();
}
@@ -3,12 +3,14 @@ package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
public class HandlerServer {
@@ -32,8 +34,9 @@ public class HandlerServer {
public void start(Context context) {
serviceExecutor = Executors.newSingleThreadExecutor();
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
locationHandler.onStart(context);
locationHandler.onSharedPreferenceChanged(context, null, null);
locationHandler.onSharedPreferenceChanged(context, sharedPreferences, null);
}
public void stop(Context context) {
@@ -45,7 +48,7 @@ public class HandlerServer {
serviceExecutor = null;
}
public void onSharedPreferenceChanged(Context context, SharedPreferences preferences, String key) {
public void onSharedPreferenceChanged(@NonNull Context context, @NonNull SharedPreferences preferences, String key) {
locationHandler.onSharedPreferenceChanged(context, preferences, key);
}
@@ -70,8 +73,10 @@ public class HandlerServer {
}
public interface Handler {
void onStart(Context context);
void onStop(Context context);
void onSharedPreferenceChanged(Context context, SharedPreferences preferences, String key);
void onStart(@NonNull Context context);
void onStop(@NonNull Context context);
void onSharedPreferenceChanged(@NonNull Context context, @NonNull SharedPreferences preferences, String key);
}
}
@@ -16,7 +16,6 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
class LocationHandler implements HandlerServer.Handler, LocationListener, GpsStatus.GpsStatusListener {
@@ -35,14 +34,14 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
}
@Override
public void onStart(Context context) {
gpsStatus = new GpsStatus(context, this, Duration.ofSeconds(PreferencesUtils.getMinRecordingInterval(context)));
public void onStart(@NonNull Context context) {
gpsStatus = new GpsStatus(context, this);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
registerLocationListener();
}
@Override
public void onStop(Context context) {
public void onStop(@NonNull Context context) {
unregisterLocationListener();
locationManager = null;
if (gpsStatus != null) {
@@ -52,9 +51,9 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
}
@Override
public void onSharedPreferenceChanged(Context context, SharedPreferences preferences, String key) {
public void onSharedPreferenceChanged(@NonNull Context context, @NonNull SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(sharedPreferences, context);
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
// Choose battery life over moving time accuracy.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(Duration.ofSeconds(30), Duration.ofSeconds(5), 5);
@@ -70,16 +69,16 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
}
}
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(sharedPreferences, context);
}
if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
if (gpsStatus != null) {
gpsStatus.onMinRecordingIntervalChanged(PreferencesUtils.getMinRecordingInterval(context));
gpsStatus.onMinRecordingIntervalChanged(PreferencesUtils.getMinRecordingInterval(sharedPreferences, context));
}
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
if (gpsStatus != null) {
gpsStatus.onRecordingDistanceChanged(PreferencesUtils.getRecordingDistanceInterval(context));
gpsStatus.onRecordingDistanceChanged(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context));
}
}
}
@@ -92,7 +91,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
@Override
public void onLocationChanged(@NonNull Location location) {
TrackPoint trackPoint = new TrackPoint(location);
boolean isAccurate = TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy);
boolean isAccurate = trackPoint.fulfillsAccuracy(recordingGpsAccuracy);
boolean isValid = LocationUtils.isValidLocation(location);
if (gpsStatus != null) {
@@ -101,11 +101,11 @@ public abstract class BluetoothConnectionManager {
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
String sensorName = gatt.getDevice().getName();
Log.d(TAG, "Received data from " + sensorName);
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress());
SensorData sensorData = parsePayload(sensorName, gatt.getDevice().getAddress(), characteristic);
SensorData<?> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
observer.onChanged(sensorData);
}
}
@@ -125,7 +125,7 @@ public abstract class BluetoothConnectionManager {
Log.d(TAG, "Connecting to: " + device);
bluetoothGatt = device.connectGatt(context, true, connectCallback);
SensorData sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
SensorData<?> sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
observer.onChanged(sensorData);
}
@@ -148,12 +148,12 @@ public abstract class BluetoothConnectionManager {
return address.equals(bluetoothGatt.getDevice().getAddress());
}
protected abstract SensorData createEmptySensorData(String address);
protected abstract SensorData<?> createEmptySensorData(String address);
/**
* @return null if data could not be parsed.
*/
protected abstract SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
protected abstract SensorData<?> parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
public static class HeartRate extends BluetoothConnectionManager {
@@ -162,7 +162,7 @@ public abstract class BluetoothConnectionManager {
}
@Override
protected SensorData createEmptySensorData(String address) {
protected SensorDataHeartRate createEmptySensorData(String address) {
return new SensorDataHeartRate(address);
}
@@ -181,7 +181,7 @@ public abstract class BluetoothConnectionManager {
}
@Override
protected SensorData createEmptySensorData(String address) {
protected SensorDataCycling.Cadence createEmptySensorData(String address) {
return new SensorDataCycling.Cadence(address);
}
@@ -197,30 +197,36 @@ public abstract class BluetoothConnectionManager {
}
//Workaround for Wahoo CADENCE: this sensor reports speed (instead of cadence)
if (cadenceAndSpeed.getSpeed() != null) {
return new SensorDataCycling.Cadence(cadenceAndSpeed.getSpeed());
if (cadenceAndSpeed.getDistanceSpeed() != null) {
return new SensorDataCycling.Cadence(cadenceAndSpeed.getDistanceSpeed());
}
return null;
}
}
public static class CyclingSpeed extends BluetoothConnectionManager {
public static class CyclingDistanceSpeed extends BluetoothConnectionManager {
CyclingSpeed(SensorDataObserver observer) {
CyclingDistanceSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorData createEmptySensorData(String address) {
return new SensorDataCycling.Speed(address);
protected SensorDataCycling.DistanceSpeed createEmptySensorData(String address) {
return new SensorDataCycling.DistanceSpeed(address);
}
@Override
protected SensorDataCycling.Speed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
protected SensorDataCycling.DistanceSpeed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed != null) {
return cadenceAndSpeed.getSpeed();
// Workaround for Garmin Speed Sensor 2: provides cadence instead of speed
SensorDataCycling.DistanceSpeed result = cadenceAndSpeed.getDistanceSpeed();
if (result == null && cadenceAndSpeed.getCadence() != null) {
result = new SensorDataCycling.DistanceSpeed(cadenceAndSpeed.getSensorAddress(), cadenceAndSpeed.getSensorName(), (int) cadenceAndSpeed.getCadence().getCrankRevolutionsCount(), cadenceAndSpeed.getCadence().getCrankRevolutionsTime());
}
return result;
}
return null;
}
@@ -233,7 +239,7 @@ public abstract class BluetoothConnectionManager {
}
@Override
protected SensorData createEmptySensorData(String address) {
protected SensorDataCyclingPower createEmptySensorData(String address) {
return new SensorDataCyclingPower(address);
}
@@ -247,8 +253,8 @@ public abstract class BluetoothConnectionManager {
interface SensorDataObserver {
void onChanged(SensorData sensorData);
void onChanged(SensorData<?> sensorData);
void onDisconnecting(SensorData sensorData);
void onDisconnecting(SensorData<?> sensorData);
}
}
@@ -53,43 +53,50 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private final BluetoothAdapter bluetoothAdapter;
private final Context context;
private final SharedPreferences sharedPreferences;
private boolean started = false;
private final SharedPreferences sharedPreferences;
private int preferenceWheelCircumference;
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
private final BluetoothConnectionManager.CyclingSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingSpeed(this);
private final BluetoothConnectionManager.CyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingDistanceSpeed(this);
private final BluetoothConnectionManager.CyclingPower cyclingPower = new BluetoothConnectionManager.CyclingPower(this);
private final SensorDataSet sensorDataSet = new SensorDataSet();
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (!started) return;
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(sharedPreferences, context);
connect(heartRate, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress(context);
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress(sharedPreferences, context);
connect(cyclingCadence, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingSpeedSensorAddress(context);
String address = PreferencesUtils.getBluetoothCyclingSpeedSensorAddress(sharedPreferences, context);
connect(cyclingSpeed, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress(context);
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress(sharedPreferences, context);
connect(cyclingPower, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
preferenceWheelCircumference = PreferencesUtils.getWheelCircumference(sharedPreferences, context);
}
}
};
@@ -104,7 +111,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
//Trigger connection startup
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
}
public synchronized void stop() {
@@ -155,11 +162,15 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
return sensorDataSet;
}
public void reset() {
sensorDataSet.reset();
}
@Override
public synchronized void onChanged(SensorData sensorData) {
public synchronized void onChanged(SensorData<?> sensorData) {
if (sensorData instanceof SensorDataCycling.Cadence) {
SensorDataCycling.Cadence previous = sensorDataSet.getCyclingCadence();
Log.d(TAG, "previous " + previous + "; current" + sensorData);
Log.d(TAG, "Previous: " + previous + "; current: " + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cadence data repeated.");
@@ -167,21 +178,21 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
}
((SensorDataCycling.Cadence) sensorData).compute(previous);
}
if (sensorData instanceof SensorDataCycling.Speed) {
SensorDataCycling.Speed previous = sensorDataSet.getCyclingSpeed();
Log.d(TAG, "previous " + previous + "; current" + sensorData);
if (sensorData instanceof SensorDataCycling.DistanceSpeed) {
SensorDataCycling.DistanceSpeed previous = sensorDataSet.getCyclingDistanceSpeed();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: speed data repeated.");
return;
}
((SensorDataCycling.Speed) sensorData).compute(previous, PreferencesUtils.getWheelCircumference(context));
((SensorDataCycling.DistanceSpeed) sensorData).compute(previous, PreferencesUtils.getWheelCircumference(sharedPreferences, context));
}
sensorDataSet.set(sensorData);
}
@Override
public void onDisconnecting(SensorData sensorData) {
public void onDisconnecting(SensorData<?> sensorData) {
sensorDataSet.remove(sensorData);
}
}
@@ -17,17 +17,19 @@
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.AnnouncementUtils;
@@ -102,6 +104,8 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
}
};
private SharedPreferences sharedPreferences;
private TextToSpeech tts;
// Response from TTS after its initialization
private int ttsInitStatus = TextToSpeech.ERROR;
@@ -112,12 +116,15 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
contentProviderUtils = new ContentProviderUtils(context);
}
@Override
public void start() {
Log.d(TAG, "Start");
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
if (tts == null) {
tts = new TextToSpeech(context, status -> {
Log.i(TAG, "TextToSpeech initialized with status " + status);
@@ -164,14 +171,23 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
return;
}
Track track = contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context));
Track track = contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(sharedPreferences, context));
String category = track != null ? track.getCategory() : "";
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(track.getId());
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, (float) (PreferencesUtils.isMetricUnits(context) ? 1d * UnitConversions.KM_TO_M : 1d * UnitConversions.MI_TO_M));
//TODO Querying all TrackPoints all the time is inefficient; use TrackDataHub or something else.
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
while (trackPointIterator.hasNext()) {
trackPoints.add(trackPointIterator.next());
}
boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category);
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, (float) (isMetricUnits ? 1d * UnitConversions.KM_TO_M : 1d * UnitConversions.MI_TO_M));
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, category, lastInterval);
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval);
speakAnnouncement(announcement);
}
@@ -181,6 +197,8 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
tts.shutdown();
tts = null;
}
sharedPreferences = null;
}
private void onTtsReady() {
@@ -94,7 +94,7 @@ public class PeriodicTaskExecutor {
if (timerTaskExecutor == null) {
timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
}
timerTaskExecutor.scheduleTask(Duration.ofSeconds(taskFrequency));
timerTaskExecutor.scheduleTask(Duration.ofMinutes(taskFrequency));
} else {
// For distance periodic task
calculateNextTaskDistance();
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
@@ -48,7 +49,7 @@ public class ActivityTypePreference extends DialogPreference {
setDialogIcon(null);
setPersistent(true);
SummaryProvider<DialogPreference> summaryProvider = preference -> PreferencesUtils.getDefaultActivity(ActivityTypePreference.this.getContext());
SummaryProvider<DialogPreference> summaryProvider = preference -> PreferencesUtils.getDefaultActivity(PreferencesUtils.getSharedPreferences(context), ActivityTypePreference.this.getContext());
setSummaryProvider(summaryProvider);
}
@@ -60,6 +61,8 @@ public class ActivityTypePreference extends DialogPreference {
public static class ActivityPreferenceDialog extends PreferenceDialogFragmentCompat {
private SharedPreferences sharedPreferences;
private AutoCompleteTextView textView;
private ImageView iconView;
@@ -77,9 +80,11 @@ public class ActivityTypePreference extends DialogPreference {
super.onBindDialogView(view);
final Context context = getActivity();
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
textView = view.findViewById(R.id.activity_type_preference_text_view);
String category = PreferencesUtils.getDefaultActivity(context);
String category = PreferencesUtils.getDefaultActivity(sharedPreferences, context);
textView.setText(category);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
textView.setAdapter(adapter);
@@ -100,8 +105,14 @@ public class ActivityTypePreference extends DialogPreference {
updateIcon(TrackIconUtils.getIconValue(context, category));
}
@Override
public void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
private void showIconSelectDialog() {
String category = PreferencesUtils.getDefaultActivity(getActivity());
String category = PreferencesUtils.getDefaultActivity(sharedPreferences, getActivity());
ChooseActivityTypeDialogFragment.showDialog(getActivity().getSupportFragmentManager(), category);
}
@@ -110,7 +121,7 @@ public class ActivityTypePreference extends DialogPreference {
if (positiveResult) {
String newDefaultActivity = textView.getText().toString();
if (getPreference().callChangeListener(newDefaultActivity)) {
PreferencesUtils.setDefaultActivity(getActivity(), newDefaultActivity);
PreferencesUtils.setDefaultActivity(sharedPreferences, getActivity(), newDefaultActivity);
HackUtils.invalidatePreference(getPreference());
}
}
@@ -91,7 +91,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
public static class PrefsFragment extends PreferenceFragmentCompat {
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
private SharedPreferences sharedPreferences;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(getActivity(), R.string.recording_track_id_key, key)) {
getActivity().runOnUiThread(this::updateReset);
}
@@ -99,7 +101,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
getActivity().runOnUiThread(this::updateUnits);
}
if (PreferencesUtils.isKey(getActivity(), R.string.night_mode_key, key)) {
getActivity().runOnUiThread(() -> ActivityUtils.applyNightMode(getContext()));
getActivity().runOnUiThread(() -> ActivityUtils.applyNightMode(sharedPreferences, getContext()));
}
};
@@ -108,6 +110,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
try {
setPreferencesFromResource(R.xml.settings, rootKey);
} catch (ClassCastException e) {
@@ -123,7 +127,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
Preference instantExportDirectoryPreference = findPreference(getString(R.string.settings_default_export_directory_key));
instantExportDirectoryPreference.setSummaryProvider(preference -> {
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(getContext());
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(sharedPreferences, getContext());
//Use same value for not set as Androidx ListPreference and EditTextPreference
return directory != null ? directory.getName() : getString(R.string.not_set);
});
@@ -132,7 +136,10 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
@Override
public void onResume() {
super.onResume();
PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
updateUnits();
updateReset();
updateBluetooth();
@@ -142,13 +149,13 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
private void updatePostWorkoutExport() {
Preference instantExportEnabledPreference = findPreference(getString(R.string.post_workout_export_enabled_key));
instantExportEnabledPreference.setEnabled(PreferencesUtils.isDefaultExportDirectoryUri(getContext()));
instantExportEnabledPreference.setEnabled(PreferencesUtils.isDefaultExportDirectoryUri(sharedPreferences, getContext()));
}
@Override
public void onPause() {
super.onPause();
PreferencesUtils.getSharedPreferences(getActivity()).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
@@ -176,6 +183,12 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
super.onDisplayPreferenceDialog(preference);
}
@Override
public void onDestroy() {
super.onDestroy();
sharedPreferences = null;
}
public void setDefaultActivity(String iconValue) {
if (activityPreferenceDialog != null) {
activityPreferenceDialog.updateUI(iconValue);
@@ -183,7 +196,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}
private void updateReset() {
final boolean isRecording = PreferencesUtils.isRecording(getActivity());
final boolean isRecording = PreferencesUtils.isRecording(sharedPreferences, getActivity());
Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
resetPreference.setEnabled(!isRecording);
@@ -196,7 +209,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}
private void updateUnits() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getActivity());
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getActivity());
ListPreference voiceFrequency = findPreference(getString(R.string.voice_frequency_key));
voiceFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), metricUnits));
@@ -0,0 +1,49 @@
package de.dennisguse.opentracks.stats;
public class SensorStatistics {
private final Float maxHr;
private final Float avgHr;
private final Float maxCadence;
private final Float avgCadence;
private final Float avgPower;
public SensorStatistics(Float maxHr, Float avgHr, Float maxCadence, Float avgCadence, Float avgPower) {
this.maxHr = maxHr;
this.avgHr = avgHr;
this.maxCadence = maxCadence;
this.avgCadence = avgCadence;
this.avgPower = avgPower;
}
public boolean hasHeartRate() {
return avgHr != null && maxHr != null;
}
public float getMaxHeartRate() {
return maxHr;
}
public float getAvgHeartRate() {
return avgHr;
}
public boolean hasCadence() {
return avgCadence != null && maxCadence != null;
}
public float getMaxCadence() {
return maxCadence;
}
public float getAvgCadence() {
return avgCadence;
}
public boolean hasPower() {
return avgPower != null;
}
public float getAvgPower() {
return avgPower;
}
}
@@ -25,7 +25,6 @@ import java.time.Duration;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.TrackPointUtils;
/**
* Updater for {@link TrackStatistics}.
@@ -104,10 +103,15 @@ public class TrackStatisticsUpdater {
/**
* Adds a trackPoint.
*
* @param trackPoint the trackPoint
* @param minRecordingDistance the min recording distance
* @param trackPoint the trackPoint
* @param minGPSDistance the min recording distance
*/
public void addTrackPoint(TrackPoint trackPoint, int minRecordingDistance) {
public void addTrackPoint(TrackPoint trackPoint, int minGPSDistance) {
internalAddTrackPoint(trackPoint, minGPSDistance);
Log.d(TAG, this.toString());
}
private void internalAddTrackPoint(TrackPoint trackPoint, int minGPSDistance) {
if (!trackInitialized) {
trackStatistics.setStartTime(trackPoint.getTime());
trackInitialized = true;
@@ -121,19 +125,26 @@ public class TrackStatisticsUpdater {
currentSegment.setStopTime(trackPoint.getTime());
currentSegment.setTotalTime(Duration.between(currentSegment.getStartTime(), trackPoint.getTime()));
if (trackPoint.isSegmentStart() || trackPoint.isSegmentEnd()) {
if (trackPoint.isSegmentEnd()) {
if (lastTrackPoint != null && lastMovingTrackPoint != null && lastTrackPoint != lastMovingTrackPoint) {
currentSegment.addTotalDistance(lastMovingTrackPoint.distanceTo(lastTrackPoint));
}
}
trackStatistics.merge(currentSegment);
currentSegment.reset(trackPoint.getTime());
if (trackPoint.isSegmentStart()) {
reset(trackPoint);
return;
}
lastTrackPoint = null;
lastMovingTrackPoint = null;
elevationBuffer_m.reset();
speedBuffer_ms.reset();
// Process sensor data
if (trackPoint.hasElevationGain()) {
currentSegment.addTotalElevationGain(trackPoint.getElevationGain());
}
if (trackPoint.hasElevationLoss()) {
currentSegment.addTotalElevationLoss(trackPoint.getElevationLoss());
}
if (trackPoint.hasSensorDistance()) {
currentSegment.addTotalDistance(trackPoint.getSensorDistance());
}
if (trackPoint.isSegmentEnd()) {
reset(trackPoint);
return;
}
@@ -142,39 +153,30 @@ public class TrackStatisticsUpdater {
updateAbsoluteElevation(trackPoint.getAltitude());
}
//Get elevation gain
if (trackPoint.hasElevationGain()) {
currentSegment.addTotalElevationGain(trackPoint.getElevationGain());
Log.d(TAG, "elevation gain: " + trackPoint.getElevationGain());
}
//Get elevation loss
if (trackPoint.hasElevationLoss()) {
currentSegment.addTotalElevationLoss(trackPoint.getElevationLoss());
Log.d(TAG, "elevation loss: " + trackPoint.getElevationLoss());
}
if (lastTrackPoint == null || lastMovingTrackPoint == null) {
lastTrackPoint = trackPoint;
lastMovingTrackPoint = trackPoint;
return;
}
double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint);
if (movingDistance < minRecordingDistance && !TrackPointUtils.isMoving(trackPoint)) {
speedBuffer_ms.reset();
lastTrackPoint = trackPoint;
return;
if (!trackPoint.hasSensorDistance()) {
// GPS-based distance/speed
float movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
if (movingDistance < minGPSDistance && !trackPoint.isMoving()) {
speedBuffer_ms.reset();
lastTrackPoint = trackPoint;
return;
}
// Update total distance
currentSegment.addTotalDistance(movingDistance);
}
Duration movingTime = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
if (movingTime.isNegative()) {
lastTrackPoint = trackPoint;
return;
}
// Update total distance
currentSegment.addTotalDistance(movingDistance);
// Update moving time
currentSegment.addMovingTime(movingTime);
@@ -187,6 +189,16 @@ public class TrackStatisticsUpdater {
lastMovingTrackPoint = trackPoint;
}
private void reset(TrackPoint trackPoint) {
trackStatistics.merge(currentSegment);
currentSegment.reset(trackPoint.getTime());
lastTrackPoint = null;
lastMovingTrackPoint = null;
elevationBuffer_m.reset();
speedBuffer_ms.reset();
}
public void addTrackPoint(TrackPointIterator iterator, int minRecordingDistance) {
while (iterator.hasNext()) {
TrackPoint location = iterator.next();
@@ -211,7 +223,7 @@ public class TrackStatisticsUpdater {
*/
@VisibleForTesting
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
if (!TrackPointUtils.isMoving(trackPoint)) {
if (!trackPoint.isMoving()) {
speedBuffer_ms.reset();
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_ms.setNext(trackPoint.getSpeed());
@@ -225,20 +237,14 @@ public class TrackStatisticsUpdater {
/**
* Updates an elevation reading.
*
* @param elevation the elevation
* @return the difference
*/
@VisibleForTesting
private double updateAbsoluteElevation(double elevation) {
private void updateAbsoluteElevation(double elevation) {
// Update elevation using the smoothed average
double oldAverage = elevationBuffer_m.getAverage();
elevationBuffer_m.setNext(elevation);
double newAverage = elevationBuffer_m.getAverage();
currentSegment.updateElevationExtremities(newAverage);
return newAverage - oldAverage;
}
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
@@ -268,4 +274,11 @@ public class TrackStatisticsUpdater {
return true;
}
@Override
public String toString() {
return "TrackStatisticsUpdater{" +
"trackStatistics=" + trackStatistics +
'}';
}
}
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.util;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
@@ -150,7 +151,7 @@ public class ActivityUtils {
toast.show();
}
public static void applyNightMode(Context context) {
AppCompatDelegate.setDefaultNightMode(PreferencesUtils.getDefaultNightMode(context));
public static void applyNightMode(SharedPreferences sharedPreferences, Context context) {
AppCompatDelegate.setDefaultNightMode(PreferencesUtils.getDefaultNightMode(sharedPreferences, context));
}
}
@@ -15,9 +15,7 @@ public class AnnouncementUtils {
private AnnouncementUtils() {
}
public static String getAnnouncement(Context context, TrackStatistics trackStatistics, String category, @Nullable IntervalStatistics.Interval currentInterval) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context, category);
public static String getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double distancePerTime = trackStatistics.getAverageMovingSpeed() * UnitConversions.MPS_TO_KMH;
double currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH : 0;
@@ -26,7 +24,7 @@ public class AnnouncementUtils {
return context.getString(R.string.voice_total_distance_zero);
}
if (!metricUnits) {
if (!isMetricUnits) {
distance *= UnitConversions.KM_TO_MI;
distancePerTime *= UnitConversions.KM_TO_MI;
currentDistancePerTime *= UnitConversions.KM_TO_MI;
@@ -35,15 +33,15 @@ public class AnnouncementUtils {
String rate;
String currentRate;
String currentRateMsg;
if (reportSpeed) {
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
if (isReportSpeed) {
int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime);
currentRate = context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTime), currentDistancePerTime);
currentRateMsg = context.getString(R.string.voice_speed_lap, currentRate);
} else {
double timePerDistance = distancePerTime == 0 ? 0.0 : 1 / distancePerTime;
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
int paceId = isMetricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
Duration time = Duration.ofMillis((long) (timePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS));
rate = context.getString(paceId, getAnnounceTime(context, time));
@@ -53,7 +51,7 @@ public class AnnouncementUtils {
currentRateMsg = context.getString(R.string.voice_pace_lap, currentRate);
}
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
int totalDistanceId = isMetricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg;
@@ -65,8 +63,8 @@ public class AnnouncementUtils {
private static String getAnnounceTime(Context context, Duration duration) {
String result = "";
int hours = (int) (duration.getSeconds() / (60 * 60));
int minutes = (int) (duration.getSeconds() / 60);
int hours = (int) (duration.toHours());
int minutes = (int) (duration.toMinutes() % 60);
int seconds = (int) (duration.getSeconds() % 60);
if (hours != 0) {
@@ -82,29 +80,8 @@ public class AnnouncementUtils {
return result + minutesText + " " + secondsText;
}
/**
* Gets the plural count to be used by getQuantityString.
* getQuantityString only supports integer quantities, not a double quantity like "2.2".
* <p>
* As a temporary workaround, we convert a double quantity to an integer quantity.
* If the double quantity is exactly 0, 1, or 2, then we can return these integer quantities.
* Otherwise, we cast the double quantity to an integer quantity.
* However, we need to make sure that if the casted value is 0, 1, or 2, we don't return those, instead, return the next biggest integer 3.
*
* @param d the double value
*/
private static int getQuantityCount(double d) {
if (d == 0) {
return 0;
} else if (d == 1) {
return 1;
} else if (d == 2) {
return 2;
} else {
//TODO This seems weird; why not use Math.round(d) or Math.ceil()?
int count = (int) d;
return Math.max(count, 3);
}
static int getQuantityCount(double d) {
return (int) d;
}
}
@@ -126,12 +126,12 @@ public class BluetoothUtils {
cadence = new SensorDataCycling.Cadence(address, sensorName, crankCount, crankTime);
}
SensorDataCycling.Speed speed = null;
SensorDataCycling.DistanceSpeed speed = null;
if (hasWheel && valueLength - index >= 4) {
int wheelCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
speed = new SensorDataCycling.Speed(address, sensorName, wheelCount, wheelTime);
speed = new SensorDataCycling.DistanceSpeed(address, sensorName, wheelCount, wheelTime);
}
return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed);
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.util;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
@@ -14,6 +15,8 @@ import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -26,9 +29,10 @@ public class ExportUtils {
private static final String TAG = ExportUtils.class.getSimpleName();
public static void postWorkoutExport(Context context, Track track, ExportServiceResultReceiver resultReceiver) {
if (PreferencesUtils.shouldInstantExportAfterWorkout(context)) {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(context);
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
if (PreferencesUtils.shouldInstantExportAfterWorkout(sharedPreferences, context)) {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(sharedPreferences, context);
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(sharedPreferences, context);
ExportService.enqueue(context, resultReceiver, track.getId(), trackFileFormat, directory.getUri());
}
@@ -37,7 +41,7 @@ public class ExportUtils {
public static boolean exportTrack(Context context, TrackFileFormat trackFileFormat, DocumentFile directory, Track track) {
TrackExporter trackExporter = trackFileFormat.createTrackExporter(context);
Uri exportDocumentFileUri = getExportDocumentFileUri(context, track.getId(), trackFileFormat, directory);
Uri exportDocumentFileUri = getExportDocumentFileUri(context, track, trackFileFormat, directory);
if (exportDocumentFileUri == null) {
Log.e(TAG, "Couldn't create document file for export");
return false;
@@ -62,8 +66,10 @@ public class ExportUtils {
}
}
public static boolean isExportFileExists(Track.Id trackId, String trackFileFormatExtension, List<String> filesName) {
return filesName.contains(getExportFileNameByTrackId(trackId, trackFileFormatExtension));
public static boolean isExportFileExists(UUID uuuid, String trackFileFormatExtension, List<String> filesName) {
String word = uuuid.toString().substring(0, 8) + ".*\\." + trackFileFormatExtension;
Pattern pattern = Pattern.compile(word);
return filesName.stream().anyMatch(w -> pattern.matcher(w).matches());
}
public static List<String> getAllFiles(Context context, Uri directoryUri) {
@@ -82,8 +88,8 @@ public class ExportUtils {
return fileNames;
}
private static Uri getExportDocumentFileUri(Context context, Track.Id trackId, TrackFileFormat trackFileFormat, DocumentFile directory) {
String exportFileName = getExportFileNameByTrackId(trackId, trackFileFormat.getExtension());
private static Uri getExportDocumentFileUri(Context context, Track track, TrackFileFormat trackFileFormat, DocumentFile directory) {
String exportFileName = getExportFileNameForTrack(track, trackFileFormat.getExtension());
Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
if (exportDocumentFileUri == null) {
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
@@ -94,8 +100,8 @@ public class ExportUtils {
return exportDocumentFileUri;
}
private static String getExportFileNameByTrackId(Track.Id trackId, String trackFileFormatExtension) {
return trackId.getId() + "." + trackFileFormatExtension;
private static String getExportFileNameForTrack(Track track, String trackFileFormatExtension) {
return track.getUuid().toString().substring(0, 8) + "_" + track.getName() + "." + trackFileFormatExtension;
}
private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.util;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import androidx.annotation.NonNull;
@@ -73,11 +74,13 @@ public class IntentDashboardUtils {
intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris);
intent.putExtra(EXTRAS_SHOULD_KEEP_SCREEN_ON, PreferencesUtils.shouldKeepScreenOn(context));
intent.putExtra(EXTRAS_SHOW_WHEN_LOCKED, PreferencesUtils.shouldShowStatsOnLockscreen(context));
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
intent.putExtra(EXTRAS_SHOULD_KEEP_SCREEN_ON, PreferencesUtils.shouldKeepScreenOn(sharedPreferences, context));
intent.putExtra(EXTRAS_SHOW_WHEN_LOCKED, PreferencesUtils.shouldShowStatsOnLockscreen(sharedPreferences, context));
intent.putExtra(EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK, isRecording);
if (isRecording) {
intent.putExtra(EXTRAS_SHOW_FULLSCREEN, PreferencesUtils.shouldUseFullscreen(context));
intent.putExtra(EXTRAS_SHOW_FULLSCREEN, PreferencesUtils.shouldUseFullscreen(sharedPreferences, context));
}
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -91,7 +91,7 @@ public class IntentUtils {
continue;
}
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, track.getName(), PreferencesUtils.getExportTrackFileFormat(context));
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, track.getName(), PreferencesUtils.getExportTrackFileFormat(PreferencesUtils.getSharedPreferences(context), context));
uris.add(uriAndMime.first);
mime = uriAndMime.second;
}
@@ -17,11 +17,6 @@ package de.dennisguse.opentracks.util;
import android.location.Location;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* Utility class for decimating tracks at a given level of precision.
*
@@ -31,14 +26,11 @@ public class LocationUtils {
private static final String TAG = LocationUtils.class.getSimpleName();
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
private LocationUtils() {
}
/**
* Checks if a given location is a valid (i.e. physically possible) location on Earth.
* Neither will locations with lat=0 and lng=0 as these are most likely "bad" measurements which often cause trouble.
*
* @param location the location to test
* @return true if the location is a valid location.
@@ -48,10 +40,4 @@ public class LocationUtils {
&& Math.abs(location.getLatitude()) <= 90
&& Math.abs(location.getLongitude()) <= 180;
}
public static boolean isTrackPointOld(TrackPoint trackPoint) {
return Duration.between(trackPoint.getTime(), Instant.now())
.minus(MAX_LOCATION_AGE)
.isNegative();
}
}
@@ -42,35 +42,32 @@ public class PreferencesUtils {
private PreferencesUtils() {
}
@Deprecated //Should only be used to get a sharedPreference for more than one interaction!
public static SharedPreferences getSharedPreferences(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
public static void register(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
getSharedPreferences(context).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
public static void unregister(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
getSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Deprecated
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
//NOTE: This was also used to recover from a service restart, but this data should not be exposed to the whole application.
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
@Deprecated //Use the TrackRecordingService
public static Track.Id getRecordingTrackId(Context context) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
public static Track.Id getRecordingTrackId(Context context, SharedPreferences sharedPreferences) {
return new Track.Id(sharedPreferences.getLong(getKey(context, R.string.recording_track_id_key), RECORDING_TRACK_ID_DEFAULT));
}
public static String getDefaultActivity(Context context) {
return getString(context, R.string.default_activity_key, context.getString(R.string.default_activity_default));
@Deprecated //Use the TrackRecordingService
public static Track.Id getRecordingTrackId(SharedPreferences sharedPreferences, Context context) {
return getRecordingTrackId(context, sharedPreferences);
}
public static void setDefaultActivity(Context context, String newDefaultActivity) {
setString(context, R.string.default_activity_key, newDefaultActivity);
public static String getDefaultActivity(SharedPreferences sharedPreferences, Context context) {
return getString(sharedPreferences, context, R.string.default_activity_key, context.getString(R.string.default_activity_default));
}
public static void setDefaultActivity(SharedPreferences sharedPreferences, Context context, String newDefaultActivity) {
setString(sharedPreferences, context, R.string.default_activity_key, newDefaultActivity);
}
/**
@@ -94,44 +91,19 @@ public class PreferencesUtils {
return key == null || key.equals(getKey(context, keyId));
}
/**
* Gets a boolean preference value.
*
* @param context the context
* @param keyId the key id
* @param defaultValue the default value
*/
private static boolean getBoolean(Context context, int keyId, boolean defaultValue) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
private static boolean getBoolean(SharedPreferences sharedPreferences, Context context, int keyId, boolean defaultValue) {
return sharedPreferences.getBoolean(getKey(context, keyId), defaultValue);
}
/**
* Sets a boolean preference value.
*
* @param context the context
* @param keyId the key id
* @param value the value
*/
//TODO Don't use; this function is only to be used TrackRecordingService and will be removed.
@VisibleForTesting
public static void setBoolean(Context context, int keyId, boolean value) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
public static void setBoolean(SharedPreferences sharedPreferences, Context context, int keyId, boolean value) {
Editor editor = sharedPreferences.edit();
editor.putBoolean(getKey(context, keyId), value);
editor.apply();
}
/**
* Gets an integer preference value.
*
* @param context the context
* @param keyId the key id
* @param defaultValue the default value
*/
private static int getInt(Context context, int keyId, int defaultValue) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
private static int getInt(SharedPreferences sharedPreferences, Context context, int keyId, int defaultValue) {
try {
return sharedPreferences.getInt(getKey(context, keyId), defaultValue);
} catch (ClassCastException e) {
@@ -147,74 +119,42 @@ public class PreferencesUtils {
}
}
/**
* Sets an integer preference value.
*
* @param context the context
* @param keyId the key id
* @param value the value
*/
@VisibleForTesting
public static void setInt(Context context, int keyId, int value) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
public static void setInt(SharedPreferences sharedPreferences, Context context, int keyId, int value) {
Editor editor = sharedPreferences.edit();
editor.putInt(getKey(context, keyId), value);
editor.apply();
}
/**
* Sets a long preference value.
*
* @param context the context
* @param keyId the key id
* @param value the value
*/
//TODO Don't use; this function is only to be used TrackRecordingService and will be removed.
@Deprecated
@VisibleForTesting
public static void setLong(Context context, int keyId, long value) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
public static void setLong(SharedPreferences sharedPreferences, Context context, int keyId, long value) {
Editor editor = sharedPreferences.edit();
editor.putLong(getKey(context, keyId), value);
editor.apply();
}
/**
* Gets a string preference value.
*
* @param context the context
* @param keyId the key id
* @param defaultValue default value
*/
public static String getString(Context context, int keyId, String defaultValue) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
public static String getString(SharedPreferences sharedPreferences, Context context, int keyId, String defaultValue) {
return sharedPreferences.getString(getKey(context, keyId), defaultValue);
}
/**
* Sets a string preference value.
*
* @param context the context
* @param keyId the key id
* @param value the value
*/
@VisibleForTesting
public static void setString(Context context, int keyId, String value) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
public static void setString(SharedPreferences sharedPreferences, Context context, int keyId, String value) {
Editor editor = sharedPreferences.edit();
editor.putString(getKey(context, keyId), value);
editor.apply();
}
public static boolean isMetricUnits(Context context) {
public static boolean isMetricUnits(SharedPreferences sharedPreferences, Context context) {
final String STATS_UNIT = context.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(context, R.string.stats_units_key, STATS_UNIT));
return STATS_UNIT.equals(getString(sharedPreferences, context, R.string.stats_units_key, STATS_UNIT));
}
public static boolean isReportSpeed(Context context, String category) {
public static boolean isReportSpeed(SharedPreferences sharedPreferences, Context context, String category) {
final String STATS_RATE_DEFAULT = context.getString(R.string.stats_rate_default);
String currentStatsRate = getString(context, R.string.stats_rate_key, STATS_RATE_DEFAULT);
if (currentStatsRate.equals(getString(context, R.string.stats_rate_speed_or_pace_default, STATS_RATE_DEFAULT))) {
String currentStatsRate = getString(sharedPreferences, context, R.string.stats_rate_key, STATS_RATE_DEFAULT);
if (currentStatsRate.equals(getString(sharedPreferences, context, R.string.stats_rate_speed_or_pace_default, STATS_RATE_DEFAULT))) {
return TrackIconUtils.isSpeedIcon(context, category);
}
@@ -222,8 +162,8 @@ public class PreferencesUtils {
}
@Deprecated //Use TrackRecordingService
public static boolean isRecordingTrackPaused(Context context) {
return getBoolean(context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(context));
public static boolean isRecordingTrackPaused(SharedPreferences sharedPreferences, Context context) {
return getBoolean(sharedPreferences, context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(context));
}
@Deprecated //Use TrackRecordingService
@@ -232,9 +172,9 @@ public class PreferencesUtils {
}
@Deprecated //Use TrackRecordingService
public static void defaultRecordingTrackPaused(Context context) {
public static void defaultRecordingTrackPaused(SharedPreferences sharedPreferences, Context context) {
final boolean RECORDING_TRACK_PAUSED = context.getResources().getBoolean(R.bool.recording_track_paused_default);
setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
setBoolean(sharedPreferences, context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
}
private static String getBluetoothSensorAddressNone(Context context) {
@@ -245,78 +185,74 @@ public class PreferencesUtils {
return getBluetoothSensorAddressNone(context).equals(currentValue);
}
public static String getBluetoothHeartRateSensorAddress(Context context) {
return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone(context));
public static String getBluetoothHeartRateSensorAddress(SharedPreferences sharedPreferences, Context context) {
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone(context));
}
public static String getBluetoothCyclingCadenceSensorAddress(Context context) {
return getString(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone(context));
public static String getBluetoothCyclingCadenceSensorAddress(SharedPreferences sharedPreferences, Context context) {
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone(context));
}
public static String getBluetoothCyclingSpeedSensorAddress(Context context) {
return getString(context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
public static String getBluetoothCyclingSpeedSensorAddress(SharedPreferences sharedPreferences, Context context) {
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
}
public static int getWheelCircumference(Context context) {
public static int getWheelCircumference(SharedPreferences sharedPreferences, Context context) {
final int DEFAULT = Integer.parseInt(context.getResources().getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default));
return getInt(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT);
return getInt(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT);
}
public static boolean isBluetoothCyclingPowerSensorAddressNone(Context context) {
return isBluetoothSensorAddressNone(context, getBluetoothCyclingPowerSensorAddress(context));
public static String getBluetoothCyclingPowerSensorAddress(SharedPreferences sharedPreferences, Context context) {
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_power_key, getBluetoothSensorAddressNone(context));
}
public static String getBluetoothCyclingPowerSensorAddress(Context context) {
return getString(context, R.string.settings_sensor_bluetooth_cycling_power_key, getBluetoothSensorAddressNone(context));
}
public static boolean shouldShowStatsOnLockscreen(Context context) {
public static boolean shouldShowStatsOnLockscreen(SharedPreferences sharedPreferences, Context context) {
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
return getBoolean(context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
return getBoolean(sharedPreferences, context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
}
public static boolean shouldKeepScreenOn(Context context) {
public static boolean shouldKeepScreenOn(SharedPreferences sharedPreferences, Context context) {
final boolean DEFAULT = context.getResources().getBoolean(R.bool.stats_keep_screen_on_while_recording_default);
return getBoolean(context, R.string.stats_keep_screen_on_while_recording_key, DEFAULT);
return getBoolean(sharedPreferences, context, R.string.stats_keep_screen_on_while_recording_key, DEFAULT);
}
public static boolean shouldUseFullscreen(Context context) {
public static boolean shouldUseFullscreen(SharedPreferences sharedPreferences, Context context) {
final boolean DEFAULT = context.getResources().getBoolean(R.bool.stats_fullscreen_while_recording_default);
return getBoolean(context, R.string.stats_fullscreen_while_recording_key, DEFAULT);
return getBoolean(sharedPreferences, context, R.string.stats_fullscreen_while_recording_key, DEFAULT);
}
public static boolean isShowStatsElevation(Context context) {
public static boolean isShowStatsElevation(SharedPreferences sharedPreferences, Context context) {
final boolean STATS_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_elevation_default);
return getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
return getBoolean(sharedPreferences, context, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
}
public static boolean isStatsShowCoordinate(Context context) {
public static boolean isStatsShowCoordinate(SharedPreferences sharedPreferences, Context context) {
final boolean STATS_SHOW_COORDINATE = context.getResources().getBoolean(R.bool.stats_show_coordinate_default);
return getBoolean(context, R.string.stats_show_coordinate_key, STATS_SHOW_COORDINATE);
return getBoolean(sharedPreferences, context, R.string.stats_show_coordinate_key, STATS_SHOW_COORDINATE);
}
public static int getVoiceFrequency(Context context) {
public static int getVoiceFrequency(SharedPreferences sharedPreferences, Context context) {
final int VOICE_FREQUENCY_DEFAULT = Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default));
return getInt(context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
return getInt(sharedPreferences, context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
}
public static int getRecordingDistanceInterval(Context context) {
return getInt(context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context));
public static int getRecordingDistanceInterval(SharedPreferences sharedPreferences, Context context) {
return getInt(sharedPreferences, context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context));
}
public static int getRecordingDistanceIntervalDefault(Context context) {
return Integer.parseInt(context.getResources().getString(R.string.recording_distance_interval_default));
}
public static int getMaxRecordingDistance(Context context) {
public static int getMaxRecordingDistance(SharedPreferences sharedPreferences, Context context) {
final int MAX_RECORDING_DISTANCE = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
return getInt(context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE);
return getInt(sharedPreferences, context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE);
}
public static int getMinRecordingInterval(Context context) {
//TODO Duration
public static int getMinRecordingInterval(SharedPreferences sharedPreferences, Context context) {
final int MIN_RECORDING_INTERVAL = Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
return getInt(context, R.string.min_recording_interval_key, MIN_RECORDING_INTERVAL);
return getInt(sharedPreferences, context, R.string.min_recording_interval_key, MIN_RECORDING_INTERVAL);
}
public static int getMinRecordingIntervalAdaptAccuracy(Context context) {
@@ -331,19 +267,19 @@ public class PreferencesUtils {
return Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
}
public static int getRecordingGPSAccuracy(Context context) {
public static int getRecordingGPSAccuracy(SharedPreferences sharedPreferences, Context context) {
final int RECORDING_GPS_ACCURACY = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
return getInt(context, R.string.recording_gps_accuracy_key, RECORDING_GPS_ACCURACY);
return getInt(sharedPreferences, context, R.string.recording_gps_accuracy_key, RECORDING_GPS_ACCURACY);
}
public static boolean shouldInstantExportAfterWorkout(Context context) {
public static boolean shouldInstantExportAfterWorkout(SharedPreferences sharedPreferences, Context context) {
final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = context.getResources().getBoolean(R.bool.post_workout_export_enabled_default);
return getBoolean(context, R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT);
return getBoolean(sharedPreferences, context, R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT);
}
public static TrackFileFormat getExportTrackFileFormat(Context context) {
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(context, R.string.export_trackfileformat_default, null);
String trackFileFormatName = getString(context, R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
public static TrackFileFormat getExportTrackFileFormat(SharedPreferences sharedPreferences, Context context) {
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(sharedPreferences, context, R.string.export_trackfileformat_default, null);
String trackFileFormatName = getString(sharedPreferences, context, R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
try {
return TrackFileFormat.valueOf(trackFileFormatName);
} catch (Exception e) {
@@ -351,24 +287,24 @@ public class PreferencesUtils {
}
}
public static boolean getPreventReimportTracks(Context context) {
final boolean defaultValue = getBoolean(context, R.bool.import_prevent_reimport_default, false);
return getBoolean(context, R.string.import_prevent_reimport_key, defaultValue);
public static boolean getPreventReimportTracks(SharedPreferences sharedPreferences, Context context) {
final boolean defaultValue = getBoolean(sharedPreferences, context, R.bool.import_prevent_reimport_default, false);
return getBoolean(sharedPreferences, context, R.string.import_prevent_reimport_key, defaultValue);
}
/**
* @return {@link androidx.appcompat.app.AppCompatDelegate}.MODE_*
*/
public static int getDefaultNightMode(Context context) {
public static int getDefaultNightMode(SharedPreferences sharedPreferences, Context context) {
final String defaultValue = getKey(context, R.string.night_mode_default);
final String value = getString(context, R.string.night_mode_key, defaultValue);
final String value = getString(sharedPreferences, context, R.string.night_mode_key, defaultValue);
return Integer.parseInt(value);
}
@Deprecated //Use TrackRecordingService
public static boolean isRecording(Context context) {
return isRecording(getRecordingTrackId(context));
public static boolean isRecording(SharedPreferences sharedPreferences, Context context) {
return isRecording(getRecordingTrackId(context, sharedPreferences));
}
@Deprecated
@@ -380,16 +316,19 @@ public class PreferencesUtils {
return recordingTrackId.getId() != RECORDING_TRACK_ID_DEFAULT;
}
public static void resetPreferences(Context context, boolean readAgain) {
public static SharedPreferences resetPreferences(Context context, boolean readAgain) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
if (readAgain) {
// We want to really clear settings now.
getSharedPreferences(context).edit().clear().commit();
sharedPreferences.edit().clear().commit();
}
PreferenceManager.setDefaultValues(context, R.xml.settings, readAgain);
return sharedPreferences;
}
public static DocumentFile getDefaultExportDirectoryUri(Context context) {
String singleExportDirectorySettingsKey = getString(context, R.string.settings_default_export_directory_key, null);
public static DocumentFile getDefaultExportDirectoryUri(SharedPreferences sharedPreferences, Context context) {
String singleExportDirectorySettingsKey = getString(sharedPreferences, context, R.string.settings_default_export_directory_key, null);
if (singleExportDirectorySettingsKey == null) {
return null;
}
@@ -401,12 +340,12 @@ public class PreferencesUtils {
return null;
}
public static void setDefaultExportDirectoryUri(Context context, Uri directoryUri) {
public static void setDefaultExportDirectoryUri(SharedPreferences sharedPreferences, Context context, Uri directoryUri) {
String value = directoryUri != null ? directoryUri.toString() : null;
setString(context, R.string.settings_default_export_directory_key, value);
setString(sharedPreferences, context, R.string.settings_default_export_directory_key, value);
}
public static boolean isDefaultExportDirectoryUri(Context context) {
return getDefaultExportDirectoryUri(context) != null;
public static boolean isDefaultExportDirectoryUri(SharedPreferences sharedPreferences, Context context) {
return getDefaultExportDirectoryUri(sharedPreferences, context) != null;
}
}
@@ -41,8 +41,9 @@ public class TrackNameUtils {
private TrackNameUtils() {
}
//TODO Should not access sharedPreferences; trackName should be an ENUM.
public static String getTrackName(Context context, Track.Id trackId, Instant startTime) {
String trackName = PreferencesUtils.getString(context, R.string.track_name_key, context.getString(R.string.track_name_default));
String trackName = PreferencesUtils.getString(PreferencesUtils.getSharedPreferences(context), context, R.string.track_name_key, context.getString(R.string.track_name_default));
if (trackName.equals(context.getString(R.string.settings_recording_track_name_date_local_value))) {
return StringUtils.formatDateTime(context, startTime);

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