forked from upstream-mirrors/OpenTracks
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45874d3749 | |||
| 594f69320a | |||
| d4927c9444 | |||
| 00dbd056d6 | |||
| 8f19ad8f66 | |||
| 2fdad54a7c | |||
| 5a5f040ed5 | |||
| 0ada9a2463 | |||
| 719bb981ec | |||
| 3c95d8744d | |||
| 63d1c9ee5e | |||
| 1ccd2e3c0a | |||
| cdbddabc1b | |||
| 9f6ca83f27 | |||
| f2ee99a477 | |||
| 7009ede095 | |||
| 2b530a1c53 | |||
| 33673f5921 | |||
| a7398d43e3 | |||
| 59b016a4a3 | |||
| 78aa5d51c6 | |||
| 95f584b554 | |||
| cf759d526d | |||
| 4c44ea12a4 | |||
| 9d09865907 | |||
| 5667d2d83e | |||
| 386f017fb7 | |||
| 87594e822d | |||
| ee832c7bd2 | |||
| 30042d5bc3 | |||
| 73cbb7a29c | |||
| c100fdde4f | |||
| 04e1496084 | |||
| 21d1852505 | |||
| 1502b0cfa7 | |||
| e7d6f65508 | |||
| 0e69a444a5 | |||
| 3b62999fde | |||
| 97431233ac | |||
| d49675afc8 | |||
| c1f7ab36a8 | |||
| eb14cc7bc6 | |||
| a47b9164af | |||
| 502e3ecb21 | |||
| 71903f174a | |||
| ab6606a295 | |||
| c4e2023992 | |||
| cb10bdda3f | |||
| f913472aae | |||
| af4826c578 | |||
| 63172ec66f | |||
| 3d6c339509 | |||
| 49f86c4eb9 | |||
| 7664bd248d | |||
| 73615aa0d6 | |||
| 81bb6699d8 | |||
| 93c67e3728 | |||
| 993816a34a | |||
| 3c0999c3a0 | |||
| 4528e7148f | |||
| 74042f5d85 | |||
| 92f6fbe04a | |||
| 5fa50250de | |||
| 8ee9daf7af | |||
| a54c8c1d25 | |||
| 8137ec8de2 | |||
| ae2a97576b | |||
| 3e830a086e | |||
| 618a1df3ea | |||
| 90cfc67c59 | |||
| cecc586868 | |||
| f956e7fab3 | |||
| 128b95cd83 | |||
| 895a5c2b2a | |||
| 91ce5cf8d7 | |||
| a283464331 | |||
| 1c3a2bd2e4 | |||
| 0b326a77c2 |
+1
-1
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
liberapay: OpenTracks
|
liberapay: OpenTracks
|
||||||
github: [rgmf, pstorch, dennisguse]
|
github: [rgmf, pstorch, dennisguse]
|
||||||
custom: [ https://liberapay.com/OpenTracks, https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore ]
|
custom: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Data not recorded
|
||||||
|
about: Somehow OpenTracks did not record GPS data
|
||||||
|
title: ''
|
||||||
|
labels: potential bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Please describe the problem and how to reproduce it:**
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
**Checklist**
|
||||||
|
|
||||||
|
- [ ] Consulted [https://dontkillmyapp.com/]
|
||||||
|
- [ ] Power management: battery optimization is disabled for OpenTracks
|
||||||
|
|
||||||
|
**Technical information**
|
||||||
|
|
||||||
|
- Device: [e.g. Nexus 7 (2013)]
|
||||||
|
- OS: [e.g. LineageOS 15.1]
|
||||||
|
- OpenTracks version: [e.g. v3.2.4]
|
||||||
|
- OpenTracks commit id (only for nightly builds): [e.g. 4b333bb]
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
#https://github.com/marketplace/actions/android-emulator-runner
|
#https://github.com/marketplace/actions/android-emulator-runner
|
||||||
name: Build nightly
|
name: Build APK
|
||||||
on: [ push ]
|
on:
|
||||||
|
push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nightly-build:
|
apk-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
@@ -14,7 +15,7 @@ jobs:
|
|||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
|
|
||||||
- name: Assemble nighlty APK
|
- name: Assemble APK
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo ${NIGHTLY_STORE_FILE} | base64 -d > KEY_NIGHTLY.jks
|
echo ${NIGHTLY_STORE_FILE} | base64 -d > KEY_NIGHTLY.jks
|
||||||
@@ -28,7 +29,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: OpenTracks-nightly.apk
|
name: OpenTracks-APK.apk
|
||||||
path: ./build/outputs/apk/nightly/*.apk
|
path: ./build/outputs/apk/nightly/*.apk
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ name: Linting
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyse:
|
analyse:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: Test
|
name: Test
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
java-version: '11'
|
java-version: '11'
|
||||||
|
|
||||||
- name: run tests
|
- name: run tests
|
||||||
uses: reactivecircus/android-emulator-runner@v2.24.0
|
uses: reactivecircus/android-emulator-runner@v2.25.0
|
||||||
with:
|
with:
|
||||||
api-level: ${{ matrix.api-level }}
|
api-level: ${{ matrix.api-level }}
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ OpenTracks can be used with [Gadgetbridge](https://www.gadgetbridge.org/):
|
|||||||
__Only required permission:__
|
__Only required permission:__
|
||||||
* _ACCESS_FINE_LOCATION_: required to use the GPS.
|
* _ACCESS_FINE_LOCATION_: required to use the GPS.
|
||||||
|
|
||||||
### API
|
### Public API
|
||||||
|
|
||||||
OpenTracks includes an API for starting/stopping recording by another installed application (e.g., [Automate](https://llamalab.com/automate/), [Tasker](https://tasker.joaoapps.com), or [Easer](https://github.com/renyuneyun/Easer)).
|
OpenTracks includes an API for starting/stopping recording by another installed application (e.g., [Automate](https://llamalab.com/automate/), [Tasker](https://tasker.joaoapps.com), or [Easer](https://github.com/renyuneyun/Easer)).
|
||||||
The API can be invoked by sending an explicit Intent to start an activity.
|
The API can be invoked by sending an explicit Intent to start an activity.
|
||||||
@@ -107,20 +107,37 @@ The API can be invoked by sending an explicit Intent to start an activity.
|
|||||||
* Nightly: `de.dennisguse.opentracks.nightly`
|
* Nightly: `de.dennisguse.opentracks.nightly`
|
||||||
|
|
||||||
`Class`:
|
`Class`:
|
||||||
|
|
||||||
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording`
|
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording`
|
||||||
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
|
* **Stop a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
|
||||||
|
|
||||||
For testing via adb: `adb shell am start -n "package/class"`
|
For testing via adb: `adb shell am start -n "package/class"`
|
||||||
|
|
||||||
The Start/Stop API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
|
`StartRecording` supports the following parameters:
|
||||||
|
|
||||||
## Custom Dashboards (incl. map)
|
* Set track data: `TRACK_NAME`, `TRACK_CATEGORY`, and `TRACK_DESCRIPTION`
|
||||||
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live tracks.
|
* Send recorded data to another application via _Dashboard API_: `STATS_TARGET_PACKAGE`
|
||||||
|
and `STATS_TARGET_CLASS`
|
||||||
|
|
||||||
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track statistics on supported wearables.
|
The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in
|
||||||
|
the settings.
|
||||||
|
|
||||||
Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
|
__IMPORTANT__: triggering `StartRecording` does not check if Android permissions (location or
|
||||||
However, this is rather slow and does not provide updates while recording.
|
Bluetooth) were granted. If they are not granted`
|
||||||
|
|
||||||
|
## Dashboard API (incl. map)
|
||||||
|
|
||||||
|
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live
|
||||||
|
tracks.
|
||||||
|
|
||||||
|
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which
|
||||||
|
presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also
|
||||||
|
used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track
|
||||||
|
statistics on supported wearables.
|
||||||
|
|
||||||
|
Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (
|
||||||
|
e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)). However, this is rather
|
||||||
|
slow and does not provide updates while recording.
|
||||||
|
|
||||||
The developer documentation is in [README_API.md](README_API.md).
|
The developer documentation is in [README_API.md](README_API.md).
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,6 @@ However, often only one value is provided.
|
|||||||
|
|
||||||
We only support _Instantaneous Power_.
|
We only support _Instantaneous Power_.
|
||||||
|
|
||||||
_WARNING_: we only support physically movements with valid GPS coordinates.
|
|
||||||
Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
|
|
||||||
Also the distance is not computed.
|
|
||||||
|
|
||||||
For cycling:
|
For cycling:
|
||||||
* Assioma Duo Powermeter
|
* Assioma Duo Powermeter
|
||||||
* Elite drivo II
|
* Elite drivo II
|
||||||
|
|||||||
+12
-9
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ android {
|
|||||||
targetCompatibility JavaVersion.VERSION_11
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdkVersion 32
|
compileSdk 33
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
@@ -59,13 +59,13 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "de.dennisguse.opentracks"
|
applicationId "de.dennisguse.opentracks"
|
||||||
versionCode 4926
|
versionCode 5004
|
||||||
versionName "v4.0.1"
|
versionName "v4.0.7"
|
||||||
|
|
||||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||||
|
|
||||||
minSdkVersion 21
|
minSdk 21
|
||||||
targetSdkVersion 32
|
targetSdk 33
|
||||||
|
|
||||||
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
|
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
@@ -100,6 +100,7 @@ android {
|
|||||||
releasePlayStore {
|
releasePlayStore {
|
||||||
applicationIdSuffix ".playstore"
|
applicationIdSuffix ".playstore"
|
||||||
versionNameSuffix "-PlayStore"
|
versionNameSuffix "-PlayStore"
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +118,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
implementation 'androidx.appcompat:appcompat:1.5.0'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
implementation 'androidx.preference:preference:1.2.0'
|
implementation 'androidx.preference:preference:1.2.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'com.google.android.material:material:1.6.1'
|
implementation 'com.google.android.material:material:1.6.1'
|
||||||
@@ -130,7 +133,7 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
androidTestImplementation 'org.mockito:mockito-android:4.6.1'
|
androidTestImplementation 'org.mockito:mockito-android:4.7.0'
|
||||||
|
|
||||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
v4.0.2: OpenTracks
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Add nautical miles
|
||||||
|
- Settings can be reset while recording
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- Voice announcement rounding
|
||||||
|
- Chart: elevation was weirdly smoothed
|
||||||
|
- Export was not checking if file exists
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
v4.0.3: OpenTracks
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Max. speed as reported by sensor (GPS or BLE)
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- Prevent crash if OSM Dashboard is not installed
|
||||||
|
- GPX track export: do not export empty track segments
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
v4.0.4: OpenTracks
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Store BLE sensor data even without distance (every 10s)
|
||||||
|
- Public API: allows setting name, category, and description
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- Crash when taking a picture and rotating the phone
|
||||||
|
-
|
||||||
|
|
||||||
|
Developer:
|
||||||
|
- Target SDK 33
|
||||||
|
- Barometer uses interval of 5s
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
v4.0.5: OpenTracks
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- PlayStore relase get's another logo
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- Interval list shows "-" if gain/loss is not present
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
v4.0.6: OpenTracks
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- EGM2008 offset computed wrong for negative longitudes
|
||||||
|
- PublicAPI crashed if OpenTracks was in background (API26+)
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
v4.0.7: OpenTracks
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- GPS signal lost timeout: 30s
|
||||||
|
- Android13: request permission to show notifications
|
||||||
|
- Faster deletion of tracks (vacuum database only if needed)
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- PublicAPI stop command resulted in crash (introduced in v4.0.6)
|
||||||
|
- Permissions: request all permissions during on startup
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
v3.20.2 : OpenTracks
|
||||||
|
|
||||||
|
Changements :
|
||||||
|
- Le gain et la perte d'altitude sont des entiers au lieu de floats
|
||||||
|
- Demander la permission avant d'utiliser Bluetooth par un message Toast
|
||||||
|
|
||||||
|
Correction d'un bug :
|
||||||
|
- Définir FLAG_IMMUTABLE pour PendingIntents pour éviter un crash sur Android 12
|
||||||
|
- Le capteur de cadence renvoie 0 au lieu de None lorsque l'utilisateur ne pédale pas.
|
||||||
|
- Le capteur de vitesse renvoie 0 au lieu de la dernière vitesse connue quand c'est arrêté
|
||||||
|
- Avec les capteurs, le temps de déplacement s'arrête lorsqu'il n'est pas en mouvement
|
||||||
@@ -7,5 +7,11 @@ Fonctionnalités :
|
|||||||
* Dénivelé positif et négatif: via capteur de pression barométrique
|
* Dénivelé positif et négatif: via capteur de pression barométrique
|
||||||
* Synthèse vocale
|
* Synthèse vocale
|
||||||
* Prise en charge des capteurs Bluetooth LE : rythme cardiaque, vitesse et distance (vélo), cadence de pédalage (vélo), et capteur de puissance (vélo)
|
* Prise en charge des capteurs Bluetooth LE : rythme cardiaque, vitesse et distance (vélo), cadence de pédalage (vélo), et capteur de puissance (vélo)
|
||||||
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
|
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
|
||||||
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
|
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
|
||||||
|
* Thème sombre ou claire, avec le respect des paramètres du système
|
||||||
|
* Pas de publicités
|
||||||
|
|
||||||
|
Logiciel Open Source
|
||||||
|
Vous pouvez utiliser, étudier, changer et partager le code source.
|
||||||
|
Licence Apache 2.0
|
||||||
|
|||||||
@@ -3,48 +3,25 @@ package de.dennisguse.opentracks;
|
|||||||
|
|
||||||
import static androidx.test.espresso.Espresso.onData;
|
import static androidx.test.espresso.Espresso.onData;
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
import static androidx.test.espresso.Espresso.pressBack;
|
|
||||||
import static androidx.test.espresso.action.ViewActions.click;
|
import static androidx.test.espresso.action.ViewActions.click;
|
||||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
|
||||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||||
import static org.hamcrest.Matchers.allOf;
|
import static org.hamcrest.Matchers.allOf;
|
||||||
import static org.hamcrest.Matchers.anything;
|
import static org.hamcrest.Matchers.anything;
|
||||||
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
|
|
||||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
||||||
import static de.dennisguse.opentracks.util.EspressoUtils.withListSize;
|
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.app.Instrumentation;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ListView;
|
|
||||||
|
|
||||||
import androidx.test.espresso.IdlingRegistry;
|
|
||||||
import androidx.test.espresso.IdlingResource;
|
|
||||||
import androidx.test.espresso.ViewInteraction;
|
|
||||||
import androidx.test.espresso.assertion.ViewAssertions;
|
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.LargeTest;
|
import androidx.test.filters.LargeTest;
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
|
||||||
import androidx.test.rule.GrantPermissionRule;
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
|
|
||||||
import org.hamcrest.Description;
|
|
||||||
import org.hamcrest.TypeSafeMatcher;
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.services.TrackDeleteService;
|
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class EspressoDeleteTrackTest {
|
public class EspressoDeleteTrackTest {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import static androidx.test.espresso.Espresso.onData;
|
|||||||
import static androidx.test.espresso.Espresso.onView;
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
import static androidx.test.espresso.action.ViewActions.click;
|
import static androidx.test.espresso.action.ViewActions.click;
|
||||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
|
||||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
|
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class ChartPointTest {
|
|||||||
statistics.setTotalTime(Duration.ofSeconds(1000));
|
statistics.setTotalTime(Duration.ofSeconds(1000));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, UnitSystem.IMPERIAL);
|
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), false, UnitSystem.IMPERIAL);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(1000000, (long) point.getTimeOrDistance());
|
assertEquals(1000000, (long) point.getTimeOrDistance());
|
||||||
@@ -40,7 +40,7 @@ public class ChartPointTest {
|
|||||||
statistics.setTotalDistance(Distance.of(1000));
|
statistics.setTotalDistance(Distance.of(1000));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, UnitSystem.METRIC);
|
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), true, UnitSystem.METRIC);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(1, (long) point.getTimeOrDistance());
|
assertEquals(1, (long) point.getTimeOrDistance());
|
||||||
@@ -50,9 +50,11 @@ public class ChartPointTest {
|
|||||||
public void create_get_altitude_speed_and_pace() {
|
public void create_get_altitude_speed_and_pace() {
|
||||||
// given
|
// given
|
||||||
TrackStatistics statistics = new TrackStatistics();
|
TrackStatistics statistics = new TrackStatistics();
|
||||||
|
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
|
||||||
|
.setAltitude(Altitude.EGM2008.of(50));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(50, point.getAltitude(), 0.01);
|
assertEquals(50, point.getAltitude(), 0.01);
|
||||||
@@ -64,9 +66,10 @@ public class ChartPointTest {
|
|||||||
public void create_sensorNotAvailable() {
|
public void create_sensorNotAvailable() {
|
||||||
// given
|
// given
|
||||||
TrackStatistics statistics = new TrackStatistics();
|
TrackStatistics statistics = new TrackStatistics();
|
||||||
|
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
|
||||||
|
.setAltitude(Altitude.EGM2008.of(50));
|
||||||
// when
|
// when
|
||||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertNull(point.getHeartRate());
|
assertNull(point.getHeartRate());
|
||||||
@@ -77,15 +80,16 @@ public class ChartPointTest {
|
|||||||
@Test
|
@Test
|
||||||
public void create_sensorAvailable() {
|
public void create_sensorAvailable() {
|
||||||
// given
|
// given
|
||||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
|
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
|
||||||
trackPoint.setHeartRate(100f);
|
.setAltitude(Altitude.EGM2008.of(50))
|
||||||
trackPoint.setCadence(101f);
|
.setHeartRate(100f)
|
||||||
trackPoint.setPower(102f);
|
.setCadence(101f)
|
||||||
|
.setPower(102f);
|
||||||
|
|
||||||
TrackStatistics statistics = new TrackStatistics();
|
TrackStatistics statistics = new TrackStatistics();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(100.0, point.getHeartRate(), 0.01);
|
assertEquals(100.0, point.getHeartRate(), 0.01);
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
package de.dennisguse.opentracks.chart;
|
package de.dennisguse.opentracks.chart;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
@@ -25,10 +29,6 @@ import org.junit.runner.RunWith;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests {@link ChartValueSeries}.
|
* Tests {@link ChartValueSeries}.
|
||||||
*
|
*
|
||||||
@@ -47,6 +47,7 @@ public class ChartValueSeriesTest {
|
|||||||
new int[]{1, 100, 1000},
|
new int[]{1, 100, 1000},
|
||||||
R.string.description_altitude_metric,
|
R.string.description_altitude_metric,
|
||||||
R.string.description_altitude_imperial,
|
R.string.description_altitude_imperial,
|
||||||
|
R.string.description_altitude_imperial,
|
||||||
R.color.chart_altitude_fill,
|
R.color.chart_altitude_fill,
|
||||||
R.color.chart_altitude_border,
|
R.color.chart_altitude_border,
|
||||||
15,
|
15,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.util.Pair;
|
|||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
@@ -24,10 +23,7 @@ public class ShareContentProviderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateAndParseURI_valid() {
|
public void testCreateAndParseURI_valid() {
|
||||||
Set<Track.Id> trackIds = new HashSet<>();
|
Set<Track.Id> trackIds = Set.of(new Track.Id(1), new Track.Id(3), new Track.Id(5));
|
||||||
trackIds.add(new Track.Id(1));
|
|
||||||
trackIds.add(new Track.Id(3));
|
|
||||||
trackIds.add(new Track.Id(5));
|
|
||||||
|
|
||||||
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "filename.suffix", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "filename.suffix", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||||
|
|
||||||
@@ -36,9 +32,7 @@ public class ShareContentProviderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateURIescapeFilename() {
|
public void testCreateURIescapeFilename() {
|
||||||
Set<Track.Id> trackIds = new HashSet<>();
|
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(Set.of(new Track.Id(1)), "../../&1=1", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||||
trackIds.add(new Track.Id(1));
|
|
||||||
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "../../&1=1", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
|
||||||
|
|
||||||
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/KML_WITH_TRACKDETAIL_AND_SENSORDATA/1/..%2F..%2F%261%3D1"), shareURIandMIME.first);
|
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/KML_WITH_TRACKDETAIL_AND_SENSORDATA/1/..%2F..%2F%261%3D1"), shareURIandMIME.first);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.junit.runner.RunWith;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||||
|
|
||||||
|
//TODO Parametrized tests
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class DistanceFormatterTest {
|
public class DistanceFormatterTest {
|
||||||
|
|
||||||
@@ -42,4 +43,17 @@ public class DistanceFormatterTest {
|
|||||||
// A small number in imperial
|
// A small number in imperial
|
||||||
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
|
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatDistance_nautical() {
|
||||||
|
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||||
|
.setDecimalCount(2)
|
||||||
|
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||||
|
.build(context);
|
||||||
|
|
||||||
|
// A large number in nautical
|
||||||
|
assertEquals("2.70 NM", formatter.formatDistance(Distance.of(5000)));
|
||||||
|
// A small number in nautical
|
||||||
|
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import org.junit.runner.RunWith;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||||
|
|
||||||
|
//TODO Use make parametrized tests?
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class SpeedFormatterTest {
|
public class SpeedFormatterTest {
|
||||||
|
|
||||||
@@ -39,12 +40,22 @@ public class SpeedFormatterTest {
|
|||||||
.setReportSpeedOrPace(false)
|
.setReportSpeedOrPace(false)
|
||||||
.build(context);
|
.build(context);
|
||||||
|
|
||||||
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.34)).first);
|
assertEquals("8:02", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||||
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.33)).first);
|
|
||||||
|
|
||||||
assertEquals("min/mi", formatter.getSpeedParts(Speed.zero()).second);
|
assertEquals("min/mi", formatter.getSpeedParts(Speed.zero()).second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSpeedParts_pace_nautical() {
|
||||||
|
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||||
|
.setDecimalCount(2)
|
||||||
|
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||||
|
.setReportSpeedOrPace(false)
|
||||||
|
.build(context);
|
||||||
|
|
||||||
|
assertEquals("9:14", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||||
|
assertEquals("min/NM", formatter.getSpeedParts(Speed.zero()).second);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetSpeedParts_speed_metric() {
|
public void testGetSpeedParts_speed_metric() {
|
||||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||||
@@ -69,6 +80,18 @@ public class SpeedFormatterTest {
|
|||||||
assertEquals("mph", formatter.getSpeedParts(Speed.zero()).second);
|
assertEquals("mph", formatter.getSpeedParts(Speed.zero()).second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSpeedParts_speed_nautical() {
|
||||||
|
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||||
|
.setDecimalCount(2)
|
||||||
|
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||||
|
.setReportSpeedOrPace(true)
|
||||||
|
.build(context);
|
||||||
|
|
||||||
|
assertEquals("6.5", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||||
|
assertEquals("kn", formatter.getSpeedParts(Speed.zero()).second);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFormatSpeed() {
|
public void testFormatSpeed() {
|
||||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||||
|
|||||||
+32
-27
@@ -155,26 +155,31 @@ public class ExportImportTest {
|
|||||||
mockAltitudeChange(trackPointCreator, 1);
|
mockAltitudeChange(trackPointCreator, 1);
|
||||||
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
|
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
|
||||||
|
|
||||||
trackPointCreator.setClock("2020-02-02T02:02:05Z");
|
trackPointCreator.setClock("2020-02-02T02:02:14Z");
|
||||||
|
mockBLESensorData(trackPointCreator, 15f, null, 66f, 3f, 50f);
|
||||||
|
trackPointCreator.setClock("2020-02-02T02:02:15Z");
|
||||||
|
mockBLESensorData(trackPointCreator, null, null, 66f, 3f, 50f);
|
||||||
|
|
||||||
|
trackPointCreator.setClock("2020-02-02T02:02:16Z");
|
||||||
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
|
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
|
||||||
|
|
||||||
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 13, 15, 10, 0);
|
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0);
|
||||||
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
||||||
|
|
||||||
trackPointCreator.setClock("2020-02-02T02:02:06Z");
|
trackPointCreator.setClock("2020-02-02T02:02:18Z");
|
||||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
|
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
|
||||||
service.endCurrentTrack();
|
service.endCurrentTrack();
|
||||||
|
|
||||||
trackPointCreator.setClock("2020-02-02T02:02:20Z");
|
trackPointCreator.setClock("2020-02-02T02:03:20Z");
|
||||||
service.resumeTrack(trackId);
|
service.resumeTrack(trackId);
|
||||||
|
|
||||||
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 13, 15, 10, 0);
|
sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0);
|
||||||
|
|
||||||
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 13, 15, 10, 0);
|
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0);
|
||||||
|
|
||||||
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 27, 15, 10, 0);
|
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
|
||||||
|
|
||||||
trackPointCreator.setClock("2020-02-02T02:02:24Z");
|
trackPointCreator.setClock("2020-02-02T02:03:24Z");
|
||||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
|
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
|
||||||
service.endCurrentTrack();
|
service.endCurrentTrack();
|
||||||
|
|
||||||
@@ -187,7 +192,7 @@ public class ExportImportTest {
|
|||||||
track = contentProviderUtils.getTrack(trackId);
|
track = contentProviderUtils.getTrack(trackId);
|
||||||
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
|
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
|
||||||
markers = contentProviderUtils.getMarkers(trackId);
|
markers = contentProviderUtils.getMarkers(trackId);
|
||||||
assertEquals(10, trackPoints.size());
|
assertEquals(12, trackPoints.size());
|
||||||
assertEquals(2, markers.size());
|
assertEquals(2, markers.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,29 +236,29 @@ public class ExportImportTest {
|
|||||||
// Time
|
// Time
|
||||||
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
|
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
|
||||||
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
|
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
|
||||||
assertEquals(Instant.parse("2020-02-02T02:02:24Z"), importedTrackStatistics.getStopTime());
|
assertEquals(Instant.parse("2020-02-02T02:03:24Z"), importedTrackStatistics.getStopTime());
|
||||||
|
|
||||||
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
|
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
|
||||||
|
|
||||||
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
|
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
|
||||||
assertEquals(Duration.ofSeconds(7), importedTrackStatistics.getTotalTime());
|
assertEquals(Duration.ofSeconds(19), importedTrackStatistics.getTotalTime());
|
||||||
|
|
||||||
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
|
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
|
||||||
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
|
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
|
||||||
|
|
||||||
// Distance
|
// Distance
|
||||||
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
|
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
|
||||||
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
|
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
|
||||||
|
|
||||||
// Speed
|
// Speed
|
||||||
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
|
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
|
||||||
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||||
|
|
||||||
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
|
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
|
||||||
assertEquals(17.59, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
|
assertEquals(6.58, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
|
||||||
|
|
||||||
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
|
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
|
||||||
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||||
|
|
||||||
// Altitude
|
// Altitude
|
||||||
assertEquals(originalTrackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
|
assertEquals(originalTrackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
|
||||||
@@ -345,19 +350,19 @@ public class ExportImportTest {
|
|||||||
.setAltitudeLoss(1f)
|
.setAltitudeLoss(1f)
|
||||||
.setAltitudeGain(1f)
|
.setAltitudeGain(1f)
|
||||||
.setHorizontalAccuracy(Distance.of(10)),
|
.setHorizontalAccuracy(Distance.of(10)),
|
||||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
|
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:17Z"))
|
||||||
.setLatitude(3)
|
.setLatitude(3)
|
||||||
.setLongitude(14.001)
|
.setLongitude(14.001)
|
||||||
.setAltitude(10)
|
.setAltitude(10)
|
||||||
.setSpeed(Speed.of(5))
|
.setSpeed(Speed.of(5))
|
||||||
.setAltitudeLoss(1f)
|
.setAltitudeLoss(1f)
|
||||||
.setAltitudeGain(1f)
|
.setAltitudeGain(1f)
|
||||||
.setSensorDistance(Distance.of(12))
|
.setSensorDistance(Distance.of(14))
|
||||||
.setHeartRate(66f)
|
.setHeartRate(66f)
|
||||||
.setPower(50f)
|
.setPower(50f)
|
||||||
.setCadence(3f)
|
.setCadence(3f)
|
||||||
.setHorizontalAccuracy(Distance.of(10)),
|
.setHorizontalAccuracy(Distance.of(10)),
|
||||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z"))
|
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:21Z"))
|
||||||
.setLatitude(3)
|
.setLatitude(3)
|
||||||
.setLongitude(14.002)
|
.setLongitude(14.002)
|
||||||
.setAltitude(10)
|
.setAltitude(10)
|
||||||
@@ -365,7 +370,7 @@ public class ExportImportTest {
|
|||||||
.setAltitudeGain(0f)
|
.setAltitudeGain(0f)
|
||||||
.setSpeed(Speed.of(15))
|
.setSpeed(Speed.of(15))
|
||||||
.setHorizontalAccuracy(Distance.of(10)),
|
.setHorizontalAccuracy(Distance.of(10)),
|
||||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:22Z"))
|
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:22Z"))
|
||||||
.setLatitude(3)
|
.setLatitude(3)
|
||||||
.setLongitude(16)
|
.setLongitude(16)
|
||||||
.setAltitude(10)
|
.setAltitude(10)
|
||||||
@@ -373,7 +378,7 @@ public class ExportImportTest {
|
|||||||
.setAltitudeGain(0f)
|
.setAltitudeGain(0f)
|
||||||
.setSpeed(Speed.of(15))
|
.setSpeed(Speed.of(15))
|
||||||
.setHorizontalAccuracy(Distance.of(10)),
|
.setHorizontalAccuracy(Distance.of(10)),
|
||||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:23Z"))
|
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:23Z"))
|
||||||
.setLatitude(3)
|
.setLatitude(3)
|
||||||
.setLongitude(16.001)
|
.setLongitude(16.001)
|
||||||
.setAltitude(10)
|
.setAltitude(10)
|
||||||
@@ -390,18 +395,18 @@ public class ExportImportTest {
|
|||||||
// Time
|
// Time
|
||||||
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
|
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
|
||||||
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
|
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
|
||||||
assertEquals(Instant.parse("2020-02-02T02:02:23Z"), importedTrackStatistics.getStopTime());
|
assertEquals(Instant.parse("2020-02-02T02:03:23Z"), importedTrackStatistics.getStopTime());
|
||||||
|
|
||||||
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getTotalTime());
|
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getTotalTime());
|
||||||
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
|
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getMovingTime());
|
||||||
|
|
||||||
// Distance
|
// Distance
|
||||||
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
|
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
|
||||||
|
|
||||||
// Speed
|
// Speed
|
||||||
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
assertEquals(8.34, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||||
assertEquals(41.05, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
|
assertEquals(8.34, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
|
||||||
assertEquals(41.05, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
|
assertEquals(8.34, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
|
||||||
|
|
||||||
// Altitude
|
// Altitude
|
||||||
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
|
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
|
||||||
|
|||||||
+35
-1
@@ -15,11 +15,16 @@ import org.junit.Test;
|
|||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.JUnit4;
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||||
@@ -27,6 +32,8 @@ import de.dennisguse.opentracks.data.models.Distance;
|
|||||||
import de.dennisguse.opentracks.data.models.Speed;
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
|
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,7 +188,7 @@ public class GPXTrackImporterTest {
|
|||||||
|
|
||||||
// 3. trackstatistics
|
// 3. trackstatistics
|
||||||
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
|
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
|
||||||
assertEquals(4, trackStatistics.getMaxSpeed().toMPS(), 0.01);
|
assertEquals(4.0, trackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||||
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
|
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
|
||||||
|
|
||||||
// 4. trackpoints
|
// 4. trackpoints
|
||||||
@@ -208,4 +215,31 @@ public class GPXTrackImporterTest {
|
|||||||
.setSpeed(Speed.of(3))
|
.setSpeed(Speed.of(3))
|
||||||
), importedTrackPoints);
|
), importedTrackPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
|
@Test
|
||||||
|
public void importExportTest_timezone() throws IOException {
|
||||||
|
// given
|
||||||
|
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||||
|
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx_timezone);
|
||||||
|
InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx_timezone);
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
// when
|
||||||
|
// 1. import
|
||||||
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||||
|
|
||||||
|
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
|
||||||
|
trackExporter.writeTrack(importedTrack, outputStream);
|
||||||
|
|
||||||
|
// then
|
||||||
|
String expected = new BufferedReader(
|
||||||
|
new InputStreamReader(inputStreamExpected, StandardCharsets.UTF_8))
|
||||||
|
.lines()
|
||||||
|
.collect(Collectors.joining("\n")) + "\n";
|
||||||
|
|
||||||
|
assertEquals(expected, outputStream.toString()); //TODO inputStream.readAllBytes() ?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+43
@@ -31,12 +31,14 @@ import de.dennisguse.opentracks.R;
|
|||||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
import de.dennisguse.opentracks.data.models.Distance;
|
||||||
|
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
|
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
|
||||||
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
|
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
|
||||||
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
|
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
|
||||||
|
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
|
||||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
@@ -257,6 +259,47 @@ public class TrackRecordingServiceTestRecording {
|
|||||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MediumTest
|
||||||
|
@Test
|
||||||
|
public void testRecording_blesensor_only_no_distance() {
|
||||||
|
// given
|
||||||
|
String startTime = "2020-02-02T02:02:02Z";
|
||||||
|
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||||
|
|
||||||
|
trackPointCreator.setClock(startTime);
|
||||||
|
Track.Id trackId = service.startNewTrack();
|
||||||
|
trackPointCreator.stopGPS();
|
||||||
|
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
|
||||||
|
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager();
|
||||||
|
|
||||||
|
// when
|
||||||
|
String sensor1 = "2020-02-02T02:02:03Z";
|
||||||
|
trackPointCreator.setClock(sensor1);
|
||||||
|
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored
|
||||||
|
|
||||||
|
String sensor3 = "2020-02-02T02:02:13Z";
|
||||||
|
trackPointCreator.setClock(sensor3);
|
||||||
|
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(7)));
|
||||||
|
|
||||||
|
String stopTime = "2020-02-02T02:02:15Z";
|
||||||
|
trackPointCreator.setClock(stopTime);
|
||||||
|
service.endCurrentTrack();
|
||||||
|
|
||||||
|
// then
|
||||||
|
new TrackPointAssert().assertEquals(List.of(
|
||||||
|
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
|
||||||
|
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3))
|
||||||
|
.setAltitudeGain(0f)
|
||||||
|
.setAltitudeLoss(0f)
|
||||||
|
.setHeartRate(HeartRate.of(7)),
|
||||||
|
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
|
||||||
|
.setAltitudeGain(0f)
|
||||||
|
.setAltitudeLoss(0f)
|
||||||
|
.setHeartRate(HeartRate.of(7))
|
||||||
|
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@MediumTest
|
@MediumTest
|
||||||
@Test
|
@Test
|
||||||
public void testRecording_gpsOnly_recordingDistance_above() {
|
public void testRecording_gpsOnly_recordingDistance_above() {
|
||||||
|
|||||||
+48
@@ -67,6 +67,54 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
|
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAnnouncement_metric_speed_rounding_check() {
|
||||||
|
TrackStatistics stats = new TrackStatistics();
|
||||||
|
stats.setTotalDistance(Distance.of(20000));
|
||||||
|
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||||
|
stats.setMovingTime(Duration.ofHours(1).plusSeconds(1));
|
||||||
|
stats.setMaxSpeed(Speed.of(100));
|
||||||
|
stats.setTotalAltitudeGain(6000f);
|
||||||
|
|
||||||
|
// when
|
||||||
|
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals("Total distance 20.00 kilometers. 1 hour 1 second. Speed 20.0 kilometers per hour.", announcement);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAnnouncement_metric_distance_rounding_check() {
|
||||||
|
TrackStatistics stats = new TrackStatistics();
|
||||||
|
stats.setTotalDistance(Distance.of(19999));
|
||||||
|
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||||
|
stats.setMovingTime(Duration.ofHours(1));
|
||||||
|
stats.setMaxSpeed(Speed.of(100));
|
||||||
|
stats.setTotalAltitudeGain(6000f);
|
||||||
|
|
||||||
|
// when
|
||||||
|
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals("Total distance 20.00 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAnnouncement_metric_distance_rounding_check_two() {
|
||||||
|
TrackStatistics stats = new TrackStatistics();
|
||||||
|
stats.setTotalDistance(Distance.of(19990));
|
||||||
|
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||||
|
stats.setMovingTime(Duration.ofHours(1));
|
||||||
|
stats.setMaxSpeed(Speed.of(100));
|
||||||
|
stats.setTotalAltitudeGain(6000f);
|
||||||
|
|
||||||
|
// when
|
||||||
|
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals("Total distance 19.99 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getAnnouncement_withInterval_metric_speed() {
|
public void getAnnouncement_withInterval_metric_speed() {
|
||||||
// given
|
// given
|
||||||
|
|||||||
+66
-1
@@ -11,6 +11,7 @@ import org.junit.runner.RunWith;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||||
import de.dennisguse.opentracks.data.models.Altitude;
|
import de.dennisguse.opentracks.data.models.Altitude;
|
||||||
@@ -86,7 +87,7 @@ public class TrackStatisticsUpdaterTest {
|
|||||||
assertEquals(Duration.ofSeconds(9), statistics.getMovingTime());
|
assertEquals(Duration.ofSeconds(9), statistics.getMovingTime());
|
||||||
|
|
||||||
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
|
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
|
||||||
assertEquals(28.75, statistics.getMaxAltitude(), 0.01);
|
assertEquals(32.5, statistics.getMaxAltitude(), 0.01);
|
||||||
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
|
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
|
||||||
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
|
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
|
||||||
|
|
||||||
@@ -245,6 +246,70 @@ public class TrackStatisticsUpdaterTest {
|
|||||||
public void addTrackPoint_speed_from_GPS_moving_and_sensor_speed() {
|
public void addTrackPoint_speed_from_GPS_moving_and_sensor_speed() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addTrackPoint_maxSpeed_ignore_above_acceleration() {
|
||||||
|
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||||
|
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
|
||||||
|
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
// Ignore as we set max speed if two consecutive trackpoints were considered moving
|
||||||
|
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
|
||||||
|
.setSpeed(Speed.of(1f)));
|
||||||
|
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
// Update max speed
|
||||||
|
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
|
||||||
|
.setSpeed(Speed.of(1f)));
|
||||||
|
assertEquals(Speed.of(1f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
// Update max speed
|
||||||
|
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(12))
|
||||||
|
.setSpeed(Speed.of(50f)));
|
||||||
|
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
// Ignore; we were getting slower
|
||||||
|
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(13))
|
||||||
|
.setSpeed(Speed.of(5f)));
|
||||||
|
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
// Ignore acceleration above 2g
|
||||||
|
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(14))
|
||||||
|
.setSpeed(Speed.of(500f)));
|
||||||
|
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addTrackPoint_maxSpeed_multiple_segments() {
|
||||||
|
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||||
|
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
subject.addTrackPoints(List.of(
|
||||||
|
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
|
||||||
|
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
|
||||||
|
.setSpeed(Speed.of(2f)),
|
||||||
|
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
|
||||||
|
.setSpeed(Speed.of(2f)),
|
||||||
|
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(4))
|
||||||
|
));
|
||||||
|
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
|
||||||
|
// when
|
||||||
|
subject.addTrackPoints(List.of(
|
||||||
|
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(5)),
|
||||||
|
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6))
|
||||||
|
.setSpeed(Speed.of(1f)),
|
||||||
|
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7))
|
||||||
|
.setSpeed(Speed.of(1f)),
|
||||||
|
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(8))
|
||||||
|
));
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void copy_constructor() {
|
public void copy_constructor() {
|
||||||
// given
|
// given
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ public class EGM2008UtilsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void data_Berlin() throws IOException {
|
public void data_Berlin_Germany_() throws IOException {
|
||||||
// given
|
// given
|
||||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||||
trackPoint.setLatitude(52.530644);
|
trackPoint.setLatitude(52.530644);
|
||||||
@@ -157,7 +157,7 @@ public class EGM2008UtilsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void data_Berlin_Caching() throws IOException {
|
public void data_Berlin_Germany_Caching() throws IOException {
|
||||||
// given
|
// given
|
||||||
TrackPoint trackPoint1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
TrackPoint trackPoint1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||||
trackPoint1.setLatitude(52.530644);
|
trackPoint1.setLatitude(52.530644);
|
||||||
@@ -176,6 +176,22 @@ public class EGM2008UtilsTest {
|
|||||||
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
|
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void data_Seattle_USA() throws IOException {
|
||||||
|
// given
|
||||||
|
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||||
|
|
||||||
|
trackPoint.setLatitude(47.63153);
|
||||||
|
trackPoint.setLongitude(-122.30938);
|
||||||
|
trackPoint.setAltitude(0);
|
||||||
|
|
||||||
|
// when
|
||||||
|
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(22.99, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void data_MaxUndulation() throws IOException {
|
public void data_MaxUndulation() throws IOException {
|
||||||
// given
|
// given
|
||||||
@@ -191,6 +207,7 @@ public class EGM2008UtilsTest {
|
|||||||
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getIndices() {
|
public void getIndices() {
|
||||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||||
@@ -208,7 +225,7 @@ public class EGM2008UtilsTest {
|
|||||||
|
|
||||||
trackPoint.setLatitude(-90);
|
trackPoint.setLatitude(-90);
|
||||||
trackPoint.setLongitude(-180);
|
trackPoint.setLongitude(-180);
|
||||||
assertEquals(new EGM2008Utils.Indices(2160, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
|
assertEquals(new EGM2008Utils.Indices(2160, 2160), EGM2008Utils.getIndices(trackPoint.getLocation()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
package de.dennisguse.opentracks.util;
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.junit.runners.JUnit4;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RunWith(JUnit4.class)
|
|
||||||
public class ExportUtilsTest extends TestCase {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsExportFileExists_exists() {
|
|
||||||
// given
|
|
||||||
UUID uuid = UUID.randomUUID();
|
|
||||||
String format = "kmz";
|
|
||||||
List<String> fileNames = new ArrayList<>();
|
|
||||||
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
|
|
||||||
fileNames.add(uuid.toString().substring(0, 8) + "_other name name.kmz");
|
|
||||||
|
|
||||||
// when
|
|
||||||
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertTrue(exists);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsExportFileExists_not_exists1() {
|
|
||||||
// given
|
|
||||||
UUID uuid = UUID.randomUUID();
|
|
||||||
String format = "gpx";
|
|
||||||
List<String> fileNames = new ArrayList<>();
|
|
||||||
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_name.gpx");
|
|
||||||
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
|
|
||||||
|
|
||||||
// when
|
|
||||||
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertFalse(exists);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsExportFileExists_not_exists2() {
|
|
||||||
// given
|
|
||||||
UUID uuid = UUID.randomUUID();
|
|
||||||
String format = "kmz";
|
|
||||||
List<String> fileNames = new ArrayList<>();
|
|
||||||
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
|
|
||||||
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
|
|
||||||
|
|
||||||
// when
|
|
||||||
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertFalse(exists);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,12 +41,12 @@ import java.time.format.TextStyle;
|
|||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.GregorianCalendar;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.LocaleRule;
|
import de.dennisguse.opentracks.LocaleRule;
|
||||||
|
import de.dennisguse.opentracks.TimezoneRule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link StringUtils}.
|
* Tests for {@link StringUtils}.
|
||||||
@@ -59,6 +59,9 @@ public class StringUtilsTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
|
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
|
||||||
|
|
||||||
private final Context context = ApplicationProvider.getApplicationContext();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,51 +102,13 @@ public class StringUtilsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetTime_fractional() {
|
public void testParseTime() {
|
||||||
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
|
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352").toInstant());
|
||||||
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 352);
|
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352Z").toInstant());
|
||||||
}
|
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352+00:00").toInstant());
|
||||||
|
|
||||||
@Test
|
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T01:00:00.352+01:00").toInstant());
|
||||||
public void testGetTime_timezone() {
|
assertEquals(Instant.ofEpochMilli(352).plus(Duration.ofHours(1)), StringUtils.parseTime("1970-01-01T00:00:00.352-01:00").toInstant());
|
||||||
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
|
|
||||||
assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0);
|
|
||||||
assertGetTime("2010-05-04T03:02:01+00:00", 2010, 5, 4, 3, 2, 1, 0);
|
|
||||||
assertGetTime("2010-05-04T03:02:01-00:00", 2010, 5, 4, 3, 2, 1, 0);
|
|
||||||
assertGetTime("2010-05-04T03:02:01+01:00", 2010, 5, 4, 2, 2, 1, 0);
|
|
||||||
assertGetTime("2010-05-04T03:02:01+10:30", 2010, 5, 3, 16, 32, 1, 0);
|
|
||||||
assertGetTime("2010-05-04T03:02:01-09:30", 2010, 5, 4, 12, 32, 1, 0);
|
|
||||||
assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetTime_fractionalAndTimezone() {
|
|
||||||
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
|
|
||||||
assertGetTime("2010-05-04T03:02:01.47+00:00", 2010, 5, 4, 3, 2, 1, 470);
|
|
||||||
assertGetTime("2010-05-04T03:02:01.5791+03:00", 2010, 5, 4, 0, 2, 1, 579);
|
|
||||||
assertGetTime("2010-05-04T03:02:01.8-05:30", 2010, 5, 4, 8, 32, 1, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asserts the {@link StringUtils#parseTime(String)} returns the expected values.
|
|
||||||
*
|
|
||||||
* @param xmlDateTime the xml date time string
|
|
||||||
* @param year the expected year
|
|
||||||
* @param month the expected month
|
|
||||||
* @param day the expected day
|
|
||||||
* @param hour the expected hour
|
|
||||||
* @param minute the expected minute
|
|
||||||
* @param second the expected second
|
|
||||||
* @param millisecond the expected milliseconds
|
|
||||||
*/
|
|
||||||
private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, int second, int millisecond) {
|
|
||||||
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
|
||||||
calendar.set(year, month - 1, day, hour, minute, second);
|
|
||||||
calendar.set(GregorianCalendar.MILLISECOND, millisecond);
|
|
||||||
|
|
||||||
// This comparision tends to be flaky (difference of 1ms)
|
|
||||||
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
|
|
||||||
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toInstant().toEpochMilli()) <= 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gpx
|
||||||
|
version="1.1"
|
||||||
|
creator="OpenTracks (Debug)"
|
||||||
|
xmlns="http://www.topografix.com/GPX/1/1"
|
||||||
|
xmlns:topografix="http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1"
|
||||||
|
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v2"
|
||||||
|
xmlns:gpxtrkx="http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
|
||||||
|
xmlns:cluetrust="http://www.cluetrust.com/Schemas/"
|
||||||
|
xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1"
|
||||||
|
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.garmin.com/xmlschemas/TrackStatsExtension/v1 http://www.cluetrust.com/Schemas http://www.cluetrust.com/Schemas/gpxdata10.xsd http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
|
||||||
|
<wpt lat="3" lon="14">
|
||||||
|
<ele>10</ele>
|
||||||
|
<time>2021-01-07T21:51:59.179+01:15</time>
|
||||||
|
<name><![CDATA[Marker 1]]></name>
|
||||||
|
<desc><![CDATA[Marker 1 desc]]></desc>
|
||||||
|
<type><![CDATA[Marker 1 category]]></type>
|
||||||
|
</wpt>
|
||||||
|
<trk>
|
||||||
|
<name><![CDATA[2021-01-07 22:51]]></name>
|
||||||
|
<desc><![CDATA[the description]]></desc>
|
||||||
|
<type><![CDATA[the category]]></type>
|
||||||
|
<extensions>
|
||||||
|
<topografix:color>c0c0c0</topografix:color>
|
||||||
|
<opentracks:trackid>7002101e-4198-4613-8c24-544e01ca3981</opentracks:trackid>
|
||||||
|
<gpxtrkx:TrackStatsExtension>
|
||||||
|
<gpxtrkx:Distance>0.0</gpxtrkx:Distance>
|
||||||
|
<gpxtrkx:TimerTime>0</gpxtrkx:TimerTime>
|
||||||
|
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
|
||||||
|
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
|
||||||
|
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
|
||||||
|
<gpxtrkx:Ascent>null</gpxtrkx:Ascent>
|
||||||
|
<gpxtrkx:Descent>null</gpxtrkx:Descent>
|
||||||
|
</gpxtrkx:TrackStatsExtension>
|
||||||
|
</extensions>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="3" lon="14">
|
||||||
|
<ele>10</ele>
|
||||||
|
<time>2021-01-07T21:51:59.179+01:15</time>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
||||||
@@ -59,6 +59,8 @@ limitations under the License.
|
|||||||
tools:targetApi="s" />
|
tools:targetApi="s" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Startup"
|
android:name=".Startup"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
|
|||||||
/**
|
/**
|
||||||
* Called every time a track is deleted.
|
* Called every time a track is deleted.
|
||||||
*/
|
*/
|
||||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
|
||||||
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
|
if (trackDeleteServiceConnection != null) {
|
||||||
trackDeleteServiceConnection.unbind(this);
|
trackDeleteServiceConnection.unbind(this);
|
||||||
trackDeleteServiceConnection = null;
|
trackDeleteServiceConnection = null;
|
||||||
onDeleteFinished();
|
onDeleteFinished();
|
||||||
|
|||||||
@@ -3,19 +3,15 @@ package de.dennisguse.opentracks;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.databinding.ActivityShowErrorBinding;
|
import de.dennisguse.opentracks.databinding.ActivityShowErrorBinding;
|
||||||
import de.dennisguse.opentracks.databinding.TrackListBinding;
|
|
||||||
|
|
||||||
public class ShowErrorActivity extends AbstractActivity {
|
public class ShowErrorActivity extends AbstractActivity {
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import de.dennisguse.opentracks.ui.util.ActivityUtils;
|
|||||||
import de.dennisguse.opentracks.ui.util.ListItemUtils;
|
import de.dennisguse.opentracks.ui.util.ListItemUtils;
|
||||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||||
import de.dennisguse.opentracks.util.StringUtils;
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
|||||||
startActivity(newIntent);
|
startActivity(newIntent);
|
||||||
|
|
||||||
connection.unbind(this);
|
connection.unbind(this);
|
||||||
}).startAndBind(this);
|
}).startAndBind(this, true);
|
||||||
});
|
});
|
||||||
viewBinding.trackListFabAction.setOnLongClickListener((view) -> {
|
viewBinding.trackListFabAction.setOnLongClickListener((view) -> {
|
||||||
if (!recordingStatus.isRecording()) {
|
if (!recordingStatus.isRecording()) {
|
||||||
@@ -263,13 +263,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void requestRequiredPermissions() {
|
private void requestRequiredPermissions() {
|
||||||
if (!PermissionUtils.hasGPSPermission(this)) {
|
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
|
||||||
PermissionUtils.requestGPSPermission(this, null, () -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PermissionUtils.hasBluetoothPermissions(this)) {
|
|
||||||
PermissionUtils.requestBluetoothPermission(this, null, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -323,8 +317,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
|||||||
searchView.findViewById(R.id.search_edit_frame).setPadding(0, 0, 48, 0);
|
searchView.findViewById(R.id.search_edit_frame).setPadding(0, 0, 48, 0);
|
||||||
|
|
||||||
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(R.id.search_src_text);
|
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(R.id.search_src_text);
|
||||||
searchAutoComplete.setHintTextColor(getResources().getColor(android.R.color.white));
|
searchAutoComplete.setHintTextColor(ContextCompat.getColor(this, android.R.color.white));
|
||||||
searchAutoComplete.setTextColor(getResources().getColor(android.R.color.white));
|
searchAutoComplete.setTextColor(ContextCompat.getColor(this, android.R.color.white));
|
||||||
|
|
||||||
searchView.setOnCloseListener(() -> {
|
searchView.setOnCloseListener(() -> {
|
||||||
searchView.clearFocus();
|
searchView.clearFocus();
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
|||||||
|
|
||||||
connection.unbind(this);
|
connection.unbind(this);
|
||||||
finish();
|
finish();
|
||||||
}).startAndBind(this);
|
}).startAndBind(this, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,9 +232,9 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
|
||||||
super.onTrackDeleteStatus(deleteStatus);
|
super.onTrackDeleteStatus(deletionFinishedStatus);
|
||||||
if (deleteStatus.isDeleted(trackId)) {
|
if (deletionFinishedStatus.isDeleted(trackId)) {
|
||||||
runOnUiThread(this::finish);
|
runOnUiThread(this::finish);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import android.util.Log;
|
|||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowInsets;
|
|
||||||
import android.view.WindowInsetsController;
|
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
@@ -49,7 +47,6 @@ import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
|
|||||||
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
|
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
|
||||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackUtils;
|
import de.dennisguse.opentracks.util.TrackUtils;
|
||||||
|
|
||||||
@@ -138,7 +135,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
|
|
||||||
viewBinding.trackRecordingFabAction.setImageResource(R.drawable.ic_baseline_stop_24);
|
viewBinding.trackRecordingFabAction.setImageResource(R.drawable.ic_baseline_stop_24);
|
||||||
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color));
|
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color));
|
||||||
viewBinding.trackRecordingFabAction.setBackgroundColor(getResources().getColor(R.color.opentracks_secondary_color));
|
viewBinding.trackRecordingFabAction.setBackgroundColor(ContextCompat.getColor(this, R.color.opentracks_secondary_color));
|
||||||
viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> {
|
viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> {
|
||||||
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
|
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
|
||||||
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
|
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
|
||||||
|
|||||||
@@ -90,18 +90,25 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewBinding.finishButton.setOnClickListener(v -> {
|
viewBinding.finishButton.setOnClickListener(v -> {
|
||||||
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.trackEditName.getText().toString(),
|
storeTrackMetaData(contentProviderUtils, track);
|
||||||
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
|
|
||||||
contentProviderUtils);
|
|
||||||
ExportUtils.postWorkoutExport(this, trackId);
|
ExportUtils.postWorkoutExport(this, trackId);
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|
||||||
viewBinding.resumeButton.setOnClickListener(v -> resumeTrackAndFinish());
|
viewBinding.resumeButton.setOnClickListener(v -> {
|
||||||
|
storeTrackMetaData(contentProviderUtils, track);
|
||||||
|
resumeTrackAndFinish();
|
||||||
|
});
|
||||||
|
|
||||||
viewBinding.discardButton.setOnClickListener(v -> ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackId));
|
viewBinding.discardButton.setOnClickListener(v -> ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void storeTrackMetaData(ContentProviderUtils contentProviderUtils, Track track) {
|
||||||
|
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.trackEditName.getText().toString(),
|
||||||
|
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
|
||||||
|
contentProviderUtils);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (isDiscarding) {
|
if (isDiscarding) {
|
||||||
@@ -138,7 +145,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
|
|||||||
|
|
||||||
connection.unbind(this);
|
connection.unbind(this);
|
||||||
finish();
|
finish();
|
||||||
}).startAndBind(this);
|
}).startAndBind(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.data.TrackDataHub;
|
import de.dennisguse.opentracks.data.TrackDataHub;
|
||||||
import de.dennisguse.opentracks.data.models.Altitude;
|
|
||||||
import de.dennisguse.opentracks.data.models.Marker;
|
import de.dennisguse.opentracks.data.models.Marker;
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
import de.dennisguse.opentracks.databinding.ChartBinding;
|
import de.dennisguse.opentracks.databinding.ChartBinding;
|
||||||
@@ -188,9 +186,9 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
|
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getUnitSystem());
|
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, trackPoint.getSpeed(), chartByDistance, viewBinding.chartView.getUnitSystem());
|
||||||
pendingPoints.add(point);
|
pendingPoints.add(point);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.chart;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Altitude;
|
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
import de.dennisguse.opentracks.data.models.Distance;
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
@@ -29,15 +28,15 @@ public class ChartPoint {
|
|||||||
this.altitude = altitude;
|
this.altitude = altitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, UnitSystem unitSystem) {
|
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, boolean chartByDistance, UnitSystem unitSystem) {
|
||||||
if (chartByDistance) {
|
if (chartByDistance) {
|
||||||
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
|
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
|
||||||
} else {
|
} else {
|
||||||
timeOrDistance = trackStatistics.getTotalTime().toMillis();
|
timeOrDistance = trackStatistics.getTotalTime().toMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smoothedAltitude != null) {
|
if (trackPoint.hasAltitude()) {
|
||||||
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(unitSystem);
|
altitude = Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smoothedSpeed != null) {
|
if (smoothedSpeed != null) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.graphics.Paint.Style;
|
|||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ abstract class ChartValueSeries {
|
|||||||
private final int[] intervalValues;
|
private final int[] intervalValues;
|
||||||
private final int metricTitleId;
|
private final int metricTitleId;
|
||||||
private final int imperialTitleId;
|
private final int imperialTitleId;
|
||||||
|
private final int nauticalTitleId;
|
||||||
private final Paint fillPaint;
|
private final Paint fillPaint;
|
||||||
private final Paint strokePaint;
|
private final Paint strokePaint;
|
||||||
private final Paint titlePaint;
|
private final Paint titlePaint;
|
||||||
@@ -57,33 +59,22 @@ abstract class ChartValueSeries {
|
|||||||
private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS;
|
private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS;
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
|
|
||||||
/**
|
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int nauticalTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param context the context
|
|
||||||
* @param absoluteMin the absolute min value
|
|
||||||
* @param absoluteMax the absolute max value
|
|
||||||
* @param intervalValues the list of interval values
|
|
||||||
* @param metricTitleId the metric title id
|
|
||||||
* @param imperialTitleId the imperial title id
|
|
||||||
* @param fillColor the fill color
|
|
||||||
* @param strokeColor the stroke color
|
|
||||||
*/
|
|
||||||
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
|
|
||||||
this.absoluteMin = absoluteMin;
|
this.absoluteMin = absoluteMin;
|
||||||
this.absoluteMax = absoluteMax;
|
this.absoluteMax = absoluteMax;
|
||||||
this.intervalValues = intervalValues;
|
this.intervalValues = intervalValues;
|
||||||
this.metricTitleId = metricTitleId;
|
this.metricTitleId = metricTitleId;
|
||||||
this.imperialTitleId = imperialTitleId;
|
this.imperialTitleId = imperialTitleId;
|
||||||
|
this.nauticalTitleId = nauticalTitleId;
|
||||||
|
|
||||||
fillPaint = new Paint();
|
fillPaint = new Paint();
|
||||||
fillPaint.setStyle(Style.FILL);
|
fillPaint.setStyle(Style.FILL);
|
||||||
fillPaint.setColor(context.getResources().getColor(fillColor));
|
fillPaint.setColor(ContextCompat.getColor(context, fillColor));
|
||||||
fillPaint.setAntiAlias(true);
|
fillPaint.setAntiAlias(true);
|
||||||
|
|
||||||
strokePaint = new Paint();
|
strokePaint = new Paint();
|
||||||
strokePaint.setStyle(Style.STROKE);
|
strokePaint.setStyle(Style.STROKE);
|
||||||
strokePaint.setColor(context.getResources().getColor(strokeColor));
|
strokePaint.setColor(ContextCompat.getColor(context, strokeColor));
|
||||||
strokePaint.setAntiAlias(true);
|
strokePaint.setAntiAlias(true);
|
||||||
|
|
||||||
// Make copies of the stroke paint with the default thickness
|
// Make copies of the stroke paint with the default thickness
|
||||||
@@ -224,39 +215,27 @@ abstract class ChartValueSeries {
|
|||||||
return metricTitleId;
|
return metricTitleId;
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
return imperialTitleId;
|
return imperialTitleId;
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
return nauticalTitleId;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the title paint.
|
|
||||||
*/
|
|
||||||
Paint getTitlePaint() {
|
Paint getTitlePaint() {
|
||||||
return titlePaint;
|
return titlePaint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the marker paint.
|
|
||||||
*/
|
|
||||||
Paint getMarkerPaint() {
|
Paint getMarkerPaint() {
|
||||||
return markerPaint;
|
return markerPaint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the largest marker.
|
|
||||||
*/
|
|
||||||
String getLargestMarker() {
|
String getLargestMarker() {
|
||||||
String minMarker = numberFormat.format(getMinMarkerValue());
|
String minMarker = numberFormat.format(getMinMarkerValue());
|
||||||
String maxMarker = numberFormat.format(getMaxMarkerValue());
|
String maxMarker = numberFormat.format(getMaxMarkerValue());
|
||||||
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
|
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a marker value.
|
|
||||||
*
|
|
||||||
* @param value the value
|
|
||||||
*/
|
|
||||||
String formatMarker(int value) {
|
String formatMarker(int value) {
|
||||||
return numberFormat.format(value);
|
return numberFormat.format(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ public class ChartView extends View {
|
|||||||
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
|
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
|
||||||
R.string.description_altitude_metric,
|
R.string.description_altitude_metric,
|
||||||
R.string.description_altitude_imperial,
|
R.string.description_altitude_imperial,
|
||||||
|
R.string.description_altitude_imperial,
|
||||||
R.color.chart_altitude_fill,
|
R.color.chart_altitude_fill,
|
||||||
R.color.chart_altitude_border,
|
R.color.chart_altitude_border,
|
||||||
fontSizeSmall,
|
fontSizeSmall,
|
||||||
@@ -224,6 +225,7 @@ public class ChartView extends View {
|
|||||||
new int[]{1, 5, 10, 20, 50, 100},
|
new int[]{1, 5, 10, 20, 50, 100},
|
||||||
R.string.description_speed_metric,
|
R.string.description_speed_metric,
|
||||||
R.string.description_speed_imperial,
|
R.string.description_speed_imperial,
|
||||||
|
R.string.description_speed_nautical,
|
||||||
R.color.chart_speed_fill,
|
R.color.chart_speed_fill,
|
||||||
R.color.chart_speed_border,
|
R.color.chart_speed_border,
|
||||||
fontSizeSmall,
|
fontSizeSmall,
|
||||||
@@ -246,6 +248,7 @@ public class ChartView extends View {
|
|||||||
new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120},
|
new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120},
|
||||||
R.string.description_pace_metric,
|
R.string.description_pace_metric,
|
||||||
R.string.description_pace_imperial,
|
R.string.description_pace_imperial,
|
||||||
|
R.string.description_pace_nautical,
|
||||||
R.color.chart_pace_fill,
|
R.color.chart_pace_fill,
|
||||||
R.color.chart_pace_border,
|
R.color.chart_pace_border,
|
||||||
fontSizeSmall,
|
fontSizeSmall,
|
||||||
@@ -268,6 +271,7 @@ public class ChartView extends View {
|
|||||||
new int[]{25, 50},
|
new int[]{25, 50},
|
||||||
R.string.description_sensor_heart_rate,
|
R.string.description_sensor_heart_rate,
|
||||||
R.string.description_sensor_heart_rate,
|
R.string.description_sensor_heart_rate,
|
||||||
|
R.string.description_sensor_heart_rate,
|
||||||
R.color.chart_heart_rate_fill,
|
R.color.chart_heart_rate_fill,
|
||||||
R.color.chart_heart_rate_border,
|
R.color.chart_heart_rate_border,
|
||||||
fontSizeSmall,
|
fontSizeSmall,
|
||||||
@@ -289,6 +293,7 @@ public class ChartView extends View {
|
|||||||
new int[]{5, 10, 25, 50},
|
new int[]{5, 10, 25, 50},
|
||||||
R.string.description_sensor_cadence,
|
R.string.description_sensor_cadence,
|
||||||
R.string.description_sensor_cadence,
|
R.string.description_sensor_cadence,
|
||||||
|
R.string.description_sensor_cadence,
|
||||||
R.color.chart_cadence_fill,
|
R.color.chart_cadence_fill,
|
||||||
R.color.chart_cadence_border,
|
R.color.chart_cadence_border,
|
||||||
fontSizeSmall,
|
fontSizeSmall,
|
||||||
@@ -309,6 +314,7 @@ public class ChartView extends View {
|
|||||||
new int[]{5, 50, 100, 200},
|
new int[]{5, 50, 100, 200},
|
||||||
R.string.description_sensor_power,
|
R.string.description_sensor_power,
|
||||||
R.string.description_sensor_power,
|
R.string.description_sensor_power,
|
||||||
|
R.string.description_sensor_power,
|
||||||
R.color.chart_power_fill,
|
R.color.chart_power_fill,
|
||||||
R.color.chart_power_border,
|
R.color.chart_power_border,
|
||||||
fontSizeSmall,
|
fontSizeSmall,
|
||||||
@@ -732,6 +738,8 @@ public class ChartView extends View {
|
|||||||
return context.getString(R.string.unit_kilometer);
|
return context.getString(R.string.unit_kilometer);
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
return context.getString(R.string.unit_mile);
|
return context.getString(R.string.unit_mile);
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
return context.getString(R.string.unit_nautical_mile);
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ public class ContentProviderUtils {
|
|||||||
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete track last since it triggers a database vacuum call
|
|
||||||
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
||||||
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new));
|
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new));
|
||||||
}
|
}
|
||||||
@@ -195,8 +194,6 @@ public class ContentProviderUtils {
|
|||||||
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
|
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
|
||||||
// Delete track folder resources.
|
// Delete track folder resources.
|
||||||
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||||
|
|
||||||
// Delete track last since it triggers a database vacuum call
|
|
||||||
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
|
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.database.sqlite.SQLiteDatabase;
|
|||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
import android.database.sqlite.SQLiteQueryBuilder;
|
import android.database.sqlite.SQLiteQueryBuilder;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
|||||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
||||||
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
||||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||||
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
|
* A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
|
||||||
@@ -53,6 +55,8 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
|
|
||||||
private static final String SQL_LIST_DELIMITER = ",";
|
private static final String SQL_LIST_DELIMITER = ",";
|
||||||
|
|
||||||
|
private static final int TOTAL_DELETED_ROWS_VACUUM_THRESHOLD = 10000;
|
||||||
|
|
||||||
private final UriMatcher uriMatcher;
|
private final UriMatcher uriMatcher;
|
||||||
|
|
||||||
private SQLiteDatabase db;
|
private SQLiteDatabase db;
|
||||||
@@ -131,14 +135,12 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
@Override
|
@Override
|
||||||
public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
|
public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
|
||||||
String table;
|
String table;
|
||||||
boolean shouldVacuum = false;
|
|
||||||
switch (getUrlType(url)) {
|
switch (getUrlType(url)) {
|
||||||
case TRACKPOINTS:
|
case TRACKPOINTS:
|
||||||
table = TrackPointsColumns.TABLE_NAME;
|
table = TrackPointsColumns.TABLE_NAME;
|
||||||
break;
|
break;
|
||||||
case TRACKS:
|
case TRACKS:
|
||||||
table = TracksColumns.TABLE_NAME;
|
table = TracksColumns.TABLE_NAME;
|
||||||
shouldVacuum = true;
|
|
||||||
break;
|
break;
|
||||||
case MARKERS:
|
case MARKERS:
|
||||||
table = MarkerColumns.TABLE_NAME;
|
table = MarkerColumns.TABLE_NAME;
|
||||||
@@ -147,23 +149,40 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
throw new IllegalArgumentException("Unknown URL " + url);
|
throw new IllegalArgumentException("Unknown URL " + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.w(TAG, "Deleting table " + table);
|
Log.w(TAG, "Deleting from table " + table);
|
||||||
int count;
|
int totalChangesBefore = getTotalChanges();
|
||||||
|
int deletedRowsFromTable;
|
||||||
try {
|
try {
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
count = db.delete(table, where, selectionArgs);
|
deletedRowsFromTable = db.delete(table, where, selectionArgs);
|
||||||
|
Log.i(TAG, "Deleted " + deletedRowsFromTable + " rows of table " + table);
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
getContext().getContentResolver().notifyChange(url, null, false);
|
getContext().getContentResolver().notifyChange(url, null, false);
|
||||||
|
|
||||||
if (shouldVacuum) {
|
int totalChanges = getTotalChanges() - totalChangesBefore;
|
||||||
// If a potentially large amount of data was deleted, reclaim its space.
|
Log.i(TAG, "Deleted " + totalChanges + " total rows from database");
|
||||||
Log.i(TAG, "Vacuuming the database.");
|
|
||||||
|
PreferencesUtils.addTotalRowsDeleted(totalChanges);
|
||||||
|
int totalRowsDeleted = PreferencesUtils.getTotalRowsDeleted();
|
||||||
|
if (totalRowsDeleted > TOTAL_DELETED_ROWS_VACUUM_THRESHOLD) {
|
||||||
|
Log.i(TAG, "TotalRowsDeleted " + totalRowsDeleted + ", starting to vacuum the database.");
|
||||||
db.execSQL("VACUUM");
|
db.execSQL("VACUUM");
|
||||||
|
PreferencesUtils.resetTotalRowsDeleted();
|
||||||
}
|
}
|
||||||
return count;
|
|
||||||
|
return deletedRowsFromTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getTotalChanges() {
|
||||||
|
int totalCount;
|
||||||
|
try (Cursor cursor = db.rawQuery("SELECT total_changes()", null)) {
|
||||||
|
cursor.moveToNext();
|
||||||
|
totalCount = cursor.getInt(0);
|
||||||
|
}
|
||||||
|
return totalCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import java.util.Collections;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||||
@@ -72,9 +73,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
|||||||
* @return An URI for one file containing this tracks.
|
* @return An URI for one file containing this tracks.
|
||||||
*/
|
*/
|
||||||
public static Pair<Uri, String> createURI(Track.Id trackId, String trackName, @NonNull TrackFileFormat trackFileFormat) {
|
public static Pair<Uri, String> createURI(Track.Id trackId, String trackName, @NonNull TrackFileFormat trackFileFormat) {
|
||||||
Set<Track.Id> trackIds = new HashSet<>(1);
|
return createURI(Set.of(trackId), trackName, trackFileFormat);
|
||||||
trackIds.add(trackId);
|
|
||||||
return createURI(trackIds, trackName, trackFileFormat);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,11 +107,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
|||||||
|
|
||||||
String[] uriTrackIds = uriPaths.get(2).split(TRACKID_DELIMITER);
|
String[] uriTrackIds = uriPaths.get(2).split(TRACKID_DELIMITER);
|
||||||
|
|
||||||
Set<Track.Id> trackIds = new HashSet<>();
|
return Arrays.stream(uriTrackIds).map(uriTrackId -> new Track.Id(Long.parseLong(uriTrackId))).collect(Collectors.toSet());
|
||||||
for (String uriTrackId : uriTrackIds) {
|
|
||||||
trackIds.add(new Track.Id(Long.parseLong(uriTrackId)));
|
|
||||||
}
|
|
||||||
return trackIds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,16 +25,13 @@ import android.os.HandlerThread;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Altitude;
|
|
||||||
import de.dennisguse.opentracks.data.models.Marker;
|
import de.dennisguse.opentracks.data.models.Marker;
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
||||||
@@ -367,7 +364,7 @@ public class TrackDataHub {
|
|||||||
// Also include the last point if the selected track is not recording.
|
// Also include the last point if the selected track is not recording.
|
||||||
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
||||||
for (Listener trackDataListener : listeners) {
|
for (Listener trackDataListener : listeners) {
|
||||||
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
|
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (Listener trackDataListener : listeners) {
|
for (Listener trackDataListener : listeners) {
|
||||||
@@ -432,7 +429,7 @@ public class TrackDataHub {
|
|||||||
*
|
*
|
||||||
* @param trackPoint the trackPoint
|
* @param trackPoint the trackPoint
|
||||||
*/
|
*/
|
||||||
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, @Nullable Altitude smoothedAltitude_m) {
|
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ public class Distance {
|
|||||||
return new Distance(distance_m);
|
return new Distance(distance_m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Distance of(Double distance_m) {
|
||||||
|
if (distance_m == null) {
|
||||||
|
return Distance.of(Double.NaN);
|
||||||
|
} else {
|
||||||
|
return Distance.of((double) distance_m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Distance of(String distance_m) {
|
public static Distance of(String distance_m) {
|
||||||
return of(Float.parseFloat(distance_m));
|
return of(Float.parseFloat(distance_m));
|
||||||
}
|
}
|
||||||
@@ -29,6 +37,10 @@ public class Distance {
|
|||||||
return of(distance_mile * UnitConversions.MI_TO_M);
|
return of(distance_mile * UnitConversions.MI_TO_M);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Distance ofNauticalMile(double distance_mile) {
|
||||||
|
return of(distance_mile * UnitConversions.NAUTICAL_MILE_TO_M);
|
||||||
|
}
|
||||||
|
|
||||||
public static Distance ofKilometer(double distance_km) {
|
public static Distance ofKilometer(double distance_km) {
|
||||||
return of(distance_km * UnitConversions.KM_TO_M);
|
return of(distance_km * UnitConversions.KM_TO_M);
|
||||||
}
|
}
|
||||||
@@ -51,6 +63,8 @@ public class Distance {
|
|||||||
return Distance.ofKilometer(1);
|
return Distance.ofKilometer(1);
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
return Distance.ofMile(1);
|
return Distance.ofMile(1);
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
return Distance.ofNauticalMile(1);
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
@@ -114,12 +128,18 @@ public class Distance {
|
|||||||
return toKM() * UnitConversions.KM_TO_MI;
|
return toKM() * UnitConversions.KM_TO_MI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double toNauticalMiles() {
|
||||||
|
return toKM() * UnitConversions.KM_TO_NAUTICAL_MILE;
|
||||||
|
}
|
||||||
|
|
||||||
public double toKM_Miles(UnitSystem unitSystem) {
|
public double toKM_Miles(UnitSystem unitSystem) {
|
||||||
switch (unitSystem) {
|
switch (unitSystem) {
|
||||||
case METRIC:
|
case METRIC:
|
||||||
return toKM();
|
return toKM();
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
return toMI();
|
return toMI();
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
return toNauticalMiles();
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
@@ -129,6 +149,7 @@ public class Distance {
|
|||||||
switch (unitSystem) {
|
switch (unitSystem) {
|
||||||
case METRIC:
|
case METRIC:
|
||||||
return toM();
|
return toM();
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
return toFT();
|
return toFT();
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -42,11 +42,13 @@ public class DistanceFormatter {
|
|||||||
*/
|
*/
|
||||||
public Pair<String, String> getDistanceParts(Distance distance) {
|
public Pair<String, String> getDistanceParts(Distance distance) {
|
||||||
if (distance.isInvalid()) {
|
if (distance.isInvalid()) {
|
||||||
|
String valueUnknown = resources.getString(R.string.value_unknown);
|
||||||
switch (unitSystem) {
|
switch (unitSystem) {
|
||||||
case METRIC:
|
case METRIC:
|
||||||
return new Pair<>(null, resources.getString(R.string.unit_meter));
|
return new Pair<>(valueUnknown, resources.getString(R.string.unit_meter));
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
return new Pair<>(null, resources.getString(R.string.unit_feet));
|
case NAUTICAL_IMPERIAL:
|
||||||
|
return new Pair<>(valueUnknown, resources.getString(R.string.unit_feet));
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
@@ -65,6 +67,12 @@ public class DistanceFormatter {
|
|||||||
} else {
|
} else {
|
||||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||||
}
|
}
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
if (distance.greaterThan(Distance.ofNauticalMile(0.5))) {
|
||||||
|
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
|
||||||
|
} else {
|
||||||
|
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ public class Speed {
|
|||||||
return toKMH() * UnitConversions.KM_TO_MI;
|
return toKMH() * UnitConversions.KM_TO_MI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double toKnots() {
|
||||||
|
return toKMH() * UnitConversions.KM_TO_NAUTICAL_MILE;
|
||||||
|
}
|
||||||
|
|
||||||
public Duration toPace(UnitSystem unitSystem) {
|
public Duration toPace(UnitSystem unitSystem) {
|
||||||
if (isZero()) {
|
if (isZero()) {
|
||||||
return Duration.ofSeconds(0);
|
return Duration.ofSeconds(0);
|
||||||
@@ -106,6 +110,9 @@ public class Speed {
|
|||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
distance *= UnitConversions.M_TO_MI;
|
distance *= UnitConversions.M_TO_MI;
|
||||||
break;
|
break;
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
distance *= UnitConversions.M_TO_NAUTICAL_MILE;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
@@ -119,6 +126,8 @@ public class Speed {
|
|||||||
return toKMH();
|
return toKMH();
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
return toMPH();
|
return toMPH();
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
return toKnots();
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ public class SpeedFormatter {
|
|||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
|
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
|
||||||
break;
|
break;
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
unitId = reportSpeedOrPace ? R.string.unit_knots : R.string.unit_minute_per_nautical_mile;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class TrackPoint {
|
|||||||
private Double longitude;
|
private Double longitude;
|
||||||
private Distance horizontalAccuracy;
|
private Distance horizontalAccuracy;
|
||||||
private Distance verticalAccuracy;
|
private Distance verticalAccuracy;
|
||||||
private Altitude altitude;
|
private Altitude altitude; //TODO use Altitude.WGS84
|
||||||
private Speed speed;
|
private Speed speed;
|
||||||
private Float bearing;
|
private Float bearing;
|
||||||
private Distance sensorDistance;
|
private Distance sensorDistance;
|
||||||
@@ -56,7 +56,7 @@ public class TrackPoint {
|
|||||||
|
|
||||||
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
|
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
|
||||||
TRACKPOINT(0), //Just GPS data and may contain BLE sensor data
|
TRACKPOINT(0), //Just GPS data and may contain BLE sensor data
|
||||||
SENSORPOINT(2), //Just BLE sensor data; required to have speed and sensorDistance
|
SENSORPOINT(2), //Just BLE sensor data; may have speed and sensorDistance
|
||||||
|
|
||||||
SEGMENT_END_MANUAL(1); //End of a segment
|
SEGMENT_END_MANUAL(1); //End of a segment
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class UnitConversions {
|
|||||||
|
|
||||||
// Time //TODO Use Duration
|
// Time //TODO Use Duration
|
||||||
// multiplication factor to convert seconds to milliseconds
|
// multiplication factor to convert seconds to milliseconds
|
||||||
|
@Deprecated
|
||||||
public static final long S_TO_MS = 1000;
|
public static final long S_TO_MS = 1000;
|
||||||
|
|
||||||
// multiplication factor to convert milliseconds to seconds
|
// multiplication factor to convert milliseconds to seconds
|
||||||
@@ -53,6 +54,9 @@ public class UnitConversions {
|
|||||||
public static final double M_TO_MI = M_TO_KM * KM_TO_MI;
|
public static final double M_TO_MI = M_TO_KM * KM_TO_MI;
|
||||||
// multiplication factor to convert meters to feet
|
// multiplication factor to convert meters to feet
|
||||||
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
|
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
|
||||||
|
public static final double NAUTICAL_MILE_TO_M = 1852.0;
|
||||||
|
public static final double M_TO_NAUTICAL_MILE = 1 / NAUTICAL_MILE_TO_M;
|
||||||
|
public static final double KM_TO_NAUTICAL_MILE = 1000 * M_TO_NAUTICAL_MILE;
|
||||||
// multiplication factor to convert meters per second to kilometers per hour
|
// multiplication factor to convert meters per second to kilometers per hour
|
||||||
public static final double MPS_TO_KMH = M_TO_KM / (S_TO_MIN * MIN_TO_HR);
|
public static final double MPS_TO_KMH = M_TO_KM / (S_TO_MIN * MIN_TO_HR);
|
||||||
// multiplication factor to convert miles to km
|
// multiplication factor to convert miles to km
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import de.dennisguse.opentracks.data.tables.TracksColumns;
|
|||||||
import de.dennisguse.opentracks.databinding.ExportActivityBinding;
|
import de.dennisguse.opentracks.databinding.ExportActivityBinding;
|
||||||
import de.dennisguse.opentracks.io.file.ErrorListDialog;
|
import de.dennisguse.opentracks.io.file.ErrorListDialog;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.ExportUtils;
|
import de.dennisguse.opentracks.util.ExportUtils;
|
||||||
import de.dennisguse.opentracks.util.FileUtils;
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
|
|
||||||
@@ -234,7 +235,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
|||||||
* @param conflictResolution conflict resolution to be applied if needed.
|
* @param conflictResolution conflict resolution to be applied if needed.
|
||||||
*/
|
*/
|
||||||
private void export(Track track, int conflictResolution) {
|
private void export(Track track, int conflictResolution) {
|
||||||
boolean fileExists = ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles);
|
boolean fileExists = exportFileExists(track);
|
||||||
|
|
||||||
if (fileExists && conflictResolution == CONFLICT_NONE) {
|
if (fileExists && conflictResolution == CONFLICT_NONE) {
|
||||||
conflict(track);
|
conflict(track);
|
||||||
@@ -250,6 +251,11 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
|||||||
export(track, autoConflict);
|
export(track, autoConflict);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean exportFileExists(Track track) {
|
||||||
|
String filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
|
||||||
|
return directoryFiles.stream().anyMatch(filename::equals);
|
||||||
|
}
|
||||||
|
|
||||||
private void setConflictVisibility(int visibility) {
|
private void setConflictVisibility(int visibility) {
|
||||||
viewBinding.exportProgressAlertIcon.setVisibility(visibility);
|
viewBinding.exportProgressAlertIcon.setVisibility(visibility);
|
||||||
viewBinding.exportProgressAlertMsg.setVisibility(visibility);
|
viewBinding.exportProgressAlertMsg.setVisibility(visibility);
|
||||||
@@ -315,7 +321,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
|||||||
public void onExportSuccess(Track.Id trackId) {
|
public void onExportSuccess(Track.Id trackId) {
|
||||||
Track track = contentProviderUtils.getTrack(trackId);
|
Track track = contentProviderUtils.getTrack(trackId);
|
||||||
|
|
||||||
if (ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles)) {
|
if (exportFileExists(track)) {
|
||||||
trackExportOverwrittenCount++;
|
trackExportOverwrittenCount++;
|
||||||
} else {
|
} else {
|
||||||
trackExportSuccessCount++;
|
trackExportSuccessCount++;
|
||||||
|
|||||||
@@ -146,13 +146,10 @@ public class GPXTrackExporter implements TrackExporter {
|
|||||||
|
|
||||||
switch (trackPoint.getType()) {
|
switch (trackPoint.getType()) {
|
||||||
case SEGMENT_START_MANUAL:
|
case SEGMENT_START_MANUAL:
|
||||||
if (wroteSegment) writeCloseSegment();
|
|
||||||
writeOpenSegment();
|
|
||||||
wroteSegment = true;
|
|
||||||
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
|
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
|
||||||
break;
|
break;
|
||||||
case SEGMENT_END_MANUAL:
|
case SEGMENT_END_MANUAL:
|
||||||
writeCloseSegment();
|
if (wroteSegment) writeCloseSegment();
|
||||||
wroteSegment = false;
|
wroteSegment = false;
|
||||||
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_END_MANUAL.name() + " is not supported.");
|
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_END_MANUAL.name() + " is not supported.");
|
||||||
break;
|
break;
|
||||||
|
|||||||
+1
-1
@@ -17,8 +17,8 @@ import androidx.documentfile.provider.DocumentFile;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.io.file.exporter.ExportActivity;
|
import de.dennisguse.opentracks.io.file.exporter.ExportActivity;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
|
|
||||||
public abstract class DirectoryChooserActivity extends AppCompatActivity {
|
public abstract class DirectoryChooserActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.data.models.Marker;
|
|||||||
import de.dennisguse.opentracks.data.models.Speed;
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||||
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
||||||
@@ -290,4 +291,5 @@ public class TrackImporter {
|
|||||||
public void cleanImport() {
|
public void cleanImport() {
|
||||||
contentProviderUtils.deleteTracks(context, trackIds);
|
contentProviderUtils.deleteTracks(context, trackIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
|
|||||||
if (PreferencesUtils.isPublicAPIenabled()) {
|
if (PreferencesUtils.isPublicAPIenabled()) {
|
||||||
Log.i(TAG, "Received and trying to execute requested action.");
|
Log.i(TAG, "Received and trying to execute requested action.");
|
||||||
new TrackRecordingServiceConnection(serviceConnectedCallback)
|
new TrackRecordingServiceConnection(serviceConnectedCallback)
|
||||||
.startAndBind(this);
|
.startAndBind(this, isStartServiceForeground());
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, getString(R.string.settings_public_api_disabled_toast), Toast.LENGTH_LONG).show();
|
Toast.makeText(this, getString(R.string.settings_public_api_disabled_toast), Toast.LENGTH_LONG).show();
|
||||||
Log.w(TAG, "Public API is disabled; ignoring request.");
|
Log.w(TAG, "Public API is disabled; ignoring request.");
|
||||||
@@ -42,6 +42,10 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean isStartServiceForeground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract void execute(TrackRecordingService service);
|
protected abstract void execute(TrackRecordingService service);
|
||||||
|
|
||||||
protected abstract boolean isPostExecuteStopService();
|
protected abstract boolean isPostExecuteStopService();
|
||||||
|
|||||||
@@ -2,28 +2,64 @@ package de.dennisguse.opentracks.publicapi;
|
|||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||||
|
import de.dennisguse.opentracks.util.TrackUtils;
|
||||||
|
|
||||||
public class StartRecording extends AbstractAPIActivity {
|
public class StartRecording extends AbstractAPIActivity {
|
||||||
|
|
||||||
|
public static final String EXTRA_TRACK_NAME = "TRACK_NAME";
|
||||||
|
public static final String EXTRA_TRACK_CATEGORY = "TRACK_CATEGORY";
|
||||||
|
public static final String EXTRA_TRACK_DESCRIPTION = "TRACK_DESCRIPTION";
|
||||||
|
|
||||||
|
public static final String EXTRA_STATS_TARGET_PACKAGE = "STATS_TARGET_PACKAGE";
|
||||||
|
public static final String EXTRA_STATS_TARGET_CLASS = "STATS_TARGET_CLASS";
|
||||||
|
|
||||||
private static final String TAG = StartRecording.class.getSimpleName();
|
private static final String TAG = StartRecording.class.getSimpleName();
|
||||||
|
|
||||||
protected void execute(TrackRecordingService service) {
|
protected void execute(TrackRecordingService service) {
|
||||||
Track.Id trackId = service.startNewTrack();
|
Track.Id trackId = service.startNewTrack();
|
||||||
if ((trackId != null) && (PreferencesUtils.isPublicAPIDashboardEnabled())) {
|
if (trackId != null) {
|
||||||
Bundle bundle = getIntent().getExtras();
|
Bundle bundle = getIntent().getExtras();
|
||||||
if (bundle != null) {
|
if (bundle != null) {
|
||||||
String targetPackage = bundle.getString("STATS_TARGET_PACKAGE", null);
|
updateTrackMetadata(trackId, bundle);
|
||||||
String targetClass = bundle.getString("STATS_TARGET_CLASS", null);
|
|
||||||
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
|
if (PreferencesUtils.isPublicAPIDashboardEnabled()) {
|
||||||
|
startDashboardAPI(trackId, bundle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateTrackMetadata(@NonNull Track.Id trackId, @NonNull Bundle bundle) {
|
||||||
|
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||||
|
Track track = contentProviderUtils.getTrack(trackId);
|
||||||
|
|
||||||
|
TrackUtils.updateTrack(this, track, bundle.getString(EXTRA_TRACK_NAME, null),
|
||||||
|
bundle.getString(EXTRA_TRACK_CATEGORY, null), bundle.getString(EXTRA_TRACK_DESCRIPTION, null),
|
||||||
|
contentProviderUtils);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDashboardAPI(@NonNull Track.Id trackId, @NonNull Bundle bundle) {
|
||||||
|
String targetPackage = bundle.getString(EXTRA_STATS_TARGET_PACKAGE, null);
|
||||||
|
String targetClass = bundle.getString(EXTRA_STATS_TARGET_CLASS, null);
|
||||||
|
if (targetClass != null && targetPackage != null) {
|
||||||
|
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isPostExecuteStopService() {
|
protected boolean isPostExecuteStopService() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isStartServiceForeground() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import android.hardware.Sensor;
|
|||||||
import android.hardware.SensorEvent;
|
import android.hardware.SensorEvent;
|
||||||
import android.hardware.SensorEventListener;
|
import android.hardware.SensorEventListener;
|
||||||
import android.hardware.SensorManager;
|
import android.hardware.SensorManager;
|
||||||
|
import android.os.Handler;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,7 +32,7 @@ public class AltitudeSumManager implements SensorEventListener {
|
|||||||
private Float altitudeGain_m;
|
private Float altitudeGain_m;
|
||||||
private Float altitudeLoss_m;
|
private Float altitudeLoss_m;
|
||||||
|
|
||||||
public void start(Context context) {
|
public void start(Context context, Handler handler) {
|
||||||
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||||
|
|
||||||
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
|
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
|
||||||
@@ -37,7 +40,7 @@ public class AltitudeSumManager implements SensorEventListener {
|
|||||||
Log.w(TAG, "No pressure sensor available.");
|
Log.w(TAG, "No pressure sensor available.");
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
} else {
|
} else {
|
||||||
isConnected = sensorManager.registerListener(this, pressureSensor, SensorManager.SENSOR_DELAY_FASTEST);
|
isConnected = sensorManager.registerListener(this, pressureSensor, (int) TimeUnit.SECONDS.toMicros(5), handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAcceptedPressureValue_hPa = Float.NaN;
|
lastAcceptedPressureValue_hPa = Float.NaN;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
|
|||||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
|
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
|
||||||
@@ -162,7 +162,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
|||||||
} else {
|
} else {
|
||||||
connectionManager.disconnect();
|
connectionManager.disconnect();
|
||||||
}
|
}
|
||||||
if (!PermissionUtils.hasBluetoothPermissions(context)) {
|
if (!PermissionRequester.BLUETOOTH.hasPermission(context)) {
|
||||||
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
|
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ import androidx.lifecycle.LiveData;
|
|||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
|
|
||||||
public class TrackDeleteService extends Service {
|
public class TrackDeleteService extends Service {
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ public class TrackDeleteService extends Service {
|
|||||||
|
|
||||||
private final Binder binder = new Binder();
|
private final Binder binder = new Binder();
|
||||||
private ExecutorService serviceExecutor;
|
private ExecutorService serviceExecutor;
|
||||||
private MutableLiveData<DeleteStatus> deleteResultObservable;
|
private MutableLiveData<DeletionFinishedStatus> deleteResultObservable;
|
||||||
private NotificationManager notificationManager;
|
private NotificationManager notificationManager;
|
||||||
private NotificationCompat.Builder notificationBuilder;
|
private NotificationCompat.Builder notificationBuilder;
|
||||||
|
|
||||||
@@ -59,22 +61,18 @@ public class TrackDeleteService extends Service {
|
|||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteTracks(@NonNull ArrayList<Track.Id> trackIds) {
|
private void deleteTracks(@NonNull List<Track.Id> trackIds) {
|
||||||
sendResult(null,0, trackIds.size());
|
|
||||||
serviceExecutor.execute(() -> {
|
serviceExecutor.execute(() -> {
|
||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||||
for (int i = 0; i < trackIds.size(); i++) {
|
contentProviderUtils.deleteTracks(this, trackIds);
|
||||||
updateNotification(i + 1, trackIds.size());
|
sendResult(trackIds);
|
||||||
contentProviderUtils.deleteTrack(this, trackIds.get(i));
|
|
||||||
sendResult(trackIds.get(i), i + 1, trackIds.size());
|
|
||||||
}
|
|
||||||
stopSelf();
|
stopSelf();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendResult(Track.Id trackId, int deletes, int total) {
|
private void sendResult(List<Track.Id> trackIds) {
|
||||||
if (deleteResultObservable != null) {
|
if (deleteResultObservable != null) {
|
||||||
deleteResultObservable.postValue(new DeleteStatus(trackId, deletes, total));
|
deleteResultObservable.postValue(new DeletionFinishedStatus(trackIds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +81,7 @@ public class TrackDeleteService extends Service {
|
|||||||
return binder;
|
return binder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<DeleteStatus> getDeletingStatusObservable() {
|
public LiveData<DeletionFinishedStatus> getDeletingStatusObservable() {
|
||||||
return deleteResultObservable;
|
return deleteResultObservable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,25 +104,13 @@ public class TrackDeleteService extends Service {
|
|||||||
notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
|
notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
|
||||||
notificationBuilder
|
notificationBuilder
|
||||||
.setContentTitle(this.getString(R.string.track_delete_progress_message))
|
.setContentTitle(this.getString(R.string.track_delete_progress_message))
|
||||||
.setContentText(this.getString(R.string.track_delete_progress, 0, tracksToDelete))
|
.setContentText(this.getString(R.string.track_delete_number_of_tracks, tracksToDelete))
|
||||||
.setSmallIcon(R.drawable.ic_logo_color_24dp)
|
.setSmallIcon(R.drawable.ic_logo_color_24dp)
|
||||||
.setProgress(tracksToDelete, 0, false);
|
.setProgress(0, 0, true);
|
||||||
|
|
||||||
startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates notification progress.
|
|
||||||
*
|
|
||||||
* @param progress number of tracks already deleted.
|
|
||||||
* @param total total of tracks to be deleted.
|
|
||||||
*/
|
|
||||||
private void updateNotification(int progress, int total) {
|
|
||||||
notificationBuilder.setProgress(total, progress, false);
|
|
||||||
notificationBuilder.setContentText(this.getString(R.string.track_delete_progress, progress, total));
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Binder extends android.os.Binder {
|
public class Binder extends android.os.Binder {
|
||||||
|
|
||||||
private Binder() {
|
private Binder() {
|
||||||
@@ -136,36 +122,25 @@ public class TrackDeleteService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DeleteStatus {
|
public static class DeletionFinishedStatus {
|
||||||
private final int progress;
|
private final List<Track.Id> trackIds;
|
||||||
private final int max;
|
|
||||||
private final Track.Id trackId;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param trackId Track.Id just deleted if any.
|
* @param trackIds List of deleted Track.Ids.
|
||||||
* @param progress number of deletes.
|
|
||||||
* @param max total of deletes to be done.
|
|
||||||
*/
|
*/
|
||||||
private DeleteStatus(@Nullable Track.Id trackId, int progress, int max) {
|
private DeletionFinishedStatus(@Nullable List<Track.Id> trackIds) {
|
||||||
this.trackId = trackId;
|
this.trackIds = trackIds;
|
||||||
this.progress = progress;
|
|
||||||
this.max = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFinished() {
|
|
||||||
return progress == max;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDeleted(Track.Id trackId) {
|
public boolean isDeleted(Track.Id trackId) {
|
||||||
return this.trackId != null && this.trackId.equals(trackId);
|
return this.trackIds != null && this.trackIds.contains(trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "DeleteStatus{" +
|
return "DeleteStatus{" +
|
||||||
"number of deletes=" + progress +
|
"trackIds=" + trackIds +
|
||||||
", total=" + max +
|
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.util.Pair;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
@@ -180,8 +181,16 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!trackPoint.hasLocation() && !trackPoint.hasSensorDistance()) {
|
if (!trackPoint.hasLocation() && !trackPoint.hasSensorDistance()) {
|
||||||
Log.d(TAG, "Ignoring TrackPoint as it has no distance.");
|
Duration minStorageInterval = Duration.ofSeconds(10); // TODO Should be configurable.
|
||||||
return false;
|
boolean shouldStore = lastStoredTrackPoint.getTime().plus(minStorageInterval)
|
||||||
|
.isBefore(trackPoint.getTime());
|
||||||
|
if (!shouldStore) {
|
||||||
|
Log.d(TAG, "Ignoring TrackPoint as it has no distance (and sensor data is not new enough).");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
insertTrackPoint(trackPoint, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Distance distanceToLastStoredTrackPoint;
|
Distance distanceToLastStoredTrackPoint;
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
trackRecordingManager = null;
|
trackRecordingManager = null;
|
||||||
|
|
||||||
// Reverse order from onCreate
|
// Reverse order from onCreate
|
||||||
showNotification(false); //TODO Why?
|
stopForeground(true);
|
||||||
|
|
||||||
notificationManager.stop();
|
notificationManager.stop();
|
||||||
notificationManager = null;
|
notificationManager = null;
|
||||||
|
|
||||||
@@ -205,7 +206,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
private void startSensors() {
|
private void startSensors() {
|
||||||
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
|
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
|
||||||
trackPointCreator.start(this, handler);
|
trackPointCreator.start(this, handler);
|
||||||
showNotification(true);
|
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void endCurrentTrack() {
|
public void endCurrentTrack() {
|
||||||
@@ -247,7 +248,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
if (!isRecording()) return;
|
if (!isRecording()) return;
|
||||||
|
|
||||||
trackPointCreator.stop();
|
trackPointCreator.stop();
|
||||||
showNotification(false);
|
stopForeground(true);
|
||||||
|
notificationManager.cancelNotification();
|
||||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,19 +275,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showNotification(boolean isGpsStarted) {
|
|
||||||
if (isRecording()) {
|
|
||||||
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this, recordingStatus.getTrackId()));
|
|
||||||
}
|
|
||||||
if (!isRecording() && isGpsStarted) {
|
|
||||||
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
|
|
||||||
}
|
|
||||||
if (!isRecording() && !isGpsStarted) {
|
|
||||||
stopForeground(true);
|
|
||||||
notificationManager.cancelNotification();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
|
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
|
||||||
if (!isRecording()) {
|
if (!isRecording()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+10
-3
@@ -28,6 +28,7 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.BuildConfig;
|
import de.dennisguse.opentracks.BuildConfig;
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
@@ -66,15 +67,21 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts and binds the service.
|
* Starts and binds the service.
|
||||||
|
*
|
||||||
|
* @param foreground is the service expected to call `startForeground()`?
|
||||||
*/
|
*/
|
||||||
public void startAndBind(Context context) {
|
public void startAndBind(Context context, boolean foreground) {
|
||||||
if (trackRecordingService != null) {
|
if (trackRecordingService != null) {
|
||||||
// Service is already started and bound.
|
// Service is already started and bound.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Starting the service.");
|
Log.i(TAG, "Starting the service.");
|
||||||
context.startService(new Intent(context, TrackRecordingService.class));
|
if (foreground) {
|
||||||
|
ContextCompat.startForegroundService(context, new Intent(context, TrackRecordingService.class));
|
||||||
|
} else {
|
||||||
|
context.startService(new Intent(context, TrackRecordingService.class));
|
||||||
|
}
|
||||||
|
|
||||||
startConnection(context);
|
startConnection(context);
|
||||||
}
|
}
|
||||||
@@ -88,7 +95,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
|||||||
@Deprecated
|
@Deprecated
|
||||||
public void startAndBindWithCallback(Context context) {
|
public void startAndBindWithCallback(Context context) {
|
||||||
if (trackRecordingService == null) {
|
if (trackRecordingService == null) {
|
||||||
startAndBind(context);
|
startAndBind(context, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
|
|||||||
+1
@@ -68,6 +68,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
|||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
|
cancelNotification();
|
||||||
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(this);
|
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-10
@@ -17,6 +17,7 @@
|
|||||||
package de.dennisguse.opentracks.services.announcement;
|
package de.dennisguse.opentracks.services.announcement;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.media.AudioAttributes;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.MediaPlayer;
|
import android.media.MediaPlayer;
|
||||||
import android.speech.tts.TextToSpeech;
|
import android.speech.tts.TextToSpeech;
|
||||||
@@ -34,8 +35,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
|||||||
import de.dennisguse.opentracks.data.TrackPointIterator;
|
import de.dennisguse.opentracks.data.TrackPointIterator;
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
import de.dennisguse.opentracks.data.models.Distance;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
|
||||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
|
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
|
||||||
@@ -56,6 +57,10 @@ public class VoiceAnnouncement {
|
|||||||
private final AudioManager audioManager;
|
private final AudioManager audioManager;
|
||||||
|
|
||||||
private final ContentProviderUtils contentProviderUtils;
|
private final ContentProviderUtils contentProviderUtils;
|
||||||
|
private TrackPoint.Id startTrackPointId = null;
|
||||||
|
|
||||||
|
private IntervalStatistics intervalStatistics;
|
||||||
|
private Distance intervalDistance;
|
||||||
|
|
||||||
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
|
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -116,6 +121,8 @@ public class VoiceAnnouncement {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
contentProviderUtils = new ContentProviderUtils(context);
|
contentProviderUtils = new ContentProviderUtils(context);
|
||||||
|
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
|
||||||
|
intervalStatistics = new IntervalStatistics(intervalDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
@@ -132,7 +139,10 @@ public class VoiceAnnouncement {
|
|||||||
if (ttsFallback == null) {
|
if (ttsFallback == null) {
|
||||||
Log.w(TAG, "MediaPlayer for ttsFallback could not be created.");
|
Log.w(TAG, "MediaPlayer for ttsFallback could not be created.");
|
||||||
} else {
|
} else {
|
||||||
ttsFallback.setAudioStreamType(AUDIO_STREAM);
|
ttsFallback.setAudioAttributes(
|
||||||
|
new AudioAttributes.Builder()
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
|
.build());
|
||||||
ttsFallback.setLooping(false);
|
ttsFallback.setLooping(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,21 +175,22 @@ public class VoiceAnnouncement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
|
||||||
|
if (currentIntervalDistance != intervalDistance) {
|
||||||
|
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
|
||||||
|
intervalDistance = currentIntervalDistance;
|
||||||
|
startTrackPointId = null;
|
||||||
|
}
|
||||||
|
|
||||||
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
|
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
|
||||||
boolean isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
|
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
|
||||||
|
|
||||||
//TODO Do not load all trackpoints for every announcement
|
|
||||||
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
|
|
||||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(unitSystem));
|
|
||||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
|
||||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
|
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
|
||||||
SensorStatistics sensorStatistics = null;
|
SensorStatistics sensorStatistics = null;
|
||||||
if (track.getId() != null) {
|
if (track.getId() != null) {
|
||||||
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
|
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics);
|
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track.getCategory()), lastInterval, sensorStatistics);
|
||||||
|
|
||||||
if (announcement.length() > 0) {
|
if (announcement.length() > 0) {
|
||||||
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
|
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
|
||||||
|
|||||||
+25
-23
@@ -5,8 +5,8 @@ import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnno
|
|||||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace;
|
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace;
|
||||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate;
|
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate;
|
||||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace;
|
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace;
|
||||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
|
|
||||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceMovingTime;
|
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceMovingTime;
|
||||||
|
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
@@ -57,6 +57,13 @@ class VoiceAnnouncementUtils {
|
|||||||
unitDistanceTTS = "mile";
|
unitDistanceTTS = "mile";
|
||||||
unitSpeedTTS = "mile per hour";
|
unitSpeedTTS = "mile per hour";
|
||||||
break;
|
break;
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
perUnitStringId = R.string.voice_per_nautical_mile;
|
||||||
|
distanceId = R.plurals.voiceDistanceNauticalMiles;
|
||||||
|
speedId = R.plurals.voiceSpeedMKnots;
|
||||||
|
unitDistanceTTS = "nautical mile";
|
||||||
|
unitSpeedTTS = "knots";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
@@ -65,12 +72,9 @@ class VoiceAnnouncementUtils {
|
|||||||
|
|
||||||
if (shouldVoiceAnnounceTotalDistance()) {
|
if (shouldVoiceAnnounceTotalDistance()) {
|
||||||
builder.append(context.getString(R.string.total_distance));
|
builder.append(context.getString(R.string.total_distance));
|
||||||
long distanceIntegerPart = (long) distanceInUnit;
|
|
||||||
// Extract the decimal part
|
|
||||||
String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2);
|
|
||||||
// Units should always be english singular for TTS.
|
// Units should always be english singular for TTS.
|
||||||
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
|
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
|
||||||
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, unitDistanceTTS);
|
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 2, unitDistanceTTS);
|
||||||
// Punctuation helps introduce natural pauses in TTS
|
// Punctuation helps introduce natural pauses in TTS
|
||||||
builder.append(".");
|
builder.append(".");
|
||||||
}
|
}
|
||||||
@@ -88,27 +92,17 @@ class VoiceAnnouncementUtils {
|
|||||||
if (isReportSpeed) {
|
if (isReportSpeed) {
|
||||||
if (shouldVoiceAnnounceAverageSpeedPace()) {
|
if (shouldVoiceAnnounceAverageSpeedPace()) {
|
||||||
double speedInUnit = distancePerTime.to(unitSystem);
|
double speedInUnit = distancePerTime.to(unitSystem);
|
||||||
|
|
||||||
builder.append(" ")
|
builder.append(" ")
|
||||||
.append(context.getString(R.string.speed));
|
.append(context.getString(R.string.speed));
|
||||||
long speedIntegerPart = (long) speedInUnit;
|
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
|
||||||
// Extract the decimal part
|
|
||||||
String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2);
|
|
||||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, unitSpeedTTS);
|
|
||||||
builder.append(".");
|
builder.append(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
|
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
|
||||||
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
|
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
|
||||||
|
|
||||||
if (currentDistancePerTimeInUnit > 0) {
|
if (currentDistancePerTimeInUnit > 0) {
|
||||||
|
|
||||||
builder.append(" ")
|
builder.append(" ")
|
||||||
.append(context.getString(R.string.lap_speed));
|
.append(context.getString(R.string.lap_speed));
|
||||||
long currentDistanceIntegerPart = (long) currentDistancePerTimeInUnit;
|
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
|
||||||
// Extract the decimal part
|
|
||||||
String currentDistanceFractionalPart = String.format("%.1f", (currentDistancePerTimeInUnit - currentDistanceIntegerPart)).substring(2);
|
|
||||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, unitSpeedTTS);
|
|
||||||
builder.append(".");
|
builder.append(".");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,26 +158,34 @@ class VoiceAnnouncementUtils {
|
|||||||
int seconds = (int) (duration.getSeconds() % 60);
|
int seconds = (int) (duration.getSeconds() % 60);
|
||||||
|
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, null, "hour");
|
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, 0, "hour");
|
||||||
}
|
}
|
||||||
if (minutes > 0) {
|
if (minutes > 0) {
|
||||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, null, "minute");
|
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, 0, "minute");
|
||||||
}
|
}
|
||||||
if (seconds > 0 || duration.isZero()) {
|
if (seconds > 0 || duration.isZero()) {
|
||||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, null, "second");
|
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, 0, "second");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Speaks as: 98.14 [UNIT] - ninety eight point one four [UNIT with correct plural form]
|
* Speaks as: 98.14 [UNIT] - ninety eight point one four [UNIT with correct plural form]
|
||||||
|
*
|
||||||
|
* @param number The number to speak
|
||||||
|
* @param precision The number of decimal places to announce
|
||||||
*/
|
*/
|
||||||
private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, long integerPart, @Nullable String fractionalPart, @NonNull String unit) {
|
private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, double number, int precision, @NonNull String unit) {
|
||||||
TtsSpan.MeasureBuilder measureBuilder = new TtsSpan.MeasureBuilder()
|
TtsSpan.MeasureBuilder measureBuilder = new TtsSpan.MeasureBuilder()
|
||||||
.setUnit(unit);
|
.setUnit(unit);
|
||||||
|
|
||||||
if (fractionalPart == null) {
|
if (precision == 0) {
|
||||||
measureBuilder.setNumber(integerPart);
|
measureBuilder.setNumber((long)number);
|
||||||
} else {
|
} else {
|
||||||
|
// Round before extracting integral and decimal parts
|
||||||
|
double roundedNumber = Math.round(Math.pow(10, precision) * number) / Math.pow(10.0, precision);
|
||||||
|
long integerPart = (long) roundedNumber;
|
||||||
|
// Extract the decimal part
|
||||||
|
String fractionalPart = String.format("%." + precision + "f", (roundedNumber - integerPart)).substring(2);
|
||||||
measureBuilder.setIntegerPart(integerPart)
|
measureBuilder.setIntegerPart(integerPart)
|
||||||
.setFractionalPart(fractionalPart);
|
.setFractionalPart(fractionalPart);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import de.dennisguse.opentracks.data.models.Distance;
|
|||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.LocationUtils;
|
import de.dennisguse.opentracks.util.LocationUtils;
|
||||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||||
public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
@@ -54,7 +54,7 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
|
|||||||
//TODO upgrade to AGP7.0.0/API31 started complaining about removeUpdates.
|
//TODO upgrade to AGP7.0.0/API31 started complaining about removeUpdates.
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
if (locationManager != null && context != null) {
|
if (locationManager != null && context != null) {
|
||||||
if (PermissionUtils.hasGPSPermission(context)) {
|
if (PermissionRequester.GPS.hasPermission(context)) {
|
||||||
locationManager.removeUpdates(this);
|
locationManager.removeUpdates(this);
|
||||||
}
|
}
|
||||||
locationManager = null;
|
locationManager = null;
|
||||||
@@ -152,7 +152,7 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
|
|||||||
Log.e(TAG, "Not started.");
|
Log.e(TAG, "Not started.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (PermissionUtils.hasGPSPermission(context)) {
|
if (PermissionRequester.GPS.hasPermission(context)) {
|
||||||
try {
|
try {
|
||||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, gpsInterval.toMillis(), 0, this);
|
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, gpsInterval.toMillis(), 0, this);
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class GpsStatus {
|
|||||||
private static final String TAG = GpsStatus.class.getSimpleName();
|
private static final String TAG = GpsStatus.class.getSimpleName();
|
||||||
|
|
||||||
// The duration that GpsStatus waits from minimal interval to consider GPS lost.
|
// The duration that GpsStatus waits from minimal interval to consider GPS lost.
|
||||||
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(10);
|
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(30);
|
||||||
|
|
||||||
private Distance thresholdHorizontalAccuracy;
|
private Distance thresholdHorizontalAccuracy;
|
||||||
// Threshold for time without points.
|
// Threshold for time without points.
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
|||||||
altitudeSumManager = new AltitudeSumManager();
|
altitudeSumManager = new AltitudeSumManager();
|
||||||
|
|
||||||
remoteSensorManager.start();
|
remoteSensorManager.start();
|
||||||
altitudeSumManager.start(context);
|
altitudeSumManager.start(context, handler);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
|
|||||||
entriesId = R.array.stats_rate_metric_options;
|
entriesId = R.array.stats_rate_metric_options;
|
||||||
break;
|
break;
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
entriesId = R.array.stats_rate_imperial_options;
|
entriesId = R.array.stats_rate_imperial_options;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import androidx.preference.Preference;
|
|||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.services.RecordingStatus;
|
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
|
||||||
|
|
||||||
public class MainSettingsFragment extends PreferenceFragmentCompat {
|
public class MainSettingsFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ public class PreferencesUtils {
|
|||||||
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||||
public static void applyDefaultUnit() {
|
public static void applyDefaultUnit() {
|
||||||
if (getString(R.string.stats_units_key, "").equals("")) {
|
if (getString(R.string.stats_units_key, "").equals("")) {
|
||||||
|
|
||||||
if (!Locale.US.equals(Locale.getDefault())) {
|
if (!Locale.US.equals(Locale.getDefault())) {
|
||||||
setUnit(UnitSystem.METRIC);
|
setUnit(UnitSystem.METRIC);
|
||||||
} else {
|
} else {
|
||||||
@@ -324,7 +323,7 @@ public class PreferencesUtils {
|
|||||||
UnitSystem unitSystem = getUnitSystem();
|
UnitSystem unitSystem = getUnitSystem();
|
||||||
|
|
||||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||||
.setDecimalCount(0)
|
.setDecimalCount(1)
|
||||||
.setUnit(unitSystem)
|
.setUnit(unitSystem)
|
||||||
.build(resources);
|
.build(resources);
|
||||||
for (int i = 0; i < values.length; i++) {
|
for (int i = 0; i < values.length; i++) {
|
||||||
@@ -418,7 +417,7 @@ public class PreferencesUtils {
|
|||||||
|
|
||||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||||
.setUnit(unitSystem)
|
.setUnit(unitSystem)
|
||||||
.setDecimalCount(0)
|
.setDecimalCount(1)
|
||||||
.build(resources);
|
.build(resources);
|
||||||
for (int i = 0; i < entryValues.length; i++) {
|
for (int i = 0; i < entryValues.length; i++) {
|
||||||
int value = Integer.parseInt(entryValues[i]);
|
int value = Integer.parseInt(entryValues[i]);
|
||||||
@@ -434,6 +433,7 @@ public class PreferencesUtils {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
if (value == recordingDistanceIntervalDefault) {
|
if (value == recordingDistanceIntervalDefault) {
|
||||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||||
} else {
|
} else {
|
||||||
@@ -461,7 +461,7 @@ public class PreferencesUtils {
|
|||||||
UnitSystem unitSystem = getUnitSystem();
|
UnitSystem unitSystem = getUnitSystem();
|
||||||
|
|
||||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||||
.setDecimalCount(0)
|
.setDecimalCount(1)
|
||||||
.setUnit(unitSystem)
|
.setUnit(unitSystem)
|
||||||
.build(resources);
|
.build(resources);
|
||||||
for (int i = 0; i < entryValues.length; i++) {
|
for (int i = 0; i < entryValues.length; i++) {
|
||||||
@@ -478,6 +478,7 @@ public class PreferencesUtils {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
if (value == maxRecordingDistanceDefault) {
|
if (value == maxRecordingDistanceDefault) {
|
||||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||||
} else {
|
} else {
|
||||||
@@ -533,7 +534,7 @@ public class PreferencesUtils {
|
|||||||
UnitSystem unitSystem = getUnitSystem();
|
UnitSystem unitSystem = getUnitSystem();
|
||||||
|
|
||||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||||
.setDecimalCount(0)
|
.setDecimalCount(1)
|
||||||
.setUnit(unitSystem)
|
.setUnit(unitSystem)
|
||||||
.build(resources);
|
.build(resources);
|
||||||
|
|
||||||
@@ -555,6 +556,7 @@ public class PreferencesUtils {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case IMPERIAL:
|
case IMPERIAL:
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
if (value == recordingGPSAccuracyDefault) {
|
if (value == recordingGPSAccuracyDefault) {
|
||||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||||
} else if (value == recordingGPSAccuracyExcellent) {
|
} else if (value == recordingGPSAccuracyExcellent) {
|
||||||
@@ -606,6 +608,14 @@ public class PreferencesUtils {
|
|||||||
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
|
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case NAUTICAL_IMPERIAL:
|
||||||
|
double valueKnots = Speed.ofKMH(value).toKnots();
|
||||||
|
if (value == idleSpeedDefault) {
|
||||||
|
entries[i] = resources.getString(R.string.value_float_knots_recommended, valueKnots);
|
||||||
|
} else {
|
||||||
|
entries[i] = resources.getString(R.string.value_float_knots, valueKnots);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented");
|
throw new RuntimeException("Not implemented");
|
||||||
}
|
}
|
||||||
@@ -816,4 +826,17 @@ public class PreferencesUtils {
|
|||||||
return getString(R.string.show_on_map_format_key, IntentDashboardUtils.PREFERENCE_ID_ASK);
|
return getString(R.string.show_on_map_format_key, IntentDashboardUtils.PREFERENCE_ID_ASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getTotalRowsDeleted() {
|
||||||
|
return getInt(R.string.total_rows_deleted_key, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addTotalRowsDeleted(final int totalRowsDeletedToAdd) {
|
||||||
|
int newTotalRowsDeleted = getTotalRowsDeleted() + totalRowsDeletedToAdd;
|
||||||
|
setInt(R.string.total_rows_deleted_key, newTotalRowsDeleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void resetTotalRowsDeleted() {
|
||||||
|
setInt(R.string.total_rows_deleted_key, 0);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Bundle;
|
|||||||
|
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.BuildConfig;
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
|
|
||||||
public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
|
public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
|
||||||
@@ -17,5 +18,7 @@ public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
|
|||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_api_title);
|
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_api_title);
|
||||||
|
findPreference(getString(R.string.publicapi_package_key))
|
||||||
|
.setSummary(BuildConfig.APPLICATION_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||||||
import de.dennisguse.opentracks.AbstractActivity;
|
import de.dennisguse.opentracks.AbstractActivity;
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.databinding.SettingsBinding;
|
import de.dennisguse.opentracks.databinding.SettingsBinding;
|
||||||
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
|
|
||||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||||
|
|
||||||
public class SettingsActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
|
public class SettingsActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import de.dennisguse.opentracks.R;
|
|||||||
|
|
||||||
public enum UnitSystem {
|
public enum UnitSystem {
|
||||||
METRIC(R.string.stats_units_metric),
|
METRIC(R.string.stats_units_metric),
|
||||||
IMPERIAL(R.string.stats_units_imperial);
|
IMPERIAL(R.string.stats_units_imperial),
|
||||||
|
NAUTICAL_IMPERIAL(R.string.stats_units_nautical); // Nautical miles with feet
|
||||||
|
|
||||||
private final int preferenceIdentifier;
|
private final int preferenceIdentifier;
|
||||||
|
|
||||||
|
|||||||
+13
-15
@@ -29,7 +29,7 @@ import java.util.stream.Collectors;
|
|||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference to select a discoverable Bluetooth LE device.
|
* Preference to select a discoverable Bluetooth LE device.
|
||||||
@@ -145,20 +145,18 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
|||||||
bluetoothIcon = AnimatedVectorDrawableCompat.create(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
|
bluetoothIcon = AnimatedVectorDrawableCompat.create(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
|
||||||
bluetoothIcon.start();
|
bluetoothIcon.start();
|
||||||
|
|
||||||
if (!PermissionUtils.hasBluetoothPermissions(getContext())) {
|
PermissionRequester.BLUETOOTH.requestPermissionsIfNeeded(getContext(), this,
|
||||||
PermissionUtils.requestBluetoothPermission(this,
|
this::startBluetoothScan,
|
||||||
this::startBluetoothScan,
|
(requester) -> {
|
||||||
() -> {
|
if (requester.shouldShowRequestPermissionRationale(this)) {
|
||||||
if (PermissionUtils.shouldShowRequestPermissionRationaleBluetooth(this)) {
|
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
|
||||||
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
|
} else {
|
||||||
} else {
|
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
|
||||||
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
|
}
|
||||||
}
|
dismiss();
|
||||||
dismiss();
|
});
|
||||||
});
|
|
||||||
} else {
|
startBluetoothScan();
|
||||||
startBluetoothScan();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startBluetoothScan() {
|
private void startBluetoothScan() {
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package de.dennisguse.opentracks.stats;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Altitude;
|
|
||||||
|
|
||||||
public class AltitudeRingBuffer extends RingBuffer<Altitude> {
|
|
||||||
|
|
||||||
private Altitude firstAltitude;
|
|
||||||
|
|
||||||
AltitudeRingBuffer(int size) {
|
|
||||||
super(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
AltitudeRingBuffer(RingBuffer<Altitude> toCopy) {
|
|
||||||
super(toCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
protected Number from(Altitude object) {
|
|
||||||
if (firstAltitude == null) {
|
|
||||||
firstAltitude = object;
|
|
||||||
}
|
|
||||||
return object.toM();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Altitude to(double object) {
|
|
||||||
return firstAltitude.replace(object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2009 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.dennisguse.opentracks.stats;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class maintains a ring buffer of doubles.
|
|
||||||
* This buffer is a convenient class for storing a series of doubles and calculating information about them.
|
|
||||||
* This is a FIFO buffer.
|
|
||||||
*
|
|
||||||
* @author Sandor Dornbush
|
|
||||||
*/
|
|
||||||
abstract class RingBuffer<T> {
|
|
||||||
|
|
||||||
private final ArrayList<T> buffer;
|
|
||||||
|
|
||||||
// The location that the next write will occur at.
|
|
||||||
private int index;
|
|
||||||
|
|
||||||
// True if the buffer is full
|
|
||||||
private boolean isFull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a buffer with a certain size.
|
|
||||||
*
|
|
||||||
* @param size the size
|
|
||||||
*/
|
|
||||||
RingBuffer(int size) {
|
|
||||||
if (size < 1) {
|
|
||||||
throw new IllegalArgumentException("The buffer size must be greater than 1.");
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer = new ArrayList<T>(size);
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
RingBuffer(RingBuffer<T> toCopy) {
|
|
||||||
this.buffer = new ArrayList<>(toCopy.buffer);
|
|
||||||
this.index = toCopy.index;
|
|
||||||
this.isFull = toCopy.isFull;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the buffer.
|
|
||||||
*/
|
|
||||||
public void reset() {
|
|
||||||
index = 0;
|
|
||||||
isFull = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the buffer is full.
|
|
||||||
*/
|
|
||||||
boolean isFull() {
|
|
||||||
return isFull;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the average of the buffer.
|
|
||||||
*/
|
|
||||||
public T getAverage() {
|
|
||||||
int numberOfEntries = isFull ? buffer.size() : index;
|
|
||||||
if (numberOfEntries == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Double sum = null;
|
|
||||||
int numberOfUsedEntries = 0;
|
|
||||||
for (int i = 0; i < numberOfEntries; i++) {
|
|
||||||
Number value = from(buffer.get(i));
|
|
||||||
if (value != null) {
|
|
||||||
if (sum == null) {
|
|
||||||
sum = 0.0;
|
|
||||||
}
|
|
||||||
sum += value.doubleValue();
|
|
||||||
numberOfUsedEntries++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sum == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return to(sum / numberOfUsedEntries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
protected abstract Number from(T object);
|
|
||||||
|
|
||||||
protected abstract T to(double object);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a double to the buffer.
|
|
||||||
* If the buffer is full the oldest element is overwritten.
|
|
||||||
*
|
|
||||||
* @param value the double to add
|
|
||||||
*/
|
|
||||||
public void setNext(T value) {
|
|
||||||
if (index == buffer.size()) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
buffer.add(index, value);
|
|
||||||
index++;
|
|
||||||
if (index == buffer.size()) {
|
|
||||||
isFull = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
StringBuilder builder = new StringBuilder("Full: ");
|
|
||||||
builder.append(isFull);
|
|
||||||
builder.append("\n");
|
|
||||||
for (int i = 0; i < buffer.size(); i++) {
|
|
||||||
builder.append((i == index) ? "<<" : "[");
|
|
||||||
builder.append(buffer.get(i));
|
|
||||||
builder.append((i == index) ? ">> " : "] ");
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package de.dennisguse.opentracks.stats;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
|
||||||
|
|
||||||
public class SpeedRingBuffer extends RingBuffer<Speed> {
|
|
||||||
|
|
||||||
SpeedRingBuffer(int size) {
|
|
||||||
super(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
SpeedRingBuffer(RingBuffer<Speed> toCopy) {
|
|
||||||
super(toCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
protected Number from(Speed object) {
|
|
||||||
return object.toMPS();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Speed to(double object) {
|
|
||||||
return Speed.of(object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,16 +19,15 @@ package de.dennisguse.opentracks.stats;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Altitude;
|
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
import de.dennisguse.opentracks.data.models.Distance;
|
||||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
|
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updater for {@link TrackStatistics}.
|
* Updater for {@link TrackStatistics}.
|
||||||
@@ -41,30 +40,15 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
|||||||
*/
|
*/
|
||||||
public class TrackStatisticsUpdater {
|
public class TrackStatisticsUpdater {
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of altitude readings to smooth to get a somewhat accurate signal.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
private static final int ALTITUDE_SMOOTHING_FACTOR = 25;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of speed reading to smooth to get a somewhat accurate signal.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
private static final int SPEED_SMOOTHING_FACTOR = 25;
|
|
||||||
|
|
||||||
private static final String TAG = TrackStatisticsUpdater.class.getSimpleName();
|
private static final String TAG = TrackStatisticsUpdater.class.getSimpleName();
|
||||||
/**
|
/**
|
||||||
* Ignore any acceleration faster than this.
|
* Ignore any acceleration faster than this.
|
||||||
* Will ignore any speeds that imply acceleration greater than 2g's
|
* Will ignore any speeds that imply acceleration greater than 2g's
|
||||||
* 2g = 19.6 m/s^2 = 0.0002 m/ms^2 = 0.02 m/(m*ms)
|
|
||||||
*/
|
*/
|
||||||
private static final double MAX_ACCELERATION = 0.02;
|
private static final double SPEED_MAX_ACCELERATION = 2 * 9.81;
|
||||||
|
|
||||||
private final TrackStatistics trackStatistics;
|
private final TrackStatistics trackStatistics;
|
||||||
|
|
||||||
private final AltitudeRingBuffer altitudeBuffer;
|
|
||||||
private final SpeedRingBuffer speedBuffer;
|
|
||||||
private float averageHeartRateBPM;
|
private float averageHeartRateBPM;
|
||||||
private Duration totalHeartRateDuration = Duration.ZERO;
|
private Duration totalHeartRateDuration = Duration.ZERO;
|
||||||
|
|
||||||
@@ -86,8 +70,6 @@ public class TrackStatisticsUpdater {
|
|||||||
this.trackStatistics = trackStatistics;
|
this.trackStatistics = trackStatistics;
|
||||||
this.currentSegment = new TrackStatistics();
|
this.currentSegment = new TrackStatistics();
|
||||||
|
|
||||||
altitudeBuffer = new AltitudeRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
|
|
||||||
speedBuffer = new SpeedRingBuffer(SPEED_SMOOTHING_FACTOR);
|
|
||||||
resetAverageHeartRate();
|
resetAverageHeartRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +77,6 @@ public class TrackStatisticsUpdater {
|
|||||||
this.currentSegment = new TrackStatistics(toCopy.currentSegment);
|
this.currentSegment = new TrackStatistics(toCopy.currentSegment);
|
||||||
this.trackStatistics = new TrackStatistics(toCopy.trackStatistics);
|
this.trackStatistics = new TrackStatistics(toCopy.trackStatistics);
|
||||||
|
|
||||||
this.altitudeBuffer = new AltitudeRingBuffer(toCopy.altitudeBuffer);
|
|
||||||
this.speedBuffer = new SpeedRingBuffer(toCopy.speedBuffer);
|
|
||||||
|
|
||||||
this.lastTrackPoint = toCopy.lastTrackPoint;
|
this.lastTrackPoint = toCopy.lastTrackPoint;
|
||||||
resetAverageHeartRate();
|
resetAverageHeartRate();
|
||||||
}
|
}
|
||||||
@@ -140,11 +119,7 @@ public class TrackStatisticsUpdater {
|
|||||||
|
|
||||||
//Update absolute (GPS-based) altitude
|
//Update absolute (GPS-based) altitude
|
||||||
if (trackPoint.hasAltitude()) {
|
if (trackPoint.hasAltitude()) {
|
||||||
// Update altitude using the smoothed average
|
currentSegment.updateAltitudeExtremities(trackPoint.getAltitude());
|
||||||
altitudeBuffer.setNext(trackPoint.getAltitude());
|
|
||||||
Altitude newAverage = altitudeBuffer.getAverage();
|
|
||||||
|
|
||||||
currentSegment.updateAltitudeExtremities(newAverage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update heart rate
|
// Update heart rate
|
||||||
@@ -178,11 +153,8 @@ public class TrackStatisticsUpdater {
|
|||||||
|
|
||||||
// Update max speed
|
// Update max speed
|
||||||
updateSpeed(trackPoint, lastTrackPoint);
|
updateSpeed(trackPoint, lastTrackPoint);
|
||||||
} else {
|
|
||||||
speedBuffer.reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (trackPoint.isSegmentEnd()) {
|
if (trackPoint.isSegmentEnd()) {
|
||||||
reset(trackPoint);
|
reset(trackPoint);
|
||||||
return;
|
return;
|
||||||
@@ -198,8 +170,6 @@ public class TrackStatisticsUpdater {
|
|||||||
currentSegment.reset(trackPoint.getTime());
|
currentSegment.reset(trackPoint.getTime());
|
||||||
|
|
||||||
lastTrackPoint = null;
|
lastTrackPoint = null;
|
||||||
altitudeBuffer.reset();
|
|
||||||
speedBuffer.reset();
|
|
||||||
resetAverageHeartRate();
|
resetAverageHeartRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,30 +178,14 @@ public class TrackStatisticsUpdater {
|
|||||||
totalHeartRateDuration = Duration.ZERO;
|
totalHeartRateDuration = Duration.ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the smoothed altitude over several readings.
|
|
||||||
* The altitude readings is noisy so the smoothed altitude is better than the raw altitude for many tasks.
|
|
||||||
*/
|
|
||||||
public Altitude getSmoothedAltitude() {
|
|
||||||
return altitudeBuffer.getAverage();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Speed getSmoothedSpeed() {
|
|
||||||
return speedBuffer.getAverage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a speed reading while assuming the user is moving.
|
* Updates a speed reading while assuming the user is moving.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
|
||||||
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
|
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
|
||||||
if (!trackPoint.isMoving()) {
|
if (isValidSpeed(trackPoint, lastTrackPoint)) {
|
||||||
speedBuffer.reset();
|
Speed currentSpeed = trackPoint.getSpeed();
|
||||||
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
|
if (currentSpeed.greaterThan(currentSegment.getMaxSpeed())) {
|
||||||
speedBuffer.setNext(trackPoint.getSpeed());
|
currentSegment.setMaxSpeed(currentSpeed);
|
||||||
Speed average = speedBuffer.getAverage();
|
|
||||||
if (average.greaterThan(currentSegment.getMaxSpeed())) {
|
|
||||||
currentSegment.setMaxSpeed(average);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
|
Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
|
||||||
@@ -239,30 +193,13 @@ public class TrackStatisticsUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
|
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
|
||||||
// There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last.
|
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
|
||||||
if (trackPoint.getSpeed().isZero()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||||
Speed maxAcceleration = Speed.of(MAX_ACCELERATION * timeDifference.toMillis());
|
Speed maxSpeedDifference = Speed.of(Distance.of(SPEED_MAX_ACCELERATION), Duration.ofMillis(1000))
|
||||||
{
|
.mul(timeDifference.toMillis() / UnitConversions.S_TO_MS);
|
||||||
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
|
|
||||||
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
|
|
||||||
if (speedDifference.greaterThan(maxAcceleration)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration.
|
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
|
||||||
if (speedBuffer.isFull()) {
|
return speedDifference.lessThan(maxSpeedDifference);
|
||||||
Speed average = speedBuffer.getAverage();
|
|
||||||
Speed speedDifference = Speed.absDiff(average, trackPoint.getSpeed());
|
|
||||||
|
|
||||||
return trackPoint.getSpeed().lessThan(average.mul(10)) && speedDifference.lessThan(maxAcceleration);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|||||||
-1
@@ -5,7 +5,6 @@ import android.content.Context;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.DatePicker;
|
import android.widget.DatePicker;
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ public class IntervalStatistics {
|
|||||||
interval.add(trackStatisticsUpdater.getTrackStatistics(), trackPoint);
|
interval.add(trackStatisticsUpdater.getTrackStatistics(), trackPoint);
|
||||||
|
|
||||||
double adjustFactor = distanceInterval.dividedBy(interval.distance);
|
double adjustFactor = distanceInterval.dividedBy(interval.distance);
|
||||||
Interval adjustedInterval = new Interval(interval);
|
Interval adjustedInterval = new Interval(interval, adjustFactor);
|
||||||
adjustedInterval.adjust(adjustFactor);
|
|
||||||
|
|
||||||
intervalList.set(intervalList.size() - 1, adjustedInterval);
|
intervalList.set(intervalList.size() - 1, adjustedInterval);
|
||||||
|
|
||||||
@@ -116,7 +115,6 @@ public class IntervalStatistics {
|
|||||||
public Interval(Interval i, double adjustFactor) {
|
public Interval(Interval i, double adjustFactor) {
|
||||||
distance = i.distance.multipliedBy(adjustFactor);
|
distance = i.distance.multipliedBy(adjustFactor);
|
||||||
time = Duration.ofMillis((long) (i.time.toMillis() * adjustFactor));
|
time = Duration.ofMillis((long) (i.time.toMillis() * adjustFactor));
|
||||||
time = i.time;
|
|
||||||
gain_m = i.gain_m;
|
gain_m = i.gain_m;
|
||||||
loss_m = i.loss_m;
|
loss_m = i.loss_m;
|
||||||
avgHeartRate = i.avgHeartRate;
|
avgHeartRate = i.avgHeartRate;
|
||||||
@@ -130,11 +128,6 @@ public class IntervalStatistics {
|
|||||||
avgHeartRate = i.avgHeartRate;
|
avgHeartRate = i.avgHeartRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void adjust(double adjustFactor) {
|
|
||||||
distance = distance.multipliedBy(adjustFactor);
|
|
||||||
time = Duration.ofMillis((long) (time.toMillis() * adjustFactor));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Distance getDistance() {
|
public Distance getDistance() {
|
||||||
return distance;
|
return distance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,17 +107,17 @@ public class IntervalsFragment extends Fragment {
|
|||||||
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
|
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
|
||||||
viewBinding.intervalList.setAdapter(adapter);
|
viewBinding.intervalList.setAdapter(adapter);
|
||||||
|
|
||||||
final DistanceFormatter formatter = DistanceFormatter.Builder()
|
|
||||||
.setDecimalCount(0)
|
|
||||||
.setUnit(unitSystem)
|
|
||||||
.build(getContext());
|
|
||||||
|
|
||||||
intervalsAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
|
intervalsAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||||
TextView v = (TextView) super.getView(position, convertView, parent);
|
TextView v = (TextView) super.getView(position, convertView, parent);
|
||||||
|
|
||||||
|
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||||
|
.setDecimalCount(0)
|
||||||
|
.setUnit(unitSystem)
|
||||||
|
.build(getContext());
|
||||||
|
|
||||||
IntervalStatisticsModel.IntervalOption option = getItem(position);
|
IntervalStatisticsModel.IntervalOption option = getItem(position);
|
||||||
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
|
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
|
||||||
v.setText(stringValue);
|
v.setText(stringValue);
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import android.content.pm.PackageManager;
|
|||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@@ -55,6 +57,8 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
public static final String EXTRA_TRACK_ID = "track_id";
|
public static final String EXTRA_TRACK_ID = "track_id";
|
||||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||||
|
|
||||||
|
private static final String CAMERA_PHOTO_URI_KEY = "camera_photo_uri_key";
|
||||||
|
|
||||||
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
||||||
private Track.Id trackId;
|
private Track.Id trackId;
|
||||||
private Marker marker;
|
private Marker marker;
|
||||||
@@ -86,6 +90,10 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||||
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
cameraPhotoUri = Uri.parse(savedInstanceState.getString(CAMERA_PHOTO_URI_KEY, ""));
|
||||||
|
}
|
||||||
|
|
||||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||||
|
|
||||||
// Setup UI elements
|
// Setup UI elements
|
||||||
@@ -165,6 +173,14 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
takePictureFromCamera = null;
|
takePictureFromCamera = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
if (cameraPhotoUri != null) {
|
||||||
|
outState.putString(CAMERA_PHOTO_URI_KEY, cameraPhotoUri.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
getMenuInflater().inflate(R.menu.marker_edit, menu);
|
getMenuInflater().inflate(R.menu.marker_edit, menu);
|
||||||
@@ -232,7 +248,13 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createMarkerWithGalleryImage() {
|
private void createMarkerWithGalleryImage() {
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||||
|
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 1);
|
||||||
|
} else {
|
||||||
|
intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
}
|
||||||
intent.setType("image/*");
|
intent.setType("image/*");
|
||||||
takePictureFromGallery.launch(intent);
|
takePictureFromGallery.launch(intent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import android.view.ViewGroup.LayoutParams;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@@ -169,7 +171,7 @@ public class ListItemUtils {
|
|||||||
textView.setVisibility(View.VISIBLE);
|
textView.setVisibility(View.VISIBLE);
|
||||||
textView.setText(value);
|
textView.setText(value);
|
||||||
if (addShadow) {
|
if (addShadow) {
|
||||||
textView.setShadowLayer(5, 0, 2, context.getResources().getColor(android.R.color.black));
|
textView.setShadowLayer(5, 0, 2, ContextCompat.getColor(context, android.R.color.black));
|
||||||
} else {
|
} else {
|
||||||
textView.setShadowLayer(0, 0, 0, 0);
|
textView.setShadowLayer(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ abstract public class RecyclerViewSwipeDeleteCallback extends ItemTouchHelper.Ca
|
|||||||
|
|
||||||
public RecyclerViewSwipeDeleteCallback(Context context) {
|
public RecyclerViewSwipeDeleteCallback(Context context) {
|
||||||
background = new ColorDrawable();
|
background = new ColorDrawable();
|
||||||
backgroundColor = context.getResources().getColor(R.color.colorBackgroundDelete);
|
backgroundColor = ContextCompat.getColor(context, R.color.colorBackgroundDelete);
|
||||||
clearPaint = new Paint();
|
clearPaint = new Paint();
|
||||||
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||||
deleteDrawable = ContextCompat.getDrawable(context, R.drawable.ic_delete_forever_24dp);
|
deleteDrawable = ContextCompat.getDrawable(context, R.drawable.ic_delete_forever_24dp);
|
||||||
deleteDrawable.setTint(context.getResources().getColor(R.color.colorForegroundDelete));
|
deleteDrawable.setTint(ContextCompat.getColor(context, R.color.colorForegroundDelete));
|
||||||
intrinsicWidth = deleteDrawable.getIntrinsicWidth();
|
intrinsicWidth = deleteDrawable.getIntrinsicWidth();
|
||||||
intrinsicHeight = deleteDrawable.getIntrinsicHeight();
|
intrinsicHeight = deleteDrawable.getIntrinsicHeight();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public class EGM2008Utils {
|
|||||||
if (location.getLongitude() >= 0) {
|
if (location.getLongitude() >= 0) {
|
||||||
longitude = location.getLongitude();
|
longitude = location.getLongitude();
|
||||||
} else {
|
} else {
|
||||||
longitude = 180 + Math.abs(location.getLongitude());
|
longitude = 360 + location.getLongitude();
|
||||||
}
|
}
|
||||||
int longitudeIndex = (int) (longitude * RESOLUTION_IN_MINUTES);
|
int longitudeIndex = (int) (longitude * RESOLUTION_IN_MINUTES);
|
||||||
|
|
||||||
@@ -172,5 +172,13 @@ public class EGM2008Utils {
|
|||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(latitudeIndex, longitudeIndex);
|
return Objects.hash(latitudeIndex, longitudeIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Indices{" +
|
||||||
|
"latitudeIndex=" + latitudeIndex +
|
||||||
|
", longitudeIndex=" + longitudeIndex +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
@@ -85,12 +83,6 @@ public class ExportUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isExportFileExists(UUID uuuid, String trackFileFormatExtension, List<String> filesName) {
|
|
||||||
String word = uuuid.toString().substring(0, 8) + ".*\\." + trackFileFormatExtension;
|
|
||||||
Pattern pattern = Pattern.compile(word);
|
|
||||||
return filesName.stream().anyMatch(w -> pattern.matcher(w).matches());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> getAllFiles(Context context, Uri directoryUri) {
|
public static List<String> getAllFiles(Context context, Uri directoryUri) {
|
||||||
List<String> fileNames = new ArrayList<>();
|
List<String> fileNames = new ArrayList<>();
|
||||||
final ContentResolver resolver = context.getContentResolver();
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package de.dennisguse.opentracks.util;
|
package de.dennisguse.opentracks.util;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -136,7 +139,7 @@ public class IntentDashboardUtils {
|
|||||||
* @param targetClass the target class
|
* @param targetClass the target class
|
||||||
* @param trackIds the track ids
|
* @param trackIds the track ids
|
||||||
*/
|
*/
|
||||||
public static void startDashboard(Context context, boolean isRecording, String targetPackage, String targetClass, Track.Id... trackIds) {
|
public static void startDashboard(Context context, boolean isRecording, @Nullable String targetPackage, @Nullable String targetClass, Track.Id... trackIds) {
|
||||||
if (trackIds.length == 0) {
|
if (trackIds.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -166,14 +169,19 @@ public class IntentDashboardUtils {
|
|||||||
clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
|
clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
|
||||||
intent.setClipData(clipData);
|
intent.setClipData(clipData);
|
||||||
|
|
||||||
if ((targetPackage != null) && (targetClass != null)) {
|
if (targetPackage != null && targetClass != null) {
|
||||||
Log.i(TAG, "Starting dashboard activity with explicit intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
Log.i(TAG, "Starting dashboard activity with explicit intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||||
intent.setClassName(targetPackage, targetClass);
|
intent.setClassName(targetPackage, targetClass);
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
context.startActivity(intent);
|
try {
|
||||||
|
context.startActivity(intent);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.e(TAG, "Dashboard not installed; cannot start it.");
|
||||||
|
Toast.makeText(context, R.string.show_on_dashboard_not_installed, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package de.dennisguse.opentracks.util;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultCaller;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PermissionRequester {
|
||||||
|
|
||||||
|
private final List<String> permissions;
|
||||||
|
|
||||||
|
public PermissionRequester(List<String> permissions) {
|
||||||
|
this.permissions = permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPermission(Context context) {
|
||||||
|
return permissions.stream()
|
||||||
|
.map(p -> ContextCompat.checkSelfPermission(context, p))
|
||||||
|
.allMatch(r -> r == PackageManager.PERMISSION_GRANTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestPermissionsIfNeeded(Context context, ActivityResultCaller caller, @Nullable Runnable onGranted, @Nullable RejectedCallback onRejected) {
|
||||||
|
if (!hasPermission(context)) {
|
||||||
|
requestPermission(caller, onGranted, onRejected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldShowRequestPermissionRationale(Fragment context) {
|
||||||
|
return permissions.stream()
|
||||||
|
.anyMatch(context::shouldShowRequestPermissionRationale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable RejectedCallback onRejected) {
|
||||||
|
ActivityResultLauncher<String[]> locationPermissionRequest = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
|
||||||
|
boolean isGranted = permissions.stream()
|
||||||
|
.allMatch(p -> result.getOrDefault(p, false));
|
||||||
|
if (isGranted && onGranted != null) {
|
||||||
|
onGranted.run();
|
||||||
|
}
|
||||||
|
if (!isGranted && onRejected != null) {
|
||||||
|
onRejected.rejected(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
locationPermissionRequest.launch(permissions.toArray(permissions.toArray(new String[0])));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<String> GPS_PERMISSION = List.of(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||||
|
|
||||||
|
private static final List<String> BLUETOOTH_PERMISSIONS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
BLUETOOTH_PERMISSIONS = List.of(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT);
|
||||||
|
} else {
|
||||||
|
BLUETOOTH_PERMISSIONS = Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<String> NOTIFICATION_PERMISSIONS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
NOTIFICATION_PERMISSIONS = List.of(Manifest.permission.POST_NOTIFICATIONS);
|
||||||
|
} else {
|
||||||
|
NOTIFICATION_PERMISSIONS = Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<String> ALL_PERMISSIONS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
ArrayList<String> all = new ArrayList<>(GPS_PERMISSION);
|
||||||
|
all.addAll(BLUETOOTH_PERMISSIONS);
|
||||||
|
all.addAll(NOTIFICATION_PERMISSIONS);
|
||||||
|
|
||||||
|
ALL_PERMISSIONS = Collections.unmodifiableList(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static PermissionRequester GPS = new PermissionRequester(GPS_PERMISSION);
|
||||||
|
public final static PermissionRequester BLUETOOTH = new PermissionRequester(BLUETOOTH_PERMISSIONS);
|
||||||
|
public final static PermissionRequester NOTIFICATION = new PermissionRequester(NOTIFICATION_PERMISSIONS);
|
||||||
|
|
||||||
|
public final static PermissionRequester ALL = new PermissionRequester(ALL_PERMISSIONS);
|
||||||
|
|
||||||
|
public interface RejectedCallback {
|
||||||
|
void rejected(PermissionRequester permissionRequester);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package de.dennisguse.opentracks.util;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultCaller;
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class PermissionUtils {
|
|
||||||
|
|
||||||
private static final String[] GPS = new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
|
|
||||||
private static final String[] BLUETOOTH;
|
|
||||||
|
|
||||||
static {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
||||||
BLUETOOTH = new String[]{};
|
|
||||||
} else {
|
|
||||||
BLUETOOTH = new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PermissionUtils() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasGPSPermission(Context context) {
|
|
||||||
return hasPermissions(context, GPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void requestGPSPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
|
|
||||||
requestPermission(context, GPS, onGranted, onRejected);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldShowRequestPermissionRationaleBluetooth(Fragment context) {
|
|
||||||
return Arrays.stream(BLUETOOTH).anyMatch(context::shouldShowRequestPermissionRationale);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasBluetoothPermissions(Context context) {
|
|
||||||
return hasPermissions(context, BLUETOOTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void requestBluetoothPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
|
|
||||||
requestPermission(context, BLUETOOTH, onGranted, onRejected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void requestPermission(ActivityResultCaller context, final String[] permissions, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
|
|
||||||
ActivityResultLauncher<String[]> locationPermissionRequest = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
|
|
||||||
boolean isGranted = Arrays.stream(permissions)
|
|
||||||
.allMatch(p -> result.getOrDefault(p, false));
|
|
||||||
if (isGranted && onGranted != null) {
|
|
||||||
onGranted.run();
|
|
||||||
}
|
|
||||||
if (!isGranted && onRejected != null) {
|
|
||||||
onRejected.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
locationPermissionRequest.launch(permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasPermissions(Context context, String[] permissions) {
|
|
||||||
return Arrays.stream(permissions)
|
|
||||||
.map(p -> ContextCompat.checkSelfPermission(context, p))
|
|
||||||
.allMatch(r -> r == PackageManager.PERMISSION_GRANTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -254,13 +254,14 @@ public class StringUtils {
|
|||||||
/**
|
/**
|
||||||
* @return the formatted altitude_m (or null) and it's unit as {@link Pair}
|
* @return the formatted altitude_m (or null) and it's unit as {@link Pair}
|
||||||
*/
|
*/
|
||||||
|
//TODO altitude_m should be double or a value object
|
||||||
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) {
|
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) {
|
||||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||||
.setDecimalCount(1)
|
.setDecimalCount(1)
|
||||||
.setUnit(unitSystem)
|
.setUnit(unitSystem)
|
||||||
.build(context);
|
.build(context);
|
||||||
|
|
||||||
Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of(Double.NaN); //TODO Refactor Double.NaN
|
Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of((Double) null);
|
||||||
return formatter.getDistanceParts(distance);
|
return formatter.getDistanceParts(distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
Copyright 2010 Google Inc.
|
Copyright 2010 Google Inc.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@@ -12,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
|||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
--><resources>
|
||||||
<resources>
|
|
||||||
<!-- Activity Type -->
|
<!-- Activity Type -->
|
||||||
<string name="activity_type_airplane">طائرة</string>
|
<string name="activity_type_airplane">طائرة</string>
|
||||||
<string name="activity_type_atv">ATV</string>
|
<string name="activity_type_atv">ATV</string>
|
||||||
@@ -200,14 +200,15 @@ limitations under the License.
|
|||||||
<string name="settings_stats_units_imperial">وحدات القياس البريطانية (ميل، قدم)</string>
|
<string name="settings_stats_units_imperial">وحدات القياس البريطانية (ميل، قدم)</string>
|
||||||
<string name="settings_stats_units_metric">وحدات القياس المترية (كم، م)</string>
|
<string name="settings_stats_units_metric">وحدات القياس المترية (كم، م)</string>
|
||||||
<string name="settings_stats_units_title">الوحدات المفضلة</string>
|
<string name="settings_stats_units_title">الوحدات المفضلة</string>
|
||||||
|
|
||||||
<!-- Share Track -->
|
<!-- Share Track -->
|
||||||
<string name="share_track_share_file_body">أعتقد أنك قد تكون مهتمًا بهذا المسار.\n\nلعرض هذا المسار، استخدام برنامج Google Earth أو غيره من البرامج تصميم الخرائط.\n\n%1$s</string>
|
<string name="share_track_share_file_body">أعتقد أنك قد تكون مهتمًا بهذا المسار.
|
||||||
|
\n
|
||||||
|
\n%1$s</string>
|
||||||
<string name="share_track_subject">أريد مشاركة مسار معك</string>
|
<string name="share_track_subject">أريد مشاركة مسار معك</string>
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
<string name="stats_average_moving_pace">متوسط إيقاع التحرك</string>
|
<string name="stats_average_moving_pace">متوسط إيقاع التحرك</string>
|
||||||
<string name="stats_average_moving_speed">متوسط سرعة التحرك</string>
|
<string name="stats_average_moving_speed">متوسط سرعة التحرك</string>
|
||||||
<string name="stats_average_pace">متوسط الإيقاع</string>
|
<string name="stats_average_pace">متوسط الإيقاع</string>
|
||||||
<string name="stats_average_speed">متوسط السرعة</string>
|
<string name="stats_average_speed">متوسط السرعة</string>
|
||||||
<string name="stats_altitude">الارتفاع</string>
|
<string name="stats_altitude">الارتفاع</string>
|
||||||
<string name="stats_fastest_pace">أسرع إيقاع</string>
|
<string name="stats_fastest_pace">أسرع إيقاع</string>
|
||||||
|
|||||||
@@ -411,7 +411,6 @@
|
|||||||
<string name="stats_split_interval">Intervalo partido</string>
|
<string name="stats_split_interval">Intervalo partido</string>
|
||||||
<string name="track_recording_notification_accuracy">Precisión de ubicación: %1$s</string>
|
<string name="track_recording_notification_accuracy">Precisión de ubicación: %1$s</string>
|
||||||
<string name="track_pace_notification">Ritmo: %1$s</string>
|
<string name="track_pace_notification">Ritmo: %1$s</string>
|
||||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
|
||||||
<string name="track_detail_intervals_tab">Intervalos</string>
|
<string name="track_detail_intervals_tab">Intervalos</string>
|
||||||
<string name="interval_list_empty_message">Aún no hay ningún intervalo</string>
|
<string name="interval_list_empty_message">Aún no hay ningún intervalo</string>
|
||||||
<string name="settings_recording_customize_layout_title">Personalice su pantalla de grabación</string>
|
<string name="settings_recording_customize_layout_title">Personalice su pantalla de grabación</string>
|
||||||
|
|||||||
@@ -458,7 +458,6 @@ limitations under the License.
|
|||||||
<string name="settings_recording_show_on_lockscreen_while_recording_title">Показване на статистически данни на заключения екран</string>
|
<string name="settings_recording_show_on_lockscreen_while_recording_title">Показване на статистически данни на заключения екран</string>
|
||||||
<string name="help_data_google_play_services_title">Изисква ли OpenTracks/използва ли услугата Google Play\?</string>
|
<string name="help_data_google_play_services_title">Изисква ли OpenTracks/използва ли услугата Google Play\?</string>
|
||||||
<string name="about_version_code">Код на версията: %1$d</string>
|
<string name="about_version_code">Код на версията: %1$d</string>
|
||||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
|
||||||
<string name="export_error_post_workout">Експортът след тренировка е неуспешен, моля, проверете директорията за експорт.</string>
|
<string name="export_error_post_workout">Експортът след тренировка е неуспешен, моля, проверете директорията за експорт.</string>
|
||||||
<string name="about_third_party_data">Данни от трети страни</string>
|
<string name="about_third_party_data">Данни от трети страни</string>
|
||||||
<string name="import_unsupported_format">Неподдържан формат на файла</string>
|
<string name="import_unsupported_format">Неподдържан формат на файла</string>
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
--><resources>
|
--><resources>
|
||||||
<string name="about_preference_title">O OpenTracks</string>
|
<string name="about_preference_title">O OpenTracks</string>
|
||||||
<string name="about_description"><i> OpenTracks </i> je sportovní sledovací aplikace, která zcela respektuje vaše soukromí.</string>
|
<string name="about_description"><i> OpenTracks </i> je aplikace pro záznam vašich sportů, která zcela respektuje vaše soukromí.</string>
|
||||||
<string name="about_url">Webová stránka projektu: <a href="%1$s">%1$s</a></string>
|
<string name="about_url">Webová stránka projektu: <a href="%1$s">%1$s</a></string>
|
||||||
<string name="about_version_name">Název verze: %1$s</string>
|
<string name="about_version_name">Název verze: %1$s</string>
|
||||||
<string name="about_version_code">Kód verze: %1$d</string>
|
<string name="about_version_code">Kód verze: %1$d</string>
|
||||||
<string name="about_history">OpenTracks je fork aplikace <a href="https://code.google.com/archive/p/mytracks/"> <i> MyTracks Google </i> </a> (v roce 2016 ukončena).</string>
|
<string name="about_history">OpenTracks je fork aplikace <a href="https://code.google.com/archive/p/mytracks/"> <i> MyTracks Google </i> </a> (ukončena v roce 2016).</string>
|
||||||
<string name="about_libraries">Knihovny</string>
|
<string name="about_libraries">Knihovny</string>
|
||||||
<!-- Help -->
|
<!-- Help -->
|
||||||
<string name="help_feature_title">OpenTracks má následující funkce:</string>
|
<string name="help_feature_title">OpenTracks dokáže:</string>
|
||||||
<string name="help_feature_content">* vytvářet GPS stopy
|
<string name="help_feature_content">* vytvářet GPS stopy
|
||||||
\n* použít snímač srdeční frekvence (pouze Bluetooth LE)
|
\n* použít snímač srdeční frekvence (pouze Bluetooth LE)
|
||||||
\n* hlasová oznámení (podle času nebo vzdálenosti)
|
\n* hlasová oznámení (podle času nebo vzdálenosti)
|
||||||
@@ -34,14 +34,14 @@ limitations under the License.
|
|||||||
\n a) (živé aktualizace) Nainstalujte <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Poskytuje online nebo offline mapy prostřednictvím <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
\n a) (živé aktualizace) Nainstalujte <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Poskytuje online nebo offline mapy prostřednictvím <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
||||||
\n Ve skutečnosti je OSMDashboard samostatná aplikace, která má přístup (dočasně) pouze k datům stop, které se budou zobrazovat.
|
\n Ve skutečnosti je OSMDashboard samostatná aplikace, která má přístup (dočasně) pouze k datům stop, které se budou zobrazovat.
|
||||||
\n b) Prostřednictvím „normální“ mapové aplikace (např. <a href="http://osmand.net/">OsmAnd</a> a <a href="https://maps.me/">MAPS.ME</a>). Tyto aplikace musí podporovat formát souboru KMZ (pouze umístění a časová razítka). Ve skutečnosti musí aplikace podporovat <i>ACTION_VIEW</i> pomocí MIME <i>application/vnd.google-earth.kmz</i>. </string>
|
\n b) Prostřednictvím „normální“ mapové aplikace (např. <a href="http://osmand.net/">OsmAnd</a> a <a href="https://maps.me/">MAPS.ME</a>). Tyto aplikace musí podporovat formát souboru KMZ (pouze umístění a časová razítka). Ve skutečnosti musí aplikace podporovat <i>ACTION_VIEW</i> pomocí MIME <i>application/vnd.google-earth.kmz</i>. </string>
|
||||||
<string name="help_storage_pictures_title">Jak ukládá OpenTracks obrázky pořízené jako značky?</string>
|
<string name="help_storage_pictures_title">Jak ukládá OpenTracks obrázky pořízené jako značky na cestě\?</string>
|
||||||
<string name="help_storage_pictures_content">Jsou uloženy v interním úložišti aplikace (tj. přístupné pouze v rámci OpenTracks).</string>
|
<string name="help_storage_pictures_content">Jsou uloženy v interním úložišti aplikace (tj. přístupné pouze v rámci OpenTracks).</string>
|
||||||
<string name="help_track_export_title">Jak mohu exportovat/sdílet zaznamenané stopy\?</string>
|
<string name="help_track_export_title">Jak mohu exportovat/sdílet zaznamenané stopy\?</string>
|
||||||
<string name="help_track_export_content"> Trasy lze exportovat z Nastavení jako KMZ, KML nebo GPX. KMZ a KML obsahuje všechna data, GPX pouze umístění. Trasy sdílené pomocí <i>ikony sdílení</i> jsou sdíleny jako KMZ. </string>
|
<string name="help_track_export_content"> Trasy lze exportovat z Nastavení jako KMZ, KML nebo GPX. KMZ a KML obsahuje všechna data, GPX pouze polohu. Trasy sdílené pomocí <i>ikony sdílení</i> jsou sdíleny jako KMZ. </string>
|
||||||
<string name="help_data_google_play_services_title">Vyžaduje/používá OpenTracks službu Google Play\?</string>
|
<string name="help_data_google_play_services_title">Vyžaduje/používá OpenTracks službu Google Play\?</string>
|
||||||
<string name="help_recording_title">Jak OpenTracks zaznamenává data?</string>
|
<string name="help_recording_title">Jak OpenTracks zaznamenává data?</string>
|
||||||
<string name="help_recording_content">OpenTracks používá GPS senzor zařízení k nepřetržitému zaznamenávání polohy. Pokud zařízení GPS hlásí nepřesná data (např. polohu, rychlost, nadmořskou výšku), OpenTracks s tím nemůže nic udělat.</string>
|
<string name="help_recording_content">OpenTracks používá GPS senzor zařízení k nepřetržitému zaznamenávání polohy. Pokud zařízení GPS hlásí nepřesná data (např. polohu, rychlost, nadmořskou výšku), OpenTracks s tím nemůže nic udělat.</string>
|
||||||
<string name="help_data_storage_title">Jak OpenTracks ukládá data senzorů?</string>
|
<string name="help_data_storage_title">Jak OpenTracks ukládá data ze senzorů\?</string>
|
||||||
<string name="help_data_storage_content"> Data senzoru se ukládají na zaznamenané místo. Pokud jde o snímač srdeční frekvence, znamená to, že na každé místo je uloženo pouze <i> jedno </i> měření srdeční frekvence. Pokud stojíte v klidu nebo jste uvnitř, nebudou zaznamenána žádná data ze senzorů (ale budou zobrazena). </string>
|
<string name="help_data_storage_content"> Data senzoru se ukládají na zaznamenané místo. Pokud jde o snímač srdeční frekvence, znamená to, že na každé místo je uloženo pouze <i> jedno </i> měření srdeční frekvence. Pokud stojíte v klidu nebo jste uvnitř, nebudou zaznamenána žádná data ze senzorů (ale budou zobrazena). </string>
|
||||||
<string name="help_gps_troubleshooting_title">OpenTracks nezaznamenává místa?</string>
|
<string name="help_gps_troubleshooting_title">OpenTracks nezaznamenává místa?</string>
|
||||||
<string name="help_gps_troubleshooting_content"> Co můžete udělat:
|
<string name="help_gps_troubleshooting_content"> Co můžete udělat:
|
||||||
@@ -469,7 +469,6 @@ limitations under the License.
|
|||||||
<string name="track_speed_notification">Rychlost: %1$s</string>
|
<string name="track_speed_notification">Rychlost: %1$s</string>
|
||||||
<string name="track_pace_notification">Rychlost: %1$s</string>
|
<string name="track_pace_notification">Rychlost: %1$s</string>
|
||||||
<string name="about_third_party_data">Údaje třetích stran</string>
|
<string name="about_third_party_data">Údaje třetích stran</string>
|
||||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
|
||||||
<string name="delete_service">Služba mazání</string>
|
<string name="delete_service">Služba mazání</string>
|
||||||
<string name="menu_voice_rate">Rychlost hlasových oznámení</string>
|
<string name="menu_voice_rate">Rychlost hlasových oznámení</string>
|
||||||
<string name="track_delete_not_recording">Není možno vymazat aktuálně zaznamenávanou trasu.</string>
|
<string name="track_delete_not_recording">Není možno vymazat aktuálně zaznamenávanou trasu.</string>
|
||||||
@@ -574,4 +573,5 @@ limitations under the License.
|
|||||||
<string name="settings_public_api_dashboard_enabled_summary_on">Aplikace, která zahájila nahrávání, může také přistupovat k nahraným datům.</string>
|
<string name="settings_public_api_dashboard_enabled_summary_on">Aplikace, která zahájila nahrávání, může také přistupovat k nahraným datům.</string>
|
||||||
<string name="settings_public_api_dashboard_enabled_summary_off">Zaznamenaná data nebudou automaticky sdílena s jinými aplikacemi.</string>
|
<string name="settings_public_api_dashboard_enabled_summary_off">Zaznamenaná data nebudou automaticky sdílena s jinými aplikacemi.</string>
|
||||||
<string name="pace">Tempo</string>
|
<string name="pace">Tempo</string>
|
||||||
|
<string name="track_discarding">Mazání trasy</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -467,7 +467,6 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
|
|||||||
<string name="track_speed_notification">Hastighed: %1$s</string>
|
<string name="track_speed_notification">Hastighed: %1$s</string>
|
||||||
<string name="about_third_party_data">Data fra tredjeparter</string>
|
<string name="about_third_party_data">Data fra tredjeparter</string>
|
||||||
<string name="delete_service">Tjeneste til sletning</string>
|
<string name="delete_service">Tjeneste til sletning</string>
|
||||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
|
||||||
<string name="menu_voice_rate">Stemmehastighed</string>
|
<string name="menu_voice_rate">Stemmehastighed</string>
|
||||||
<string name="track_delete_not_recording">Dette spor kan ikke slettes, da det er optaget i øjeblikket.</string>
|
<string name="track_delete_not_recording">Dette spor kan ikke slettes, da det er optaget i øjeblikket.</string>
|
||||||
<string name="settings_sensor_cycling_distance_speed">Afstand og hastighed</string>
|
<string name="settings_sensor_cycling_distance_speed">Afstand og hastighed</string>
|
||||||
|
|||||||
@@ -276,9 +276,8 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
|||||||
\n%1$s</string>
|
\n%1$s</string>
|
||||||
<string name="share_track_subject">Ich möchte einen Track mit dir teilen</string>
|
<string name="share_track_subject">Ich möchte einen Track mit dir teilen</string>
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
<string name="stats_average_moving_pace">Durchschnitt\n(Bewegung)</string>
|
<string name="stats_average_moving_pace">Durchschnittliches Bewegungstempo</string>
|
||||||
<string name="stats_average_moving_speed">Durchschnittsgeschwindigkeit
|
<string name="stats_average_moving_speed">Mittlere Geschwindigkeit in Bewegung</string>
|
||||||
\n(Bewegung)</string>
|
|
||||||
<string name="stats_average_pace">Durchschnitt</string>
|
<string name="stats_average_pace">Durchschnitt</string>
|
||||||
<string name="stats_average_speed">Durchschnittsgeschwindigkeit</string>
|
<string name="stats_average_speed">Durchschnittsgeschwindigkeit</string>
|
||||||
<string name="stats_altitude">Höhe</string>
|
<string name="stats_altitude">Höhe</string>
|
||||||
@@ -464,13 +463,12 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
|||||||
<string name="about_third_party_data">Daten von Drittherstellern</string>
|
<string name="about_third_party_data">Daten von Drittherstellern</string>
|
||||||
<string name="sensor_state_heart_rate_max">Maximaler Herzfrequenz</string>
|
<string name="sensor_state_heart_rate_max">Maximaler Herzfrequenz</string>
|
||||||
<string name="track_delete_not_recording">Dieser Track kann nicht gelöscht werden, da er gerade aufgezeichnet wird.</string>
|
<string name="track_delete_not_recording">Dieser Track kann nicht gelöscht werden, da er gerade aufgezeichnet wird.</string>
|
||||||
<string name="interval_list_empty_message">Es gibt noch keine Intervalle.</string>
|
<string name="interval_list_empty_message">Es gibt noch keine Intervalle</string>
|
||||||
<string name="import_parser_error">Parser Fehler: %1$s</string>
|
<string name="import_parser_error">Parser Fehler: %1$s</string>
|
||||||
<string name="settings_recording_fullscreen_on_while_recording_summary">Anzeige im Vollbildmodus während der Aufzeichnung.</string>
|
<string name="settings_recording_fullscreen_on_while_recording_summary">Anzeige im Vollbildmodus während der Aufzeichnung.</string>
|
||||||
<string name="sensor_state_heart_rate_avg">Durchschnittlicher Herzfrequenz</string>
|
<string name="sensor_state_heart_rate_avg">Durchschnittlicher Herzfrequenz</string>
|
||||||
<string name="settings_prevent_reimport_tracks_title">Verhindere den erneuten Import von Strecken(aufzeichnungen)</string>
|
<string name="settings_prevent_reimport_tracks_title">Verhindere den erneuten Import von Strecken(aufzeichnungen)</string>
|
||||||
<string name="import_no_kml_file_found">Keine doc.kml Datei im KMZ gefunden</string>
|
<string name="import_no_kml_file_found">Keine doc.kml Datei im KMZ gefunden</string>
|
||||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
|
||||||
<string name="import_thread_interrupted">Thread unterbrochen</string>
|
<string name="import_thread_interrupted">Thread unterbrochen</string>
|
||||||
<string name="import_unable_to_import_file">Importieren der Datei gescheitert: %1$s</string>
|
<string name="import_unable_to_import_file">Importieren der Datei gescheitert: %1$s</string>
|
||||||
<string name="delete_service">Löschdienst</string>
|
<string name="delete_service">Löschdienst</string>
|
||||||
@@ -575,4 +573,7 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
|||||||
<string name="report_bug">Fehler melden</string>
|
<string name="report_bug">Fehler melden</string>
|
||||||
<string name="settings_ui_summary">Erscheinungsbild, Aufzeichnungslayout, Theme</string>
|
<string name="settings_ui_summary">Erscheinungsbild, Aufzeichnungslayout, Theme</string>
|
||||||
<string name="settings_gps_summary">Zeitintervall, Entfernungsintervall, Max Entfernung, Genauigkeit</string>
|
<string name="settings_gps_summary">Zeitintervall, Entfernungsintervall, Max Entfernung, Genauigkeit</string>
|
||||||
|
<string name="description_speed_nautical">Geschwindigkeit (Knoten)</string>
|
||||||
|
<string name="settings_open_tracks_title">Open Tracks</string>
|
||||||
|
<string name="gps_recording_status">%1$s: %2$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user