mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-09-30 16:52:21 +02:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63e175c76a | |||
| 8142073abd | |||
| 149c620525 | |||
| c96b0e3617 | |||
| 268c27dc4a | |||
| 3146785498 | |||
| 11589dfdd3 | |||
| 96828febdb | |||
| 98ae6fe0d2 | |||
| 2e27b06c01 | |||
| 8db8b58020 | |||
| 9a9e1e6947 | |||
| d9880f259d | |||
| 08b5ab381e | |||
| a63298ef6e | |||
| 1294e944a7 | |||
| 7258335180 | |||
| 139d72affe | |||
| 3791ccf888 | |||
| dc78615c83 | |||
| 53dfa915fb | |||
| 7c6141f98c |
@@ -1,36 +0,0 @@
|
||||
name: Android Test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Android Lint
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'ubuntu:android-24.04'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Lint
|
||||
shell: bash
|
||||
run: ./gradlew lint
|
||||
|
||||
android-check:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'ubuntu:android-24.04'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: ./gradlew testNightlyDebugUnitTest --stacktrace
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Auto-Sync Upstream
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # Uruchamia się codziennie o 3:00 w nocy
|
||||
workflow_dispatch: # Pozwala uruchomić to ręcznie z poziomu Gitea
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync with Upstream
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- name: Configure Git & Fetch
|
||||
run: |
|
||||
git config user.name "Gitea Actions Bot"
|
||||
git config user.email "actions@gitea.local"
|
||||
git remote add upstream https://gitea.local.labus.uk/upstream-mirrors/OpenTracks.git
|
||||
git fetch upstream main
|
||||
|
||||
- name: Check for updates & Attempt Merge
|
||||
id: merge
|
||||
run: |
|
||||
# Sprawdzamy czy są jakiekolwiek nowe zmiany
|
||||
if git diff --quiet HEAD upstream/main; then
|
||||
echo "Repozytorium jest aktualne. Brak zmian."
|
||||
echo "has_changes=false" >> $GITHUB_ENV
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Znaleziono nowe zmiany. Próbuję scalić..."
|
||||
echo "has_changes=true" >> $GITHUB_ENV
|
||||
|
||||
# Próba automatycznego scalenia (merge)
|
||||
if git merge upstream/main --no-edit; then
|
||||
echo "Scalenie przebiegło pomyślnie!"
|
||||
else
|
||||
echo "Wykryto konflikty! Przerywam scalanie."
|
||||
git merge --abort
|
||||
exit 1 # Błąd celowy - Gitea wyśle Ci powiadomienie o failu
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: env.has_changes == 'true'
|
||||
run: |
|
||||
git push origin main
|
||||
echo "Zmiany pomyślnie wypchnięte do repozytorium!"
|
||||
@@ -0,0 +1,78 @@
|
||||
name: CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Android Lint
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'cimg/android:2024.04-node'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Lint
|
||||
shell: bash
|
||||
run: ./gradlew lint
|
||||
|
||||
android-check:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'cimg/android:2024.04-node'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: ./gradlew testNightlyDebugUnitTest --stacktrace
|
||||
|
||||
release:
|
||||
name: Build and Upload APK
|
||||
needs: [lint, android-check]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'cimg/android:2024.04-node'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build APKs
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew assembleDebug
|
||||
./gradlew assembleRelease
|
||||
|
||||
- name: Prepare APKs
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p release-apks
|
||||
find . -name "*.apk" -print -exec cp {} release-apks/ \;
|
||||
ls -la release-apks/
|
||||
|
||||
- name: Upload to Gitea Releases
|
||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||
with:
|
||||
tag_name: build-${{ github.run_number }}
|
||||
name: "Build #${{ github.run_number }}"
|
||||
prerelease: true
|
||||
files: |-
|
||||
release-apks/*.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="OpenTracks">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="androidTest">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="unitTest">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="main">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -45,6 +45,18 @@ Translations are hosted on <a href="https://translate.codeberg.org/engage/opentr
|
||||
<img src="https://translate.codeberg.org/widget/opentracks/opentracks-src-main-res-values-strings-xml-main/horizontal-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## ⚠️ About This Fork
|
||||
|
||||
This repository is a customized fork of the official OpenTracks application.
|
||||
|
||||
**Core Purpose:**
|
||||
The primary goal of this fork is to fundamentally change how Bluetooth sensors are prioritized over GPS data. In this modified version, Bluetooth sensor data is given absolute priority and actively ignores/overrides GPS signals in most scenarios. This ensures that cadence, speed, and distance measurements strictly rely on hardware sensors rather than falling back to or being averaged with GPS data.
|
||||
|
||||
**Consequential Changes:**
|
||||
As a direct result of these sensor priority changes, several downstream modifications were implemented to support the new data flow:
|
||||
* **UI & Settings:** Expanded settings menus and customized recording layouts to better expose the raw sensor data and give the user finer control.
|
||||
* **Media & Voice Announcements:** Integrated media playback controls directly into the statistics view and overhauled voice announcements to handle audio focus and integrate with the new sensor-first approach.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div>
|
||||
|
||||
+9
-8
@@ -46,9 +46,9 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'de.dennisguse.opentracks'
|
||||
versionCode 6766
|
||||
versionName "v4.29.0"
|
||||
applicationId 'de.dennisguse.opentracks.fork'
|
||||
versionCode 6751
|
||||
versionName "v4.28.3"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -98,6 +98,7 @@ android {
|
||||
release {
|
||||
crunchPngs = false
|
||||
minifyEnabled = false
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,18 +107,18 @@ android {
|
||||
productFlavors {
|
||||
nightly {
|
||||
dimension 'version'
|
||||
applicationId 'de.dennisguse.opentracks.nightly'
|
||||
applicationId 'de.dennisguse.opentracks.fork.nightly'
|
||||
|
||||
versionCode getVersionCodeNightly()
|
||||
versionName getVersionName()
|
||||
base.archivesName = "de.dennisguse.opentracks.nightly_${getVersionCodeNightly()}"
|
||||
base.archivesName = "de.dennisguse.opentracks.fork.nightly_${getVersionCodeNightly()}"
|
||||
|
||||
signingConfig = signingConfigs.nightly
|
||||
}
|
||||
irreproducible {
|
||||
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
|
||||
dimension 'version'
|
||||
applicationId 'de.dennisguse.opentracks'
|
||||
applicationId 'de.dennisguse.opentracks.fork'
|
||||
versionNameSuffix 'irreproducible'
|
||||
}
|
||||
reproducible {
|
||||
@@ -125,9 +126,9 @@ android {
|
||||
// FDroid: https://f-droid.org/de/packages/de.dennisguse.opentracks.playstore
|
||||
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
|
||||
dimension 'version'
|
||||
applicationId 'de.dennisguse.opentracks.playstore'
|
||||
applicationId 'de.dennisguse.opentracks.fork.playstore'
|
||||
|
||||
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
|
||||
base.archivesName = "de.dennisguse.opentracks.fork.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
|
||||
|
||||
signingConfig = signingConfigs.release
|
||||
}
|
||||
|
||||
-788
@@ -1,788 +0,0 @@
|
||||
<?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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
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.
|
||||
@@ -0,0 +1,13 @@
|
||||
#This file is generated by updateDaemonJvm
|
||||
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
|
||||
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
|
||||
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
|
||||
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
|
||||
toolchainVendor=JETBRAINS
|
||||
toolchainVersion=21
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
cat << 'EOC' > src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java.patch
|
||||
--- src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java
|
||||
+++ src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java
|
||||
@@ -351,6 +351,11 @@
|
||||
public static boolean shouldVoiceAnnouncementLongBreak() {
|
||||
return getBoolean(R.string.settings_announcements_long_break_key, true);
|
||||
}
|
||||
+
|
||||
+ public static long getLongBreakDuration() {
|
||||
+ int minutes = getInt(R.string.settings_long_break_duration_key, 3);
|
||||
+ return minutes * 60 * 1000L;
|
||||
+ }
|
||||
EOC
|
||||
patch -p0 < src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java.patch
|
||||
@@ -0,0 +1,13 @@
|
||||
cat << 'EOC' > src/main/res/values/strings.xml.patch
|
||||
--- src/main/res/values/strings.xml
|
||||
+++ src/main/res/values/strings.xml
|
||||
@@ -1071,6 +1071,8 @@
|
||||
|
||||
<string name="settings_announcements_long_break_key">settings_announcements_long_break_key</string>
|
||||
<string name="settings_announcements_long_break_title">Long break</string>
|
||||
+ <string name="settings_long_break_duration_key">settings_long_break_duration_key</string>
|
||||
+ <string name="settings_long_break_duration_title">Long break duration (minutes)</string>
|
||||
|
||||
</resources>
|
||||
EOC
|
||||
patch -p0 < src/main/res/values/strings.xml.patch
|
||||
@@ -0,0 +1 @@
|
||||
cat src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java | grep -n "if (movingDistance != null) {" -B 2 -A 30
|
||||
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
rootProject.name = 'OpenTracks'
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-9
@@ -127,7 +127,6 @@ 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);
|
||||
|
||||
@@ -156,7 +155,7 @@ public class ExportImportTest {
|
||||
|
||||
Distance sensorDistance = Distance.of(10); // recording distance interval
|
||||
|
||||
Position position = sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
|
||||
contentProviderUtils.insertMarker(
|
||||
new Marker(
|
||||
null,
|
||||
@@ -164,7 +163,15 @@ public class ExportImportTest {
|
||||
"Marker 1",
|
||||
"Marker 1 desc",
|
||||
"Marker 1 typeLocalized",
|
||||
position,
|
||||
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)
|
||||
),
|
||||
null
|
||||
));
|
||||
|
||||
@@ -179,7 +186,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
|
||||
|
||||
position = sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 0f);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 0f);
|
||||
contentProviderUtils.insertMarker(
|
||||
new Marker(
|
||||
null,
|
||||
@@ -187,7 +194,7 @@ public class ExportImportTest {
|
||||
"Marker 2",
|
||||
"Marker 2 desc",
|
||||
"Marker 2 typeLocalized",
|
||||
position,
|
||||
service.getLastStoredTrackPointWithLocation().position(),
|
||||
null
|
||||
));
|
||||
|
||||
@@ -762,8 +769,7 @@ public class ExportImportTest {
|
||||
sensorManager.getAltitudeChangeHandler().setAggregator(aggregatorBarometer);
|
||||
}
|
||||
|
||||
//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) {
|
||||
public static void 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);
|
||||
@@ -776,7 +782,5 @@ public class ExportImportTest {
|
||||
|
||||
trackPointCreator.setClock(time);
|
||||
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
|
||||
|
||||
return Position.of(location, Instant.parse(time));
|
||||
}
|
||||
}
|
||||
+68
-17
@@ -92,7 +92,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -132,7 +135,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -172,7 +178,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -212,7 +221,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -254,7 +266,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -276,7 +291,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -316,7 +334,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -338,7 +359,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -378,7 +402,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -418,7 +445,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -458,7 +488,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -498,7 +531,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -520,7 +556,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -560,7 +599,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -582,7 +624,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -610,7 +655,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -644,7 +692,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
<?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>
|
||||
@@ -424,6 +424,16 @@ limitations under the License.
|
||||
android:icon="@drawable/ic_logo_color_24dp"
|
||||
android:label="@string/recording_service" />
|
||||
|
||||
<service
|
||||
android:name=".services.MediaNotificationListenerService"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--Necessary for SDK < 33 to (re-)store per app locale information-->
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
||||
@@ -67,6 +67,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
||||
|
||||
private Snackbar snackbar;
|
||||
private Snackbar sensorWarningSnackbar;
|
||||
|
||||
// The following are setFrequency in onCreate
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
@@ -86,6 +87,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
service.getGpsStatusObservable()
|
||||
.observe(TrackRecordingActivity.this, this::onGpsStatusChanged);
|
||||
|
||||
service.getSensorDisconnectedWarningObservable()
|
||||
.observe(TrackRecordingActivity.this, this::onSensorDisconnectedWarningChanged);
|
||||
|
||||
if (!service.isRecording()) {
|
||||
finish();
|
||||
return;
|
||||
@@ -408,4 +412,27 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
});
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
private void onSensorDisconnectedWarningChanged(boolean show) {
|
||||
if (show) {
|
||||
if (sensorWarningSnackbar == null) {
|
||||
sensorWarningSnackbar = Snackbar
|
||||
.make(viewBinding.trackRecordingCoordinatorLayout,
|
||||
getString(R.string.sensor_disconnected_warning),
|
||||
Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(getString(R.string.sensor_disconnected_switch_action), v -> {
|
||||
if (trackRecordingServiceConnection != null && trackRecordingServiceConnection.getTrackRecordingService() != null) {
|
||||
trackRecordingServiceConnection.getTrackRecordingService().setOverrideSensorWithGPS(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!sensorWarningSnackbar.isShown()) {
|
||||
sensorWarningSnackbar.show();
|
||||
}
|
||||
} else {
|
||||
if (sensorWarningSnackbar != null && sensorWarningSnackbar.isShown()) {
|
||||
sensorWarningSnackbar.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,6 @@ 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;
|
||||
@@ -233,16 +232,12 @@ public class ContentProviderUtils {
|
||||
/**
|
||||
* 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 as it should be database generated
|
||||
if (track.uuid() == null) {
|
||||
track = new TrackBuilder(track, UUID.randomUUID()).getTrack();
|
||||
}
|
||||
//TODO Unset trackId
|
||||
Uri uri = contentResolver.insert(CustomContentProvider.UrlType.TRACKS.getUri(), createContentValues(track));
|
||||
return new Track.Id(ContentUris.parseId(uri));
|
||||
}
|
||||
|
||||
@@ -57,17 +57,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -151,12 +151,14 @@ public class SegmentStatisticUpdater {
|
||||
}
|
||||
|
||||
public void addMovingTime(TrackPoint trackPoint, TrackPoint lastTrackPoint) {
|
||||
Duration movingDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||
addMovingTime(Duration.between(lastTrackPoint.getTime(), trackPoint.getTime()));
|
||||
}
|
||||
|
||||
if (movingDuration.isNegative()) {
|
||||
throw new RuntimeException("Moving time cannot be negative: " + lastTrackPoint.getTime() + " is after " + trackPoint.getTime());
|
||||
public void addMovingTime(Duration duration) {
|
||||
if (duration.isNegative()) {
|
||||
throw new RuntimeException("Moving time cannot be negative");
|
||||
}
|
||||
this.movingDuration = this.movingDuration.plus(movingDuration);
|
||||
movingDuration = movingDuration.plus(duration);
|
||||
}
|
||||
|
||||
private Speed getAverageMovingSpeed() {
|
||||
|
||||
+21
-11
@@ -143,7 +143,6 @@ public class TrackStatisticsUpdater {
|
||||
}
|
||||
|
||||
{
|
||||
// Update total distance
|
||||
Distance movingDistance = null;
|
||||
if (trackPoint.sensorDistance() != null) {
|
||||
movingDistance = trackPoint.sensorDistance();
|
||||
@@ -153,16 +152,7 @@ public class TrackStatisticsUpdater {
|
||||
// GPS-based distance/speed
|
||||
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
|
||||
}
|
||||
if (movingDistance != null) {
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
}
|
||||
|
||||
if (!isIdle
|
||||
&& !trackPoint.isSegmentManualStart()
|
||||
&& lastTrackPoint != null) {
|
||||
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
||||
}
|
||||
|
||||
boolean isIdleTransition = false;
|
||||
if (trackPoint.isIdleTriggered()) {
|
||||
isIdle = true;
|
||||
} else if (isIdle) {
|
||||
@@ -170,6 +160,26 @@ public class TrackStatisticsUpdater {
|
||||
if (movingDistance != null
|
||||
&& movingDistance.greaterOrEqualThan(PreferencesUtils.getRecordingDistanceInterval())) {
|
||||
isIdle = false;
|
||||
isIdleTransition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (movingDistance != null && !isIdle) {
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
}
|
||||
|
||||
if (!isIdle
|
||||
&& !trackPoint.isSegmentManualStart()
|
||||
&& lastTrackPoint != null) {
|
||||
if (isIdleTransition) {
|
||||
if (trackPoint.position().hasSpeed() && trackPoint.position().speed().toMPS() > 0) {
|
||||
double timeSeconds = movingDistance.toM() / trackPoint.position().speed().toMPS();
|
||||
currentSegment.addMovingTime(Duration.ofMillis((long)(timeSeconds * 1000)));
|
||||
} else {
|
||||
currentSegment.addMovingTime(Duration.ofMillis((long)(movingDistance.toM() * 1000)));
|
||||
}
|
||||
} else {
|
||||
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,6 @@ 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,6 +30,5 @@ 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);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ public class TrackImporter {
|
||||
}
|
||||
|
||||
//TODO This is a workaround until we have proper UI.
|
||||
trackData.uuid = null;
|
||||
trackData.uuid = UUID.randomUUID();
|
||||
}
|
||||
|
||||
trackPoints.sort(Comparator.comparing(TrackPoint::getTime));
|
||||
|
||||
@@ -189,6 +189,17 @@ public class DataProvider {
|
||||
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||
}
|
||||
|
||||
// Explicitly grant URI permissions to all packages that can resolve this intent.
|
||||
// This works around issues where ClipData multiple URI grants fail on some Android versions.
|
||||
android.content.pm.PackageManager packageManager = context.getPackageManager();
|
||||
java.util.List<android.content.pm.ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, android.content.pm.PackageManager.MATCH_DEFAULT_ONLY);
|
||||
for (android.content.pm.ResolveInfo resolveInfo : resolveInfos) {
|
||||
String packageName = resolveInfo.activityInfo.packageName;
|
||||
for (Uri uri : uris) {
|
||||
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
|
||||
import de.dennisguse.opentracks.sensors.driver.SensorType;
|
||||
@@ -28,6 +29,9 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
|
||||
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
|
||||
|
||||
@Nullable
|
||||
private Integer batteryLevel = null;
|
||||
|
||||
protected SensorHandler(SensorManager sensorManager) {
|
||||
this.sensorManager = sensorManager;
|
||||
}
|
||||
@@ -115,7 +119,8 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
return;
|
||||
}
|
||||
|
||||
SensorHandler.this.aggregator.resetAggregated();
|
||||
SensorHandler.this.aggregator = null;
|
||||
sensorManager.onChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,5 +135,18 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
SensorHandler.this.aggregator = null;
|
||||
batteryLevel = null;
|
||||
sensorManager.onChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatteryLevelReceived(int percentage) {
|
||||
batteryLevel = percentage;
|
||||
sensorManager.onChange();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getBatteryLevel() {
|
||||
return batteryLevel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
@@ -239,6 +241,9 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
}
|
||||
|
||||
public SensorDataSet getSensorDataSet(Instant now) {
|
||||
if (!isStarted()) {
|
||||
return new SensorDataSet(new SensorData<>(Position.of(now), ""), null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
//We always need a Position with now
|
||||
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
|
||||
if (gpsSensorData != null && gpsSensorData.data() != null) {
|
||||
@@ -247,6 +252,13 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
gpsSensorData = new SensorData<>(Position.of(now), "");
|
||||
}
|
||||
|
||||
BatteryLevel cadenceBattery = cyclingCadenceHandler.getBatteryLevel() != null
|
||||
? BatteryLevel.of(cyclingCadenceHandler.getBatteryLevel()) : null;
|
||||
BatteryLevel speedBattery = cyclingDistanceSpeedHandler.getBatteryLevel() != null
|
||||
? BatteryLevel.of(cyclingDistanceSpeedHandler.getBatteryLevel()) : null;
|
||||
BatteryLevel heartRateBattery = heartRateHandler.getBatteryLevel() != null
|
||||
? BatteryLevel.of(heartRateHandler.getBatteryLevel()) : null;
|
||||
|
||||
return new SensorDataSet(
|
||||
gpsSensorData,
|
||||
getSpeed(now),
|
||||
@@ -255,7 +267,10 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
temperatureHandler.getSensorData(now),
|
||||
getCadence(now),
|
||||
cyclingPowerHandler.getSensorData(now),
|
||||
altitudeChangeHandler.getSensorData(now)
|
||||
altitudeChangeHandler.getSensorData(now),
|
||||
cadenceBattery,
|
||||
speedBattery,
|
||||
heartRateBattery
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
@@ -53,6 +54,9 @@ public class BluetoothDriver<T> implements Driver {
|
||||
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
private String address;
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
@@ -68,10 +72,21 @@ public class BluetoothDriver<T> implements Driver {
|
||||
case BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
//This is also triggered, if no connection was established (ca. 30s)
|
||||
Log.i(TAG, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
|
||||
if (gatt.connect()) {
|
||||
Log.e(TAG, gatt.getDevice() + ": could not trigger reconnect for sensor");
|
||||
}
|
||||
observer.onConnectionLost();
|
||||
|
||||
synchronized (BluetoothDriver.this) {
|
||||
gatt.close();
|
||||
if (bluetoothGatt == gatt) {
|
||||
bluetoothGatt = null;
|
||||
}
|
||||
|
||||
if (context != null && handler != null && address != null) {
|
||||
final Context ctx = context;
|
||||
final Handler hnd = handler;
|
||||
final String addr = address;
|
||||
hnd.post(() -> connect(ctx, hnd, addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,9 +131,39 @@ public class BluetoothDriver<T> implements Driver {
|
||||
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
|
||||
}
|
||||
|
||||
// NOTE: battery read is triggered in onDescriptorWrite, after this write completes.
|
||||
// Attempting readCharacteristic here would fail silently because GATT
|
||||
// allows only one pending operation at a time.
|
||||
|
||||
observer.onConnected(device.getAddress(), device.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
// After the notification descriptor write completes, try to read the battery level.
|
||||
// This is the correct moment — GATT is idle and can process a new operation.
|
||||
BluetoothGattService batteryService = gatt.getService(BluetoothUtils.BATTERY.serviceUUID());
|
||||
if (batteryService != null) {
|
||||
BluetoothGattCharacteristic batteryChar = batteryService.getCharacteristic(BluetoothUtils.BATTERY.measurementUUID());
|
||||
if (batteryChar != null) {
|
||||
boolean queued = gatt.readCharacteristic(batteryChar);
|
||||
Log.d(TAG, gatt.getDevice() + ": battery read queued=" + queued);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, int status) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS
|
||||
&& characteristic.getUuid().equals(BluetoothUtils.BATTERY.measurementUUID())) {
|
||||
BatteryLevel level = BluetoothUtils.parseBatteryLevel(characteristic);
|
||||
if (level != null) {
|
||||
Log.d(TAG, gatt.getDevice() + ": battery level " + level.percentage() + "%");
|
||||
observer.onBatteryLevelReceived(level.percentage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
UUID serviceUUID = characteristic.getService().getUuid();
|
||||
@@ -169,6 +214,10 @@ public class BluetoothDriver<T> implements Driver {
|
||||
|
||||
Log.d(TAG, device + ": trying to connect");
|
||||
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
this.address = address;
|
||||
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
|
||||
}
|
||||
|
||||
@@ -184,6 +233,10 @@ public class BluetoothDriver<T> implements Driver {
|
||||
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
|
||||
bluetoothGatt = null;
|
||||
|
||||
this.context = null;
|
||||
this.handler = null;
|
||||
this.address = null;
|
||||
|
||||
observer.onDisconnected();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,10 @@ public interface DriverObserver<T> {
|
||||
* For example, the user deactivated the internal GPS.
|
||||
*/
|
||||
default void onSensorDeactivated() {}
|
||||
|
||||
/**
|
||||
* The Driver read the battery level from the Battery Service (0x180F).
|
||||
* Only called if the device exposes the Battery Service.
|
||||
*/
|
||||
default void onBatteryLevelReceived(int percentage) {}
|
||||
}
|
||||
|
||||
+15
-2
@@ -5,6 +5,7 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
@@ -18,17 +19,20 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
private Instant lastValidUpdate;
|
||||
|
||||
@Override
|
||||
protected void computeValue(@NonNull Raw<CyclingCadenceBluetooth.CyclingCadenceMeasurement> current) {
|
||||
if (current.value() instanceof CyclingCadenceBluetooth.DirectCadenceData directCadenceData) {
|
||||
output = directCadenceData.cadence();
|
||||
lastValidUpdate = current.time();
|
||||
return;
|
||||
}
|
||||
|
||||
computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value());
|
||||
computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value(), current.time());
|
||||
}
|
||||
|
||||
private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw) {
|
||||
private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw, Instant now) {
|
||||
CyclingCadenceBluetooth.CrankData previousRaw = getPreviousRaw();
|
||||
if (previousRaw == null) return;
|
||||
|
||||
@@ -36,6 +40,9 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
|
||||
if (timeDiff.isZero()) {
|
||||
if (lastValidUpdate != null && Duration.between(lastValidUpdate, now).compareTo(getMaxSensorAge()) > 0) {
|
||||
resetOutdated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timeDiff.isNegative()) {
|
||||
@@ -52,9 +59,15 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
}
|
||||
|
||||
long crankDiff = UintUtils.diff(currentRaw.crankRevolutionsCount(), previousRaw.crankRevolutionsCount(), UintUtils.UINT32_MAX);
|
||||
lastValidUpdate = now;
|
||||
output = Cadence.of(crankDiff, timeDiff);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetOutdated() {
|
||||
output = Cadence.of(0f);
|
||||
}
|
||||
|
||||
private CyclingCadenceBluetooth.CrankData getPreviousRaw() {
|
||||
if (previous == null) {
|
||||
return null;
|
||||
|
||||
+8
@@ -5,6 +5,7 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
@@ -21,9 +22,12 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
private Instant lastValidUpdate;
|
||||
|
||||
@Override
|
||||
protected void computeValue(@NonNull Raw<CyclingDistanceSpeedBluetooth.WheelData> current) {
|
||||
if (previous == null) {
|
||||
lastValidUpdate = current.time();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,6 +35,9 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
|
||||
if (timeDiff.isZero()) {
|
||||
if (lastValidUpdate != null && Duration.between(lastValidUpdate, current.time()).compareTo(getMaxSensorAge()) > 0) {
|
||||
resetOutdated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timeDiff.isNegative()) {
|
||||
@@ -52,6 +59,7 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
distanceOverall = distance.plus(output.distanceOverall);
|
||||
}
|
||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||
lastValidUpdate = current.time();
|
||||
output = new Data(distance, distanceOverall, speed_mps);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
@@ -27,6 +28,12 @@ public record SensorDataSet(
|
||||
@Nullable
|
||||
SensorData<Power> power,
|
||||
@Nullable
|
||||
SensorData<AltitudeGainLoss> altitudeGainLoss
|
||||
SensorData<AltitudeGainLoss> altitudeGainLoss,
|
||||
@Nullable
|
||||
BatteryLevel cadenceBattery,
|
||||
@Nullable
|
||||
BatteryLevel speedBattery,
|
||||
@Nullable
|
||||
BatteryLevel heartRateBattery
|
||||
) {
|
||||
}
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.media.MediaMetadata;
|
||||
import android.media.session.MediaController;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MediaNotificationListenerService extends NotificationListenerService {
|
||||
|
||||
private static final String TAG = MediaNotificationListenerService.class.getSimpleName();
|
||||
|
||||
private MediaSessionManager mediaSessionManager;
|
||||
private MediaController currentController;
|
||||
|
||||
private static final MutableLiveData<MediaData> mediaDataLiveData = new MutableLiveData<>(new MediaData(null, null, false, false));
|
||||
private static MediaNotificationListenerService instance;
|
||||
|
||||
public static LiveData<MediaData> getMediaDataObservable() {
|
||||
return mediaDataLiveData;
|
||||
}
|
||||
|
||||
public static MediaNotificationListenerService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final MediaController.Callback mediaControllerCallback = new MediaController.Callback() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(PlaybackState state) {
|
||||
updateMediaData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataChanged(MediaMetadata metadata) {
|
||||
updateMediaData();
|
||||
}
|
||||
};
|
||||
|
||||
private final MediaSessionManager.OnActiveSessionsChangedListener sessionsChangedListener = controllers -> {
|
||||
Log.d(TAG, "Active sessions changed");
|
||||
findActiveMediaController(controllers);
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
mediaSessionManager = (MediaSessionManager) getSystemService(MEDIA_SESSION_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListenerConnected() {
|
||||
super.onListenerConnected();
|
||||
Log.d(TAG, "Listener connected");
|
||||
mediaDataLiveData.postValue(new MediaData(null, null, false, true));
|
||||
try {
|
||||
ComponentName componentName = new ComponentName(this, MediaNotificationListenerService.class);
|
||||
mediaSessionManager.addOnActiveSessionsChangedListener(sessionsChangedListener, componentName);
|
||||
findActiveMediaController(mediaSessionManager.getActiveSessions(componentName));
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "SecurityException while accessing MediaSessionManager", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListenerDisconnected() {
|
||||
super.onListenerDisconnected();
|
||||
Log.d(TAG, "Listener disconnected");
|
||||
mediaDataLiveData.postValue(new MediaData(null, null, false, false));
|
||||
if (mediaSessionManager != null) {
|
||||
mediaSessionManager.removeOnActiveSessionsChangedListener(sessionsChangedListener);
|
||||
}
|
||||
if (currentController != null) {
|
||||
currentController.unregisterCallback(mediaControllerCallback);
|
||||
currentController = null;
|
||||
}
|
||||
instance = null;
|
||||
}
|
||||
|
||||
private void findActiveMediaController(List<MediaController> controllers) {
|
||||
if (controllers == null || controllers.isEmpty()) {
|
||||
if (currentController != null) {
|
||||
currentController.unregisterCallback(mediaControllerCallback);
|
||||
currentController = null;
|
||||
}
|
||||
updateMediaData();
|
||||
return;
|
||||
}
|
||||
|
||||
MediaController newController = controllers.get(0);
|
||||
for (MediaController controller : controllers) {
|
||||
PlaybackState state = controller.getPlaybackState();
|
||||
if (state != null && (state.getState() == PlaybackState.STATE_PLAYING || state.getState() == PlaybackState.STATE_BUFFERING)) {
|
||||
newController = controller;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentController != null && currentController.getSessionToken().equals(newController.getSessionToken())) {
|
||||
// Already tracking this controller
|
||||
updateMediaData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentController != null) {
|
||||
currentController.unregisterCallback(mediaControllerCallback);
|
||||
}
|
||||
|
||||
currentController = newController;
|
||||
currentController.registerCallback(mediaControllerCallback);
|
||||
updateMediaData();
|
||||
}
|
||||
|
||||
private void updateMediaData() {
|
||||
if (currentController == null) {
|
||||
mediaDataLiveData.postValue(new MediaData(null, null, false, true));
|
||||
return;
|
||||
}
|
||||
|
||||
MediaMetadata metadata = currentController.getMetadata();
|
||||
PlaybackState playbackState = currentController.getPlaybackState();
|
||||
|
||||
String title = null;
|
||||
String artist = null;
|
||||
if (metadata != null) {
|
||||
title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
|
||||
artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
|
||||
}
|
||||
|
||||
boolean isPlaying = false;
|
||||
if (playbackState != null) {
|
||||
isPlaying = playbackState.getState() == PlaybackState.STATE_PLAYING ||
|
||||
playbackState.getState() == PlaybackState.STATE_BUFFERING;
|
||||
}
|
||||
|
||||
mediaDataLiveData.postValue(new MediaData(title, artist, isPlaying, true));
|
||||
}
|
||||
|
||||
public void sendMediaCommand(int keyCode) {
|
||||
if (currentController != null) {
|
||||
currentController.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
currentController.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
} else {
|
||||
// Fallback if no active controller (will broadcast to the system)
|
||||
android.media.AudioManager audioManager = (android.media.AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (audioManager != null) {
|
||||
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MediaData {
|
||||
public final String title;
|
||||
public final String artist;
|
||||
public final boolean isPlaying;
|
||||
public final boolean isPermissionGranted;
|
||||
|
||||
public MediaData(String title, String artist, boolean isPlaying, boolean isPermissionGranted) {
|
||||
this.title = title;
|
||||
this.artist = artist;
|
||||
this.isPlaying = isPlaying;
|
||||
this.isPermissionGranted = isPermissionGranted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,13 @@ public record RecordingData(
|
||||
*/
|
||||
TrackPoint latestTrackPoint,
|
||||
SensorDataSet sensorDataSet,
|
||||
Statistics currentSegment) {
|
||||
Statistics currentSegment,
|
||||
String autoPauseStatus,
|
||||
String idleTimer,
|
||||
String walkingTimer,
|
||||
String markerTimer) {
|
||||
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null, null);
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null, null, "", "", "", "");
|
||||
|
||||
public Statistics trackStatistics() {
|
||||
return track.statistics();
|
||||
|
||||
@@ -36,10 +36,12 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
private static final AltitudeCorrectionManager ALTITUDE_CORRECTION_MANAGER = new AltitudeCorrectionManager();
|
||||
|
||||
private final Runnable ON_IDLE = this::onIdle;
|
||||
private final Runnable ON_LONG_BREAK = this::onLongBreak;
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final Context context;
|
||||
private final IdleObserver idleObserver;
|
||||
private final SensorDisconnectObserver sensorDisconnectObserver;
|
||||
|
||||
private final Handler handler;
|
||||
|
||||
@@ -59,9 +61,25 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
private TrackPoint lastStoredTrackPoint;
|
||||
private TrackPoint lastStoredTrackPointWithLocation;
|
||||
|
||||
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, IdleObserver idleObserver, Handler handler) {
|
||||
private boolean hasSensorBeenConnected = false;
|
||||
private boolean isSensorDisconnectedWarningActive = false;
|
||||
|
||||
private boolean autoPauseOnCadence0;
|
||||
private de.dennisguse.opentracks.data.models.Speed autoPauseCadence0Speed;
|
||||
private boolean hasCadenceSensorBeenConnected = false;
|
||||
private String autoPauseStatus = "N/A";
|
||||
|
||||
private java.time.Instant idleTimeoutScheduledAt;
|
||||
private java.time.Instant walkingStartedAt;
|
||||
private java.time.Instant idleStartedAt;
|
||||
private boolean longBreakRecorded = false;
|
||||
|
||||
private boolean gpsOverriden = false;
|
||||
|
||||
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, IdleObserver idleObserver, SensorDisconnectObserver sensorDisconnectObserver, Handler handler) {
|
||||
this.context = context;
|
||||
this.idleObserver = idleObserver;
|
||||
this.sensorDisconnectObserver = sensorDisconnectObserver;
|
||||
this.trackPointCreator = trackPointCreator;
|
||||
this.handler = handler;
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
@@ -79,7 +97,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
trackId = contentProviderUtils.insertTrack(
|
||||
new Track(
|
||||
null,
|
||||
null,
|
||||
UUID.randomUUID(), //TODO This is not really cool: should happen in ContentProviderUtils?
|
||||
"",
|
||||
"",
|
||||
activityTypeLocalized,
|
||||
@@ -97,6 +115,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
onNewTrackPoint(segmentStartTrackPoint);
|
||||
|
||||
handler.post(() -> {
|
||||
if (trackId != null) {
|
||||
onIdle();
|
||||
}
|
||||
});
|
||||
String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue());
|
||||
if (!de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address)) {
|
||||
handler.postDelayed(() -> {
|
||||
if (trackId != null && !hasSensorBeenConnected && !isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = true;
|
||||
if (sensorDisconnectObserver != null) {
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(true);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
return trackId;
|
||||
}
|
||||
|
||||
@@ -116,6 +151,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
reset();
|
||||
|
||||
handler.post(() -> {
|
||||
if (this.trackId != null) {
|
||||
onIdle();
|
||||
}
|
||||
});
|
||||
String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue());
|
||||
if (!de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address)) {
|
||||
handler.postDelayed(() -> {
|
||||
if (trackId != null && !hasSensorBeenConnected && !isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = true;
|
||||
if (sensorDisconnectObserver != null) {
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(true);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,6 +178,12 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
trackId = null;
|
||||
trackStatisticsUpdater = null;
|
||||
|
||||
idleTimeoutScheduledAt = null;
|
||||
walkingStartedAt = null;
|
||||
idleStartedAt = null;
|
||||
longBreakRecorded = false;
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
|
||||
reset();
|
||||
}
|
||||
@@ -148,32 +206,115 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
TrackBuilder trackBuilder = new TrackBuilder(track);
|
||||
trackBuilder.setStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
|
||||
|
||||
return new RecordingData(trackBuilder.getTrack(), TrackPointWithEGM2008, current.second, tmpTrackStatisticsUpdater.getCurrentSegment());
|
||||
|
||||
java.time.Instant now = java.time.Instant.now();
|
||||
String idleTimer = "";
|
||||
if (idleTimeoutScheduledAt != null && !"IDLE".equals(autoPauseStatus) && !"WALKING".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(now, idleTimeoutScheduledAt);
|
||||
if (!d.isNegative()) idleTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d);
|
||||
}
|
||||
|
||||
String walkingTimer = "";
|
||||
if (walkingStartedAt != null && "WALKING".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(walkingStartedAt, now);
|
||||
if (!d.isNegative()) walkingTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d);
|
||||
}
|
||||
|
||||
String markerTimer = "";
|
||||
if (idleStartedAt != null && "IDLE".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(idleStartedAt, now);
|
||||
java.time.Duration markerDuration = de.dennisguse.opentracks.settings.PreferencesUtils.getMarkerIdleDuration();
|
||||
if (longBreakRecorded || d.compareTo(markerDuration) >= 0) {
|
||||
d = markerDuration;
|
||||
}
|
||||
if (!d.isNegative()) markerTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d);
|
||||
}
|
||||
|
||||
return new RecordingData(trackBuilder.getTrack(), TrackPointWithEGM2008, current.second, tmpTrackStatisticsUpdater.getCurrentSegment(), autoPauseStatus, idleTimer, walkingTimer, markerTimer);
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public synchronized void onIdle() {
|
||||
Log.d(TAG, "Becoming idle");
|
||||
onNewTrackPoint(trackPointCreator.createIdle());
|
||||
TrackPoint idlePoint = trackPointCreator.createIdle();
|
||||
|
||||
boolean preventIdle = de.dennisguse.opentracks.settings.PreferencesUtils.shouldPreventIdleIfCadence();
|
||||
if (preventIdle && hasCadenceSensorBeenConnected) {
|
||||
if (idlePoint.cadence() != null && idlePoint.cadence().value_rpm() > 0) {
|
||||
scheduleNewIdleTimeout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!"IDLE".equals(autoPauseStatus)) {
|
||||
idleStartedAt = java.time.Instant.now();
|
||||
longBreakRecorded = false;
|
||||
java.time.Duration markerDuration = de.dennisguse.opentracks.settings.PreferencesUtils.getMarkerIdleDuration();
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
handler.postDelayed(ON_LONG_BREAK, markerDuration.toMillis());
|
||||
}
|
||||
autoPauseStatus = "IDLE";
|
||||
|
||||
onNewTrackPoint(idlePoint);
|
||||
|
||||
idleObserver.onIdle();
|
||||
}
|
||||
|
||||
private synchronized void onLongBreak() {
|
||||
if ("IDLE".equals(autoPauseStatus) && idleStartedAt != null && !longBreakRecorded) {
|
||||
longBreakRecorded = true;
|
||||
TrackPoint trackPoint = trackPointCreator.createSegmentStartManual();
|
||||
insertTrackPoint(trackPoint, true);
|
||||
idleObserver.onLongBreak();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TrackPoint was stored?
|
||||
*/
|
||||
synchronized boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
lastTrackPointUIWithSpeed = trackPoint;
|
||||
}
|
||||
if (trackPoint.position().hasAltitude()) {
|
||||
lastTrackPointUIWithAltitude = trackPoint;
|
||||
}
|
||||
|
||||
boolean sensorHasDistance = trackPoint.sensorDistance() != null;
|
||||
if (sensorHasDistance) {
|
||||
hasSensorBeenConnected = true;
|
||||
if (isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = false;
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||
sensorDisconnectObserver.onSensorConnected();
|
||||
}
|
||||
} else if (hasSensorBeenConnected) {
|
||||
if (!isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = true;
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(true);
|
||||
}
|
||||
}
|
||||
|
||||
String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue());
|
||||
boolean isSensorExpected = !de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address);
|
||||
boolean strictSensorMode = isSensorExpected && !sensorHasDistance && !gpsOverriden;
|
||||
if (strictSensorMode) {
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
trackPoint = trackPoint.with(trackPoint.position().with(de.dennisguse.opentracks.data.models.Speed.ZERO));
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
lastTrackPointUIWithSpeed = trackPoint;
|
||||
}
|
||||
|
||||
|
||||
if (trackPoint.type() == TrackPoint.Type.IDLE) {
|
||||
longBreakRecorded = false;
|
||||
insertTrackPoint(trackPoint, true);
|
||||
handler.removeCallbacks(ON_IDLE);
|
||||
|
||||
long durationMs = PreferencesUtils.getMarkerIdleDuration().toMillis();
|
||||
if (durationMs > 0) {
|
||||
handler.postDelayed(ON_LONG_BREAK, durationMs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
//Storing trackPoint
|
||||
@@ -204,11 +345,37 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trackPoint.cadence() != null) {
|
||||
hasCadenceSensorBeenConnected = true;
|
||||
}
|
||||
|
||||
boolean isWalkingWithBike = false;
|
||||
if (autoPauseOnCadence0 && hasCadenceSensorBeenConnected && autoPauseCadence0Speed != null) {
|
||||
if (trackPoint.cadence() == null || trackPoint.cadence().getRPM() == 0) {
|
||||
if (trackPoint.position().hasSpeed() && trackPoint.position().speed().toMPS() > 0 && trackPoint.position().speed().lessThan(autoPauseCadence0Speed)) {
|
||||
isWalkingWithBike = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isWalkingWithBike && "WALKING".equals(autoPauseStatus)) {
|
||||
autoPauseStatus = "REC";
|
||||
walkingStartedAt = null;
|
||||
scheduleNewIdleTimeout();
|
||||
}
|
||||
|
||||
Distance distanceToLastStoredTrackPoint;
|
||||
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
|
||||
if (isWalkingWithBike || strictSensorMode) {
|
||||
distanceToLastStoredTrackPoint = Distance.ZERO;
|
||||
if (isWalkingWithBike) {
|
||||
autoPauseStatus = "WALKING";
|
||||
}
|
||||
} else {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
|
||||
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
|
||||
} else {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
|
||||
@@ -216,6 +383,20 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
insertTrackPoint(trackPoint, true);
|
||||
|
||||
scheduleNewIdleTimeout();
|
||||
if (!isWalkingWithBike) {
|
||||
if ("IDLE".equals(autoPauseStatus)) {
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
}
|
||||
autoPauseStatus = "REC";
|
||||
} else {
|
||||
if (!"WALKING".equals(autoPauseStatus)) {
|
||||
walkingStartedAt = java.time.Instant.now();
|
||||
if ("IDLE".equals(autoPauseStatus)) {
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
}
|
||||
}
|
||||
autoPauseStatus = "WALKING";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -223,6 +404,20 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
insertTrackPoint(trackPoint, false);
|
||||
|
||||
scheduleNewIdleTimeout();
|
||||
if (!isWalkingWithBike) {
|
||||
if ("IDLE".equals(autoPauseStatus)) {
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
}
|
||||
autoPauseStatus = "REC";
|
||||
} else {
|
||||
if (!"WALKING".equals(autoPauseStatus)) {
|
||||
walkingStartedAt = java.time.Instant.now();
|
||||
if ("IDLE".equals(autoPauseStatus)) {
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
}
|
||||
}
|
||||
autoPauseStatus = "WALKING";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -239,6 +434,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
handler.removeCallbacks(ON_IDLE);
|
||||
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
|
||||
idleTimeoutScheduledAt = java.time.Instant.now().plus(idleDuration);
|
||||
}
|
||||
|
||||
Statistics getTrackStatistics() {
|
||||
@@ -288,6 +484,13 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
lastStoredTrackPoint = null;
|
||||
lastStoredTrackPointWithLocation = null;
|
||||
|
||||
hasSensorBeenConnected = false;
|
||||
isSensorDisconnectedWarningActive = false;
|
||||
hasCadenceSensorBeenConnected = false;
|
||||
if (sensorDisconnectObserver != null) {
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -301,13 +504,36 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
if (PreferencesUtils.isKey(R.string.idle_duration_key, key)) {
|
||||
idleDuration = PreferencesUtils.getIdleDurationTimeout();
|
||||
}
|
||||
if (PreferencesUtils.isKey(R.string.settings_recording_auto_pause_on_cadence_0_key, key)) {
|
||||
autoPauseOnCadence0 = PreferencesUtils.shouldAutoPauseOnCadence0();
|
||||
}
|
||||
if (PreferencesUtils.isKey(R.string.settings_recording_auto_pause_on_cadence_0_speed_key, key)) {
|
||||
autoPauseCadence0Speed = PreferencesUtils.getAutoPauseCadence0Speed();
|
||||
}
|
||||
}
|
||||
|
||||
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||
return lastStoredTrackPointWithLocation;
|
||||
}
|
||||
|
||||
public void setOverrideSensorWithGPS(boolean override) {
|
||||
if (override) {
|
||||
hasSensorBeenConnected = false;
|
||||
gpsOverriden = true;
|
||||
if (isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = false;
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IdleObserver {
|
||||
void onIdle();
|
||||
void onLongBreak();
|
||||
}
|
||||
|
||||
public interface SensorDisconnectObserver {
|
||||
void onSensorDisconnectedWarning(boolean show);
|
||||
void onSensorConnected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver {
|
||||
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver, TrackRecordingManager.SensorDisconnectObserver {
|
||||
|
||||
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
||||
|
||||
@@ -84,6 +84,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
private RecordingStatus recordingStatus;
|
||||
private MutableLiveData<RecordingStatus> recordingStatusObservable;
|
||||
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
|
||||
private MutableLiveData<Boolean> sensorDisconnectedWarningObservable;
|
||||
private MutableLiveData<RecordingData> recordingDataObservable;
|
||||
|
||||
private Handler handler;
|
||||
@@ -104,10 +105,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
recordingStatusObservable = new MutableLiveData<>();
|
||||
updateRecordingStatus(STATUS_DEFAULT);
|
||||
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
|
||||
sensorDisconnectedWarningObservable = new MutableLiveData<>(false);
|
||||
recordingDataObservable = new MutableLiveData<>(RecordingData.NOT_RECORDING);
|
||||
|
||||
trackPointCreator = new TrackPointCreator(this);
|
||||
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, handler);
|
||||
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, this, handler);
|
||||
|
||||
voiceAnnouncementManager = new VoiceAnnouncementManager(this);
|
||||
notificationManager = new TrackRecordingServiceNotificationManager(this);
|
||||
@@ -138,6 +140,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
|
||||
recordingStatusObservable = null;
|
||||
gpsStatusObservable = null;
|
||||
sensorDisconnectedWarningObservable = null;
|
||||
recordingDataObservable = null;
|
||||
|
||||
Log.d(TAG, "Destroyed");
|
||||
@@ -315,6 +318,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
return gpsStatusObservable;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getSensorDisconnectedWarningObservable() {
|
||||
return sensorDisconnectedWarningObservable;
|
||||
}
|
||||
|
||||
public LiveData<RecordingData> getRecordingDataObservable() {
|
||||
return recordingDataObservable;
|
||||
}
|
||||
@@ -337,6 +344,32 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
voiceAnnouncementManager.announceIdle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongBreak() {
|
||||
voiceAnnouncementManager.announceLongBreak();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorDisconnectedWarning(boolean show) {
|
||||
if (show) {
|
||||
voiceAnnouncementManager.announceSensorDisconnected();
|
||||
}
|
||||
if (sensorDisconnectedWarningObservable != null) {
|
||||
sensorDisconnectedWarningObservable.postValue(show);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorConnected() {
|
||||
voiceAnnouncementManager.announceSensorConnected();
|
||||
}
|
||||
|
||||
public void setOverrideSensorWithGPS(boolean override) {
|
||||
if (trackRecordingManager != null) {
|
||||
trackRecordingManager.setOverrideSensorWithGPS(override);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void stopUpdateRecordingData() {
|
||||
handler.removeCallbacks(updateRecordingData);
|
||||
|
||||
+24
@@ -119,6 +119,30 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createIdle(context));
|
||||
}
|
||||
|
||||
public void announceLongBreak() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementLongBreak()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createLongBreak(context));
|
||||
}
|
||||
|
||||
public void announceSensorDisconnected() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementSensorDisconnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createSensorDisconnected(context));
|
||||
}
|
||||
|
||||
public void announceSensorConnected() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementSensorConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createSensorConnected(context));
|
||||
}
|
||||
|
||||
public void announceStatisticsIfNeeded(@NonNull RecordingData data) {
|
||||
if (shouldNotAnnounce()) {
|
||||
return;
|
||||
|
||||
+15
@@ -50,6 +50,21 @@ class VoiceAnnouncementUtils {
|
||||
.append(context.getString(R.string.voiceIdle));
|
||||
}
|
||||
|
||||
static Spannable createLongBreak(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceLongBreak));
|
||||
}
|
||||
|
||||
static Spannable createSensorDisconnected(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceSensorDisconnected));
|
||||
}
|
||||
|
||||
static Spannable createSensorConnected(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceSensorConnected));
|
||||
}
|
||||
|
||||
static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable Statistics currentInterval, @Nullable SensorStatistics sensorStatistics) {
|
||||
Statistics trackStatistics = track.statistics();
|
||||
|
||||
|
||||
@@ -101,8 +101,11 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
position = position.withCoordinates(lastStoredTrackPointWithLocation.position());
|
||||
}
|
||||
|
||||
if (lastTrackPointUISpeed != null)
|
||||
if (sensorDataSet.speed() != null) {
|
||||
position = position.with(sensorDataSet.speed().data());
|
||||
} else if (lastTrackPointUISpeed != null) {
|
||||
position = position.with(lastTrackPointUISpeed.position().speed());
|
||||
}
|
||||
|
||||
if (lastTrackPointUIAltitude != null)
|
||||
position = position.with(lastTrackPointUIAltitude.position().altitude());
|
||||
|
||||
@@ -38,11 +38,13 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.HeartRateZones;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -248,6 +250,10 @@ public class PreferencesUtils {
|
||||
setString(R.string.stats_units_key, unitSystem.getPreferenceId());
|
||||
}
|
||||
|
||||
public static Duration getMarkerIdleDuration() {
|
||||
return Duration.ofMinutes(getInt(R.string.marker_idle_duration_key, 3));
|
||||
}
|
||||
|
||||
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||
public static void applyDefaultUnit() {
|
||||
if (getString(R.string.stats_units_key, "").isEmpty()) {
|
||||
@@ -341,6 +347,20 @@ public class PreferencesUtils {
|
||||
return getBoolean(R.string.voice_announce_idle_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnouncementLongBreak() {
|
||||
return getBoolean(R.string.voice_announce_long_break_key, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean shouldVoiceAnnouncementSensorDisconnected() {
|
||||
return getBoolean(R.string.voice_announce_sensor_disconnected_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnouncementSensorConnected() {
|
||||
return getBoolean(R.string.voice_announce_sensor_connected_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceTime() {
|
||||
return getBoolean(R.string.voice_announce_time_key, true);
|
||||
}
|
||||
@@ -688,6 +708,51 @@ public class PreferencesUtils {
|
||||
return entries;
|
||||
}
|
||||
|
||||
static String[] getMarkerIdleDurationEntries() {
|
||||
String[] entryValues = resources.getStringArray(R.array.marker_idle_duration_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
final int defaultDuration = Integer.parseInt(resources.getString(R.string.marker_idle_duration_default));
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
|
||||
if (value == defaultDuration) {
|
||||
entries[i] = resources.getString(R.string.value_integer_minute_recommended, value);
|
||||
} else {
|
||||
entries[i] = resources.getString(R.string.value_integer_minute, value);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static boolean shouldAutoPauseOnCadence0() {
|
||||
return getBoolean(R.string.settings_recording_auto_pause_on_cadence_0_key, false);
|
||||
}
|
||||
|
||||
public static boolean shouldPreventIdleIfCadence() {
|
||||
return getBoolean(R.string.settings_recording_prevent_idle_if_cadence_key, true);
|
||||
}
|
||||
|
||||
public static Speed getAutoPauseCadence0Speed() {
|
||||
final int DEFAULT = Integer.parseInt(resources.getString(R.string.auto_pause_cadence_0_speed_default));
|
||||
int value = getInt(R.string.settings_recording_auto_pause_on_cadence_0_speed_key, DEFAULT);
|
||||
return Speed.ofKMH(value);
|
||||
}
|
||||
|
||||
static String[] getAutoPauseCadence0SpeedEntries() {
|
||||
String[] entryValues = resources.getStringArray(R.array.auto_pause_cadence_0_speed_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
entries[i] = value + " km/h";
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static boolean shouldInstantExportToFile() {
|
||||
final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = resources.getBoolean(R.bool.post_workout_export_enabled_default);
|
||||
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
||||
@@ -845,7 +910,15 @@ public class PreferencesUtils {
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_cadence_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_power_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_clock_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false)
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_auto_pause_status_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_hr_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_cadence_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_speed_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_idle_timer_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_walking_timer_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_marker_timer_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_media_key), false, false, true)
|
||||
);
|
||||
|
||||
RecordingLayout defaultLayout = new RecordingLayout(resources.getString(R.string.stats_custom_layout_default_layout), 2);
|
||||
@@ -861,7 +934,7 @@ public class PreferencesUtils {
|
||||
String selectedLayout = getSelectedLayoutName();
|
||||
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
||||
|
||||
List<RecordingLayout> recordingLayouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
||||
List<RecordingLayout> recordingLayouts = getAllCustomLayouts();
|
||||
|
||||
RecordingLayout fallback = !recordingLayouts.isEmpty() ? recordingLayouts.get(0) : getDefaultLayout();
|
||||
|
||||
@@ -907,7 +980,18 @@ public class PreferencesUtils {
|
||||
public static List<RecordingLayout> getAllCustomLayouts() {
|
||||
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
||||
|
||||
return RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
||||
List<RecordingLayout> layouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
||||
List<DataField> defaultFields = getDefaultLayout().getFields();
|
||||
|
||||
for (RecordingLayout layout : layouts) {
|
||||
for (DataField df : defaultFields) {
|
||||
boolean hasField = layout.getFields().stream().anyMatch(f -> f.getKey().equals(df.getKey()));
|
||||
if (!hasField) {
|
||||
layout.addField(new DataField(df.getKey(), false, false, df.isWide()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return layouts;
|
||||
}
|
||||
|
||||
public static void resetCustomLayoutPreferences() {
|
||||
|
||||
@@ -57,5 +57,15 @@ public class RecordingSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
ListPreference idleDuration = findPreference(getString(R.string.idle_duration_key));
|
||||
idleDuration.setEntries(PreferencesUtils.getIdleDurationEntries());
|
||||
|
||||
ListPreference autoPauseCadence0Speed = findPreference(getString(R.string.settings_recording_auto_pause_on_cadence_0_speed_key));
|
||||
if (autoPauseCadence0Speed != null) {
|
||||
autoPauseCadence0Speed.setEntries(PreferencesUtils.getAutoPauseCadence0SpeedEntries());
|
||||
}
|
||||
|
||||
ListPreference markerIdleDuration = findPreference(getString(R.string.marker_idle_duration_key));
|
||||
if (markerIdleDuration != null) {
|
||||
markerIdleDuration.setEntries(PreferencesUtils.getMarkerIdleDurationEntries());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-2
@@ -40,6 +40,13 @@ public class RecordingLayoutIO {
|
||||
Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine);
|
||||
return recordingLayout;
|
||||
}
|
||||
|
||||
String key = fieldParts[0];
|
||||
if (key.equals(resources.getString(R.string.stats_custom_layout_sensor_status_key))) {
|
||||
// Ignore deprecated sensor_status key to safely remove it from user's custom layout
|
||||
continue;
|
||||
}
|
||||
|
||||
recordingLayout.addField(fromCSV(fieldParts, resources));
|
||||
}
|
||||
return recordingLayout;
|
||||
@@ -50,11 +57,24 @@ public class RecordingLayoutIO {
|
||||
}
|
||||
|
||||
private static DataField fromCSV(String[] fieldParts, @NonNull Resources resources) {
|
||||
boolean isWide;
|
||||
String key = fieldParts[0];
|
||||
|
||||
// Force isWide to true for specific keys that must always be wide
|
||||
if (key.equals(resources.getString(R.string.stats_custom_layout_coordinates_key)) ||
|
||||
key.equals(resources.getString(R.string.stats_custom_layout_media_key))) {
|
||||
isWide = true;
|
||||
} else if (fieldParts.length > 3) {
|
||||
isWide = YES_VALUE.equals(fieldParts[3]);
|
||||
} else {
|
||||
isWide = false;
|
||||
}
|
||||
|
||||
return new DataField(
|
||||
fieldParts[0],
|
||||
key,
|
||||
YES_VALUE.equals(fieldParts[1]),
|
||||
YES_VALUE.equals(fieldParts[2]),
|
||||
fieldParts[0].equals(resources.getString(R.string.stats_custom_layout_coordinates_key)));
|
||||
isWide);
|
||||
}
|
||||
|
||||
static String toCsv(DataField datafield) {
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter<Recycl
|
||||
Speed.ZERO
|
||||
);
|
||||
|
||||
demoData = new RecordingData(track, new TrackPoint(TrackPoint.Type.TRACKPOINT, position), null, statistics);
|
||||
demoData = new RecordingData(track, new TrackPoint(TrackPoint.Type.TRACKPOINT, position), null, statistics, "", "", "", "");
|
||||
}
|
||||
|
||||
private RecordingLayout recordingLayout;
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
@@ -288,4 +289,86 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
getBinding().statsDescriptionMain.setText(R.string.stats_coordinates);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AutoPauseStatus extends GenericStatisticsViewHolder {
|
||||
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String value = data.autoPauseStatus();
|
||||
if (value == null || value.isEmpty()) {
|
||||
value = "N/A";
|
||||
}
|
||||
getBinding().statsValue.setText(value);
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_auto_pause_status));
|
||||
}
|
||||
}
|
||||
|
||||
public static class SensorStatusHR extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String status = getContext().getString(R.string.value_unknown);
|
||||
if (data.sensorDataSet() != null && data.sensorDataSet().heartRate() != null) {
|
||||
BatteryLevel battery = data.sensorDataSet().heartRateBattery();
|
||||
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||
}
|
||||
getBinding().statsValue.setText(status);
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_hr));
|
||||
}
|
||||
}
|
||||
|
||||
public static class SensorStatusCadence extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String status = getContext().getString(R.string.value_unknown);
|
||||
if (data.sensorDataSet() != null && data.sensorDataSet().cadence() != null) {
|
||||
BatteryLevel battery = data.sensorDataSet().cadenceBattery();
|
||||
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||
}
|
||||
getBinding().statsValue.setText(status);
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_cadence));
|
||||
}
|
||||
}
|
||||
|
||||
public static class SensorStatusSpeed extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String status = getContext().getString(R.string.value_unknown);
|
||||
if (data.sensorDataSet() != null && data.sensorDataSet().speed() != null) {
|
||||
BatteryLevel battery = data.sensorDataSet().speedBattery();
|
||||
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||
}
|
||||
getBinding().statsValue.setText(status);
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_speed));
|
||||
}
|
||||
}
|
||||
public static class DebugIdleTimer extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
getBinding().statsValue.setText(data.idleTimer().isEmpty() ? "---" : data.idleTimer());
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_idle_timer));
|
||||
}
|
||||
}
|
||||
|
||||
public static class DebugWalkingTimer extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
getBinding().statsValue.setText(data.walkingTimer().isEmpty() ? "---" : data.walkingTimer());
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_walking_timer));
|
||||
}
|
||||
}
|
||||
|
||||
public static class DebugMarkerTimer extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
getBinding().statsValue.setText(data.markerTimer().isEmpty() ? "---" : data.markerTimer());
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_marker_timer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,14 @@ public class Mapping {
|
||||
m.put(context.getString(R.string.stats_custom_layout_cadence_key), SensorStatisticsViewHolder.SensorCadence::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_power_key), SensorStatisticsViewHolder.SensorPower::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_clock_key), ClockViewHolder::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_auto_pause_status_key), GenericStatisticsViewHolder.AutoPauseStatus::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_sensor_status_hr_key), GenericStatisticsViewHolder.SensorStatusHR::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_sensor_status_cadence_key), GenericStatisticsViewHolder.SensorStatusCadence::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_sensor_status_speed_key), GenericStatisticsViewHolder.SensorStatusSpeed::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_debug_idle_timer_key), GenericStatisticsViewHolder.DebugIdleTimer::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_debug_walking_timer_key), GenericStatisticsViewHolder.DebugWalkingTimer::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_debug_marker_timer_key), GenericStatisticsViewHolder.DebugMarkerTimer::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_media_key), MediaViewHolder::new);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.databinding.StatsMediaItemBinding;
|
||||
import de.dennisguse.opentracks.services.MediaNotificationListenerService;
|
||||
import de.dennisguse.opentracks.services.RecordingData;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
|
||||
|
||||
public class MediaViewHolder extends StatisticViewHolder<StatsMediaItemBinding> {
|
||||
|
||||
@Override
|
||||
protected StatsMediaItemBinding createViewBinding(LayoutInflater inflater) {
|
||||
return StatsMediaItemBinding.inflate(inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureUI(DataField dataField) {
|
||||
getBinding().statsMediaPrev.setOnClickListener(v -> {
|
||||
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||
if (service != null) {
|
||||
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
|
||||
}
|
||||
});
|
||||
|
||||
getBinding().statsMediaNext.setOnClickListener(v -> {
|
||||
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||
if (service != null) {
|
||||
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_NEXT);
|
||||
}
|
||||
});
|
||||
|
||||
getBinding().statsMediaPlayPause.setOnClickListener(v -> {
|
||||
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||
if (service != null) {
|
||||
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
|
||||
}
|
||||
});
|
||||
|
||||
getBinding().statsMediaPermissionButton.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
});
|
||||
|
||||
if (getContext() instanceof LifecycleOwner) {
|
||||
MediaNotificationListenerService.getMediaDataObservable().observe((LifecycleOwner) getContext(), mediaData -> {
|
||||
if (mediaData.isPermissionGranted) {
|
||||
getBinding().statsMediaPermissionButton.setVisibility(View.GONE);
|
||||
getBinding().statsMediaControls.setVisibility(View.VISIBLE);
|
||||
getBinding().statsMediaTitle.setVisibility(View.VISIBLE);
|
||||
getBinding().statsMediaArtist.setVisibility(View.VISIBLE);
|
||||
|
||||
getBinding().statsMediaTitle.setText(mediaData.title != null ? mediaData.title : getContext().getString(R.string.media_title_default));
|
||||
getBinding().statsMediaArtist.setText(mediaData.artist != null ? mediaData.artist : getContext().getString(R.string.media_artist_default));
|
||||
|
||||
if (mediaData.isPlaying) {
|
||||
getBinding().statsMediaPlayPause.setImageResource(R.drawable.ic_baseline_pause_24); // Needs to exist, or fallback to something else
|
||||
} else {
|
||||
getBinding().statsMediaPlayPause.setImageResource(R.drawable.ic_baseline_play_arrow_24);
|
||||
}
|
||||
} else {
|
||||
getBinding().statsMediaPermissionButton.setVisibility(View.VISIBLE);
|
||||
getBinding().statsMediaControls.setVisibility(View.GONE);
|
||||
getBinding().statsMediaTitle.setVisibility(View.GONE);
|
||||
getBinding().statsMediaArtist.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
// Not used, as we observe LiveData directly
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,19h4V5H6v14zm8,-14v14h4V5h-4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8,5v14l11,-7z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,6h2v12H6zm3.5,6l8.5,6V6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_media_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/media_title_default"
|
||||
android:textAppearance="@style/TextAppearance.OpenTracks.PrimaryValue"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_media_artist"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/media_artist_default"
|
||||
android:textAppearance="@style/TextAppearance.OpenTracks.SecondaryHeader"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_media_title" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/stats_media_controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_media_artist"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stats_media_prev"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/media_prev"
|
||||
android:src="@drawable/ic_baseline_skip_previous_24"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stats_media_play_pause"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/media_play_pause"
|
||||
android:src="@drawable/ic_baseline_play_arrow_24"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stats_media_next"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/media_next"
|
||||
android:src="@drawable/ic_baseline_skip_next_24"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/stats_media_permission_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/media_permission_request"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -197,6 +197,7 @@ limitations under the License.
|
||||
<string name="settings_recording_max_recording_distance_summary">Nowy segment jest tworzony, jeśli odległość między dwiema lokalizacjami jest większa niż %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Maksymalna odległość</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Rejestrowanie odcinka</string>
|
||||
<string name="settings_recording_marker_idle_duration_title">Minimalny czas długiej przerwy</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Rejestrowanie interwału</string>
|
||||
<string name="settings_recording_min_required_accuracy_title">Dokładność GPS-u</string>
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Data (ISO 8601)</string>
|
||||
@@ -281,6 +282,7 @@ limitations under the License.
|
||||
<string name="value_integer_meter_recommended">%1$d m (zalecane)</string>
|
||||
<string name="value_integer_mile_hour">%1$d mil/h</string>
|
||||
<string name="value_integer_minute">%1$d min</string>
|
||||
<string name="value_integer_minute_recommended">%1$d min (zalecane)</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
<string name="value_none">Brak</string>
|
||||
<string name="value_off">Wyłącz</string>
|
||||
@@ -443,6 +445,8 @@ limitations under the License.
|
||||
<string name="settings_announcements_totaltime_frequency">Przedział czasu</string>
|
||||
<string name="stats_sensors_heart_rate">Tętno</string>
|
||||
<string name="stats_clock">Zegar</string>
|
||||
<string name="stats_auto_pause_status">Status auto-pauzy</string>
|
||||
<string name="stats_sensor_status">Status czujników</string>
|
||||
<string name="settings_announcements_distance_frequency">Długość okrążenia</string>
|
||||
<string name="settings_layout_reset_confirm_title">Zresetować twoje układy\?</string>
|
||||
<string name="settings_cycling_sensor">Kolarstwo</string>
|
||||
@@ -523,6 +527,10 @@ limitations under the License.
|
||||
<string name="settings_announcements_lap_speed_pace">Tempo/prędkość okrążenia</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d ścieżki</string>
|
||||
<string name="settings_announcements_idle_title">Przerwa</string>
|
||||
<string name="settings_announcements_long_break_title">Długa przerwa (filiżanka)</string>
|
||||
<string name="settings_announcements_sensor_category_title">Czujniki</string>
|
||||
<string name="settings_announcements_sensor_disconnected_title">Odłączono czujnik</string>
|
||||
<string name="settings_announcements_sensor_connected_title">Podłączono czujnik</string>
|
||||
<string name="settings_recording_title">Nagrywanie</string>
|
||||
<string name="settings_theme_switch_restart">Zmiana motywu może wymagać ręcznego restartu.</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Użyj ręcznie zdefiniowanych Dynamicznych Kolorów (tylko Android 12+; wymaga ręcznego restartu)</string>
|
||||
@@ -585,4 +593,15 @@ limitations under the License.
|
||||
<string name="show_on_dashboard">API panelu OpenTracks</string>
|
||||
<string name="marker_detail_title">%1$d z %2$d: %3$s</string>
|
||||
<string name="voice_on_device_speaker_title">Użyj wbudowanego głośnika</string>
|
||||
<string name="sensor_disconnected_warning">Czujnik rozłączony. Kontynuować z GPS?</string>
|
||||
<string name="sensor_disconnected_switch_action">Przełącz</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_title">Zatrzymaj przy prowadzeniu roweru</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_summary">Uruchom autopauzę jeżeli kadencja wynosi 0 a prędkość wskazuje na chód</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_title">Maksymalna prędkość prowadzenia</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_summary">Prędkość poniżej której wykrywane jest prowadzenie roweru (gdy kadencja to 0)</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_title">Zapobiegaj pauzie podczas pedałowania</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_summary">Nie włączaj trybu postoju jeżeli pedałujesz, nawet przy braku ruchu</string>
|
||||
<string name="voiceLongBreak">Rozpoczęto długą przerwę.</string>
|
||||
<string name="voiceSensorDisconnected">Odłączono czujnik.</string>
|
||||
<string name="voiceSensorConnected">Podłączono czujnik.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,8 +15,8 @@ limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_web_url" translatable="false">https://codeberg.org/OpenTracksApp/OpenTracks</string>
|
||||
<string name="app_name" translatable="false">OpenTracks</string>
|
||||
<string name="launcher_name" translatable="false">OpenTracks</string>
|
||||
<string name="app_name" translatable="false">OpenTracks (Fork)</string>
|
||||
<string name="launcher_name" translatable="false">OpenTracks (Fork)</string>
|
||||
|
||||
<string name="egm2008" translatable="false">EGM2008</string>
|
||||
<string name="wgs84" translatable="false">WGS84</string>
|
||||
|
||||
@@ -130,6 +130,14 @@
|
||||
<string name="idle_duration_key" translatable="false">idleSpeedDuration</string>
|
||||
<string name="idle_duration_default" translatable="false">10</string>
|
||||
<string name="idle_duration_off" translatable="false">0</string>
|
||||
<string-array name="auto_pause_cadence_0_speed_values">
|
||||
<item>4</item>
|
||||
<item>6</item>
|
||||
<item>8</item>
|
||||
<item>10</item>
|
||||
<item>12</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="idle_duration_values">
|
||||
<item>@string/idle_duration_off</item>
|
||||
<item>5</item>
|
||||
@@ -141,6 +149,17 @@
|
||||
<item>120</item>
|
||||
</string-array>
|
||||
|
||||
<string name="marker_idle_duration_default" translatable="false">3</string>
|
||||
<string-array name="marker_idle_duration_values">
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>@string/marker_idle_duration_default</item>
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>15</item>
|
||||
<item>30</item>
|
||||
</string-array>
|
||||
|
||||
<string name="stats_rate_key" translatable="false">statsRate</string>
|
||||
<string name="stats_rate_default" translatable="false">@string/stats_rate_speed_or_pace_default</string>
|
||||
<string-array name="stats_rate_metric_options">
|
||||
@@ -245,6 +264,12 @@
|
||||
<bool name="voice_on_device_speaker_default" translatable="false">true</bool>
|
||||
<string name="voice_announce_idle_key" translatable="false">voice_announce_idle_key</string>
|
||||
<bool name="voice_announce_idle_default" translatable="false">false</bool>
|
||||
<string name="voice_announce_long_break_key" translatable="false">voice_announce_long_break_key</string>
|
||||
<bool name="voice_announce_long_break_default" translatable="false">true</bool>
|
||||
<string name="voice_announce_sensor_disconnected_key" translatable="false">voice_announce_sensor_disconnected_key</string>
|
||||
<bool name="voice_announce_sensor_disconnected_default" translatable="false">true</bool>
|
||||
<string name="voice_announce_sensor_connected_key" translatable="false">voice_announce_sensor_connected_key</string>
|
||||
<bool name="voice_announce_sensor_connected_default" translatable="false">true</bool>
|
||||
|
||||
<string name="voice_announce_total_distance_key" translatable="false">voiceAnnounceTotalDistance</string>
|
||||
<bool name="voice_announce_total_distance_default" translatable="false">true</bool>
|
||||
@@ -381,6 +406,16 @@
|
||||
<string name="stats_custom_layout_average_cadence_key" translatable="false">average_cadence</string>
|
||||
<string name="stats_custom_layout_power_key" translatable="false">power</string>
|
||||
<string name="stats_custom_layout_clock_key" translatable="false">time</string>
|
||||
<string name="stats_custom_layout_auto_pause_status_key" translatable="false">auto_pause_status</string>
|
||||
<string name="stats_custom_layout_sensor_status_key" translatable="false">sensor_status</string>
|
||||
<string name="stats_custom_layout_sensor_status_hr_key" translatable="false">sensor_status_hr</string>
|
||||
<string name="stats_custom_layout_sensor_status_cadence_key" translatable="false">sensor_status_cadence</string>
|
||||
<string name="stats_custom_layout_sensor_status_speed_key" translatable="false">sensor_status_speed</string>
|
||||
<string name="stats_custom_layout_debug_idle_timer_key" translatable="false">debug_idle_timer</string>
|
||||
<string name="stats_custom_layout_debug_walking_timer_key" translatable="false">debug_walking_timer</string>
|
||||
<string name="stats_custom_layout_debug_marker_timer_key" translatable="false">debug_marker_timer</string>
|
||||
|
||||
<string name="stats_custom_layout_media_key" translatable="false">media_control</string>
|
||||
|
||||
<!-- Stats empty values -->
|
||||
<string name="stats_empty_value_integer" translatable="false">0</string>
|
||||
|
||||
@@ -331,6 +331,10 @@ limitations under the License.
|
||||
<string name="settings_sensors_summary">Cycling Sensors, Wheel Circumference, Running Sensors</string>
|
||||
<string name="settings_announcements_idle_category_title">Idle Announcements</string>
|
||||
<string name="settings_announcements_idle_title">Idle</string>
|
||||
<string name="settings_announcements_long_break_title">Long break</string>
|
||||
<string name="settings_announcements_sensor_category_title">Sensors</string>
|
||||
<string name="settings_announcements_sensor_disconnected_title">Sensor disconnected</string>
|
||||
<string name="settings_announcements_sensor_connected_title">Sensor connected</string>
|
||||
<string name="settings_announcements_statistics_title">Statistics Announcements</string>
|
||||
<string name="settings_announcements_summary">Time, Distance, Voice Speed</string>
|
||||
<string name="settings_import_export_title">Import and Export</string>
|
||||
@@ -351,6 +355,8 @@ limitations under the License.
|
||||
<string name="settings_chart_display_heart_rate_title">Display heart rate</string>
|
||||
<!-- Settings Recording -->
|
||||
<string name="settings_recording_title">Recording</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Recording time interval</string>
|
||||
<string name="stats_sensor_status">Sensor status</string>
|
||||
<string name="settings_theme_switch_restart">Theme changes may require a manual restart.</string>
|
||||
<string name="settings_ui_dynamic_colors_title">Dynamic Colors</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Use user-defined Dynamic Colors (Android 12+ only; requires a manual restart)</string>
|
||||
@@ -369,7 +375,9 @@ limitations under the License.
|
||||
<string name="settings_recording_max_recording_distance_summary">A new segment is created if the distance between two locations is greater than %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Max distance</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Recording distance interval</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Recording time interval</string>
|
||||
<string name="marker_idle_duration_key" translatable="false">marker_idle_duration</string>
|
||||
<string name="settings_recording_marker_idle_duration_title">Marker idle duration</string>
|
||||
<string name="settings_recording_min_recording_interval_key" translatable="false">recording_min_recording_interval_key</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">Location is ignored if its accuracy is greater than %1$s</string>
|
||||
<string name="settings_recording_min_required_accuracy_title">GPS accuracy</string>
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Date (ISO 8601)</string>
|
||||
@@ -478,6 +486,14 @@ limitations under the License.
|
||||
<string name="stats_sensors_cadence">Cadence</string>
|
||||
<string name="stats_sensors_power">Power</string>
|
||||
<string name="stats_clock">Clock</string>
|
||||
<string name="stats_auto_pause_status">Auto-Pause Status</string>
|
||||
<string name="stats_sensor_status_hr">Sensor HR</string>
|
||||
<string name="stats_sensor_status_cadence">Sensor Cadence</string>
|
||||
<string name="stats_sensor_status_speed">Sensor Speed</string>
|
||||
<string name="stats_debug_idle_timer">Idle Timer</string>
|
||||
<string name="stats_debug_walking_timer">Walking Timer</string>
|
||||
<string name="stats_debug_marker_timer">Marker Timer</string>
|
||||
|
||||
<!-- Track -->
|
||||
<string name="track_name_format">Track %1$d</string>
|
||||
<string name="track_recording_notification_accuracy">Location accuracy: %1$s</string>
|
||||
@@ -536,6 +552,7 @@ limitations under the License.
|
||||
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (recommended)</string>
|
||||
<string name="value_float_knots_recommended">%1$.1f knots (recommended)</string>
|
||||
<string name="value_integer_minute">%1$d min</string>
|
||||
<string name="value_integer_minute_recommended">%1$d min (recommended)</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
<string name="value_none">None</string>
|
||||
<string name="value_off">Off</string>
|
||||
@@ -543,6 +560,9 @@ limitations under the License.
|
||||
<string name="value_smallest_recommended">Smallest (recommended)</string>
|
||||
<string name="value_int_seconds">%1$d s (recommended)</string>
|
||||
<string name="voiceIdle">Becoming idle.</string>
|
||||
<string name="voiceLongBreak">Long break recorded.</string>
|
||||
<string name="voiceSensorDisconnected">Sensor disconnected.</string>
|
||||
<string name="voiceSensorConnected">Sensor connected.</string>
|
||||
<string name="voiceHoursPlural">
|
||||
{n, plural,
|
||||
=1 {1 hour}
|
||||
@@ -705,4 +725,22 @@ limitations under the License.
|
||||
<string name="settings_announcements_lap_power">Lap power</string>
|
||||
<string name="power_x_watt">Power {x, plural, =1 {1 watt} other {# watts} }</string>
|
||||
<string name="power">Power</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_key" translatable="false">preventIdleIfCadence</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_title">Prevent auto-pause if pedaling</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_summary">Do not enter idle mode while pedaling even if not moving</string>
|
||||
<string name="sensor_disconnected_warning">Sensor disconnected. Continue with GPS?</string>
|
||||
<string name="sensor_disconnected_switch_action">Switch</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_key" translatable="false">autoPauseCadence0</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_title">Auto-pause when carrying bike</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_summary">Pause recording if cadence is 0 and speed is low</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_key" translatable="false">autoPauseCadence0Speed</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_title">Auto-pause max speed</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_summary">Speed below which walking with bike is detected (if cadence is 0)</string>
|
||||
<string name="auto_pause_cadence_0_speed_default" translatable="false">8</string> <string name="media_title_default">No media playing</string>
|
||||
<string name="media_artist_default">OpenTracks</string>
|
||||
<string name="media_prev">Previous</string>
|
||||
<string name="media_next">Next</string>
|
||||
<string name="media_play_pause">Play/Pause</string>
|
||||
<string name="media_permission_request">Enable Notification Access</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -22,6 +22,21 @@
|
||||
android:defaultValue="@bool/voice_announce_idle_default"
|
||||
android:key="@string/voice_announce_idle_key"
|
||||
android:title="@string/settings_announcements_idle_title" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/voice_announce_long_break_default"
|
||||
android:key="@string/voice_announce_long_break_key"
|
||||
android:title="@string/settings_announcements_long_break_title" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_announcements_sensor_category_title">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/voice_announce_sensor_disconnected_default"
|
||||
android:key="@string/voice_announce_sensor_disconnected_key"
|
||||
android:title="@string/settings_announcements_sensor_disconnected_title" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/voice_announce_sensor_connected_default"
|
||||
android:key="@string/voice_announce_sensor_connected_key"
|
||||
android:title="@string/settings_announcements_sensor_connected_title" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_announcements_statistics_title">
|
||||
|
||||
@@ -24,13 +24,12 @@
|
||||
android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity"
|
||||
android:targetPackage="@string/applicationId" />
|
||||
</Preference>
|
||||
<!-- Disabled as the used structure for KML/KMZ does not allow assigning markers correctly on import. -->
|
||||
<!-- <Preference-->
|
||||
<!-- android:title="@string/menu_export_all_one_file">-->
|
||||
<!-- <intent-->
|
||||
<!-- android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserOneFileActivity"-->
|
||||
<!-- android:targetPackage="@string/applicationId" />-->
|
||||
<!-- </Preference>-->
|
||||
<Preference
|
||||
android:title="@string/menu_export_all_one_file">
|
||||
<intent
|
||||
android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserOneFileActivity"
|
||||
android:targetPackage="@string/applicationId" />
|
||||
</Preference>
|
||||
<Preference
|
||||
android:key="@string/settings_default_export_directory_key"
|
||||
android:title="@string/settings_default_export_uri_title">
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/settings_recording_title">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="@string/settings_recording_auto_pause_on_cadence_0_key"
|
||||
android:title="@string/settings_recording_auto_pause_on_cadence_0_title"
|
||||
android:summary="@string/settings_recording_auto_pause_on_cadence_0_summary" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="@string/settings_recording_prevent_idle_if_cadence_key"
|
||||
android:title="@string/settings_recording_prevent_idle_if_cadence_title"
|
||||
android:summary="@string/settings_recording_prevent_idle_if_cadence_summary" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/auto_pause_cadence_0_speed_default"
|
||||
android:entryValues="@array/auto_pause_cadence_0_speed_values"
|
||||
android:key="@string/settings_recording_auto_pause_on_cadence_0_speed_key"
|
||||
android:title="@string/settings_recording_auto_pause_on_cadence_0_speed_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/recording_distance_interval_default"
|
||||
android:entryValues="@array/recording_distance_interval_values"
|
||||
@@ -19,8 +38,17 @@
|
||||
<ListPreference
|
||||
android:defaultValue="@string/idle_duration_default"
|
||||
android:entryValues="@array/idle_duration_values"
|
||||
android:entries="@array/idle_duration_values"
|
||||
android:key="@string/idle_duration_key"
|
||||
android:title="@string/settings_recording_idle_timeout_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/marker_idle_duration_default"
|
||||
android:entryValues="@array/marker_idle_duration_values"
|
||||
android:entries="@array/marker_idle_duration_values"
|
||||
android:key="@string/marker_idle_duration_key"
|
||||
android:title="@string/settings_recording_marker_idle_duration_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
+1466
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user