Compare commits

...

68 Commits

Author SHA1 Message Date
Dennis Guse c691f6faba Release: v4.29.0 2026-09-28 21:31:19 +02:00
Dennis Guse 58c4b38c0b Export: disable "export all tracks into one file"-functionality as markers cannot be correctly restored (KML/KMZ).
Part of #2383.
2026-09-21 21:04:47 +02:00
Dennis Guse 0225497d88 XML: add XSD for GPX1.1 and KML2.3. 2026-09-19 10:37:12 +02:00
Dennis Guse 1834f943e4 Cleanup.
Part of #2383.
2026-09-12 19:46:04 +02:00
Dennis Guse 7505e6d693 Added KMLTrackExporterTest.
Part of #2383.
2026-09-12 19:43:54 +02:00
Dennis Guse d5e83cf057 Cleanup: move track.uuid generation to ContentProviderUtils.
Part of #2383.
2026-09-12 19:34:18 +02:00
gallegonovato f8279c7095 Translated using Weblate (Spanish)
Currently translated at 100.0% (551 of 551 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/es/
2026-09-05 10:26:13 +00:00
Kito 465e3e9be8 Translated using Weblate (Indonesian)
Currently translated at 3.6% (7 of 191 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/id/
2026-09-05 10:26:13 +00:00
gallegonovato f38999a970 Translated using Weblate (Spanish)
Currently translated at 23.5% (45 of 191 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/es/
2026-09-05 10:26:13 +00:00
Dennis Guse 4dfd2485bf Bugfix: avoid crash in exporter. 2026-09-03 17:50:47 +02:00
Dennis Guse 10273cd772 TrackRecordingManager: rather crash than try to load invalid data. 2026-09-03 17:50:47 +02:00
Bone NI fbfe6f3ab8 Added translation using Weblate (Lao) 2026-09-02 09:54:56 +00:00
love80312 86726c5e0e Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (551 of 551 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hant/
2026-09-02 09:54:55 +00:00
love80312 b711c4fdc5 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 1.5% (3 of 190 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/zh_Hant/
2026-09-02 09:54:55 +00:00
CI/CD Bot by pstorch cefdb1280a chore(deps): update dependency com.android.tools.build:gradle to v9.4.0 2026-09-01 14:35:00 +00:00
Dennis Guse 91b379c328 Release: v4.28.3 2026-08-31 20:07:23 +02:00
Dennis Guse e28a84fc88 Allow to disable segment creation (max recording distance).
Fixes #2389.
2026-08-28 07:07:06 +02:00
Dennis Guse c2d8d06204 Merge remote-tracking branch 'weblate/main' 2026-08-28 06:47:06 +02:00
José Rebelo 7ca871c699 Bugfix: Respect declared namespace prefixes 2026-08-26 15:21:06 +01:00
Dennis Guse 337c50eb91 Cleanup: qName. 2026-08-26 07:34:22 +02:00
Dany Mestas 2ff8d21897 GPX: match activity types case-insensitively on import. 2026-08-25 22:23:57 +02:00
Dennis Guse 2d4e987812 GPX: export moving speed in summary. 2026-08-24 17:26:11 +02:00
CI/CD Bot by pstorch fb928ccc45 chore(deps): update dependency com.android.tools.build:gradle to v9.3.2 2026-08-24 03:34:20 +00:00
Dennis Guse ab8791a620 Release: v4.28.2 2026-08-22 19:20:38 +02:00
Dennis Guse 7c03085761 GPX: on import ignore TrackPoints without time.
Previous behavior: abort import.

Fixes #2384.
2026-08-19 20:45:23 +02:00
Dennis Guse 12a8686896 Cleanup. 2026-08-19 20:38:05 +02:00
Dennis Guse e12bb967e6 Cleanup. 2026-08-19 20:29:25 +02:00
Dennis Guse d56b018676 Bugfix: when stopping a recording, immediately stop the idle detection timer.
Fixes #2386.
2026-08-19 20:29:25 +02:00
CI/CD Bot by pstorch a6fd701004 chore(deps): update dependency gradle to v9.7.1 2026-08-19 15:34:29 +00:00
Dennis Guse ce26e20571 Sensor: on start register GPS only once.
Part of #2369.
2026-08-17 15:49:14 +02:00
CI/CD Bot by pstorch d3ff2061c6 chore(deps): update dependency androidx.appcompat:appcompat to v1.8.0 2026-08-12 17:34:26 +00:00
Dennis Guse aa63474114 Release: v4.28.1 2026-08-08 12:06:32 +02:00
CI/CD Bot by pstorch cf545dc17b chore(deps): update dependency gradle to v9.7.0 2026-08-06 14:36:45 +00:00
Dennis Guse a542f45661 Bugfix: HealthConnect doesn't provide granted permissions in time.
Fixes #2376.
2026-08-05 19:16:00 +02:00
Dennis Guse 1eeb706330 Bugfix: HealthConnect requires unique timestamps for locations.
Part of #2376.
2026-08-05 19:13:16 +02:00
Dennis Guse 01ea65742b HealthConnect: send total distance, elevation gain and speed. 2026-08-04 17:25:46 +02:00
Dennis Guse e9456919cb Bug: don't crash in settings if HealthConnect is not installed.
Part of #2376.
2026-08-03 20:27:47 +02:00
Priit Jõerüüt 2a8e1c99b6 Translated using Weblate (Estonian)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-07-29 20:34:20 +02:00
jimkats 245854581b Translated using Weblate (Greek)
Currently translated at 70.3% (390 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-07-29 20:34:20 +02:00
CI/CD Bot by pstorch e372a780f0 Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 2026-07-29 17:43:19 +00:00
Priit Jõerüüt 25a2f978c4 Translated using Weblate (Estonian)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-07-29 03:59:43 +00:00
jimkats 36c3885dce Translated using Weblate (Greek)
Currently translated at 70.3% (390 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-07-29 03:59:43 +00:00
Dennis Guse 00be0b565c Cleanup: removed unused methods and constants. 2026-07-26 15:50:01 +02:00
Dennis Guse 8b1e1cf294 Cleanup. 2026-07-26 15:34:34 +02:00
Dennis Guse ce313ba4fd Cleanup: add missing @NonNull annotation. 2026-07-26 15:33:29 +02:00
Dennis Guse df501bde0d Cleanup: removed empty localization (ia). 2026-07-26 15:30:55 +02:00
Dennis Guse 30ed8d8ba3 Cleanup: CustomContentProvider by inlining into UrlType. 2026-07-24 20:54:16 +02:00
Dennis Guse ea2127846d Cleanup: CustomContentProvider by inlining into UrlType. 2026-07-24 19:51:19 +02:00
Dennis Guse ac0da5e9d1 Cleanup: CustomContentProvider by inlining into UrlType. 2026-07-24 19:51:19 +02:00
Dennis Guse 47fea690c9 FOREGROUND_SERVICE is mandatory.
Revert a1fa95094f
2026-07-24 19:51:01 +02:00
Dennis Guse a1fa95094f Removed unused translations. 2026-07-23 22:39:52 +02:00
Dennis Guse c4a1fb0d98 Cleanup. 2026-07-23 22:39:17 +02:00
Dennis Guse 57cd4b3780 Bugfix: gravel was not selectable.
Introduced in fb25055f3e
2026-07-23 22:34:43 +02:00
Dennis Guse 5c645712ff Fix translations. 2026-07-23 22:31:09 +02:00
Dennis Guse e724baaafa Sensors: Polar Loop (Gen 2) can be used for heartrate. 2026-07-23 22:10:06 +02:00
CI/CD Bot by pstorch f55dcd7162 Update dependency com.android.tools.build:gradle to v9.3.1 2026-07-23 14:36:34 +00:00
Dennis Guse 74169cb819 Code cleanup using static code analysis (Android Studio). 2026-07-21 07:31:25 +02:00
Dennis Guse 1821f47cf1 Code analyze: only use String.replace() instead of String.replaceAll(). 2026-07-21 07:30:19 +02:00
Dennis Guse 925a253bbb Fix tests: PreferencesUtils wasn't visible enough (anymore). 2026-07-21 07:22:51 +02:00
Dennis Guse e89ae1c2df Translations: must use <a>. 2026-07-21 06:39:06 +02:00
Dennis Guse 75058118e0 Remove unused translation. 2026-07-21 06:39:06 +02:00
CI/CD Bot by pstorch 96559230f8 Update dependency com.android.tools.build:gradle to v9.3.0 2026-07-14 14:34:39 +00:00
ghose f16f2cfc64 Translated using Weblate (Galician)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2026-07-12 06:07:51 +00:00
Outbreak2096 7f353a5d13 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2026-07-08 18:08:05 +00:00
SomeTr 2453edaa65 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2026-07-08 18:08:04 +00:00
Priit Jõerüüt 1b1568511a Translated using Weblate (Estonian)
Currently translated at 98.7% (547 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-07-08 18:08:04 +00:00
miler 29a36c3ea7 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/es_419/
2026-07-08 18:08:04 +00:00
gallegonovato 71d0c7cc92 Translated using Weblate (Spanish)
Currently translated at 8.5% (16 of 187 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://translate.codeberg.org/projects/opentracks/app-store-metadata-files/es/
2026-07-08 18:08:04 +00:00
163 changed files with 4948 additions and 671 deletions
+1
View File
@@ -8,6 +8,7 @@
* Polar H10
* Polar OH1
Has updatable firmware that requires a user account at polar.com
* Polar Loop Gen 2
* Wahoo Tickr (Model: WFBTHR02)
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
* Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again)
+5 -5
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:9.2.1'
classpath 'com.android.tools.build:gradle:9.4.0'
}
}
@@ -47,8 +47,8 @@ android {
defaultConfig {
applicationId 'de.dennisguse.opentracks'
versionCode 6698
versionName "v4.28.0"
versionCode 6766
versionName "v4.29.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -145,12 +145,12 @@ android {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.appcompat:appcompat:1.8.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.documentfile:documentfile:1.1.0'
implementation 'androidx.gridlayout:gridlayout:1.1.0'
implementation 'com.google.android.material:material:1.14.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.2'
implementation 'androidx.core:core:1.19.0'
implementation 'androidx.core:core-splashscreen:1.2.0'
implementation 'androidx.mediarouter:mediarouter:1.8.1'
+788
View File
@@ -0,0 +1,788 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.topografix.com/GPX/1/1"
targetNamespace="http://www.topografix.com/GPX/1/1"
elementFormDefault="qualified">
<xsd:annotation>
<xsd:documentation>
GPX schema version 1.1 - For more information on GPX and this schema, visit http://www.topografix.com/gpx.asp
GPX uses the following conventions: all coordinates are relative to the WGS84 datum. All measurements are in metric units.
</xsd:documentation>
</xsd:annotation>
<xsd:element name="gpx" type="gpxType">
<xsd:annotation>
<xsd:documentation>
GPX is the root element in the XML file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:complexType name="gpxType">
<xsd:annotation>
<xsd:documentation>
GPX documents contain a metadata header, followed by waypoints, routes, and tracks. You can add your own elements
to the extensions section of the GPX document.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="metadata" type="metadataType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Metadata about the file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="wpt" type="wptType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
A list of waypoints.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="rte" type="rteType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
A list of routes.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="trk" type="trkType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
A list of tracks.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="extensions" type="extensionsType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
You can add extend GPX by adding your own elements from another schema here.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="version" type="xsd:string" use="required" fixed="1.1">
<xsd:annotation>
<xsd:documentation>
You must include the version number in your GPX document.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="creator" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
You must include the name or URL of the software that created your GPX document. This allows others to
inform the creator of a GPX instance document that fails to validate.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="metadataType">
<xsd:annotation>
<xsd:documentation>
Information about the GPX file, author, and copyright restrictions goes in the metadata section. Providing rich,
meaningful information about your GPX files allows others to search for and use your GPS data.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence> <!-- elements must appear in this order -->
<xsd:element name="name" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
The name of the GPX file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="desc" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
A description of the contents of the GPX file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="author" type="personType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
The person or organization who created the GPX file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="copyright" type="copyrightType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Copyright and license information governing use of the file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="link" type="linkType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
URLs associated with the location described in the file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="time" type="xsd:dateTime" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
The creation date of the file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="keywords" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Keywords associated with the file. Search engines or databases can use this information to classify the data.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="bounds" type="boundsType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Minimum and maximum coordinates which describe the extent of the coordinates in the file.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="extensions" type="extensionsType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
You can add extend GPX by adding your own elements from another schema here.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="wptType">
<xsd:annotation>
<xsd:documentation>
wpt represents a waypoint, point of interest, or named feature on a map.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence> <!-- elements must appear in this order -->
<!-- Position info -->
<xsd:element name="ele" type="xsd:decimal" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Elevation (in meters) of the point.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="time" type="xsd:dateTime" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! Conforms to ISO 8601 specification for date/time representation. Fractional seconds are allowed for millisecond timing in tracklogs.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="magvar" type="degreesType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Magnetic variation (in degrees) at the point
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="geoidheight" type="xsd:decimal" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<!-- Description info -->
<xsd:element name="name" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
The GPS name of the waypoint. This field will be transferred to and from the GPS. GPX does not place restrictions on the length of this field or the characters contained in it. It is up to the receiving application to validate the field before sending it to the GPS.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="cmt" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
GPS waypoint comment. Sent to GPS as comment.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="desc" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
A text description of the element. Holds additional information about the element intended for the user, not the GPS.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="src" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Source of data. Included to give user some idea of reliability and accuracy of data. "Garmin eTrex", "USGS quad Boston North", e.g.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="link" type="linkType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
Link to additional information about the waypoint.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="sym" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. If the GPS abbreviates words, spell them out.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="type" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Type (classification) of the waypoint.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<!-- Accuracy info -->
<xsd:element name="fix" type="fixType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Type of GPX fix.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="sat" type="xsd:nonNegativeInteger" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Number of satellites used to calculate the GPX fix.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="hdop" type="xsd:decimal" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Horizontal dilution of precision.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="vdop" type="xsd:decimal" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Vertical dilution of precision.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="pdop" type="xsd:decimal" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Position dilution of precision.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="ageofdgpsdata" type="xsd:decimal" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Number of seconds since last DGPS update.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="dgpsid" type="dgpsStationType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
ID of DGPS station used in differential correction.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="extensions" type="extensionsType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
You can add extend GPX by adding your own elements from another schema here.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="lat" type="latitudeType" use="required">
<xsd:annotation>
<xsd:documentation>
The latitude of the point. This is always in decimal degrees, and always in WGS84 datum.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="lon" type="longitudeType" use="required">
<xsd:annotation>
<xsd:documentation>
The longitude of the point. This is always in decimal degrees, and always in WGS84 datum.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="rteType">
<xsd:annotation>
<xsd:documentation>
rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="name" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
GPS name of route.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="cmt" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
GPS comment for route.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="desc" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Text description of route for user. Not sent to GPS.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="src" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Source of data. Included to give user some idea of reliability and accuracy of data.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="link" type="linkType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
Links to external information about the route.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="number" type="xsd:nonNegativeInteger" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
GPS route number.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="type" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Type (classification) of route.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="extensions" type="extensionsType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
You can add extend GPX by adding your own elements from another schema here.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="rtept" type="wptType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
A list of route points.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="trkType">
<xsd:annotation>
<xsd:documentation>
trk represents a track - an ordered list of points describing a path.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="name" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
GPS name of track.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="cmt" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
GPS comment for track.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="desc" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
User description of track.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="src" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Source of data. Included to give user some idea of reliability and accuracy of data.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="link" type="linkType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
Links to external information about track.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="number" type="xsd:nonNegativeInteger" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
GPS track number.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="type" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Type (classification) of track.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="extensions" type="extensionsType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
You can add extend GPX by adding your own elements from another schema here.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="trkseg" type="trksegType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="extensionsType">
<xsd:annotation>
<xsd:documentation>
You can add extend GPX by adding your own elements from another schema here.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
You can add extend GPX by adding your own elements from another schema here.
</xsd:documentation>
</xsd:annotation>
</xsd:any>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="trksegType">
<xsd:annotation>
<xsd:documentation>
A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence> <!-- elements must appear in this order -->
<xsd:element name="trkpt" type="wptType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
A Track Point holds the coordinates, elevation, timestamp, and metadata for a single point in a track.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="extensions" type="extensionsType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
You can add extend GPX by adding your own elements from another schema here.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="copyrightType">
<xsd:annotation>
<xsd:documentation>
Information about the copyright holder and any license governing use of this file. By linking to an appropriate license,
you may place your data into the public domain or grant additional usage rights.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence> <!-- elements must appear in this order -->
<xsd:element name="year" type="xsd:gYear" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Year of copyright.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="license" type="xsd:anyURI" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Link to external file containing license text.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="author" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
Copyright holder (TopoSoft, Inc.)
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="linkType">
<xsd:annotation>
<xsd:documentation>
A link to an external resource (Web page, digital photo, video clip, etc) with additional information.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence> <!-- elements must appear in this order -->
<xsd:element name="text" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Text of hyperlink.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="type" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Mime type of content (image/jpeg)
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="href" type="xsd:anyURI" use="required">
<xsd:annotation>
<xsd:documentation>
URL of hyperlink.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="emailType">
<xsd:annotation>
<xsd:documentation>
An email address. Broken into two parts (id and domain) to help prevent email harvesting.
</xsd:documentation>
</xsd:annotation>
<xsd:attribute name="id" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
id half of email address (billgates2004)
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="domain" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
domain half of email address (hotmail.com)
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="personType">
<xsd:annotation>
<xsd:documentation>
A person or organization.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence> <!-- elements must appear in this order -->
<xsd:element name="name" type="xsd:string" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Name of person or organization.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="email" type="emailType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Email address.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="link" type="linkType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
Link to Web site or other external information about person.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ptType">
<xsd:annotation>
<xsd:documentation>
A geographic point with optional elevation and time. Available for use by other schemas.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence> <!-- elements must appear in this order -->
<xsd:element name="ele" type="xsd:decimal" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
The elevation (in meters) of the point.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="time" type="xsd:dateTime" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
The time that the point was recorded.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="lat" type="latitudeType" use="required">
<xsd:annotation>
<xsd:documentation>
The latitude of the point. Decimal degrees, WGS84 datum.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="lon" type="longitudeType" use="required">
<xsd:annotation>
<xsd:documentation>
The latitude of the point. Decimal degrees, WGS84 datum.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="ptsegType">
<xsd:annotation>
<xsd:documentation>
An ordered sequence of points. (for polygons or polylines, e.g.)
</xsd:documentation>
</xsd:annotation>
<xsd:sequence> <!-- elements must appear in this order -->
<xsd:element name="pt" type="ptType" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
Ordered list of geographic points.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="boundsType">
<xsd:annotation>
<xsd:documentation>
Two lat/lon pairs defining the extent of an element.
</xsd:documentation>
</xsd:annotation>
<xsd:attribute name="minlat" type="latitudeType" use="required">
<xsd:annotation>
<xsd:documentation>
The minimum latitude.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="minlon" type="longitudeType" use="required">
<xsd:annotation>
<xsd:documentation>
The minimum longitude.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="maxlat" type="latitudeType" use="required">
<xsd:annotation>
<xsd:documentation>
The maximum latitude.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="maxlon" type="longitudeType" use="required">
<xsd:annotation>
<xsd:documentation>
The maximum longitude.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:simpleType name="latitudeType">
<xsd:annotation>
<xsd:documentation>
The latitude of the point. Decimal degrees, WGS84 datum.
</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:decimal">
<xsd:minInclusive value="-90.0"/>
<xsd:maxInclusive value="90.0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="longitudeType">
<xsd:annotation>
<xsd:documentation>
The longitude of the point. Decimal degrees, WGS84 datum.
</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:decimal">
<xsd:minInclusive value="-180.0"/>
<xsd:maxExclusive value="180.0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="degreesType">
<xsd:annotation>
<xsd:documentation>
Used for bearing, heading, course. Units are decimal degrees, true (not magnetic).
</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:decimal">
<xsd:minInclusive value="0.0"/>
<xsd:maxExclusive value="360.0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="fixType">
<xsd:annotation>
<xsd:documentation>
Type of GPS fix. none means GPS had no fix. To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used
</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="2d"/>
<xsd:enumeration value="3d"/>
<xsd:enumeration value="dgps"/>
<xsd:enumeration value="pps"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="dgpsStationType">
<xsd:annotation>
<xsd:documentation>
Represents a differential GPS station.
</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="0"/>
<xsd:maxInclusive value="1023"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
+2274
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
v4.28.1: OpenTracks
Changes:
- HealthConnect: sending total distance, speed and elevation gain
Bugfix:
- Gravel activity was not selectable
Developer:
- Cleanup ContentProvider URLs
@@ -0,0 +1,7 @@
v4.28.2: OpenTracks
Changes:
- Import: ignore GPX waypoints without time
Bugfix:
- Only start GPS once
@@ -0,0 +1,10 @@
v4.28.3: OpenTracks
Changes:
- Settings: allow to disable segment creation (max recording distance)
- Importer: match activity type case-insensitive
- GPX: export moving speed in summary
Bugfix:
- GPX: importer must respect namespace prefixes
@@ -0,0 +1,6 @@
v4.29.0: OpenTracks
Changes:
- Removed "Export all (one file for all tracks)" functionality
For KML/KMZ, the markers are exported in such a way that on import OpenTracks cannot determine to which track a marker belonged.
Recommendation for backups: use KMZ with one track per file.
@@ -1,15 +1,15 @@
v3.0.0: OpenTracks
Primer lanzamiento de OpenTracks (es decir, una bifurcación de MyTracks)
Primera versión de OpenTracks (es decir, una bifurcación dura de MyTracks)
Cambios:
- Cambio de nombre de MyTracks a OpenTracks
- Se agregó un sensor de frecuencia cardíaca Bluetooth LE
- Se eliminaron todas las funciones que requieren acceso a Internet.
- Se eliminó la integración de Google Maps
- Se eliminó el uso de los servicios de Google Play.
- Se eliminaron los sensores ANT+
- Se eliminaron los sensores clásicos de Bluetooth.
- Se eliminó Protobuf
- Se eliminaron la estimación de calorías y la estimación de actividad.
- Se eliminó la API del servicio MyTracks (es decir, no hay integración con otras aplicaciones)
- Se ha añadido el sensor de frecuencia cardíaca Bluetooth LE
- Se han eliminado todas las funciones que requerían acceso a Internet
- Se ha eliminado la integración con Google Maps
- Se ha eliminado el uso de los servicios de Google Play
- Se han eliminado los sensores ANT+
- Se han eliminado los sensores Bluetooth clásicos
- Se ha eliminado Protobuf
- Se han eliminado la estimación de calorías y la estimación de actividad
- Se ha eliminado la API del servicio MyTracks (es decir, no hay integración con otras aplicaciones)
@@ -13,5 +13,5 @@ Correcciones de errores:
- Se ha corregido la exportación a KML/KMZ
Desarrollador:
- Las pruebas son ejecutables (pero muchas fallan)
- Las pruebas se pueden ejecutar (aunque muchas fallan)
- Se ha habilitado la integración continua con TravisCI (sin pruebas)
@@ -0,0 +1,14 @@
v3.1.0: OpenTracks
Cambios:
- Se ha añadido la opción de mostrar el recorrido en aplicaciones de mapas (probado con OsmAnd y Maps.ME)
- Se ha añadido la opción de mostrar las estadísticas del recorrido sin desbloquear el dispositivo mientras se realiza el seguimiento
- Se ha eliminado el modo horizontal para la vista detallada del recorrido (presentaba errores)
- Se ha eliminado el historial de sugerencias de búsqueda
Corrección de errores:
- Se solicita permiso de almacenamiento para «Importar todo»/«Exportar todo»
Desarrollador:
- Actualización a Gradle 5.6
- No se minimizan las compilaciones de lanzamiento
@@ -0,0 +1,16 @@
v3.2.0: OpenTracks
Cambios:
- Se ha eliminado el widget
- Se han incorporado iconos de Material Design
- Se han eliminado el snowboard y el esquí (se necesitan nuevos iconos)
Corrección de errores:
- Se ha corregido la funcionalidad de búsqueda
Desarrollador:
- Se han corregido (algunos) casos de prueba
- Se ha eliminado la funcionalidad «parcelable» en desuso
- Limpieza y reformateo a fondo
- No se utiliza la difusión para la comunicación entre la aplicación y el widget
- El archivo stats_content.xml no incluye otros archivos
@@ -0,0 +1,7 @@
v3.2.1: OpenTracks
Cambios:
- Los ajustes ahora están en una sola página
Desarrollo:
-Los ajustes se migraron a Androidx compat (usa fragments)
@@ -0,0 +1,13 @@
v3.2.2: OpenTracks
Cambios:
- Se eliminaron las estadísticas agregadas
- Migrado a Material Design
Corrección de errores:
- Se corrigió la exportación de datos de sensores
- Se corrigió la importación de datos de sensores incompletos
Desarrollador:
- Se reemplazó la barra de acción (actionbar) por la barra de herramientas (toolbar)
- Migrado a androidx appcompat (incl. Material Design)
@@ -0,0 +1,14 @@
v3.2.3: OpenTracks
Cambios:
- Compartir: usar Android Sharesheet
- Compartir: compartir múltiples pistas como archivos individuales
- Usar Android ShareSheet
- Sensor de frecuencia cardíaca (Bluetooth LE): reconexión automática
Corrección de errores:
- Sensor de frecuencia cardíaca (Bluetooth LE): desconexión correcta
- La frecuencia cardíaca no siempre se mostraba durante la grabación
Desarrollador:
- Se habilitaron las pruebas de instrumentación en TravisCI
@@ -0,0 +1,5 @@
v3.2.4: OpenTracks
Corrección de errores:
- Caída/cierre inesperado (crash) en la actividad de ajustes
Se eliminó la posibilidad de cambiar la frecuencia de voz/fracción desde el menú contextual
@@ -0,0 +1,15 @@
v3.2.5: OpenTracks
Cambios:
- Deshabilitar el cambio de actividad
- Añadir permiso para Android 10: grabación de datos de ubicación
Corrección de errores:
- Corregir el restablecimiento de los ajustes
Desarrollador:
- Preferencias
- si no se pueden cargar los Ajustes, restablecer todas las preferencias
- se movió el acceso a las preferencias a PreferencesUtil
- se movieron los valores por defecto de las preferencias del código a XML
- usar SummaryProvider
@@ -0,0 +1,8 @@
v3.2.6: OpenTracks
Cambios:
- Monitor de frecuencia cardíaca: escanear dispositivos disponibles
- Eliminar acceso directo a los ajustes de Bluetooth
Desarrollador:
- Preferencias: se movieron los valores por defecto de las preferencias del código a XML
@@ -0,0 +1,10 @@
v3.2.7: OpenTracks
Cambios:
- La notificación muestra la precisión del GPS
- Mostrar notificación emergente (heads-up) si la precisión del GPS cae por debajo del umbral
- Mostrar siempre el gráfico tanto por tiempo como por distancia
- Se añadió ayuda (en Ajustes)
Corrección de errores:
- TrackDetailActivity: se deshabilitó el modo horizontal que no funcionaba (temporal)
@@ -0,0 +1,10 @@
v3.2.8: OpenTracks
Cambios:
- Gráfico: zoom mediante gestos
Corrección de errores:
- La exportación a través de KMZ no exportaba correctamente los nombres de archivo de las imágenes
- Compartir múltiples pistas no incluía los archivos KMZ
- No se mostraba la interfaz de usuario de MarkerDetailActivity
- Los ajustes se cerraban inesperadamente si la preferencia de frecuencia cardíaca no estaba inicializada
@@ -0,0 +1,10 @@
v3.2.9: OpenTracks
Cambios:
- Opción de mantener la pantalla encendida durante la grabación
- Marcadores: se eliminaron por completo los marcadores de división (split markers)
- Los anuncios de voz ahora funcionan sin el permiso READ_PHONE_STATE
Corrección de errores:
- Estadísticas: no se mostraba la frecuencia cardíaca
- Estadísticas: el tiempo se restablecía brevemente a 0s
@@ -0,0 +1,11 @@
v3.2.10: OpenTracks
Cambios:
- ChartView ya no es configurable
- Después de reiniciar: se eliminó el reinicio automático de una grabación incompleta
Corrección de errores:
- Los anuncios de voz usan MAY_DUCK (de nuevo)
- ChartView muestra marcadores (de nuevo)
- Se corrigió el diseño de grabación en horizontal (landscape recordingLayout) de TrackListActivity
- Se solicita el comportamiento heredado de la API de archivos (necesario para Android 10)
@@ -0,0 +1,12 @@
v3.2.11: OpenTracks
Error:
- La velocidad máxima es idéntica a la velocidad promedio
Corrección de errores:
- Cierre inesperado (crash) al seleccionar el sensor de frecuencia cardíaca
Desarrollador:
- Se reemplazó TabHost por TabsLayout
- Se eliminó el primer punto de ruta (waypoint) especial
- Se eliminaron las traducciones duplicadas
@@ -0,0 +1,13 @@
v3.3.0: OpenTracks
Cambios importantes:
- Fotos en almacenamiento privado (ocultas para otras apps)
NOTA: No se verán fotos de versiones anteriores
- Español (Latinoamérica) requiere Android 7.0 o superior
Cambios:
- Importar/exportar vía Storage Access Framework (sin permisos de almacenamiento)
Desarrollador:
- Limpieza de estructuras de datos internas
- Icono/nombre distintos para versión debug
@@ -0,0 +1,11 @@
v3.3.1: OpenTracks
Cambios:
- Android 10: permite conservar datos al desinstalar
- API Dashboard: icono de mapa para abrir la pista en app externa (ej. OSMDashboard)
Corrección de errores:
- Se corrigió el nombre de la app (roto en v3.3.0)
Desarrollador:
- Documentación de la API Dashboard en README_DEVELOPER.md
@@ -0,0 +1,11 @@
v3.3.2: OpenTracks
Cambios:
- Exportación: ahora sobrescribe los archivos
Corrección de errores:
- Se elimina siempre la notificación de grabación
Desarrollador:
- En API 26+ se usa java.time para analizar y escribir fechas
- Refactorización de TrackRecordingService
@@ -0,0 +1,12 @@
v3.4.0: OpenTracks
Cambios:
- API Dashboard: múltiples pistas
- Ayuda actualizada
Corrección de errores:
- Se corrigió el cierre inesperado al recrear fragmentos
- En pausa: evita que la app se detenga
Desarrollador:
- Pruebas habilitadas vía CI (Github Actions y TravisCI)
@@ -0,0 +1,14 @@
v3.5.0: OpenTracks
Cambios:
- Marcador: seleccionar imágenes de la galería
- Marcador: compartir imagen
- Utilizar el nombre de la ruta como nombre de archivo al compartir
- Se han eliminado los valores mínimos y máximos de altitud (eran imprecisos)
Corrección de errores:
- Los gráficos utilizan las unidades y la escala preferidas
Desarrollador:
- Base de datos: eliminar columnas en desuso
- Base de datos: añadir índices
@@ -0,0 +1,10 @@
v3.6.0: OpenTracks
Cambios:
- Ahora se pueden reanudar las rutas que se habían detenido
Corrección de errores:
- La importación de archivos KMZ no procesaba correctamente la URL de las fotos
Desarrollador:
- Se ha simplificado el procesamiento de datos
@@ -0,0 +1,4 @@
v3.6.1: OpenTracks
Corrección de errores:
- Importación de KMZ: guardar la ruta correcta de la imagen
@@ -0,0 +1,4 @@
v3.6.2: OpenTracks
Corrección de errores:
- Los gráficos no se mostraban correctamente (problema introducido en la v3.6.0)
@@ -0,0 +1,13 @@
v3.7.0: OpenTracks
Cambios:
- Informe de velocidad/ritmo por tipo de actividad
- Se ha añadido una solución provisional para el cambio de semana del GPS
- (Experimental) Compatibilidad con sensores de ciclismo Bluetooth LE (cadencia y velocidad)
- Se ha mejorado la importación de KMZ/KML
Corrección de errores:
- Importación de KMZ: solo se importan las imágenes utilizadas
Desarrollador:
- Se ha refactorizado TrackRecordingService
@@ -0,0 +1,7 @@
v3.7.1: OpenTracks
Solución provisional:
- Ocultar el botón de inicio del GPS
Corrección de errores:
- Se ha corregido un puntero nulo
@@ -0,0 +1,6 @@
v3.7.2: OpenTracks
Corrección de errores:
- Se ha vuelto a añadir el botón «Iniciar GPS»
- El nombre de la ruta ya no se sobrescribe al final de una grabación
- Marcadores: al eliminar una foto, también se borra el archivo
@@ -0,0 +1,7 @@
v3.7.3: OpenTracks
Corrección de errores:
- Se ha corregido el cálculo de la cadencia y la velocidad (mediante el sensor Bluetooth LE)
Desarrollador:
- Se han eliminado las traducciones que no se utilizaban
@@ -0,0 +1,12 @@
v3.7.4: OpenTracks
Cambios:
- Formato de compartir/exportar configurable
Corrección de errores:
- El diámetro de la rueda no se almacenaba correctamente (para el sensor de velocidad)
- Ayuda: los enlaces son clicables
Desarrollador:
- Se ha dividido TrackDetailActivity
- El sucesor de StatsFragment utiliza ConstraintLayout
@@ -0,0 +1,11 @@
v3.7.5: OpenTracks
Cambios:
- Se ha añadido un resumen de estadísticas agregadas (desde el menú de la pantalla principal)
- API del panel de control: se pasan los ajustes «mostrar en la pantalla de bloqueo» y «mantener la pantalla encendida»
- Se han actualizado las traducciones
- Importación: se ha flexibilizado el formato de fecha ISO 8601
- Mostrar ruta en el mapa: se puede seleccionar el formato de archivo y la apertura del panel de control funciona en Android R (API 30+)
Corrección de errores:
- El icono de la aplicación ahora se muestra correctamente
@@ -0,0 +1,11 @@
v3.7.6: OpenTracks
Cambios:
- Modo horizontal
- La interfaz de usuario para borrar rutas ahora es solo un indicador giratorio
- La velocidad se exporta e importa (GPX/KML/KMZ)
- El formato de exportación predeterminado es KMZ, incluyendo todos los datos y las imágenes
Desarrollador:
- Se han sustituido las AsyncTasks por hilos
- Sincronización: se genera un UUID por cada ruta (aún no se exporta)
@@ -0,0 +1,16 @@
v3.8.0: OpenTracks
Cambio importante:
- Importación/exportación de GPX de velocidad, cadencia y frecuencia cardíaca a través de TrackPointExtension/v2
Cambios:
- API del panel de control: permite acceder a los marcadores (incluida una llamada de retorno para mostrarlos en OpenTracks)
- Nuevo icono de marcador (chincheta naranja, SVG)
- Los archivos que se van a importar se pueden seleccionar a través del gestor de archivos
Correcciones de errores:
- La interfaz de usuario de estadísticas agregadas respeta la frecuencia preferida
- Fallaba la edición de un marcador al cambiar la orientación
Desarrollador:
- Se ha refactorizado TrackRecordingService
@@ -0,0 +1,8 @@
v3.8.1: OpenTracks
Cambios:
- Durante la grabación: se ha añadido la opción de pantalla completa
- El panel de control se inicia en la misma tarea
Correcciones de errores:
- Los filtros de intención para importar archivos eran demasiado optimistas
@@ -0,0 +1,14 @@
v3.9.0: OpenTracks
Cambios:
- Exportación de rutas con su UUID
- Se evita la importación de rutas ya existentes por su UUID (configurable; por defecto, activado)
- Ahora hay que mantener pulsado el botón de pausa/parada
- El preinicio del GPS muestra el estado de la señal
- Se han añadido nuevos iconos de actividad
- Sensor de velocidad: ahora se puede configurar el tamaño de la rueda en milímetros
Correcciones de errores:
- Se han corregido algunos fallos poco frecuentes (NPE)
- Se ha corregido el formato del ritmo
- Se ha cambiado la posición del icono de la aplicación en el lanzador
@@ -1,7 +1,7 @@
v4.14.1: OpenTracks
Cambios:
- Detección de inactividad se puede desactivar
- Se puede desactivar la detección de inactividad
Correcciones de errores:
- UI para añadir imágenes a los marcadores estaba oculto
Corrección de errores:
- La interfaz de usuario para añadir imágenes a los marcadores estaba oculta
@@ -1,8 +1,22 @@
OpenTracks es un tracker deportivo que respeta tu privacidad.
Un compañero de seguimiento deportivo que respeta tu privacidad.
Funciones:
* Tracker deportivo: haz un seguimiento de tus actividades al aire libre
* Marca lugares de interés, incluso agrega fotos a los marcadores
* Pulsómetro (compatible con dispositivos Bluetooth LE)
* Exporta tus rutas como KML o GPX
* Completamente offline, no necesitas internet
El deporte y las actividades al aire libre aportan felicidad, destreza y confianza.
Cuida tu salud llevando un registro de tu entrenamiento.
Registra tus sesiones de carrera o caminata, y te ofrece un ciclocomputador con una pantalla más grande para el ciclismo.
Marca lugares interesantes a lo largo de tu recorrido con fotos.
Guarda estadísticas detalladas para su análisis.
Comparte solo los datos que quieras que vean los demás.
* Avisos de voz.
* Compatible con sensores Bluetooth LE: frecuencia cardíaca, velocidad y distancia (ciclismo), cadencia (ciclismo) y medidor de potencia (ciclismo).
* Desnivel positivo y negativo: mediante sensor barométrico.
* Altitud mostrada en EGM2008 (por encima del nivel medio del mar); exportada como WGS84.
* Exporta los datos como rutas en formato KMZ (incluidas fotos), KML o GPX.
* No requiere acceso a Internet ni permisos adicionales.
* Temas oscuro y claro, respetando la configuración del sistema.
* Sin anuncios.
Software libre / Software gratuito / Código abierto,
lo que significa que puedes utilizar, estudiar, modificar y compartir el código fuente.
Licencia Apache 2.0
@@ -0,0 +1,15 @@
v3.0.0: OpenTracks
Rilisan pertama OpenTracks (fork keras dari MyTracks)
Perubahan:
- Perubahan merek MyTracks ke OpenTracks
- Menambahkan sensor detak jantung Bluetooth LE
- Menghapus semua fitur yang memerlukan koneksi internet
- Menghapus integrasi Google Maps
- Menghapus penggunaan layanan Google Play
- Menghapus sensor ANT+
- Menghapus sensor klasik Bluetooth
- Menghapus Protobuf
- Menghapus estimasi kalori dan aktivitas
- Menghapus API MyTracks Service (dengan kata lain, tidak ada integrasi dengan aplikasi lain)
@@ -0,0 +1,17 @@
v3.0.1: OpenTracks
Perubahan:
- Bagikan penanda
- Perubahan struktur pengaturan
- Menghapus tombol kembali di bilah aksi
- Ekspor: menghapus pilihan pengaturan ukuran foto
- Ekspor: KML sebagai format sekarang dapat dipilih
Perbaikan Bug:
- direktori ekspor sekarang di /OpenTracks/
- mode lanskap tidak akan mogok
- memperbaiki ekspor KML/KMZ
Pengembang:
- Tes dapat dieksekusi (namun banyak yang gagal)
- Mengaktifkan integrasi terus menerus dengan TravisCI (tanpa tes)
@@ -0,0 +1,14 @@
v3.1.0: OpenTracks
Perubahan:
- Menambahkan opsi tampilan jalur salju di peta aplikasi (telah diuji dengan OsmAnd dan Maps.ME)
- Menambahkan ketika melacak, akan menampilkan statistik jalur tanpa perlu membuka kunci perangkat
- Menghapus mode landkap untuk tampilan detail jalur (tidak stabil)
- Menghapus riwayat saran penelusuran
Perbaikan Bug:
- Permintaan izin penyimpanan untuk impor/ekspor semua
Pengembang:
- Pembaruan ke Gradle 5.6
- Jangan mengecilkan (minify) rilisan build
@@ -1,4 +1,4 @@
Teman pelacakan olahraga yang menghormati privasi Anda.
Sebuah teman pelacak olahraga yang benar-benar menghormati privasi Anda.
Olahraga dan aktivitas luar ruangan memberikan kebahagiaan, keterampilan, dan kepercayaan diri.
@@ -11,12 +11,12 @@ Hanya membagikan data yang Anda ingin orang lain memilikinya.
* Notifikasi suara.
* Mendukung sensor Bluetooth LE: detak jantung, kecepatan dan jarak (bersepeda), irama (bersepeda), dan pengukur daya (bersepeda).
* Kenaikan dan penurunan ketinggian: melalui sensor barometrik.
* Ketinggian yang ditampilkan dalam EGM2008 (di atas permukaan laut); diekspor sebagai WGS84.
* Ketinggian ditampilkan dalam EGM2008 (di atas permukaan laut); diekspor sebagai WGS84.
* Mengekspor data sebagai lintasan, baik sebagai KMZ (termasuk foto), KML, atau GPX.
* Tidak ada akses Internet atau izin tambahan.
* Tema gelap dan terang, dengan memperhatikan pengaturan sistem.
* Tanpa iklan.
Perangkat lunak bebas / Perangkat Lunak Bebas / Sumber Terbuka
Perangkat Lunak Bebas / Sumber Terbuka
yang berarti Anda dapat menggunakan, mempelajari, mengubah, dan berbagi kode sumber.
Berlisensi Apache 2.0
@@ -1 +1 @@
Sebuah pelacak olahraga yang benar-benar menghormati privasi Anda.
Sebuah teman pelacak olahraga yang benar-benar menghormati privasi Anda.
@@ -1 +1 @@
一個尊重您的隱私的運動蹤跡紀錄器。
一款尊重您隱私的運動蹤跡紀錄器。
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -5,10 +5,11 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedViewCondition;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.EspressoUtils.withSelectedTab;
import static de.dennisguse.opentracks.util.EspressoUtils.withViewPagerCurrentItem;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
@@ -16,7 +17,6 @@ import static de.dennisguse.opentracks.util.TrackUiTestUtils.finishStoppedTrack;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.resumeStoppedTrack;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
import static org.hamcrest.Matchers.allOf;
import android.util.Pair;
@@ -27,7 +27,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -227,13 +227,13 @@ public class CustomContentProviderUtilsTest {
private void assertCount(int trackCount, int trackPointCount, int markerCount) {
ContentResolver contentResolver = context.getContentResolver();
try (Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID)) {
try (Cursor tracksCursor = contentResolver.query(CustomContentProvider.UrlType.TRACKS.getUri(), null, null, null, TracksColumns._ID)) {
assertEquals(trackCount, tracksCursor.getCount());
}
try (Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID)) {
try (Cursor tracksPointsCursor = contentResolver.query(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), null, null, null, TrackPointsColumns._ID)) {
assertEquals(trackPointCount, tracksPointsCursor.getCount());
}
try (Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID)) {
try (Cursor markerCursor = contentResolver.query(CustomContentProvider.UrlType.MARKERS.getUri(), null, null, null, MarkerColumns._ID)) {
assertEquals(markerCount, markerCursor.getCount());
}
}
@@ -0,0 +1,78 @@
package de.dennisguse.opentracks.data.models;
import static org.junit.Assert.assertEquals;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class ActivityTypeTest {
@Test
public void findBy_id() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("trail running"));
}
@Test
public void findBy_idOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
assertEquals(activityType, ActivityType.findBy(activityType.getId()));
}
}
@Test
public void findBy_synonymsOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
for (String synonym : activityType.synonyms) {
assertEquals(activityType, ActivityType.findBy(synonym));
assertEquals(activityType, ActivityType.findBy(synonym.toUpperCase(Locale.ROOT).replace(' ', '_')));
}
}
}
@Test
public void findBy_ignoresCase() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("RUNNING"));
assertEquals(ActivityType.ATV, ActivityType.findBy("atv"));
}
@Test
public void findBy_ignoresSeparators() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUNNING"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("mountain-biking"));
assertEquals(ActivityType.CROSS_COUNTRY_SKIING, ActivityType.findBy("cross_country_skiing"));
}
@Test
public void findBy_ignoresSurroundingAndRepeatedWhitespace() {
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy(" Mountain Biking "));
}
@Test
public void findBy_synonym() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("treadmill"));
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUN"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("MOUNTAIN_BIKE"));
assertEquals(ActivityType.SWIMMING_OPEN, ActivityType.findBy("SWIMMING_OPENWATER"));
assertEquals(ActivityType.BIKING, ActivityType.findBy("bike"));
}
@Test
public void findBy_unknown() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy("hovercraft"));
}
@Test
public void findBy_null() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(null));
}
@Test
public void findBy_blank() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(" "));
}
}
@@ -0,0 +1,131 @@
package de.dennisguse.opentracks.io.file.exporter;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.importer.ExportImportTest;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(JUnit4.class)
public class KMLTrackExporterTest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
private static final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
PreferencesUtils.resetPreferences(context, true);
}
@Test
public void twoTrack_with_oneMarker() throws TimeoutException, IOException {
// given
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
final Track.Id trackIdOne;
{ // first track
trackPointCreator.setClock("2020-02-02T02:02:02Z");
trackIdOne = service.startNewTrack();
Position position = ExportImportTest.sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
contentProviderUtils.insertMarker(
new Marker(
null,
trackIdOne,
"Marker 1",
"Marker 1 desc",
"Marker 1 typeLocalized",
position,
null
));
service.endCurrentTrack();
}
final Track.Id trackIdTwo;
{ // second track
trackPointCreator.setClock("2022-02-02T02:02:02Z");
trackIdTwo = service.startNewTrack();
Position position = ExportImportTest.sendLocation(trackPointCreator, "2022-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
contentProviderUtils.insertMarker(
new Marker(
null,
trackIdTwo,
"Marker 2",
"Marker 2 desc",
"Marker 2 typeLocalized",
position,
null
));
service.endCurrentTrack();
}
// when
TrackExporter exporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context, new ContentProviderUtils(context));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exporter.writeTrack(
List.of(contentProviderUtils.getTrack(trackIdOne), contentProviderUtils.getTrack(trackIdTwo)),
outputStream
);
// then
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml23_twotracks)) {
String expectation = new String(inputStreamExpected.readAllBytes(), StandardCharsets.UTF_8);
String expectationAdjusted = expectation
.replace("ece162fb-d614-4431-9d33-eb20f8ee8e1d", contentProviderUtils.getTrack(trackIdOne).uuid().toString())
.replace("6712f4ca-3456-4739-841a-9e62760cc447", contentProviderUtils.getTrack(trackIdTwo).uuid().toString());
assertEquals(expectationAdjusted, outputStream.toString());
}
}
}
@@ -127,6 +127,7 @@ public class ExportImportTest {
@Before
public void fileSetup() throws IOException, TimeoutException {
//TODO Replace with a ByteArrayOutputStream instead of storing a file?
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
tmpFileUri = Uri.fromFile(tmpFile);
@@ -155,7 +156,7 @@ public class ExportImportTest {
Distance sensorDistance = Distance.of(10); // recording distance interval
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
Position position = sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
contentProviderUtils.insertMarker(
new Marker(
null,
@@ -163,15 +164,7 @@ public class ExportImportTest {
"Marker 1",
"Marker 1 desc",
"Marker 1 typeLocalized",
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.1234567, 14.0014567,
Distance.of(10),
Altitude.WGS84.of(1020.25),
Distance.of(13),
null,
Speed.of(15)
),
position,
null
));
@@ -186,7 +179,7 @@ public class ExportImportTest {
trackPointCreator.setClock("2020-02-02T02:02:16Z");
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null, 30); //Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 0f);
position = sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 0f);
contentProviderUtils.insertMarker(
new Marker(
null,
@@ -194,7 +187,7 @@ public class ExportImportTest {
"Marker 2",
"Marker 2 desc",
"Marker 2 typeLocalized",
service.getLastStoredTrackPointWithLocation().position(),
position,
null
));
@@ -769,7 +762,8 @@ public class ExportImportTest {
sensorManager.getAltitudeChangeHandler().setAggregator(aggregatorBarometer);
}
public static void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
//TODO Use return value (for markers)
public static Position sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
@@ -782,5 +776,7 @@ public class ExportImportTest {
trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
return Position.of(location, Instant.parse(time));
}
}
@@ -120,6 +120,184 @@ public class GPXTrackImporterTest {
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_missing_time() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_missing_time);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("20210907_213924.gpx", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackstatistics
Statistics trackStatistics = importedTrack.statistics();
assertEquals(0, trackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(0), trackStatistics.movingDuration());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(1, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null))
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_pwd() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_with_power_pwr);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_gpxpx() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_with_power_gpxpx);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_speed_no_namespace() throws IOException {
@@ -285,8 +463,8 @@ public class GPXTrackImporterTest {
public void importExportTest_timezone() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone)) {
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_timezone);
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_timezone)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// when
@@ -6,6 +6,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.test.core.app.ApplicationProvider;
@@ -91,6 +92,7 @@ public class TrackRecordingServiceNotificationManagerTest {
setSmallIcon(R.drawable.ic_logo_color_24dp);
}
@NonNull
@Override
public NotificationCompat.Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
onlyAlertOnceValues.add(onlyAlertOnce);
@@ -63,8 +63,8 @@ public class RecordingLayoutTest extends TestCase {
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals("Default Layout", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@@ -78,8 +78,8 @@ public class RecordingLayoutTest extends TestCase {
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals("Default Layout", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@@ -144,6 +144,6 @@ public class RecordingLayoutTest extends TestCase {
String csv = recordingLayout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
assertEquals("Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;", csv);
}
}
@@ -6,6 +6,7 @@ import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
@@ -33,7 +34,7 @@ public final class ActivityTestUtils {
Application.ActivityLifecycleCallbacks callbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityResumed(Activity activity) {
public void onActivityResumed(@NonNull Activity activity) {
if (activityClass.isInstance(activity)) {
resumedActivityRef.set(activityClass.cast(activity));
latch.countDown();
@@ -41,27 +42,27 @@ public final class ActivityTestUtils {
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
public void onActivityDestroyed(@NonNull Activity activity) {
}
};
@@ -1,8 +1,8 @@
package de.dennisguse.opentracks.util;
import static org.hamcrest.Matchers.allOf;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static org.hamcrest.Matchers.allOf;
import android.app.Activity;
import android.os.SystemClock;
@@ -14,11 +14,11 @@ import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
@@ -33,8 +33,6 @@ import java.util.function.Predicate;
public class EspressoUtils {
private static final long VIEW_POLLING_INTERVAL_MS = 50;
public static Matcher<View> withRecyclerViewItemCount(final int count) {
return new TypeSafeMatcher<>() {
@Override
@@ -0,0 +1,22 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>20210907_213924.gpx</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<hdop>4</hdop>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time/>
<hdop>4</hdop>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -28,10 +28,11 @@ xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/
<opentracks:typeTranslated><![CDATA[unknown]]></opentracks:typeTranslated>
<gpxtrkx:TrackStatsExtension>
<gpxtrkx:Distance>0.0</gpxtrkx:Distance>
<gpxtrkx:TimerTime>0</gpxtrkx:TimerTime>
<gpxtrkx:TotalElapsedTime>0</gpxtrkx:TotalElapsedTime>
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
<gpxtrkx:MovingSpeed>0.0</gpxtrkx:MovingSpeed>
</gpxtrkx:TrackStatsExtension>
</extensions>
<trkseg>
@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>gpx with power</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>100</gpxpx:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>110</gpxpx:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>150</gpxpx:PowerInWatts>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>gpx with power</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>100</pwr:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>110</pwr:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>150</pwr:PowerInWatts>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
+185
View File
@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.3"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd
http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
<Document>
<open>1</open>
<visibility>1</visibility>
<name><![CDATA[2020-02-02T03:02+01]]></name>
<atom:generator><![CDATA[OpenTracks]]></atom:generator>
<Style id="track">
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
<IconStyle>
<scale>1.3</scale>
<Icon />
</IconStyle>
</Style>
<Style id="waypoint"><IconStyle>
<Icon />
</IconStyle></Style>
<Schema id="schema">
<SimpleArrayField name="speed" type="float">
<displayName><![CDATA[Speed (m/s)]]></displayName>
</SimpleArrayField>
<SimpleArrayField name="power" type="float">
<displayName><![CDATA[Power (W)]]></displayName>
</SimpleArrayField>
<SimpleArrayField name="cadence" type="float">
<displayName><![CDATA[Cadence (rpm)]]></displayName>
</SimpleArrayField>
<SimpleArrayField name="heartrate" type="float">
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
</SimpleArrayField>
<SimpleArrayField name="temperature" type="float">
<displayName><![CDATA[Temperature (Celsius)]]></displayName>
</SimpleArrayField>
</Schema>
<Folder>
<name><![CDATA[2020-02-02T03:02+01 Markers]]></name>
<open>1</open>
<Placemark>
<name><![CDATA[Marker 1]]></name>
<description><![CDATA[Marker 1 desc]]></description>
<TimeStamp><when>2020-02-02T03:02:03+01:00</when></TimeStamp>
<styleUrl>#waypoint</styleUrl>
<ExtendedData>
<Data name="type"><value><![CDATA[Marker 1 typeLocalized]]></value></Data>
</ExtendedData>
<Point>
<coordinates>14.001456,3.123456,1020.25</coordinates>
</Point>
</Placemark>
</Folder>
<Folder>
<name><![CDATA[2022-02-02T03:02+01 Markers]]></name>
<open>1</open>
<Placemark>
<name><![CDATA[Marker 2]]></name>
<description><![CDATA[Marker 2 desc]]></description>
<TimeStamp><when>2022-02-02T03:02:03+01:00</when></TimeStamp>
<styleUrl>#waypoint</styleUrl>
<ExtendedData>
<Data name="type"><value><![CDATA[Marker 2 typeLocalized]]></value></Data>
</ExtendedData>
<Point>
<coordinates>14.001456,3.123456,1020.25</coordinates>
</Point>
</Placemark>
</Folder>
<Folder id="tracks">
<name>Tracks</name>
<open>1</open>
<Placemark>
<name><![CDATA[2020-02-02T03:02+01]]></name>
<description><![CDATA[]]></description>
<opentracks:trackid>ece162fb-d614-4431-9d33-eb20f8ee8e1d</opentracks:trackid>
<styleUrl>#track</styleUrl>
<ExtendedData>
<Data name="activityType"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<ExtendedData>
<Data name="type"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<MultiTrack>
<altitudeMode>absolute</altitudeMode>
<interpolate>1</interpolate>
<Track>
<when>2020-02-02T03:02:02+01:00</when>
<coord/>
<when>2020-02-02T03:02:03+01:00</when>
<coord>14.001456 3.123456 1020.25</coord>
<when>2020-02-02T03:02:03+01:00</when>
<coord/>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<SimpleArrayData name="trackpoint_type">
<value>SEGMENT_START_MANUAL</value>
<value>TRACKPOINT</value>
<value>SEGMENT_END_MANUAL</value>
</SimpleArrayData>
<SimpleArrayData name="speed">
<value />
<value>15</value>
<value />
</SimpleArrayData>
<SimpleArrayData name="elevation_gain">
<value />
<value>1</value>
<value>1</value>
</SimpleArrayData>
<SimpleArrayData name="elevation_loss">
<value />
<value>1</value>
<value>1</value>
</SimpleArrayData>
<SimpleArrayData name="accuracy_horizontal">
<value />
<value>10</value>
<value />
</SimpleArrayData>
</SchemaData>
</ExtendedData>
</Track>
</MultiTrack>
</Placemark>
<Placemark>
<name><![CDATA[2022-02-02T03:02+01]]></name>
<description><![CDATA[]]></description>
<opentracks:trackid>6712f4ca-3456-4739-841a-9e62760cc447</opentracks:trackid>
<styleUrl>#track</styleUrl>
<ExtendedData>
<Data name="activityType"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<ExtendedData>
<Data name="type"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<MultiTrack>
<altitudeMode>absolute</altitudeMode>
<interpolate>1</interpolate>
<Track>
<when>2022-02-02T03:02:02+01:00</when>
<coord/>
<when>2022-02-02T03:02:03+01:00</when>
<coord>14.001456 3.123456 1020.25</coord>
<when>2022-02-02T03:02:03+01:00</when>
<coord/>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<SimpleArrayData name="trackpoint_type">
<value>SEGMENT_START_MANUAL</value>
<value>TRACKPOINT</value>
<value>SEGMENT_END_MANUAL</value>
</SimpleArrayData>
<SimpleArrayData name="speed">
<value />
<value>15</value>
<value />
</SimpleArrayData>
<SimpleArrayData name="elevation_gain">
<value />
<value>1</value>
<value>1</value>
</SimpleArrayData>
<SimpleArrayData name="elevation_loss">
<value />
<value>1</value>
<value>1</value>
</SimpleArrayData>
<SimpleArrayData name="accuracy_horizontal">
<value />
<value>10</value>
<value />
</SimpleArrayData>
</SchemaData>
</ExtendedData>
</Track>
</MultiTrack>
</Placemark>
</Folder>
</Document>
</kml>
+3
View File
@@ -71,6 +71,9 @@ limitations under the License.
<uses-permission android:name="android.permission.health.WRITE_EXERCISE" />
<uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE" />
<uses-permission android:name="android.permission.health.WRITE_DISTANCE" />
<uses-permission android:name="android.permission.health.WRITE_ELEVATION_GAINED" />
<uses-permission android:name="android.permission.health.WRITE_SPEED" />
<application
android:name=".Startup"
@@ -22,6 +22,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -50,6 +51,7 @@ import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
@@ -131,7 +133,7 @@ public class ContentProviderUtils {
@VisibleForTesting
public void deleteAllTracks(Context context) {
// Delete tracks last since it triggers a database vacuum call
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
contentResolver.delete(CustomContentProvider.UrlType.TRACKS.getUri(), null, null);
File dir = FileUtils.getPhotoDir(context);
FileUtils.deleteDirectoryRecurse(dir);
@@ -144,13 +146,13 @@ public class ContentProviderUtils {
}
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(trackId -> Long.toString(trackId.id())).toArray(String[]::new));
contentResolver.delete(CustomContentProvider.UrlType.TRACKS.getUri(), whereClause, trackIds.stream().map(trackId -> Long.toString(trackId.id())).toArray(String[]::new));
}
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
// Delete track folder resources.
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
contentResolver.delete(CustomContentProvider.UrlType.TRACKS.getUri(), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
}
//TODO Only use for tests; also move to tests.
@@ -194,7 +196,7 @@ public class ContentProviderUtils {
selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
}
return new TrackListIterator(contentResolver.query(TracksColumns.CONTENT_URI, PROJECTION, selection, selectionArgs, sortOrder));
return new TrackListIterator(contentResolver.query(CustomContentProvider.UrlType.TRACKS.getUri(), PROJECTION, selection, selectionArgs, sortOrder));
}
public Track getTrack(@NonNull Track.Id trackId) {
@@ -225,19 +227,23 @@ public class ContentProviderUtils {
* @param sortOrder the sort order. Can be null
*/
Cursor getTrackCursor(String selection, String[] selectionArgs, String sortOrder) {
return contentResolver.query(TracksColumns.CONTENT_URI, null, selection, selectionArgs, sortOrder);
return contentResolver.query(CustomContentProvider.UrlType.TRACKS.getUri(), null, selection, selectionArgs, sortOrder);
}
/**
* Inserts a track.
* NOTE: This doesn't insert any trackPoints.
* NOTE: if the track.uuid is null, a random value will be assigned.
*
* @param track the track
* @return the content provider URI of the inserted track.
*/
public Track.Id insertTrack(Track track) {
//TODO Unset trackId
Uri uri = contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track));
//TODO Unset trackId as it should be database generated
if (track.uuid() == null) {
track = new TrackBuilder(track, UUID.randomUUID()).getTrack();
}
Uri uri = contentResolver.insert(CustomContentProvider.UrlType.TRACKS.getUri(), createContentValues(track));
return new Track.Id(ContentUris.parseId(uri));
}
@@ -248,7 +254,7 @@ public class ContentProviderUtils {
* @param track the track
*/
public void updateTrack(Track track) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), TracksColumns._ID + "=?", new String[]{Long.toString(track.id().id())});
contentResolver.update(CustomContentProvider.UrlType.TRACKS.getUri(), createContentValues(track), TracksColumns._ID + "=?", new String[]{Long.toString(track.id().id())});
}
private ContentValues createContentValues(Track track) {
@@ -270,7 +276,7 @@ public class ContentProviderUtils {
}
public void updateTrackStatistics(@NonNull Track.Id trackId, @NonNull Statistics trackStatistics) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
contentResolver.update(CustomContentProvider.UrlType.TRACKS.getUri(), createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
}
private ContentValues createContentValues(Statistics trackStatistics) {
@@ -362,7 +368,7 @@ public class ContentProviderUtils {
public void deleteMarker(Context context, Marker.Id markerId) {
final Marker marker = getMarker(markerId);
deleteMarkerPhoto(context, marker);
contentResolver.delete(MarkerColumns.CONTENT_URI, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.id())});
contentResolver.delete(CustomContentProvider.UrlType.MARKERS.getUri(), MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.id())});
}
/**
@@ -422,7 +428,7 @@ public class ContentProviderUtils {
if (updateMarker.photoUrl() == null) {
deleteMarkerPhoto(context, savedMarker);
}
int rows = contentResolver.update(MarkerColumns.CONTENT_URI, createContentValues(updateMarker, updateMarker.trackId()), MarkerColumns._ID + "=?", new String[]{Long.toString(updateMarker.id().id())});
int rows = contentResolver.update(CustomContentProvider.UrlType.MARKERS.getUri(), createContentValues(updateMarker, updateMarker.trackId()), MarkerColumns._ID + "=?", new String[]{Long.toString(updateMarker.id().id())});
return rows == 1;
}
@@ -430,7 +436,7 @@ public class ContentProviderUtils {
assert marker.id() == null;
assert marker.trackId() != null;
Uri uri = contentResolver.insert(MarkerColumns.CONTENT_URI, createContentValues(marker, marker.trackId()));
Uri uri = contentResolver.insert(CustomContentProvider.UrlType.MARKERS.getUri(), createContentValues(marker, marker.trackId()));
return new Marker.Id(ContentUris.parseId(uri));
}
@@ -491,7 +497,7 @@ public class ContentProviderUtils {
if (maxCount >= 0) {
sortOrder += " LIMIT " + maxCount;
}
return contentResolver.query(MarkerColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
return contentResolver.query(CustomContentProvider.UrlType.MARKERS.getUri(), projection, selection, selectionArgs, sortOrder);
}
public MarkerIterator searchMarkers(Track.Id trackId, String query) {
@@ -509,7 +515,7 @@ public class ContentProviderUtils {
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
MarkerColumns.TYPE_LOCALIZED + " LIKE ?";
selectionArgs = new String[]{"%" + query + "%", "%" + query + "%", "%" + query + "%"};
sortOrder = MarkerColumns.DEFAULT_SORT_ORDER + " DESC";
sortOrder = BaseColumns._ID + " DESC";
}
return new MarkerIterator(this, getMarkerCursor(null, selection, selectionArgs, sortOrder, -1));
@@ -567,7 +573,7 @@ public class ContentProviderUtils {
for (int i = 0; i < trackPoints.size(); i++) {
values[i] = createContentValues(trackPoints.get(i), trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
return contentResolver.bulkInsert(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), values);
}
//TODO Set trackId in this method.
@@ -576,7 +582,7 @@ public class ContentProviderUtils {
for (int i = 0; i < markers.size(); i++) {
values[i] = createContentValues(markers.get(i), trackId);
}
return contentResolver.bulkInsert(MarkerColumns.CONTENT_URI, values);
return contentResolver.bulkInsert(CustomContentProvider.UrlType.MARKERS.getUri(), values);
}
/**
@@ -597,7 +603,7 @@ public class ContentProviderUtils {
selectionArgs = new String[]{Long.toString(trackId.id())};
}
return getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns.DEFAULT_SORT_ORDER);
return getTrackPointCursor(null, selection, selectionArgs, BaseColumns._ID);
}
/**
@@ -608,7 +614,7 @@ public class ContentProviderUtils {
* @return the content provider URI of the inserted trackPoint
*/
public Uri insertTrackPoint(TrackPoint trackPoint, Track.Id trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(trackPoint, trackId));
return contentResolver.insert(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), createContentValues(trackPoint, trackId));
}
/**
@@ -683,7 +689,7 @@ public class ContentProviderUtils {
* @param sortOrder the sort order
*/
private Cursor getTrackPointCursor(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, projection, selection, selectionArgs, sortOrder);
return contentResolver.query(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), projection, selection, selectionArgs, sortOrder);
}
public static String formatIdListForUri(Track.Id... trackIds) {
@@ -718,7 +724,7 @@ public class ContentProviderUtils {
public SensorStatistics getSensorStats(@NonNull Track.Id trackId) {
SensorStatistics sensorStatistics = null;
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.id()), null, null, null, null)) {
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(CustomContentProvider.UrlType.TRACKS_SENSOR_STATS.getUri(), trackId.id()), null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
final int MAX_HR_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_HR);
final int AVG_HR_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_HR);
@@ -743,7 +749,7 @@ public class ContentProviderUtils {
@NonNull
public List<AggregatedStatistic> getAggregatedStatisticsForTracks(TrackSelection selection) {
SelectionData data = selection.buildSelection();
try (Cursor cursor = contentResolver.query(TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS, null, data.selection(), data.getSelectionArgs(), null)) {
try (Cursor cursor = contentResolver.query(CustomContentProvider.UrlType.TRACKS_AGGREGATED_STATISTICS.getUri(), null, data.selection(), data.getSelectionArgs(), null)) {
ArrayList<AggregatedStatistic> aggregatedStatistics = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
aggregatedStatistics.add(new AggregatedStatistic(
@@ -27,6 +27,7 @@ import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Build;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
@@ -73,51 +74,39 @@ public class CustomContentProvider extends ContentProvider {
*/
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 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 + ", " +
"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 + ", " +
"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 + ", " +
"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 + ", " +
"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 + ", " +
"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 + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_POWER + ") " + TrackPointsColumns.ALIAS_MAX_POWER + " " +
"MAX(t." + TrackPointsColumns.SENSOR_POWER + ") " + TrackPointsColumns.ALIAS_MAX_POWER + " " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
"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(AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath() + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS.getPath(), UrlType.TRACKS_AGGREGATED_STATISTICS.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, UrlType.DASHBOARD_TRACKS_BY_IDS.getPath() + "*", UrlType.DASHBOARD_TRACKS_BY_IDS.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.getPath() + "*", UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.ordinal());
uriMatcher.addURI(AUTHORITY_PACKAGE, UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.getPath() + "*", UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.ordinal());
UrlType[] urlTypes = UrlType.values();
for (int i = 0; i < urlTypes.length; i++) {
uriMatcher.addURI(AUTHORITY_PACKAGE, urlTypes[i].getPathWithFilter(), i);
}
}
@Override
@@ -180,23 +169,7 @@ public class CustomContentProvider extends ContentProvider {
@Override
public String getType(@NonNull Uri url) {
String contentType = getUrlType(url).contentType;
if (contentType != null) {
return contentType;
}
//TODO replace with getUrlType(url).getContentType()
return switch (getUrlType(url)) {
case TRACKPOINTS -> TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_BY_TRACKID -> TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS -> TracksColumns.CONTENT_TYPE;
case MARKERS -> MarkerColumns.CONTENT_TYPE;
case MARKERS_BY_TRACKID -> MarkerColumns.CONTENT_ITEMTYPE;
case DASHBOARD_TRACKS_BY_IDS -> UrlType.DASHBOARD_TRACKS_BY_IDS.contentType;
case DASHBOARD_TRACKPOINTS_BY_TRACKIDS -> UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.contentType;
case DASHBOARD_MARKERS_BY_TRACKIDS -> UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.contentType;
default -> throw new IllegalArgumentException("Unknown URL " + url);
};
return getUrlType(url).contentType;
}
@Override
@@ -251,7 +224,7 @@ public class CustomContentProvider extends ContentProvider {
switch (getUrlType(url)) {
case TRACKPOINTS -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
sortOrder = sort != null ? sort : BaseColumns._ID;
}
case TRACKPOINTS_BY_ID -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
@@ -272,13 +245,13 @@ public class CustomContentProvider extends ContentProvider {
} else {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
}
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
sortOrder = sort != null ? sort : BaseColumns._ID;
}
case TRACKS_BY_ID -> {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
}
case DASHBOARD_TRACKS_BY_IDS -> {
case DASHBOARD_TRACKS_BY_IDS -> {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKS);
@@ -303,7 +276,7 @@ public class CustomContentProvider extends ContentProvider {
}
case MARKERS -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
sortOrder = sort != null ? sort : BaseColumns._ID;
}
case MARKERS_BY_ID -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
@@ -414,7 +387,7 @@ public class CustomContentProvider extends ContentProvider {
}
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
if (rowId >= 0) {
return ContentUris.appendId(TrackPointsColumns.CONTENT_URI_BY_ID.buildUpon(), rowId).build();
return ContentUris.appendId(UrlType.TRACKPOINTS_BY_ID.getUri().buildUpon(), rowId).build();
}
throw new SQLiteException("Failed to insert a track point " + url);
}
@@ -422,7 +395,7 @@ public class CustomContentProvider extends ContentProvider {
private Uri insertTrack(Uri url, ContentValues contentValues) {
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
return ContentUris.appendId(UrlType.TRACKS.getUri().buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a track " + url);
}
@@ -430,53 +403,49 @@ public class CustomContentProvider extends ContentProvider {
private Uri insertMarker(Uri url, ContentValues contentValues) {
long rowId = db.insert(MarkerColumns.TABLE_NAME, MarkerColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(MarkerColumns.CONTENT_URI.buildUpon(), rowId).build();
return ContentUris.appendId(UrlType.MARKERS.getUri().buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a marker " + url);
}
//TODO Add ContentType as attribute
public enum UrlType {
TRACKPOINTS,
TRACKPOINTS_BY_ID,
TRACKPOINTS("/trackpoints", "", "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint"),
TRACKPOINTS_BY_ID("/trackpoints", "#", "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint"),
//TODO Check if still used
TRACKPOINTS_BY_TRACKID,
TRACKS,
TRACKS_BY_ID,
TRACKS_SENSOR_STATS,
TRACKS_AGGREGATED_STATISTICS,
MARKERS,
MARKERS_BY_ID,
TRACKPOINTS_BY_TRACKID("/trackpoints/trackid", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint"),
TRACKS("/tracks", "", "vnd.android.cursor.dir/vnd.de.dennisguse.track"),
TRACKS_BY_ID("/tracks", "#", "vnd.android.cursor.dir/vnd.de.dennisguse.track"),
TRACKS_SENSOR_STATS("/tracks/sensorstats", "#", "vnd.android.cursor.dir/vnd.de.dennisguse.sensorstats"),
TRACKS_AGGREGATED_STATISTICS("/tracks/aggregated/", "", "vnd.android.cursor.dir/vnd.de.dennisguse.aggregatedstatistics"),
MARKERS("/markers", "", "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint"),
MARKERS_BY_ID("/markers", "#", "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint"),
@Deprecated //TODO Should not be used anymore
MARKERS_BY_TRACKID,
MARKERS_BY_TRACKID("/markers/trackid", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint"),
DASHBOARD_TRACKS_BY_IDS(Uri.parse(CONTENT_BASE_URI + "/dashboard/tracks/"), TracksColumns.CONTENT_TYPE),
DASHBOARD_TRACKPOINTS_BY_TRACKIDS(Uri.parse(CONTENT_BASE_URI + "/dashboard/trackpoints/"), TrackPointsColumns.CONTENT_TYPE),
DASHBOARD_MARKERS_BY_TRACKIDS(Uri.parse(CONTENT_BASE_URI + "/dashboard/markers/"), MarkerColumns.CONTENT_TYPE);
DASHBOARD_TRACKS_BY_IDS("/dashboard/tracks", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.track"),
DASHBOARD_TRACKPOINTS_BY_TRACKIDS("/dashboard/trackpoints", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint"),
DASHBOARD_MARKERS_BY_TRACKIDS("/dashboard/markers", "*", "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint");
private final Uri uri;
private final String path;
private final String filter;
private final String contentType;
//TODO Add Uri as attribute:
//TODO Add ContentType as attribute:
@Deprecated()
UrlType() {
this.uri = null;
this.contentType = null;
}
UrlType(Uri uri, String contentType) {
this.uri = uri;
UrlType(@NonNull String path, String filter, @NonNull String contentType) {
this.path = path;
this.filter = filter;
this.contentType = contentType;
}
public Uri getUri() {
return uri;
return Uri.parse(CONTENT_BASE_URI + path);
}
String getPath() {
return uri.getPath();
private String getPathWithFilter() {
if (TextUtils.isEmpty(filter))
return path;
return path + "/" + filter;
}
}
}
@@ -88,7 +88,7 @@ public class ShareContentProvider extends CustomContentProvider {
}
trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getPreferenceId() + "/" + trackIdBuilder + "/" + Uri.encode(filename));
Uri uri = Uri.parse(UrlType.TRACKS.getUri() + "/" + trackFileFormat.getPreferenceId() + "/" + trackIdBuilder + "/" + Uri.encode(filename));
Log.d(TAG, "Created uri " + uri + " with MIME " + trackFileFormat.getMimeType());
@@ -192,7 +192,7 @@ public class ShareContentProvider extends CustomContentProvider {
String[] trackIdsString = trackIds.stream().map(id -> String.valueOf(id.id())).toArray(String[]::new);
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
try (Cursor cursor = super.query(TracksColumns.CONTENT_URI, null, whereClause, trackIdsString, TracksColumns._ID)) {
try (Cursor cursor = super.query(UrlType.TRACKS.getUri(), null, whereClause, trackIdsString, TracksColumns._ID)) {
while (cursor.moveToNext()) {
tracks.add(ContentProviderUtils.createTrack(cursor));
}
@@ -34,8 +34,6 @@ import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
@@ -120,7 +118,7 @@ public class TrackDataHub {
notifyTracksTableUpdate(listeners);
}
};
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
contentResolver.registerContentObserver(CustomContentProvider.UrlType.TRACKS.getUri(), false, tracksTableObserver);
trackPointsTableObserver = new ContentObserver(handler) {
@Override
@@ -128,7 +126,7 @@ public class TrackDataHub {
notifyTrackPointsTableUpdate(true, listeners);
}
};
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
contentResolver.registerContentObserver(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), false, trackPointsTableObserver);
}
public void stop() {
@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.R;
@@ -14,13 +15,13 @@ public enum ActivityType {
AIRPLANE("airplane", R.string.activity_type_airplane, R.drawable.ic_activity_flight_24dp, true),
ATV("ATV", R.string.activity_type_atv, R.drawable.ic_activity_drive_24dp, true),
BIKING("biking", R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true),
BIKING("biking", List.of("bike", "indoor cycling"), R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true),
BLIMP("blimp", R.string.activity_type_blimp, ActivityIcon.ICON_UNKNOWN, true),
BOAT("boat", R.string.activity_type_boat, R.drawable.ic_activity_boat_24dp, true),
CLIMBING("climbing", R.string.activity_type_climbing, R.drawable.ic_activity_climbing_24dp, false),
COMMERCIAL_AIRPLANE("commercial airplane", R.string.activity_type_commercial_airplane, R.drawable.ic_activity_flight_24dp, true),
CROSS_COUNTRY_SKIING("cross-country skiing", R.string.activity_type_cross_country_skiing, R.drawable.ic_activity_skiing_24dp, true),
CYCLING("cycling", R.string.activity_type_cycling, R.drawable.ic_activity_bike_24dp, true),
CYCLING("cycling", List.of("cycle"), R.string.activity_type_cycling, R.drawable.ic_activity_bike_24dp, true),
DIRT_BIKE("dirt bike", R.string.activity_type_dirt_bike, R.drawable.ic_activity_mtb_24dp, true),
DONKEY_BACK_RIDING("donkey back riding", R.string.activity_type_donkey_back_riding, ActivityIcon.ICON_UNKNOWN, true),
DRIVING("driving", R.string.activity_type_driving, R.drawable.ic_activity_drive_24dp, true),
@@ -30,21 +31,21 @@ public enum ActivityType {
FERRY("ferry", R.string.activity_type_ferry, R.drawable.ic_activity_boat_24dp, true),
FRISBEE("frisbee", R.string.activity_type_frisbee, ActivityIcon.ICON_UNKNOWN, true),
GLIDING("gliding", R.string.activity_type_gliding, ActivityIcon.ICON_UNKNOWN, true),
GRAVEL_BIKING("gravel biking", R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true),
GRAVEL_BIKING("gravel biking", List.of("gravel bike"), R.string.activity_type_gravel_biking, R.drawable.ic_activity_bike_24dp, true),
HANG_GLIDING("hang gliding", R.string.activity_type_hang_gliding, ActivityIcon.ICON_UNKNOWN, true),
HELICOPTER("helicopter", R.string.activity_type_helicopter, ActivityIcon.ICON_UNKNOWN, true),
HIKING("hiking", R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false),
HIKING("hiking", List.of("hike"), R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false),
HORSE_BACK_RIDING("horse back riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true),
HOT_AIR_BALLOON("hot air balloon", R.string.activity_type_hot_air_balloon, ActivityIcon.ICON_UNKNOWN, true),
ICE_SAILING("ice sailing", R.string.activity_type_ice_sailing, ActivityIcon.ICON_UNKNOWN, true),
INLINE_SKATING("inline skating", R.string.activity_type_inline_skating, R.drawable.ic_activity_inline_skating_24dp, true),
INLINE_SKATING("inline skating", List.of("inline skate"), R.string.activity_type_inline_skating, R.drawable.ic_activity_inline_skating_24dp, true),
KAYAKING("kayaking", R.string.activity_type_kayaking, R.drawable.ic_activity_kayaking_24dp, true),
KITE_SURFING("kite surfing", R.string.activity_type_kite_surfing, ActivityIcon.ICON_UNKNOWN, true),
LAND_SAILING("land sailing", R.string.activity_type_land_sailing, ActivityIcon.ICON_UNKNOWN, true),
MIXED_TYPE("mixed type", R.string.activity_type_mixed_type, ActivityIcon.ICON_UNKNOWN, true),
MOTOR_BIKE("motor bike", R.string.activity_type_motor_bike, R.drawable.ic_activity_motorbike_24dp, true),
MOTOR_BOATING("motor boating", R.string.activity_type_motor_boating, R.drawable.ic_activity_boat_24dp, true),
MOUNTAIN_BIKING("mountain biking", R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true),
MOUNTAIN_BIKING("mountain biking", List.of("mountain bike"), R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true),
OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_off_trail_hiking, R.drawable.ic_activity_walk_24dp, false),
OTHER("other", R.string.activity_type_other, ActivityIcon.ICON_UNKNOWN, true),
PADDLING("paddling", R.string.activity_type_paddling, ActivityIcon.ICON_UNKNOWN, true),
@@ -53,46 +54,57 @@ public enum ActivityType {
RC_BOAT("RC boat", R.string.activity_type_rc_boat, R.drawable.ic_activity_boat_24dp, true),
RC_HELICOPTER("RC helicopter", R.string.activity_type_rc_helicopter, ActivityIcon.ICON_UNKNOWN, true),
RIDING("riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true),
ROAD_BIKING("road biking", R.string.activity_type_road_biking, R.drawable.ic_activity_bike_24dp, true),
ROAD_BIKING("road biking", List.of("road bike"), R.string.activity_type_road_biking, R.drawable.ic_activity_bike_24dp, true),
ROLLER_SKIING("roller skiing", R.string.activity_type_roller_skiing, ActivityIcon.ICON_UNKNOWN, true),
ROWING("rowing", R.string.activity_type_rowing, ActivityIcon.ICON_UNKNOWN, true),
RUNNING("running", R.string.activity_type_running, R.drawable.ic_activity_run_24dp, false),
RUNNING("running", List.of("run", "treadmill"), R.string.activity_type_running, R.drawable.ic_activity_run_24dp, false),
SAILING("sailing", R.string.activity_type_sailing, R.drawable.ic_activity_sailing_24dp, true),
KICKSCOOTER("kickscooter", R.string.activity_type_kickscooter, R.drawable.ic_activity_scooter_24dp, true),
SEAPLANE("seaplane", R.string.activity_type_seaplane, R.drawable.ic_activity_flight_24dp, true),
SKATE_BOARDING("skateboarding", R.string.activity_type_skate_boarding, R.drawable.ic_activity_skateboarding_24dp, true),
SKATE_BOARDING("skateboarding", List.of("skateboard"), R.string.activity_type_skate_boarding, R.drawable.ic_activity_skateboarding_24dp, true),
SKATING("skating", R.string.activity_type_skating, ActivityIcon.ICON_UNKNOWN, true),
SKIING("skiing", R.string.activity_type_skiing, R.drawable.ic_activity_skiing_24dp, true),
SKY_JUMPING("sky jumping", R.string.activity_type_sky_jumping, ActivityIcon.ICON_UNKNOWN, true),
SLED("sled", R.string.activity_type_sled, ActivityIcon.ICON_UNKNOWN, true),
SNOW_BOARDING("snowboarding", R.string.activity_type_snow_boarding, R.drawable.ic_activity_snowboarding_24dp, true),
SNOW_BOARDING("snowboarding", List.of("snowboard"), R.string.activity_type_snow_boarding, R.drawable.ic_activity_snowboarding_24dp, true),
SNOW_SHOEING("snow shoeing", R.string.activity_type_snow_shoeing, ActivityIcon.ICON_UNKNOWN, true),
SPEED_WALKING("speed walking", R.string.activity_type_speed_walking, R.drawable.ic_activity_walk_24dp, false),
STREET_RUNNING("street running", R.string.activity_type_street_running, R.drawable.ic_activity_run_24dp, false),
SURFING("surfing", R.string.activity_type_surfing, ActivityIcon.ICON_UNKNOWN, true),
TRACK_CYCLING("track cycling", R.string.activity_type_track_cycling, R.drawable.ic_activity_bike_24dp, true),
TRACK_RUNNING("track running", R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRACK_RUNNING("track running", List.of("track run"), R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRAIL_HIKING("trail hiking", R.string.activity_type_trail_hiking, R.drawable.ic_activity_walk_24dp, false),
TRAIL_RUNNING("trail running", R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRAIL_RUNNING("trail running", List.of("trail run"), R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRAIN("train", R.string.activity_type_train, ActivityIcon.ICON_UNKNOWN, true),
ULTIMATE_FRISBEE("ultimate frisbee", R.string.activity_type_ultimate_frisbee, ActivityIcon.ICON_UNKNOWN, true),
WAKEBOARDING("wakeboarding", R.string.activity_type_wakeboarding, ActivityIcon.ICON_UNKNOWN, true),
WALKING("walking", R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp, false),
WALKING("walking", List.of("walk"), R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp, false),
WATER_SKIING("water skiing", R.string.activity_type_water_skiing, ActivityIcon.ICON_UNKNOWN, true),
WIND_SURFING("wind surfing", R.string.activity_type_wind_surfing, ActivityIcon.ICON_UNKNOWN, true),
SWIMMING("swimming", R.string.activity_type_swimming, R.drawable.ic_activity_swimming_24dp, false),
SWIMMING_OPEN("swimming in open water", R.string.activity_type_swimming_open, R.drawable.ic_activity_swimming_open_24dp, false),
SWIMMING("swimming", List.of("swim"), R.string.activity_type_swimming, R.drawable.ic_activity_swimming_24dp, false),
SWIMMING_OPEN("swimming in open water", List.of("swimming openwater", "open water swimming"), R.string.activity_type_swimming_open, R.drawable.ic_activity_swimming_open_24dp, false),
WORKOUT("workout", R.string.activity_type_workout, R.drawable.ic_activity_workout_24dp, false),
UNKNOWN("unknown", R.string.activity_type_unknown, ActivityIcon.ICON_UNKNOWN, true);
final String id;
/**
* Spellings used by other applications that {@link #normalize(String)} cannot map onto {@link #id}.
* Already normalized.
*/
final List<String> synonyms;
final int iconDrawableId;
final boolean showSpeedPreferred;
final int localizedStringId;
ActivityType(String id, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) {
this(id, List.of(), localizedStringId, iconDrawableId, showSpeedPreferred);
}
ActivityType(String id, List<String> synonyms, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) {
this.id = id;
this.synonyms = synonyms;
this.localizedStringId = localizedStringId;
this.iconDrawableId = iconDrawableId;
this.showSpeedPreferred = showSpeedPreferred;
@@ -121,12 +133,39 @@ public enum ActivityType {
.toList();
}
/**
* Lowercases and reduces "_", "-" and whitespace runs to a single space.
* Lowercasing is locale-independent: under a Turkish locale the I of "RUNNING" would otherwise
* lowercase to a dotless i and stop matching "running".
*/
@NonNull
private static String normalize(String activityTypeId) {
if (activityTypeId == null) {
return "";
}
return activityTypeId.toLowerCase(Locale.ROOT)
.replaceAll("[\\s_-]+", " ")
.trim();
}
/**
* Resolves an activity type as written by another application; GPX's type element is free text.
*/
@NonNull
public static ActivityType findBy(String activityTypeId) {
return Arrays.stream(ActivityType.values()).filter(
it -> it.id.equals(activityTypeId)
).findFirst()
.orElse(ActivityType.UNKNOWN);
String normalized = normalize(activityTypeId);
if (normalized.isEmpty()) {
return ActivityType.UNKNOWN;
}
return Arrays.stream(ActivityType.values())
.filter(it -> normalize(it.id).equals(normalized))
.findFirst()
.orElseGet(() -> Arrays.stream(ActivityType.values())
.filter(it -> it.synonyms.contains(normalized))
.findFirst()
.orElse(ActivityType.UNKNOWN));
}
@NonNull
@@ -57,6 +57,17 @@ public class TrackBuilder {
this.trackStatistics = track.statistics();
}
public TrackBuilder(Track track, UUID uuid) {
this.id = track.id();
this.uuid = uuid;
this.name = track.name();
this.description = track.description();
this.activityTypeLocalized = track.activityTypeLocalized();
this.activityType = track.activityType();
this.zoneOffset = track.zoneOffset();
this.trackStatistics = track.statistics();
}
public void setName(String name) {
this.name = name;
}
@@ -50,15 +50,6 @@ public class ExtremityMonitor {
return min;
}
/**
* Sets the minimum value.
*
* @param min the minimum value
*/
public void setMin(double min) {
this.min = min;
}
/**
* Gets the maximum value seen.
*/
@@ -66,15 +57,6 @@ public class ExtremityMonitor {
return max;
}
/**
* Sets the maximum value.
*
* @param max the maximum value
*/
public void setMax(double max) {
this.max = max;
}
/**
* Updates the min and the max with a new value.
*
@@ -17,12 +17,10 @@
package de.dennisguse.opentracks.data.tables;
import android.content.Context;
import android.net.Uri;
import android.provider.BaseColumns;
import android.widget.ArrayAdapter;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.CustomContentProvider;
/**
* Constants for markers table.
@@ -32,11 +30,6 @@ import de.dennisguse.opentracks.data.CustomContentProvider;
public interface MarkerColumns extends BaseColumns {
String TABLE_NAME = "markers";
Uri CONTENT_URI = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME);
Uri CONTENT_URI_BY_TRACKID = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.waypoint";
String DEFAULT_SORT_ORDER = _ID;
// Columns
String NAME = "name";
@@ -16,11 +16,8 @@
package de.dennisguse.opentracks.data.tables;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.data.CustomContentProvider;
/**
* Constants for the track points table.
*
@@ -29,11 +26,6 @@ import de.dennisguse.opentracks.data.CustomContentProvider;
public interface TrackPointsColumns extends BaseColumns {
String TABLE_NAME = "trackpoints";
Uri CONTENT_URI_BY_ID = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME);
Uri CONTENT_URI_BY_TRACKID = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.trackpoint";
String DEFAULT_SORT_ORDER = _ID;
// Columns
String TRACKID = "trackid";
@@ -16,11 +16,8 @@
package de.dennisguse.opentracks.data.tables;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.data.CustomContentProvider;
/**
* Constants for the tracks table.
*
@@ -29,12 +26,6 @@ import de.dennisguse.opentracks.data.CustomContentProvider;
public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks";
Uri CONTENT_URI = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME);
Uri CONTENT_URI_SENSOR_STATS = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats");
Uri CONTENT_URI_AGGREGATED_STATISTICS = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME + "/aggregated/");
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;
// Columns
String UUID = "uuid"; // identifier to make tracks globally unique (prevent re-import)
@@ -56,7 +56,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
ActivityType.WORKOUT
);
private static int getPosition(Context context, ActivityType activityType) {
private static int getPosition(ActivityType activityType) {
if (activityType == null) {
return -1;
}
@@ -92,7 +92,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
}
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(iconDrawableIds);
int position = getPosition(getContext(), preselectedActivityType);
int position = getPosition(preselectedActivityType);
if (position != -1) {
imageAdapter.setSelected(position);
}
@@ -105,7 +105,7 @@ public class ExportActivity extends AbstractActivity {
private ContentProviderUtils contentProviderUtils;
// List of tracks to be exported.
private ArrayList<ExportTask> exportTasks;
private final ArrayList<ExportTask> exportTasks = new ArrayList<>();
private final LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>();
private final Handler conflictsHandler = new Handler();
@@ -183,7 +183,7 @@ public class ExportActivity extends AbstractActivity {
trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS);
exportTasks = new ArrayList<>(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS));
exportTasks.addAll(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS));
setProgress();
nextExport(null);
}
@@ -226,7 +226,6 @@ public class ExportActivity extends AbstractActivity {
return false;
}
exportTasks = new ArrayList<>();
if (allInOneFile) {
String filename = "OpenTracks-Backup";
exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::id).toList()));
@@ -282,10 +282,11 @@ public class GPXTrackExporter implements TrackExporter {
Statistics trackStatistics = track.statistics();
printWriter.println("<gpxtrkx:TrackStatsExtension>");
printWriter.println("<gpxtrkx:Distance>" + trackStatistics.totalDistance().toM() + "</gpxtrkx:Distance>");
printWriter.println("<gpxtrkx:TimerTime>" + trackStatistics.totalDuration().getSeconds() + "</gpxtrkx:TimerTime>");
printWriter.println("<gpxtrkx:TotalElapsedTime>" + trackStatistics.totalDuration().getSeconds() + "</gpxtrkx:TotalElapsedTime>");
printWriter.println("<gpxtrkx:MovingTime>" + trackStatistics.movingDuration().getSeconds() + "</gpxtrkx:MovingTime>");
printWriter.println("<gpxtrkx:StoppedTime>" + trackStatistics.getStoppedTime().getSeconds() + "</gpxtrkx:StoppedTime>");
printWriter.println("<gpxtrkx:MaxSpeed>" + trackStatistics.maxSpeed().toMPS() + "</gpxtrkx:MaxSpeed>");
printWriter.println("<gpxtrkx:MovingSpeed>" + trackStatistics.getAverageMovingSpeed().toMPS() + "</gpxtrkx:MovingSpeed>");
if (trackStatistics.altitudeGainLoss() != null) {
printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.altitudeGainLoss().gain_m() + "</gpxtrkx:Ascent>");
printWriter.println("<gpxtrkx:Descent>" + trackStatistics.altitudeGainLoss().loss_m() + "</gpxtrkx:Descent>");
@@ -384,10 +385,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("</trkpt>");
if (cumulativeDistance != null) {
return cumulativeDistance;
}
return Distance.ZERO;
return Objects.requireNonNullElse(cumulativeDistance, Distance.ZERO);
}
private Double cumulateSensorData(TrackPoint trackPoint, List<TrackPoint> sensorPoints, Function<TrackPoint, Double> map) {
@@ -49,6 +49,8 @@ import de.dennisguse.opentracks.util.StringUtils;
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML version 2.3.
* <a href="https://docs.opengeospatial.org/is/12-007r2/12-007r2.html">...</a>
*
* WARNING: exporting multiple tracks (each with markers) into one file, will not allow to restore to which track a marker belonged.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
* @author Leif Hendrik Wilden
@@ -30,5 +30,6 @@ import de.dennisguse.opentracks.data.models.Track;
*/
public interface TrackExporter {
//TODO Pass track.Id instead of track?
boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream);
}
@@ -18,8 +18,11 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.helpers.DefaultHandler;
@@ -71,28 +74,44 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type";
private static final String TAG_TYPE_LOCALIZED = "opentracks:typeTranslated";
private static final String TAG_MARKER = "wpt";
private static final String TAG_ID = "opentracks:trackid";
private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon";
private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed";
/**
* Often speed is exported without the proper namespace.
*/
private static final String TAG_EXTENSION_SPEED_COMPAT = "speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_TEMPERATURE = "gpxtpx:atemp";
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";
private static final String TAG_EXTENSION_ACCURACY_HORIZONTAL = "opentracks:accuracy_horizontal";
private static final String TAG_EXTENSION_ACCURACY_VERTICAL = "opentracks:accuracy_vertical";
/**
* Garmin Trackpoint Extensions.
*/
private static final String NS_GARMIN_TRACKPOINT_EXTENSION_V1 = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1";
private static final String NS_GARMIN_TRACKPOINT_EXTENSION_V2 = "http://www.garmin.com/xmlschemas/TrackPointExtension/v2";
private static final String TAG_GARMIN_TPX_EXTENSION_SPEED = "speed";
private static final String TAG_GARMIN_TPX_EXTENSION_HEARTRATE = "hr";
private static final String TAG_GARMIN_TPX_EXTENSION_TEMPERATURE = "atemp";
private static final String TAG_GARMIN_TPX_EXTENSION_CADENCE = "cad";
/**
* Garmin Trackpoint Extensions.
*/
private static final String NS_GARMIN_POWER_EXTENSION_V1 = "http://www.garmin.com/xmlschemas/PowerExtension/v1";
private static final String TAG_GARMIN_PWR_EXTENSION_POWER = "PowerInWatts";
/**
* OpenTracks Extensions.
*/
private static final String NS_OPENTRACKS = "http://opentracksapp.com/xmlschemas/v1";
private static final String TAG_OPENTRACKS_EXTENSION_TRACK_ID = "trackid";
private static final String TAG_OPENTRACKS_EXTENSION_TYPE_LOCALIZED = "typeTranslated";
private static final String TAG_OPENTRACKS_EXTENSION_GAIN = "gain";
private static final String TAG_OPENTRACKS_EXTENSION_LOSS = "loss";
private static final String TAG_OPENTRACKS_EXTENSION_DISTANCE = "distance";
private static final String TAG_OPENTRACKS_EXTENSION_ACCURACY_HORIZONTAL = "accuracy_horizontal";
private static final String TAG_OPENTRACKS_EXTENSION_ACCURACY_VERTICAL = "accuracy_vertical";
private Locator locator;
private final Context context;
@@ -142,8 +161,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) {
switch (tag) {
public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (qName) {
case TAG_MARKER:
onMarkerStart(attributes);
break;
@@ -165,105 +184,128 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void endElement(String uri, String localName, String tag) {
switch (tag) {
case TAG_GPX -> onFileEnd();
case TAG_MARKER -> onMarkerEnd();
case TAG_TRACK -> {
if (activityTypeLocalized == null) {
// Backward compatibility: up v4.9.1 as <type> contained localized content.
activityTypeLocalized = activityType;
public void endElement(String uri, String localName, String qName) {
if (NS_GARMIN_TRACKPOINT_EXTENSION_V1.equals(uri) || NS_GARMIN_TRACKPOINT_EXTENSION_V2.equals(uri)) {
switch (localName) {
case TAG_GARMIN_TPX_EXTENSION_SPEED -> {
if (content != null) {
speed = content.trim();
}
}
trackImporter.setTrackData(context, name, uuid, description, activityTypeLocalized, activityType, zoneOffset);
zoneOffset = null;
}
case TAG_TRACK_SEGMENT -> onTrackSegmentEnd();
case TAG_TRACK_POINT -> currentSegment.add(createTrackPoint());
case TAG_NAME -> {
if (content != null) {
name = content.trim();
case TAG_GARMIN_TPX_EXTENSION_HEARTRATE -> {
if (content != null) {
heartrate = content.trim();
}
}
}
case TAG_DESCRIPTION -> {
if (content != null) {
description = content.trim();
case TAG_GARMIN_TPX_EXTENSION_TEMPERATURE -> {
if (content != null) {
temperature = content.trim();
}
}
}
case TAG_TYPE -> { //Track or Marker/WPT
if (content != null) {
// In older version this might be localized content.
activityType = content.trim();
markerType = content.trim();
case TAG_GARMIN_TPX_EXTENSION_CADENCE -> {
if (content != null) {
cadence = content.trim();
}
}
default -> Log.w(TAG, "Unknown Garmin trackpoint extension tag " + localName);
}
case TAG_TYPE_LOCALIZED -> {
if (content != null) {
activityTypeLocalized = content.trim();
} else if (NS_GARMIN_POWER_EXTENSION_V1.equals(uri)) {
//noinspection SwitchStatementWithTooFewBranches
switch (localName) {
case TAG_GARMIN_PWR_EXTENSION_POWER -> {
if (content != null) {
power = content.trim();
}
}
default -> Log.w(TAG, "Unknown Garmin power extension tag " + localName);
}
case TAG_TIME -> {
if (content != null) {
time = content.trim();
} else if (NS_OPENTRACKS.equals(uri)) {
switch (localName) {
case TAG_OPENTRACKS_EXTENSION_TRACK_ID -> {
if (content != null) {
uuid = content.trim();
}
}
}
case TAG_ALTITUDE -> {
if (content != null) {
altitude = content.trim();
case TAG_OPENTRACKS_EXTENSION_TYPE_LOCALIZED -> {
if (content != null) {
activityTypeLocalized = content.trim();
}
}
}
case TAG_EXTENSION_SPEED, TAG_EXTENSION_SPEED_COMPAT -> {
if (content != null) {
speed = content.trim();
case TAG_OPENTRACKS_EXTENSION_GAIN -> {
if (content != null) {
gain = content.trim();
}
}
}
case TAG_EXTENSION_HEARTRATE -> {
if (content != null) {
heartrate = content.trim();
case TAG_OPENTRACKS_EXTENSION_LOSS -> {
if (content != null) {
loss = content.trim();
}
}
}
case TAG_EXTENSION_TEMPERATURE -> {
if (content != null) {
temperature = content.trim();
case TAG_OPENTRACKS_EXTENSION_DISTANCE -> {
if (content != null) {
sensorDistance = content.trim();
}
}
}
case TAG_EXTENSION_CADENCE -> {
if (content != null) {
cadence = content.trim();
case TAG_OPENTRACKS_EXTENSION_ACCURACY_HORIZONTAL -> {
if (content != null) {
accuracyHorizontal = content.trim();
}
}
}
case TAG_EXTENSION_POWER -> {
if (content != null) {
power = content.trim();
case TAG_OPENTRACKS_EXTENSION_ACCURACY_VERTICAL -> {
if (content != null) {
accuracyVertical = content.trim();
}
}
default -> Log.w(TAG, "Unknown OpenTracks extension tag " + localName);
}
case TAG_ID -> {
if (content != null) {
uuid = content.trim();
} else {
switch (qName) {
case TAG_GPX -> onFileEnd();
case TAG_MARKER -> onMarkerEnd();
case TAG_TRACK -> {
if (activityTypeLocalized == null) {
// Backward compatibility: up v4.9.1 as <type> contained localized content.
activityTypeLocalized = activityType;
}
trackImporter.setTrackData(context, name, uuid, description, activityTypeLocalized, activityType, zoneOffset);
zoneOffset = null;
}
}
case TAG_EXTENSION_GAIN -> {
if (content != null) {
gain = content.trim();
case TAG_TRACK_SEGMENT -> onTrackSegmentEnd();
case TAG_TRACK_POINT -> {
TrackPoint trackPoint = createTrackPoint();
if (trackPoint != null) currentSegment.add(trackPoint);
}
}
case TAG_EXTENSION_LOSS -> {
if (content != null) {
loss = content.trim();
case TAG_NAME -> {
if (content != null) {
name = content.trim();
}
}
}
case TAG_EXTENSION_DISTANCE -> {
if (content != null) {
sensorDistance = content.trim();
case TAG_DESCRIPTION -> {
if (content != null) {
description = content.trim();
}
}
}
case TAG_EXTENSION_ACCURACY_HORIZONTAL -> {
if (content != null) {
accuracyHorizontal = content.trim();
case TAG_TYPE -> { //Track or Marker/WPT
if (content != null) {
// In older version this might be localized content.
activityType = content.trim();
markerType = content.trim();
}
}
}
case TAG_EXTENSION_ACCURACY_VERTICAL -> {
if (content != null) {
accuracyVertical = content.trim();
case TAG_TIME -> {
if (content != null) {
time = content.trim();
}
}
case TAG_ALTITUDE -> {
if (content != null) {
altitude = content.trim();
}
}
case TAG_EXTENSION_SPEED_COMPAT -> {
if (content != null) {
speed = content.trim();
}
}
}
}
@@ -284,8 +326,10 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
currentSegment.clear();
}
@Nullable
private TrackPoint createTrackPoint() throws ParsingException {
if (TextUtils.isEmpty(time)) return null;
OffsetDateTime parsedTime;
try {
parsedTime = StringUtils.parseTime(time);
@@ -468,7 +512,10 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
TrackPoint trackPoint = createTrackPoint();
if (trackPoint == null) {
Log.w(TAG, "Marker with no TrackPoint");
return;
}
if (!trackPoint.position().hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.position());
return;
@@ -146,8 +146,8 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) {
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
switch (qName) {
case TAG_PLACEMARK, TAG_PHOTO_OVERLAY ->
// Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, typeLocalized, and description.
onMarkerStart();
@@ -168,8 +168,8 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
switch (tag) {
public void endElement(String uri, String localName, String qName) throws SAXException {
switch (qName) {
case TAG_KML -> onFileEnd();
case TAG_PLACEMARK, TAG_PHOTO_OVERLAY ->
// Note that a track is contained in a Placemark, calling onMarkerEnd is save since markerType is not set for a track.
@@ -131,7 +131,7 @@ public class TrackImporter {
}
//TODO This is a workaround until we have proper UI.
trackData.uuid = UUID.randomUUID();
trackData.uuid = null;
}
trackPoints.sort(Comparator.comparing(TrackPoint::getTime));
@@ -5,7 +5,10 @@ import android.content.Context;
import androidx.health.connect.client.HealthConnectClient;
import androidx.health.connect.client.PermissionController;
import androidx.health.connect.client.permission.HealthPermission;
import androidx.health.connect.client.records.DistanceRecord;
import androidx.health.connect.client.records.ElevationGainedRecord;
import androidx.health.connect.client.records.ExerciseSessionRecord;
import androidx.health.connect.client.records.SpeedRecord;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
@@ -27,13 +30,13 @@ import kotlinx.coroutines.BuildersKt;
*/
public class HealthConnectUtils {
private static final String TAG = HealthConnectUtils.class.getSimpleName();
static final String WRITE_EXERCISE_PERMISSION = HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ExerciseSessionRecord.class));
static final String WRITE_EXERCISE_ROUTE_PERMISSION = HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE;
//TODO Should be package private
public static final Set<String> PERMISSIONS = Set.of(WRITE_EXERCISE_PERMISSION, WRITE_EXERCISE_ROUTE_PERMISSION);
public static final Set<String> PERMISSIONS = Set.of(
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ExerciseSessionRecord.class)),
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(DistanceRecord.class)),
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ElevationGainedRecord.class)),
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(SpeedRecord.class)),
HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE
);
private HealthConnectUtils() {
}
@@ -43,6 +46,9 @@ public class HealthConnectUtils {
}
public static Set<String> getGrantedPermissions(Context context) {
if (!isAvailable(context))
return Set.of();
try {
HealthConnectClient client = HealthConnectClient.getOrCreate(context);
PermissionController controller = client.getPermissionController();
@@ -6,16 +6,22 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.health.connect.client.HealthConnectClient;
import androidx.health.connect.client.records.DistanceRecord;
import androidx.health.connect.client.records.ElevationGainedRecord;
import androidx.health.connect.client.records.ExerciseRoute;
import androidx.health.connect.client.records.ExerciseSessionRecord;
import androidx.health.connect.client.records.Record;
import androidx.health.connect.client.records.SpeedRecord;
import androidx.health.connect.client.records.metadata.Device;
import androidx.health.connect.client.records.metadata.Metadata;
import androidx.health.connect.client.units.Length;
import androidx.health.connect.client.units.Velocity;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -111,27 +117,65 @@ public class HealthConnectWorker extends Worker {
throw new RuntimeException("Track not found: " + trackId);
}
ExerciseRoute exerciseRoute = createExerciseRoute(contentProviderUtils, trackId);
HealthConnectClient client = HealthConnectClient.getOrCreate(context);
Metadata metadata = Metadata.activelyRecorded(
new Device(Device.TYPE_PHONE, Build.MANUFACTURER, Build.MODEL),
track.uuid().toString()
);
ExerciseSessionRecord record = new ExerciseSessionRecord(
List<Record> records = new ArrayList<>();
ExerciseData exerciseData = createExerciseData(contentProviderUtils, trackId);
//Avoid bug in HealthConnect: data must be within ExerciseSessionRecord.startTime and ExerciseSessionRecord.stopTime
List<ExerciseRoute.Location> locations = exerciseData.locations.stream()
.filter(i -> track.startTime().toInstant().isBefore(i.getTime()) && track.stopTime().toInstant().isAfter(i.getTime()))
.toList();
records.add(new ExerciseSessionRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
Metadata.activelyRecorded(
new Device(Device.TYPE_PHONE, Build.MANUFACTURER, Build.MODEL),
track.uuid().toString()
),
metadata,
toExerciseType(track.activityType()),
track.name(),
track.description(),
Collections.emptyList(),
Collections.emptyList(),
exerciseRoute);
new ExerciseRoute(locations)));
records.add(new DistanceRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
Length.meters(track.statistics().totalDistance().toM()),
metadata
));
if (track.statistics().altitudeGainLoss() != null) {
records.add(new ElevationGainedRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
Length.meters(track.statistics().altitudeGainLoss().gain_m()),
metadata
));
}
if (!exerciseData.speeds.isEmpty()) {
records.add(new SpeedRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
exerciseData.speeds,
metadata
));
}
List<ExerciseSessionRecord> records = Collections.singletonList(record);
Object ignored = BuildersKt.runBlocking(
kotlin.coroutines.EmptyCoroutineContext.INSTANCE,
(Function2) (scope, cont) -> client.insertRecords(records, (Continuation) cont));
@@ -140,30 +184,53 @@ public class HealthConnectWorker extends Worker {
return new ExportResult(track.name());
}
private static ExerciseRoute createExerciseRoute(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
List<ExerciseRoute.Location> route = new ArrayList<>();
private static ExerciseData createExerciseData(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
List<ExerciseRoute.Location> locations = new LinkedList<>();
List<SpeedRecord.Sample> speeds = new LinkedList<>();
try (TrackPointIterator iterator = contentProviderUtils.getTrackPointIterator(trackId, null)) {
while (iterator.hasNext()) {
while (iterator.hasNext()) { //TODO iterator.iterator.forEachRemaining()
TrackPoint trackPoint = iterator.next();
if (trackPoint.type() != TrackPoint.Type.TRACKPOINT) continue;
if (trackPoint.type() != TrackPoint.Type.TRACKPOINT) continue; //TODO Include SEGMENT_START_AUTOMATIC?
Position position = trackPoint.position();
route.add(new ExerciseRoute.Location(
trackPoint.getTime(),
position.latitude(),
position.longitude(),
position.horizontalAccuracy() != null ? Length.meters(position.horizontalAccuracy().toM()) : null,
position.verticalAccuracy() != null ? Length.meters(position.verticalAccuracy().toM()) : null,
position.altitude() != null ? Length.meters(position.altitude().toM()) : null)
);
if (position.hasLocation()) {
//ExcerciseRoute does not allow duplicated timestamps.
if (locations.stream().noneMatch(location -> location.getTime().equals(position.time()))) {
locations.add(new ExerciseRoute.Location(
trackPoint.getTime(),
position.latitude(),
position.longitude(),
position.horizontalAccuracy() != null ? Length.meters(position.horizontalAccuracy().toM()) : null,
position.verticalAccuracy() != null ? Length.meters(position.verticalAccuracy().toM()) : null,
position.altitude() != null ? Length.meters(position.altitude().toM()) : null)
);
}
}
if (position.hasSpeed()) {
speeds.add(new SpeedRecord.Sample(
trackPoint.getTime(),
Velocity.metersPerSecond(position.speed().toMPS())
));
}
//TODO Cadence
//TODO Power
//TODO Heartrate
}
}
return new ExerciseRoute(route);
return new ExerciseData(locations, speeds);
}
private record ExerciseData(
List<ExerciseRoute.Location> locations,
List<SpeedRecord.Sample> speeds
) {}
private static int toExerciseType(ActivityType activityType) {
return switch (activityType) {
case RUNNING, STREET_RUNNING, TRAIL_RUNNING, TRACK_RUNNING -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING;
@@ -45,8 +45,9 @@ public class GpsHandler extends SensorHandler<Location, Position> {
gpsStatusManager.onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval());
}
//TODO
connect();
if (driver != null) {
driver.updateDriverParameters(getContext(), sensorManager.getHandler());
}
}
};
@@ -20,8 +20,6 @@ import de.dennisguse.opentracks.settings.PreferencesUtils;
*/
class GpsStatusManager {
private static final String TAG = GpsStatusManager.class.getSimpleName();
// The duration that GpsStatus waits from minimal interval to consider GPS lost.
@VisibleForTesting
public static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(30);
@@ -21,11 +21,11 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
private static final String TAG = SensorHandler.class.getSimpleName();
private final SensorManager sensorManager;
@VisibleForTesting
public Driver driver;
protected final SensorManager sensorManager;
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
protected SensorHandler(SensorManager sensorManager) {
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Cadence;
@@ -98,7 +99,7 @@ public final class BoschEbikeParser {
.filter(BoschEbikeParser::isValidFrame)
.filter(BoschEbikeParser::isSupportedFrame)
.map(BoschEbikeParser::decodeFrame)
.filter(decodedFrame -> decodedFrame != null)
.filter(Objects::nonNull)
.reduce(null,
BoschEbikeParser::processMessage,
(left, right) -> right);
@@ -11,5 +11,9 @@ public interface Driver {
//TODO Remove from interface?
boolean isConnected();
default void updateDriverParameters(Context context, Handler handler) {
throw new UnsupportedOperationException();
}
void disconnect();
}
@@ -57,16 +57,11 @@ public class GpsInternal implements Driver {
return;
}
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(PreferencesUtils.getMinSamplingInterval().toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
if (PermissionRequester.GPS.hasPermission(context)) {
try {
Log.i(TAG, "Register for location updates " + context);
Log.i(TAG, "Register for location updates");
driverObserver.onConnected(null, null);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, locationListenerCompat);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, createLocationRequest(), handler::post, locationListenerCompat);
return;
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
@@ -76,6 +71,16 @@ public class GpsInternal implements Driver {
driverObserver.onDisconnected();
}
@SuppressWarnings({"MissingPermission"})
@Override
public void updateDriverParameters(Context context, Handler handler) {
if (!isConnected()) return;
Log.i(TAG, "Update for location updates");
driverObserver.onConnected(null, null);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, createLocationRequest(), handler::post, locationListenerCompat);
}
@SuppressWarnings({"MissingPermission"})
@Override
public void disconnect() {
@@ -87,9 +92,15 @@ public class GpsInternal implements Driver {
driverObserver.onDisconnected();
}
@Override
public boolean isConnected() {
return locationManager != null;
}
private LocationRequestCompat createLocationRequest() {
return new LocationRequestCompat.Builder(PreferencesUtils.getMinSamplingInterval().toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
}
}
@@ -63,12 +63,13 @@ public abstract class Aggregator<InputType, OutputType> {
*/
protected void resetOutdated() {
output = null;
};
}
/**
* Reset long-term (i.e., aggregated) values (more than derived from previous SensorData) like overall distance.
*/
public void resetAggregated() {};
public void resetAggregated() {
}
/**
* Is the data recent considering the current time.
@@ -79,7 +79,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
trackId = contentProviderUtils.insertTrack(
new Track(
null,
UUID.randomUUID(), //TODO This is not really cool: should happen in ContentProviderUtils?
null,
"",
"",
activityTypeLocalized,
@@ -120,6 +120,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
}
synchronized void endCurrentTrack() {
handler.removeCallbacks(ON_IDLE);
TrackPoint segmentEnd = trackPointCreator.createSegmentEnd();
insertTrackPoint(segmentEnd, true);
@@ -130,6 +131,10 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
}
RecordingData getDataForUI() {
if (trackId == null) {
throw new RuntimeException("Requesting data if not recording should not be done.");
}
Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastTrackPointUIWithSpeed, lastTrackPointUIWithAltitude, lastStoredTrackPointWithLocation);
TrackPoint trackPoint = current.first;
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater, trackPoint);
@@ -138,8 +143,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
Track track = contentProviderUtils.getTrack(trackId); //Get copy TODO: Avoid re-loading data from DB
if (track == null) {
Log.w(TAG, "Requesting data if not recording is taking place, should not be done.");
return null;
throw new RuntimeException("Data could not be loaded for TrackId " + trackId);
}
TrackBuilder trackBuilder = new TrackBuilder(track);
@@ -27,11 +27,6 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
setExportTrackFileFormatOptions();
setExportDirectorySummary();
setFilenameTemplate();
findPreference("healthConnectPermissions").setSummaryProvider(preference -> {
//TODO This should be localized and prefixes trimmed.
return String.join(",", HealthConnectUtils.getGrantedPermissions(getActivity()));
});
}
@Override
@@ -58,7 +53,6 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
if (!healthConnectAvailable) {
findPreference(getString(R.string.post_workout_export_health_connect_enabled_key))
.setSummary(R.string.health_connect_not_available);
return;
}
}
@@ -71,7 +71,6 @@ public class PreferencesUtils {
private static SharedPreferences sharedPreferences;
private static Resources resources;
private static final long LONG_UNSET = Long.MIN_VALUE;
/**
* Must be called during application startup.
@@ -188,17 +187,20 @@ public class PreferencesUtils {
return sharedPreferences.getString(getKey(keyId), defaultValue);
}
static void setString(int keyId, String value) {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static void setString(int keyId, String value) {
Editor editor = sharedPreferences.edit();
editor.putString(getKey(keyId), value);
editor.apply();
}
static void setString(int keyId, int valueId) {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static void setString(int keyId, int valueId) {
setString(keyId, resources.getString(valueId));
}
static void setBoolean(int keyId, Boolean value) {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static void setBoolean(int keyId, Boolean value) {
Editor editor = sharedPreferences.edit();
editor.putBoolean(getKey(keyId), value);
editor.apply();
@@ -537,6 +539,7 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length];
final int maxRecordingDistanceDefault = Integer.parseInt(resources.getString(R.string.max_recording_distance_default));
final int maxRecordingDistanceOff = Integer.parseInt(resources.getString(R.string.max_recording_distance_off));
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
@@ -553,6 +556,9 @@ public class PreferencesUtils {
case METRIC, IMPERIAL_METER -> {
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
}
if (value == maxRecordingDistanceOff) {
entries[i] = resources.getString(R.string.value_off);
} else {
entries[i] = displayValue;
}
@@ -560,6 +566,9 @@ public class PreferencesUtils {
case IMPERIAL_FEET, NAUTICAL_IMPERIAL -> {
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
}
if (value == maxRecordingDistanceOff) {
entries[i] = resources.getString(R.string.value_off);
} else {
entries[i] = displayValue;
}
@@ -96,6 +96,7 @@ public class BluetoothLeAdapter extends BaseAdapter {
this.name = name;
}
@NonNull
public String getAddress() {
return address;
}
@@ -16,42 +16,42 @@ public class ZoneOffsetAdapter extends ArrayAdapter<ZoneOffset> {
super(context, resource);
addAll(
ZoneOffset.ofHoursMinutes(-12, 00),
ZoneOffset.ofHoursMinutes(-11, 00),
ZoneOffset.ofHoursMinutes(-10, 00),
ZoneOffset.ofHoursMinutes(-12, 0),
ZoneOffset.ofHoursMinutes(-11, 0),
ZoneOffset.ofHoursMinutes(-10, 0),
ZoneOffset.ofHoursMinutes(-9, -30),
ZoneOffset.ofHoursMinutes(-8, 00),
ZoneOffset.ofHoursMinutes(-7, 00),
ZoneOffset.ofHoursMinutes(-6, 00),
ZoneOffset.ofHoursMinutes(-5, 00),
ZoneOffset.ofHoursMinutes(-4, 00),
ZoneOffset.ofHoursMinutes(-8, 0),
ZoneOffset.ofHoursMinutes(-7, 0),
ZoneOffset.ofHoursMinutes(-6, 0),
ZoneOffset.ofHoursMinutes(-5, 0),
ZoneOffset.ofHoursMinutes(-4, 0),
ZoneOffset.ofHoursMinutes(-3, -30),
ZoneOffset.ofHoursMinutes(-3, 00),
ZoneOffset.ofHoursMinutes(-2, 00),
ZoneOffset.ofHoursMinutes(-1, 00),
ZoneOffset.ofHoursMinutes(0, 00),
ZoneOffset.ofHoursMinutes(1, 00),
ZoneOffset.ofHoursMinutes(2, 00),
ZoneOffset.ofHoursMinutes(3, 00),
ZoneOffset.ofHoursMinutes(-3, 0),
ZoneOffset.ofHoursMinutes(-2, 0),
ZoneOffset.ofHoursMinutes(-1, 0),
ZoneOffset.ofHoursMinutes(0, 0),
ZoneOffset.ofHoursMinutes(1, 0),
ZoneOffset.ofHoursMinutes(2, 0),
ZoneOffset.ofHoursMinutes(3, 0),
ZoneOffset.ofHoursMinutes(3, 30),
ZoneOffset.ofHoursMinutes(4, 00),
ZoneOffset.ofHoursMinutes(4, 0),
ZoneOffset.ofHoursMinutes(4, 30),
ZoneOffset.ofHoursMinutes(5, 30),
ZoneOffset.ofHoursMinutes(5, 45),
ZoneOffset.ofHoursMinutes(6, 00),
ZoneOffset.ofHoursMinutes(6, 0),
ZoneOffset.ofHoursMinutes(6, 30),
ZoneOffset.ofHoursMinutes(7, 00),
ZoneOffset.ofHoursMinutes(8, 00),
ZoneOffset.ofHoursMinutes(7, 0),
ZoneOffset.ofHoursMinutes(8, 0),
ZoneOffset.ofHoursMinutes(8, 45),
ZoneOffset.ofHoursMinutes(9, 00),
ZoneOffset.ofHoursMinutes(9, 0),
ZoneOffset.ofHoursMinutes(9, 30),
ZoneOffset.ofHoursMinutes(10, 00),
ZoneOffset.ofHoursMinutes(10, 0),
ZoneOffset.ofHoursMinutes(10, 30),
ZoneOffset.ofHoursMinutes(11, 00),
ZoneOffset.ofHoursMinutes(12, 00),
ZoneOffset.ofHoursMinutes(11, 0),
ZoneOffset.ofHoursMinutes(12, 0),
ZoneOffset.ofHoursMinutes(12, 45),
ZoneOffset.ofHoursMinutes(13, 00),
ZoneOffset.ofHoursMinutes(14, 00)
ZoneOffset.ofHoursMinutes(13, 0),
ZoneOffset.ofHoursMinutes(14, 0)
);
}
}
@@ -136,12 +136,6 @@ public class FilterDialogFragment extends DialogFragment {
public final String value;
public boolean isChecked;
public FilterItem(String id, String value) {
this.id = id;
this.value = value;
this.isChecked = true;
}
public FilterItem(String id, String value, boolean isChecked) {
this.id = id;
this.value = value;

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