forked from upstream-mirrors/OpenTracks
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9234da4957 | |||
| 24e18347a6 | |||
| 4c68592a0c | |||
| 47a340578f | |||
| e9e48e4a79 | |||
| 40188774b1 | |||
| 2f0449c420 | |||
| 21376cd3da | |||
| e28c385e52 | |||
| 8d4418a584 | |||
| 85c7314fa8 | |||
| 06a05c82bc | |||
| 4d08729284 | |||
| 28bbf14efd | |||
| 5686322aa6 | |||
| a6c06efb62 | |||
| 80a5149207 | |||
| 5b31213770 | |||
| 19cbbb90f9 | |||
| 39e4a697c0 | |||
| 196de0c3d4 | |||
| 8aa02d009d | |||
| 9a89295b1f | |||
| 4c3c93511f | |||
| bfd6517a61 | |||
| 6334dcea04 | |||
| 5b2d9a5773 | |||
| f1ee0ea035 | |||
| 57afedb3a8 | |||
| 4e33ad4007 | |||
| eada954299 | |||
| 288fe27a57 | |||
| 54e8fd3ee3 | |||
| 68c8839ebb | |||
| 12e764c44a | |||
| 50eff0d7e8 | |||
| ee96226faf | |||
| f5ec91f0eb | |||
| b0575c0f5b | |||
| ccfcd3b306 | |||
| 7929d219ac | |||
| d6f4d3f599 | |||
| 4171d9cc4c | |||
| 9366c031b3 | |||
| 3454fe0cfa | |||
| 3ad2d725be | |||
| d3400dbdfb | |||
| dc4d2ecd89 | |||
| a1feb7d0d5 | |||
| 5cad99b237 | |||
| fb8a4de79b | |||
| b1b0a45f53 | |||
| e580a03115 | |||
| 591597e4e8 | |||
| 1d7820469c | |||
| aa34f5d55b | |||
| e71ab28701 | |||
| ae001c73dd | |||
| 56ff7827df | |||
| 2cdf122e8c | |||
| 495ef2fc6f | |||
| ff561c1b25 | |||
| 7106f64a3c | |||
| 8420f9febb | |||
| 0744ba790f | |||
| 9a7f24bddc | |||
| ef47c86e7c | |||
| 93f203bcf1 | |||
| 6b0ec72b9b | |||
| d46cea6150 | |||
| 0e3ffa58f0 | |||
| c00d769188 | |||
| 00489a121a | |||
| d7f339c6f8 | |||
| da28ea3e95 | |||
| 9872658f81 | |||
| b6b155f19c | |||
| 067f31ac21 | |||
| 3f0ce27870 | |||
| 438f8fe273 | |||
| 392c02a56f | |||
| 6dca3bea58 | |||
| 13034c8bb3 | |||
| 1175ce270e | |||
| 02cad98a4f | |||
| d6b38a390e | |||
| 98deed8ef7 | |||
| 9983168cb9 | |||
| 7b5f29db09 | |||
| 49efd04326 | |||
| 3159c62eb2 | |||
| 0e8811d8c9 | |||
| 4126e36f1c | |||
| b7e2df9f2f | |||
| 055093772d | |||
| 7f8b49814b | |||
| 5cc0e06fbb | |||
| 311a2550e5 | |||
| 4311b29833 | |||
| 86121ac27a | |||
| ed6daeb48b | |||
| af72960cf6 | |||
| 99767d0c0b | |||
| 1fc0a552c7 | |||
| 18dd1de1f9 | |||
| ae3078304d | |||
| 48b6438c06 | |||
| e822172f58 | |||
| a3aee403a1 | |||
| aac2ad4165 | |||
| 76d5e5780c | |||
| 931fd7514a | |||
| d1570ad700 | |||
| 04fff89f68 | |||
| b57fd368ae | |||
| 58d6a3b5a9 | |||
| 289a5273a9 | |||
| 120e20a0ea | |||
| 2843d19450 | |||
| 50127e311a | |||
| f83a1b3a4c | |||
| 65ecbc4521 | |||
| fa50db08c3 | |||
| b395c19832 | |||
| 7e6ffbabd2 | |||
| 6b43d341d0 | |||
| 8af0fb3250 | |||
| 49219de46a | |||
| 1baf652b81 | |||
| 6723bfb979 | |||
| 8357b234d2 | |||
| 9de9fbd058 | |||
| 6da6fc6004 | |||
| 114fc31be0 | |||
| ae795a76fa | |||
| 92004b0ec7 | |||
| f277347529 | |||
| ad1aa06407 | |||
| 3b92303511 | |||
| e62faa1253 | |||
| d3900f4dad | |||
| 00b8834795 | |||
| 1672a5c722 | |||
| bf552a9219 | |||
| 348f697b56 | |||
| 887d4700d4 | |||
| 391dfd30e2 | |||
| b416857001 | |||
| df3720ef0d | |||
| b784a47af6 | |||
| 0f3156e71b | |||
| 0035c85922 | |||
| 976a50ca15 | |||
| c0ad53eef1 | |||
| 1192b02c0a | |||
| 7a3abe9962 | |||
| 60b989c13a | |||
| 340f16dc99 | |||
| 7cf29f7cea | |||
| e39352cc9e | |||
| e2964e805e | |||
| 8cab43d558 | |||
| 13f2a09652 | |||
| 7d4072c0d8 | |||
| 310b35713d | |||
| 7e6237de3a | |||
| 772dad4837 | |||
| 99126fa357 | |||
| b2c91d7592 | |||
| 3c5a1be16b | |||
| 01d83deefd | |||
| bc2a3ddee3 | |||
| da70fca389 | |||
| 67bbd9ca24 | |||
| 5e1293fc82 | |||
| 610da5a106 | |||
| 2b5744ccc1 | |||
| f0341e831f | |||
| 99e5dfbe72 | |||
| 0fb43f6530 | |||
| 6f9c788965 | |||
| f2b1d2a386 | |||
| 18789a0729 | |||
| d0f99be9d3 | |||
| a899be4598 | |||
| 1e518a4c35 | |||
| 1c6012596e | |||
| 978206dee4 | |||
| 8713eb3d0c | |||
| d450e045b1 | |||
| 9f87f67153 | |||
| 3a073a3b0a | |||
| bc1b4e3cd2 | |||
| e09b04308c | |||
| f3cf1a2571 | |||
| 7be4ac436c | |||
| 405120a78b | |||
| e86aeace79 | |||
| afe5c6ee92 | |||
| 055764b462 | |||
| 360738f4c8 | |||
| 3bd3e8fd9b | |||
| 5cf421cf42 | |||
| 455522a03d | |||
| b4c258c686 | |||
| 05ee54d58c | |||
| 28fbcdd2ca | |||
| 0231343aa8 | |||
| 852885cca1 | |||
| d3c1247617 |
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [29]
|
||||
api-level: [ 23, 26, 29 ]
|
||||
target: [default]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
uses: reactivecircus/android-emulator-runner@v2.17.0
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
Generated
+1
@@ -3,6 +3,7 @@
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
|
||||
@@ -49,39 +49,6 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Features:
|
||||
* __Tracking:__ track your sport and outdoor activities
|
||||
* __Photos and Markers:__ mark interesting locations while tracking (also with photos)
|
||||
* __Bluetooth LE sensors:__
|
||||
* heart rate
|
||||
* cycling: speed and cadence
|
||||
* cycling: power meter
|
||||
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
|
||||
* __No Internet access:__ Internet is not used
|
||||
* __Voice announcements__
|
||||
* __No advertising__
|
||||
* __Privacy:__
|
||||
* _does not contain any in-app analytics_
|
||||
* _does not use Google Play Services_
|
||||
* _has no cloud integration_
|
||||
|
||||
__Only required permissions:__
|
||||
* _ACCESS_FINE_LOCATION_: required to use the GPS.
|
||||
|
||||
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
|
||||
|
||||
_Please note:_
|
||||
_OpenTracks_ does not contain a _map_ (as this would require internet access).
|
||||
|
||||
## Custom Dashboards (incl. Map)
|
||||
As of v3.3.1 OpenTracks enables to use custom dashboards.
|
||||
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
|
||||
The developer documentation is in [README_API.md](README_API.md).
|
||||
|
||||
Alternatively, recorded tracks can be shared as KMZ/GPX to 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.
|
||||
|
||||
|
||||
## Screenshots
|
||||
<div>
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png">
|
||||
@@ -96,12 +63,54 @@ However, this is rather slow and does not provide updates while recording.
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png">
|
||||
</div>
|
||||
|
||||
## Features
|
||||
* __Tracking:__ track your sport and outdoor activities
|
||||
* __Voice announcements__
|
||||
* __Photos and Markers:__ mark interesting locations while tracking
|
||||
* __Export:__
|
||||
* export tracks either as KMZ (incl. photos), KML, or GPX
|
||||
* export automatically after each recording (e.g., to sync)
|
||||
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
|
||||
* __Altitude:__
|
||||
* gain/loss via barometric sensor (if present)
|
||||
* shown in EGM2008 (above mean sea level); exported as WGS84
|
||||
* __Bluetooth LE sensors:__
|
||||
* heart rate
|
||||
* cycling: speed and distance
|
||||
* cycling: cadence
|
||||
* cycling: power meter
|
||||
|
||||
### Gadgetbridge integration
|
||||
|
||||
OpenTracks can be used with [Gadgetbridge](https://www.gadgetbridge.org/):
|
||||
* shows statistics via notification on smart watches (requires Gadgetbridge 0.56.1 or later), and
|
||||
* Gadgetbridge's GPX exporter generates `opentracks:trackid` to avoid duplication (Gadgetbridge 0.53.0 or later).
|
||||
|
||||
### Privacy
|
||||
* __No Internet access:__ Internet is not used
|
||||
* __No advertising__
|
||||
* __No in-app analytics__
|
||||
* __No use of Google Play Services__
|
||||
|
||||
__Only required permission:__
|
||||
* _ACCESS_FINE_LOCATION_: required to use the GPS.
|
||||
|
||||
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
|
||||
|
||||
## Custom Dashboards (incl. map)
|
||||
As of v3.3.1, OpenTracks supports custom dashboards.
|
||||
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
|
||||
The developer documentation is in [README_API.md](README_API.md).
|
||||
|
||||
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.
|
||||
|
||||
## Project history
|
||||
|
||||
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
|
||||
Initially, _My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html).
|
||||
In 2016, [Google decided to discontinue](https://support.google.com/maps/answer/6333516) _My Tracks_ and stopped distributing it via the Google Play store in April 2016.
|
||||
The [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
|
||||
Then [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
|
||||
Plonk42's version is available [here](https://github.com/Plonk42/mytracks).
|
||||
In 2019, _OpenTracks_ was forked from Plonk42's _My Tracks_ and major rework was conducted.
|
||||
|
||||
@@ -114,11 +123,11 @@ Rework of _OpenTracks_ included:
|
||||
* removing calorie estimation and activity estimation,
|
||||
* removing support for ANT+ and Classic Bluetooth,
|
||||
* adding support for Bluetooth LE heart rate sensors,
|
||||
* removing Protobuf (store sensosensorr data in SQLite columns directly), and
|
||||
* removing Protobuf (store sensor data in SQLite columns directly), and
|
||||
* removing Android Service API for other apps.
|
||||
|
||||
Artwork, logos and user interface remained more or less unchanged.
|
||||
|
||||
More information about _Google My Tracks_:
|
||||
* [Wikipedia page](https://en.wikipedia.org/wiki/MyTracks)
|
||||
* [Google code archive](https://code.google.com/archive/p/mytracks/)
|
||||
* [Google code archive](https://code.google.com/archive/p/mytracks/)
|
||||
@@ -10,6 +10,7 @@
|
||||
Has updatable firmware that requires a user account at polar.com
|
||||
* Wahoo Tickr (Model: WFBTHR02)
|
||||
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
|
||||
* Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again)
|
||||
|
||||
## CSCP: Cycling Cadence and Speed Service
|
||||
|
||||
@@ -22,11 +23,11 @@ However, often only one value is provided.
|
||||
* Has updatable firmware that requires an account for garmin.com
|
||||
* This sensor reports speed data as cadence.
|
||||
A workaround is in place.
|
||||
* This sensor sometimes counts backwards (unknown software version).
|
||||
A workaround is in place.
|
||||
|
||||
* Wahoo Speed (Model: WFRPMSPD)
|
||||
|
||||
* CooSpo cadence and speed sensor (model: BK467)
|
||||
|
||||
### Cadence
|
||||
|
||||
* Garmin Cadence Sensor 2
|
||||
@@ -47,4 +48,5 @@ Also the distance is not computed.
|
||||
* QUARQ Red DZero Powermeter
|
||||
* Tacx Satori Smart
|
||||
* Wahoo Kickr v4.0
|
||||
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is propietary @ firmware v1.061)
|
||||
|
||||
|
||||
+12
-8
@@ -3,17 +3,17 @@ apply plugin: 'com.android.application'
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +49,14 @@ android {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
cruncherEnabled = false
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 3941
|
||||
versionName "v3.16.0"
|
||||
versionCode 4151
|
||||
versionName "v3.18.4"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -86,10 +90,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha05'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
@@ -99,5 +103,5 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.8.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.10.0'
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
@startuml
|
||||
SensorManager --> ElevationSumManager
|
||||
SensorManager --> AltitudeSumManager
|
||||
LocationManager --> TrackRecordingService
|
||||
ElevationSumManager --> TrackRecordingService
|
||||
|
||||
@@ -14,6 +14,6 @@ TrackDataHub --> StatisticsRecordingFragment : TrackPoints, TrackStatistics, Way
|
||||
TrackDataHub --> StatisticsRecordedFragment : TrackPoints, TrackStatistics, Waypoints (push)
|
||||
TrackDataHub --> ChartFragment --> ChartView
|
||||
|
||||
TrackRecordingServiceConnection --> StatisticsRecordingFragment : Sensor data (heartrate, cadence, elevation gain; pull 1s)
|
||||
TrackRecordingServiceConnection --> StatisticsRecordingFragment : Sensor data (heartrate, cadence, altitude gain; pull 1s)
|
||||
TrackRecordingServiceConnection --> TrackController : Start time (pull, onStart)
|
||||
@enduml
|
||||
@@ -18,7 +18,7 @@ class TrackStatistics {
|
||||
- totalTime
|
||||
- movingTime
|
||||
- maxSpeed
|
||||
- totalElevationGain
|
||||
- totalGain
|
||||
}
|
||||
|
||||
class TrackStatisticsUpdater {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="22"
|
||||
width="22"
|
||||
fill="#000000"
|
||||
version="1.0"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 21.84952 21.99934"
|
||||
enable-background="new 0 0 99.316 99.997"
|
||||
xml:space="preserve"
|
||||
id="svg10"
|
||||
sodipodi:docname="ic_activity_kayaking.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
|
||||
id="metadata16"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs14" /><sodipodi:namedview
|
||||
pagecolor="#000000"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="1"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1024"
|
||||
id="namedview12"
|
||||
showgrid="false"
|
||||
inkscape:pagecheckerboard="false"
|
||||
inkscape:zoom="22.472123"
|
||||
inkscape:cx="11.932326"
|
||||
inkscape:cy="8.4821218"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg10"
|
||||
inkscape:document-rotation="0" /><path
|
||||
id="path2"
|
||||
style="fill:#ffffff;stroke-width:0.20758"
|
||||
d="m 18.296044,13.516712 c -1.017125,-0.01824 -1.903328,0.770666 -2.945647,0.500454 -0.952298,-0.271819 -1.950516,-0.765534 -2.949277,-0.338881 -0.938402,0.492101 -2.078868,0.493759 -3.017025,0 C 8.3845167,13.251481 7.3852162,13.744815 6.4321693,14.017166 5.229901,14.31201 4.2005498,13.256398 2.9984219,13.558671 2.2076549,13.806805 1.0222411,14.11299 1.0222411,14.11299 v 1.764949 c 1.0226762,0.01947 1.9128198,-0.776355 2.9601398,-0.498166 0.4416703,0.216761 1.6682446,0.3639 1.6529583,0.601941 l -1.3879215,1.383539 c -0.7385797,-0.502111 -1.5118619,0.0527 -1.9901253,0.625328 -0.5087615,0.568011 -1.1635382,1.025896 -1.5505475,1.687065 -0.44607206,0.952772 0.6576964,2.056499 1.6104671,1.610525 0.7016688,-0.428027 1.2006595,-1.122907 1.8093513,-1.672458 0.6218606,-0.429609 0.7776032,-1.247655 0.6297837,-1.861615 l 2.0038313,-1.999046 c 0.9553792,-0.44042 2.0942104,-0.606317 3.0559286,-0.07975 0.9566571,0.429062 1.9815791,0.09313 2.8934291,-0.263824 1.038124,-0.339466 1.95525,0.382078 2.955963,0.461915 1.087184,0.105691 2.019821,-0.784126 3.121615,-0.493622 0.55673,-0.01509 1.943197,1.034046 1.977683,0.148315 v -1.471252 c -0.878739,0.05089 -1.592095,-0.588897 -2.468753,-0.540124 z"
|
||||
sodipodi:nodetypes="sccccccccccccccccccccccss" /><path
|
||||
id="path4"
|
||||
style="fill:#ffffff;stroke-width:0.20758"
|
||||
d="M 19.135498,4.6094374 C 19.777729,3.9239548 20.518106,3.3189058 21.090567,2.5760423 21.630775,1.6723294 20.631775,0.46345938 19.642408,0.81572972 18.952145,1.1177762 18.525406,1.7968856 17.966518,2.2833866 17.44088,2.7777653 16.748383,3.4079168 17.123672,4.2079927 17.316456,4.5791871 16.670933,4.8230987 16.479267,5.1359209 L 15.065885,6.5509368 C 13.459538,5.9569743 11.849312,5.3583867 10.246781,4.7652258 9.2045735,4.5310231 8.2546176,5.3827898 8.1060604,6.3824119 7.7002321,7.7689937 7.2645314,9.1479847 6.8776693,10.539386 h -5.851069 v 2.366209 c 1.0341353,-0.06583 1.9719234,-0.790016 3.0482038,-0.491433 0.8514673,0.182595 1.7032854,0.742921 2.5975093,0.377297 1.0893234,-0.553582 2.4097276,-0.562618 3.4983496,-0.0022 0.895986,0.368665 1.751474,-0.190407 2.60469,-0.37517 1.078334,-0.302717 2.015803,0.427862 3.051521,0.490909 0.985146,-0.07753 1.867505,-0.470806 2.884112,-0.579104 0.847652,-0.0903 2.163908,-1.785603 2.163908,-1.785603 0,0 -1.819193,-0.01538 -2.935123,0.0029 -1.990467,-2.45e-4 -3.980934,-0.0021 -5.9714,-0.0044 l 2.483907,-2.4814163 c 0.762424,0.5726687 1.957001,-0.3877847 1.20272,-1.1977389 l 2.032212,-2.039685 c 0.462782,0.2519942 1.079225,0.1706574 1.448288,-0.2104864 z m -8.060346,5.9299486 c -0.486522,0.101516 -0.612149,-0.06543 -0.374498,-0.502616 l 0.909432,-3.029364 2.189559,0.8099787 z"
|
||||
sodipodi:nodetypes="cccccccccccccccccsccccccccccccc" /><path
|
||||
id="path851"
|
||||
style="fill:#ffffff;stroke-width:1.16861"
|
||||
d="m 12.520186,2.6733336 c 0.05505,1.1807821 -1.406378,2.0132887 -2.394197,1.3656163 -1.0445314,-0.553724 -1.0179219,-2.235304 0.04332,-2.7559413 1.018447,-0.59197226 2.378758,0.20782 2.350878,1.390325 z" /></svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,18 @@
|
||||
v3.17.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Add icon for kayaking
|
||||
- Charts draw missing sensor data properly
|
||||
- Default: show coordinates while recording
|
||||
|
||||
Bugfixes:
|
||||
- GPX schema corrected
|
||||
- GPX exported altitude loss as sensor distance
|
||||
- Statistics not computed correctly if using a speed/distance sensor
|
||||
|
||||
Developer:
|
||||
- Distance/speed value objects
|
||||
- Replaced communication via SharedPreferences
|
||||
- Use non-static service binder
|
||||
- Use altitude in the code base (instead of elevation)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
v3.17.1: OpenTracks
|
||||
|
||||
|
||||
Bugfix:
|
||||
- Interval length in meters instead of kilometers (introduced in v3.17.0)
|
||||
- Elevation gain/loss not recorded (introduced in v3.17.0)
|
||||
- Announcements not working on Android 11
|
||||
@@ -0,0 +1,8 @@
|
||||
v3.17.2: OpenTracks
|
||||
|
||||
WARNING: The v3.17.0+ releases contain a memory leak.
|
||||
This issues is NOT FIXED by this release.
|
||||
It is recommended to not upgrade if you still use a release before v3.17.0
|
||||
|
||||
Bugfix:
|
||||
- Crash while using voice announcements
|
||||
@@ -0,0 +1,11 @@
|
||||
v3.17.3: OpenTracks
|
||||
|
||||
WARNING: The v3.17.0+ releases contain a memory leak.
|
||||
This issues is NOT FIXED by this release.
|
||||
It is recommended to not upgrade if you still use a release before v3.17.0
|
||||
|
||||
Bugfix:
|
||||
- Crash if using pace for voice announcements (introduced in v3.17.0)
|
||||
- Statistics showed pace initially (introduced in v3.17.0)
|
||||
- Rare crash when deleting tracks
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
v3.18.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Altitude is shown in EGM2008 (stored/exported as WGS84)
|
||||
|
||||
Bugfix:
|
||||
- Fixed memory leak (introduced in v3.17.0)
|
||||
- Charts: altitude should be in M/FT (introduced in v3.17.1)
|
||||
- Charts: while recording was not correctly updated (introduced in v3.17.3)
|
||||
- TrackDataHub did not account for recordingDistanceInterval (introduced in v3.17.2)
|
||||
|
||||
Developer:
|
||||
- Disable PNG crunching (reproducible build)
|
||||
- Refactored TrackImporter and optimized KMZ import
|
||||
- Use LiveData for TrackRecordingService to UI communication (removing memory leak)
|
||||
@@ -0,0 +1,13 @@
|
||||
v3.18.1: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Deleting tracks is handled by a service
|
||||
- Uninstall deletes all data (i.e., hasFragileData=false)
|
||||
|
||||
Bugfixes:
|
||||
- Race condition: crash while recording and switching OpenTracks into background
|
||||
- Search in track list was not working
|
||||
- Cycling speed sensor: distance computation was wrong (only affected statistics)
|
||||
|
||||
Developer:
|
||||
- ControllerFragment replaces TrackController
|
||||
@@ -0,0 +1,7 @@
|
||||
v3.18.2: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Crash while moving from charts to another activity/app #787
|
||||
|
||||
Developer:
|
||||
- TrackRecordingService pushes data to UI while recording
|
||||
@@ -0,0 +1,13 @@
|
||||
v3.18.3: OpenTracks
|
||||
|
||||
Changes:
|
||||
- KML parsing does not require order of when/gx:coords
|
||||
- Track importer: reorders locations by timestamp
|
||||
- Removed rendered launcher icons for Android < 8.0.0 (API 26)
|
||||
(might look slightly different, but avoid problems for reproducible builds)
|
||||
|
||||
Bugfix:
|
||||
- Avoid crash in charts due to race condition
|
||||
|
||||
Developer:
|
||||
- Split GPX and KML importer (removed common inheritance)
|
||||
@@ -0,0 +1,10 @@
|
||||
v3.18.4: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Speed/distance are consistently formatted
|
||||
- Intervals tab is now faster
|
||||
|
||||
Bugfix:
|
||||
- Cycling speed/distance sensor:
|
||||
- fixed parsing (UINT32)
|
||||
- transmit data to UI properly
|
||||
@@ -3,9 +3,9 @@ OpenTracks is a sport tracking application that completely respects your privacy
|
||||
Features:
|
||||
* Sport tracking: track your sport and outdoor activities
|
||||
* Photos and markers: mark interesting locations while tracking
|
||||
* Supports Bluetooth LE sensors:
|
||||
* heart rate
|
||||
* cycling: speed and cadence
|
||||
* cycling: power meter
|
||||
* Export data: export tracks either as KML or GPX
|
||||
* No Internet access: Internet is not used
|
||||
* Altitude gain and loss: via barometric sensor
|
||||
* Altitude shown in EGM2008 (above mean sea level); exported as WGS84
|
||||
* Voice announcements
|
||||
* Supports Bluetooth LE sensors: heart rate, speed and distance (cycling), cadence (cycling), and power meter (cycling)
|
||||
* Export data: export tracks either as KMZ (incl. photos), KML, or GPX
|
||||
* No Internet access: Internet is not used
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -82,6 +82,7 @@ esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
@@ -129,6 +130,7 @@ fi
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
|
||||
Vendored
+104
-103
@@ -1,103 +1,104 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import androidx.test.espresso.DataInteraction;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
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.withClassName;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
/**
|
||||
* Espresso test that checks the unit for activity category selected.
|
||||
* Also it checks that unit is properly when user change from a pace activity to an speed one.
|
||||
*/
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EspressoCategoryUnitTest {
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<TrackListActivity> mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule =
|
||||
GrantPermissionRule.grant(
|
||||
"android.permission.ACCESS_FINE_LOCATION");
|
||||
|
||||
@Test
|
||||
public void changeBetweenCategoryUpdateProperlyUnitTest() {
|
||||
// Start record.
|
||||
ViewInteraction appCompatImageButton = onView(
|
||||
allOf(withId(R.id.controller_record), withContentDescription("Record"),
|
||||
childAtPosition(
|
||||
allOf(withId(R.id.controller_container),
|
||||
childAtPosition(
|
||||
withId(R.id.controller_fragment),
|
||||
0)),
|
||||
0),
|
||||
isDisplayed()));
|
||||
appCompatImageButton.perform(click());
|
||||
|
||||
{
|
||||
// Change to running activity (pace activity).
|
||||
ViewInteraction appCompatImageView = onView(
|
||||
allOf(withId(R.id.stats_activity_type_icon),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withClassName(is("android.widget.ScrollView")),
|
||||
0),
|
||||
7)));
|
||||
appCompatImageView.perform(scrollTo(), click());
|
||||
|
||||
DataInteraction imageView = onData(anything())
|
||||
.inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view),
|
||||
childAtPosition(
|
||||
withClassName(is("android.widget.LinearLayout")),
|
||||
0)))
|
||||
.atPosition(1);
|
||||
imageView.perform(click());
|
||||
}
|
||||
|
||||
// Pace (min/km) in all fields.
|
||||
{
|
||||
ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit)));
|
||||
textView.perform(waitFor(2000));
|
||||
textView.check(matches(withText("min/km")));
|
||||
}
|
||||
{
|
||||
ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit)));
|
||||
textView.check(matches(withText("min/km")));
|
||||
}
|
||||
{
|
||||
ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit)));
|
||||
textView.check(matches(withText("min/km")));
|
||||
}
|
||||
{
|
||||
ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit)));
|
||||
textView.check(matches(withText("min/km")));
|
||||
}
|
||||
|
||||
{
|
||||
// Change to biking activity (speed activity).
|
||||
ViewInteraction appCompatImageView = onView(
|
||||
allOf(withId(R.id.stats_activity_type_icon),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withClassName(is("android.widget.ScrollView")),
|
||||
0),
|
||||
7)));
|
||||
appCompatImageView.perform(scrollTo(), click());
|
||||
|
||||
DataInteraction imageView = onData(anything())
|
||||
.inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view),
|
||||
childAtPosition(
|
||||
withClassName(is("android.widget.LinearLayout")),
|
||||
0)))
|
||||
.atPosition(9);
|
||||
imageView.perform(click());
|
||||
}
|
||||
|
||||
// Speed (km/h) in all fields.
|
||||
{
|
||||
ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit)));
|
||||
textView.perform(waitFor(2000));
|
||||
textView.check(matches(withText("km/h")));
|
||||
}
|
||||
{
|
||||
ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit)));
|
||||
textView.check(matches(withText("km/h")));
|
||||
}
|
||||
{
|
||||
ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit)));
|
||||
textView.check(matches(withText("km/h")));
|
||||
}
|
||||
{
|
||||
ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit)));
|
||||
textView.check(matches(withText("km/h")));
|
||||
}
|
||||
}
|
||||
|
||||
private static Matcher<View> childAtPosition(
|
||||
final Matcher<View> parentMatcher, final int position) {
|
||||
|
||||
return new TypeSafeMatcher<View>() {
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("Child at position " + position + " in parent ");
|
||||
parentMatcher.describeTo(description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchesSafely(View view) {
|
||||
ViewParent parent = view.getParent();
|
||||
return parent instanceof ViewGroup && parentMatcher.matches(parent)
|
||||
&& view.equals(((ViewGroup) parent).getChildAt(position));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ViewAction waitFor(final long duration_ms) {
|
||||
return new ViewAction() {
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Wait for milliseconds.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, final View view) {
|
||||
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.test.espresso.Espresso;
|
||||
import androidx.test.espresso.IdlingResource;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.action.MotionEvents;
|
||||
import androidx.test.espresso.assertion.ViewAssertions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
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.scrollTo;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EspressoDeleteTrackTest {
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION");
|
||||
|
||||
private MyIdlingResource idlingResource;
|
||||
|
||||
@Before
|
||||
public void registerIntentServiceIdlingResource() {
|
||||
Instrumentation instrumentation
|
||||
= InstrumentationRegistry.getInstrumentation();
|
||||
idlingResource = new MyIdlingResource(instrumentation.getTargetContext());
|
||||
Espresso.registerIdlingResources(idlingResource);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unregisterIntentServiceIdlingResource() {
|
||||
Espresso.unregisterIdlingResources(idlingResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void espressoDeleteTrackTest() {
|
||||
int countBefore;
|
||||
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerRecordButton.perform(waitFor(5000));
|
||||
|
||||
// stop;
|
||||
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||
}
|
||||
|
||||
// back
|
||||
pressBack();
|
||||
|
||||
// get number of items before deleting a track
|
||||
countBefore = numberOfItemsListView();
|
||||
|
||||
// select track
|
||||
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(5000));
|
||||
|
||||
// open menu and delete selected track
|
||||
ViewInteraction overflowMenuButton = onView(
|
||||
allOf(withContentDescription("More options"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.toolbar),
|
||||
2),
|
||||
2),
|
||||
isDisplayed()));
|
||||
overflowMenuButton.perform(click());
|
||||
|
||||
ViewInteraction appCompatTextView = onView(
|
||||
allOf(withId(R.id.title), withText("Delete"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.content),
|
||||
0),
|
||||
0),
|
||||
isDisplayed()));
|
||||
appCompatTextView.perform(click());
|
||||
|
||||
ViewInteraction appCompatButton = onView(
|
||||
allOf(withId(android.R.id.button1), withText("Yes"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.buttonPanel),
|
||||
0),
|
||||
3)));
|
||||
appCompatButton.perform(scrollTo(), click());
|
||||
|
||||
// check number of items after delete
|
||||
onView(withId(R.id.track_list)).check(ViewAssertions.matches(withListSize(countBefore - 1)));
|
||||
}
|
||||
|
||||
private static Matcher<View> childAtPosition(
|
||||
final Matcher<View> parentMatcher, final int position) {
|
||||
|
||||
return new TypeSafeMatcher<View>() {
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("Child at position " + position + " in parent ");
|
||||
parentMatcher.describeTo(description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchesSafely(View view) {
|
||||
ViewParent parent = view.getParent();
|
||||
return parent instanceof ViewGroup && parentMatcher.matches(parent)
|
||||
&& view.equals(((ViewGroup) parent).getChildAt(position));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Matcher<View> withListSize (final int size) {
|
||||
return new TypeSafeMatcher<View> () {
|
||||
@Override public boolean matchesSafely (final View view) {
|
||||
return ((ListView) view).getCount () == size;
|
||||
}
|
||||
|
||||
@Override public void describeTo (final Description description) {
|
||||
description.appendText ("ListView should have " + size + " items");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ViewAction waitFor(final long duration_ms) {
|
||||
return new ViewAction() {
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Wait for milliseconds.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, final View view) {
|
||||
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ViewAction veryLongTouch(final int duration_ms) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Perform long touch.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, final View view) {
|
||||
// Get view absolute position
|
||||
int[] location = new int[2];
|
||||
view.getLocationOnScreen(location);
|
||||
|
||||
// Offset coordinates by view position
|
||||
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
|
||||
|
||||
// Send down event, pause, and send up
|
||||
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
|
||||
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||
MotionEvents.sendUp(uiController, down, coordinates);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private int numberOfItemsListView() {
|
||||
final int[] counts = new int[1];
|
||||
onView(withId(R.id.track_list)).check(matches(new TypeSafeMatcher<View>() {
|
||||
@Override
|
||||
public boolean matchesSafely(View view) {
|
||||
ListView listView = (ListView) view;
|
||||
|
||||
counts[0] = listView.getCount();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
|
||||
}
|
||||
}));
|
||||
|
||||
return counts[0];
|
||||
}
|
||||
|
||||
|
||||
private class MyIdlingResource implements IdlingResource {
|
||||
|
||||
private Context context;
|
||||
private ResourceCallback resourceCallback;
|
||||
|
||||
public MyIdlingResource(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyIdlingResource.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {
|
||||
this.resourceCallback = resourceCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdleNow() {
|
||||
boolean idle = !isDeleteServiceRunning();
|
||||
if (idle && resourceCallback != null) {
|
||||
resourceCallback.onTransitionToIdle();
|
||||
}
|
||||
return idle;
|
||||
}
|
||||
|
||||
private boolean isDeleteServiceRunning() {
|
||||
ActivityManager manager =
|
||||
(ActivityManager) context.getSystemService(
|
||||
Context.ACTIVITY_SERVICE);
|
||||
for (ActivityManager.RunningServiceInfo info :
|
||||
manager.getRunningServices(Integer.MAX_VALUE)) {
|
||||
if (TrackDeleteService.class.getName().equals(
|
||||
info.service.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,13 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@@ -39,13 +42,13 @@ public class EspressoUITest {
|
||||
public void record_pause_resume_stop() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerRecordButton.perform(waitFor(5000));
|
||||
@@ -72,13 +75,13 @@ public class EspressoUITest {
|
||||
public void record_move_through_tabs() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
|
||||
tabLayout.perform(selectTabAtIndex(1));
|
||||
tabLayout.perform(waitFor(1000));
|
||||
@@ -97,6 +100,13 @@ public class EspressoUITest {
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void selectAndDeleteTrack() {
|
||||
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
|
||||
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(2000));
|
||||
}
|
||||
|
||||
private static ViewAction veryLongTouch(final int duration_ms) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package de.dennisguse.opentracks.chart;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.fragments.TrackStubUtils;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ChartPointTest {
|
||||
|
||||
@Test
|
||||
public void create_by_time() {
|
||||
// given
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
statistics.setTotalTime(Duration.ofSeconds(1000));
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), 0, false, false);
|
||||
|
||||
// then
|
||||
assertEquals(1000000, (long) point.getTimeOrDistance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_by_distance() {
|
||||
// given
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
statistics.setTotalDistance(Distance.of(1000));
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), 0, true, true);
|
||||
|
||||
// then
|
||||
assertEquals(1, (long) point.getTimeOrDistance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_get_altitude_speed_and_pace() {
|
||||
// given
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
|
||||
|
||||
// then
|
||||
assertEquals(50, point.getAltitude(), 0.01);
|
||||
assertEquals(36, point.getSpeed(), 0.01);
|
||||
assertEquals(1.66, point.getPace(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_sensorNotAvailable() {
|
||||
// given
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
|
||||
|
||||
// then
|
||||
assertNull(point.getHeartRate());
|
||||
assertNull(point.getCadence());
|
||||
assertNull(point.getPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_sensorAvailable() {
|
||||
// given
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint.setHeartRate_bpm(100f);
|
||||
trackPoint.setCyclingCadence_rpm(101f);
|
||||
trackPoint.setPower(102f);
|
||||
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), 50, false, true);
|
||||
|
||||
// then
|
||||
assertEquals(100.0, point.getHeartRate(), 0.01);
|
||||
assertEquals(101.0, point.getCadence(), 0.01);
|
||||
assertEquals(102.0, point.getPower(), 0.01);
|
||||
}
|
||||
}
|
||||
@@ -44,16 +44,16 @@ public class ChartValueSeriesTest {
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
Integer.MIN_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
new int[]{100, 1000},
|
||||
R.string.description_elevation_metric,
|
||||
R.string.description_elevation_imperial,
|
||||
R.color.chart_elevation_fill,
|
||||
R.color.chart_elevation_border,
|
||||
new int[]{1, 100, 1000},
|
||||
R.string.description_altitude_metric,
|
||||
R.string.description_altitude_imperial,
|
||||
R.color.chart_altitude_fill,
|
||||
R.color.chart_altitude_border,
|
||||
15,
|
||||
18) {
|
||||
@Override
|
||||
double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getElevation();
|
||||
Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getAltitude();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,6 +77,17 @@ public class ChartValueSeriesTest {
|
||||
assertFalse(series.isEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerySmallUpdates() {
|
||||
series.update(new ChartPoint(1f));
|
||||
series.update(new ChartPoint(2f));
|
||||
series.update(new ChartPoint(3f));
|
||||
series.updateDimension();
|
||||
assertEquals(1, series.getInterval());
|
||||
assertEquals(1, series.getMinMarkerValue());
|
||||
assertEquals(6, series.getMaxMarkerValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmallUpdates() {
|
||||
series.update(new ChartPoint(0));
|
||||
|
||||
+29
-27
@@ -29,6 +29,8 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
@@ -60,24 +62,24 @@ public class DescriptionGeneratorTest {
|
||||
public void testGenerateTrackDescription() {
|
||||
Track track = new Track();
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setMaxElevation(550);
|
||||
stats.setMinElevation(-500);
|
||||
stats.setTotalElevationGain(6000f);
|
||||
stats.setTotalElevationLoss(6000f);
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setMaxAltitude(550);
|
||||
stats.setMinAltitude(-500);
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
stats.setTotalAltitudeLoss(6000f);
|
||||
stats.setStartTime(START_TIME);
|
||||
track.setTrackStatistics(stats);
|
||||
track.setCategory("hiking");
|
||||
String expected = //"Created by"
|
||||
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks (Debug)</a><p>"
|
||||
+ "Name: -<br>"
|
||||
+ "Activity type: hiking<br>"
|
||||
+ "Description: -<br>"
|
||||
+ "Total distance: 20.00 km (12.4 mi)<br>"
|
||||
+ "Total time: 10:00<br>"
|
||||
+ "Name: -<br>"
|
||||
+ "Activity type: hiking<br>"
|
||||
+ "Description: -<br>"
|
||||
+ "Total distance: 20.00 km (12.4 mi)<br>"
|
||||
+ "Total time: 10:00<br>"
|
||||
+ "Moving time: 05:00<br>"
|
||||
+ "Average speed: 120.00 km/h (74.6 mi/h)<br>"
|
||||
+ "Average moving speed: 240.00 km/h (149.1 mi/h)<br>"
|
||||
@@ -95,17 +97,17 @@ public class DescriptionGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateTrackDescriptionWithoutMaxMinElevation() {
|
||||
public void testGenerateTrackDescriptionWithoutMaxMinAltitude() {
|
||||
Track track = new Track();
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setMaxElevation(Double.POSITIVE_INFINITY);
|
||||
stats.setMinElevation(Double.NEGATIVE_INFINITY);
|
||||
stats.setTotalElevationGain(6000f);
|
||||
stats.setTotalElevationLoss(6000f);
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setMaxAltitude(Double.POSITIVE_INFINITY);
|
||||
stats.setMinAltitude(Double.NEGATIVE_INFINITY);
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
stats.setTotalAltitudeLoss(6000f);
|
||||
stats.setStartTime(START_TIME);
|
||||
track.setTrackStatistics(stats);
|
||||
track.setCategory("hiking");
|
||||
@@ -132,12 +134,12 @@ public class DescriptionGeneratorTest {
|
||||
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writeDistance(double, StringBuilder, int, String)}.
|
||||
* Tests {@link DescriptionGenerator#writeDistance(Distance, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWriteDistance() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeDistance(1100, builder, R.string.description_total_distance, "<br>");
|
||||
descriptionGenerator.writeDistance(Distance.of(1100), builder, R.string.description_total_distance, "<br>");
|
||||
assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString());
|
||||
}
|
||||
|
||||
@@ -152,32 +154,32 @@ public class DescriptionGeneratorTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writeSpeed(double, StringBuilder, int, String)}.
|
||||
* Tests {@link DescriptionGenerator#writeSpeed(Speed, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWriteSpeed() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeSpeed(1.1, builder, R.string.description_average_speed, "\n");
|
||||
descriptionGenerator.writeSpeed(Speed.of(1.1), builder, R.string.description_average_speed, "\n");
|
||||
assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writeElevation(double, StringBuilder, int, String)}.
|
||||
* Tests {@link DescriptionGenerator#writeAltitude(double, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWriteElevation() {
|
||||
public void testWriteAltitude() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeElevation(4.2, builder, R.string.description_min_elevation, "<br>");
|
||||
descriptionGenerator.writeAltitude(4.2, builder, R.string.description_min_altitude, "<br>");
|
||||
assertEquals("Min elevation: 4 m (14 ft)<br>", builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writePace(double, StringBuilder, int, String)}.
|
||||
* Tests {@link DescriptionGenerator#writePace(Speed, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWritePace() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n");
|
||||
descriptionGenerator.writePace(Speed.of(1.1), builder, R.string.description_average_pace_in_minute, "\n");
|
||||
assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ import android.util.Pair;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class TestDataUtil {
|
||||
@@ -19,8 +22,8 @@ public class TestDataUtil {
|
||||
public static final double INITIAL_LATITUDE = 37.0;
|
||||
public static final double INITIAL_LONGITUDE = -57.0;
|
||||
public static final double ALTITUDE_INTERVAL = 2.5;
|
||||
public static final float ELEVATION_GAIN = 3;
|
||||
public static final float ELEVATION_LOSS = 3;
|
||||
public static final float ALTITUDE_GAIN = 3;
|
||||
public static final float ALTITUDE_LOSS = 3;
|
||||
|
||||
/**
|
||||
* Create a track without any trackPoints.
|
||||
@@ -57,7 +60,7 @@ public class TestDataUtil {
|
||||
|
||||
int i = 0;
|
||||
List<TrackPoint> trackPoints = List.of(
|
||||
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochMilli(i++ + 1)),
|
||||
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochSecond(i++)),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
@@ -65,19 +68,23 @@ public class TestDataUtil {
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1)),
|
||||
createTrackPoint(i++, TrackPoint.Type.SEGMENT_END_MANUAL),
|
||||
|
||||
TrackPoint.createSegmentStartManualWithTime(Instant.ofEpochSecond(i++)),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
createTrackPoint(i++),
|
||||
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1))
|
||||
createTrackPoint(i, TrackPoint.Type.SEGMENT_END_MANUAL)
|
||||
);
|
||||
|
||||
//TODO Use TrackStatisticsUpdater
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(0));
|
||||
stats.setTotalTime(Duration.ofMillis(0));
|
||||
List<Marker> markers = List.of(
|
||||
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(1), null),
|
||||
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(4), null),
|
||||
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(5), null)
|
||||
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, stats, trackPoints.get(1), null),
|
||||
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, stats, trackPoints.get(4), null),
|
||||
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, stats, trackPoints.get(5), null)
|
||||
);
|
||||
|
||||
return new TrackData(track, trackPoints, markers);
|
||||
@@ -104,11 +111,6 @@ public class TestDataUtil {
|
||||
return pair.first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a location.
|
||||
*
|
||||
* @param i the index for the TrackPoint.
|
||||
*/
|
||||
public static TrackPoint createTrackPoint(int i) {
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
trackPoint.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
|
||||
@@ -116,13 +118,13 @@ public class TestDataUtil {
|
||||
trackPoint.setAccuracy((float) i / 100.0f);
|
||||
trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
|
||||
trackPoint.setTime(Instant.ofEpochSecond(i + 1));
|
||||
trackPoint.setSpeed(5f + (i / 10f));
|
||||
trackPoint.setSpeed(Speed.of(5f + (i / 10f)));
|
||||
|
||||
trackPoint.setHeartRate_bpm(100f + i);
|
||||
trackPoint.setCyclingCadence_rpm(300f + i);
|
||||
trackPoint.setPower(400f + i);
|
||||
trackPoint.setElevationGain(ELEVATION_GAIN);
|
||||
trackPoint.setElevationLoss(ELEVATION_LOSS);
|
||||
trackPoint.setAltitudeGain(ALTITUDE_GAIN);
|
||||
trackPoint.setAltitudeLoss(ALTITUDE_LOSS);
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
@@ -149,7 +151,12 @@ public class TestDataUtil {
|
||||
Uri photoUri = FileUtils.getUriForFile(context, dstFile);
|
||||
String photoUrl = photoUri.toString();
|
||||
|
||||
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, trackPoint, photoUrl);
|
||||
//TODO Use TrackStatisticsUpdater
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(0));
|
||||
stats.setTotalTime(Duration.ofMillis(0));
|
||||
|
||||
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, stats, trackPoint, photoUrl);
|
||||
}
|
||||
|
||||
public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
|
||||
@@ -160,4 +167,20 @@ public class TestDataUtil {
|
||||
}
|
||||
return trackPoints;
|
||||
}
|
||||
|
||||
public static Pair<Track.Id, TrackStatistics> buildTrackWithTrackPoints(ContentProviderUtils contentProviderUtils, int numberOfPoints) {
|
||||
Track dummyTrack = new Track();
|
||||
dummyTrack.setId(new Track.Id(System.currentTimeMillis()));
|
||||
dummyTrack.setName("Dummy Track");
|
||||
contentProviderUtils.insertTrack(dummyTrack);
|
||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
for (int i = 0; i < numberOfPoints; i++) {
|
||||
TrackPoint tp = TestDataUtil.createTrackPoint(i);
|
||||
contentProviderUtils.insertTrackPoint(tp, dummyTrack.getId());
|
||||
trackStatisticsUpdater.addTrackPoint(tp, Distance.of(0));
|
||||
}
|
||||
dummyTrack.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
|
||||
contentProviderUtils.updateTrack(dummyTrack);
|
||||
return new Pair<>(dummyTrack.getId(), trackStatisticsUpdater.getTrackStatistics());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ public class TestSensorDataUtil {
|
||||
tp.setAccuracy(1f);
|
||||
tp.setAltitude(1f);
|
||||
tp.setTime(time);
|
||||
tp.setSpeed(5f + (i / 10f));
|
||||
tp.setElevationGain(3f);
|
||||
tp.setElevationLoss(3f);
|
||||
tp.setSpeed(Speed.of(5f + (i / 10f)));
|
||||
tp.setAltitudeGain(3f);
|
||||
tp.setAltitudeLoss(3f);
|
||||
trackPointList.add(tp);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -25,4 +28,35 @@ public class TrackPointTest {
|
||||
|
||||
assertFalse(tp.isRecent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distanceToPrevious() {
|
||||
Location l1 = new Location("test");
|
||||
l1.setLatitude(0);
|
||||
l1.setLongitude(0.0001);
|
||||
TrackPoint tp1 = new TrackPoint(l1);
|
||||
|
||||
Location l2 = new Location("test");
|
||||
l2.setLatitude(0);
|
||||
l2.setLongitude(0.0002);
|
||||
TrackPoint tp2 = new TrackPoint(l2);
|
||||
|
||||
// without sensor distance
|
||||
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
|
||||
|
||||
// tp1 has sensor distance
|
||||
tp1.setSensorDistance(Distance.of(5));
|
||||
tp1.setSensorDistance(null);
|
||||
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
|
||||
|
||||
// tp2 has sensor distance
|
||||
tp1.setSensorDistance(null);
|
||||
tp2.setSensorDistance(Distance.of(5));
|
||||
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
|
||||
|
||||
// tp1 and tp2 have sensor distance
|
||||
tp1.setSensorDistance(Distance.of(10));
|
||||
tp2.setSensorDistance(Distance.of(5));
|
||||
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
|
||||
}
|
||||
}
|
||||
+14
-12
@@ -42,8 +42,10 @@ import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
@@ -105,7 +107,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_largeTrack() {
|
||||
testIterator(new Track.Id(1), 20000);
|
||||
testIterator(new Track.Id(1), 20000 / 2);
|
||||
}
|
||||
|
||||
private void testIterator(Track.Id trackId, int numPoints) {
|
||||
@@ -428,11 +430,11 @@ public class CustomContentProviderUtilsTest {
|
||||
statistics.setStopTime(Instant.ofEpochMilli(2500));
|
||||
statistics.setTotalTime(Duration.ofMillis(1500));
|
||||
statistics.setMovingTime(Duration.ofMillis(700));
|
||||
statistics.setTotalDistance(750.0);
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
statistics.setMaxSpeed(60.0);
|
||||
statistics.setMaxElevation(1250.0);
|
||||
statistics.setMinElevation(1200.0);
|
||||
statistics.setTotalDistance(Distance.of(750.0));
|
||||
statistics.setTotalAltitudeGain(50.0f);
|
||||
statistics.setMaxSpeed(Speed.of(60.0));
|
||||
statistics.setMaxAltitude(1250.0);
|
||||
statistics.setMinAltitude(1200.0);
|
||||
|
||||
track.first.setTrackStatistics(statistics);
|
||||
contentProviderUtils.insertTrack(track.first);
|
||||
@@ -564,10 +566,10 @@ public class CustomContentProviderUtilsTest {
|
||||
// statistics.setTotalTime(1500L);
|
||||
// statistics.setMovingTime(700L);
|
||||
// statistics.setTotalDistance(750.0);
|
||||
// statistics.setTotalElevationGain(50.0);
|
||||
// statistics.setTotalAltitudeGain(50.0);
|
||||
// statistics.setMaxSpeed(60.0);
|
||||
// statistics.setMaxElevation(1250.0);
|
||||
// statistics.setMinElevation(1200.0);
|
||||
// statistics.setMaxAltitude(1250.0);
|
||||
// statistics.setMinAltitude(1200.0);
|
||||
//
|
||||
// track.setTrackStatistics(statistics);
|
||||
// TestDataUtil.insertTrackWithLocations(contentProviderUtils, track);
|
||||
@@ -606,7 +608,7 @@ public class CustomContentProviderUtilsTest {
|
||||
contentProviderUtils.insertMarker(marker3);
|
||||
contentProviderUtils.insertMarker(marker4);
|
||||
|
||||
assertEquals(4, contentProviderUtils.getNextMarkerNumber(trackId));
|
||||
assertEquals(Integer.valueOf(4), contentProviderUtils.getNextMarkerNumber(trackId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -807,7 +809,7 @@ public class CustomContentProviderUtilsTest {
|
||||
assertEquals(longitude, trackPoint.getLongitude(), 0.01);
|
||||
assertEquals(latitude, trackPoint.getLatitude(), 0.01);
|
||||
assertEquals(time, trackPoint.getTime().toEpochMilli());
|
||||
assertEquals(speed, trackPoint.getSpeed(), 0.01);
|
||||
assertEquals(speed, trackPoint.getSpeed().toMPS(), 0.01);
|
||||
assertFalse(trackPoint.hasHeartRate());
|
||||
}
|
||||
|
||||
@@ -1250,7 +1252,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_veryLongActivity12h() {
|
||||
testGetSensorStats_randomData(43200, false);
|
||||
testGetSensorStats_randomData(43200 / 6, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+8
-7
@@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.util.UintUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -100,25 +101,25 @@ public class SensorDataCyclingTest {
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
||||
|
||||
// when
|
||||
current.compute(previous, 2150);
|
||||
current.compute(previous, Distance.ofMM(2150));
|
||||
|
||||
// then
|
||||
assertEquals(1.20, current.getValue().distance_m, 2150);
|
||||
assertEquals(1.20, current.getValue().speed_mps, 0.01);
|
||||
assertEquals(2.15, current.getValue().distance.toM(), 0.01);
|
||||
assertEquals(1.20, current.getValue().speed.toMPS(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_speed_rollOverCount() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT16_MAX - 1, 1024);
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous, 2000);
|
||||
current.compute(previous, Distance.ofMM(2000));
|
||||
|
||||
// then
|
||||
assertEquals(1.20, current.getValue().distance_m, 2000);
|
||||
assertEquals(2, current.getValue().speed_mps, 0.01);
|
||||
assertEquals(2, current.getValue().distance.toM(), 0.01);
|
||||
assertEquals(2, current.getValue().speed.toMPS(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,328 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 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.fragments;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.chart.ChartPoint;
|
||||
import de.dennisguse.opentracks.chart.ChartView;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Tests {@link ChartFragment}.
|
||||
*
|
||||
* @author Youtao Liu
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ChartFragmentTest {
|
||||
|
||||
private static final double HOURS_PER_UNIT = 60.0;
|
||||
|
||||
private ChartFragment chartFragment;
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void finalTearDown() {
|
||||
if (Looper.myLooper() != null) Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
boolean chartByDistance = false;
|
||||
chartFragment = ChartFragment.newInstance(chartByDistance);
|
||||
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get the incorrect values of sensor in {@link ChartFragment#createPendingPoint(TrackPoint)}.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_sensorIncorrect() {
|
||||
// given
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
|
||||
|
||||
// when
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
|
||||
|
||||
// then
|
||||
assertEquals(Float.NaN, point.getHeartRate(), 0.01);
|
||||
assertEquals(Float.NaN, point.getCadence(), 0.01);
|
||||
assertEquals(Float.NaN, point.getPower(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get the correct values of sensor in {@link ChartFragment#createPendingPoint(TrackPoint)}.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_sensorCorrect() {
|
||||
// given
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint.setHeartRate_bpm(100f);
|
||||
trackPoint.setCyclingCadence_rpm(101f);
|
||||
trackPoint.setPower(102f);
|
||||
|
||||
// when
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
|
||||
|
||||
// then
|
||||
assertEquals(100.0, point.getHeartRate(), 0.01);
|
||||
assertEquals(101.0, point.getCadence(), 0.01);
|
||||
assertEquals(102.0, point.getPower(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get the value of metric Distance in {@link ChartFragment#createPendingPoint(TrackPoint)}.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_distanceMetric() {
|
||||
chartFragment.setChartByDistance(true);
|
||||
// Resets last location and writes first location.
|
||||
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
|
||||
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
|
||||
|
||||
// The second is a same location, just different time.
|
||||
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
|
||||
|
||||
// The third location is a new location, and use metric.
|
||||
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint3.setLatitude(23);
|
||||
point = chartFragment.createPendingPoint(trackPoint3);
|
||||
|
||||
// Computes the distance between Latitude 22 and 23.
|
||||
float[] results = new float[4];
|
||||
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(),
|
||||
trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
|
||||
double distance1 = results[0] * UnitConversions.M_TO_KM;
|
||||
assertEquals(distance1, point.getTimeOrDistance(), 0.01);
|
||||
|
||||
// The fourth location is a new location, and use metric.
|
||||
TrackPoint trackPoint4 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint4.setLatitude(24);
|
||||
point = chartFragment.createPendingPoint(trackPoint4);
|
||||
|
||||
// Computes the distance between Latitude 23 and 24.
|
||||
Location.distanceBetween(trackPoint3.getLatitude(), trackPoint3.getLongitude(),
|
||||
trackPoint4.getLatitude(), trackPoint4.getLongitude(), results);
|
||||
double distance2 = results[0] * UnitConversions.M_TO_KM;
|
||||
assertEquals((distance1 + distance2), point.getTimeOrDistance(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#createPendingPoint(TrackPoint)}.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_distanceImperial() {
|
||||
// By distance.
|
||||
chartFragment.setChartByDistance(true);
|
||||
// Setups to use imperial.
|
||||
chartFragment.setMetricUnits(false);
|
||||
|
||||
// The first is a same location, just different time.
|
||||
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
|
||||
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
|
||||
|
||||
// The second location is a new location, and use imperial.
|
||||
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint2.setLatitude(23);
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
|
||||
/*
|
||||
* Computes the distance between Latitude 22 and 23.
|
||||
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
|
||||
*/
|
||||
float[] results = new float[4];
|
||||
Location.distanceBetween(trackPoint1.getLatitude(), trackPoint1.getLongitude(), trackPoint2.getLatitude(), trackPoint2.getLongitude(), results);
|
||||
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
|
||||
assertEquals(distance1, point.getTimeOrDistance(), 0.01);
|
||||
|
||||
// The third location is a new location, and use imperial.
|
||||
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint3.setLatitude(24);
|
||||
point = chartFragment.createPendingPoint(trackPoint3);
|
||||
|
||||
/*
|
||||
* Computes the distance between Latitude 23 and 24.
|
||||
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
|
||||
*/
|
||||
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(), trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
|
||||
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
|
||||
assertEquals(distance1 + distance2, point.getTimeOrDistance(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get the values of time in {@link ChartFragment#createPendingPoint(TrackPoint)}.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_time() {
|
||||
// given
|
||||
chartFragment.setChartByDistance(false);
|
||||
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint1.setTime(Instant.ofEpochMilli(TrackStubUtils.INITIAL_TIME)); //Keep old TrackPoint behavior of having time=0 for this test
|
||||
|
||||
// when
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
|
||||
|
||||
// then
|
||||
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
|
||||
Duration timeSpan = Duration.ofMillis(222);
|
||||
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint2.setTime(Instant.ofEpochMilli(TrackStubUtils.INITIAL_TIME).plus(timeSpan));
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals(timeSpan, Duration.ofMillis((long) point.getTimeOrDistance()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get the value of elevation in {@link ChartFragment#createPendingPoint(TrackPoint)} by one and two points.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_elevation() {
|
||||
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
|
||||
|
||||
/*
|
||||
* At first, clear old points of elevation, so give true to the second parameter.
|
||||
* Then only one value INITIAL_ALTITUDE in buffer.
|
||||
*/
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
|
||||
assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getElevation(), 0.01);
|
||||
|
||||
/*
|
||||
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
|
||||
*/
|
||||
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getElevation(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get the value of speed in {@link ChartFragment#createPendingPoint(TrackPoint)}.
|
||||
* In this test, firstly remove all points in memory, and then fill in two points one by one.
|
||||
* The speed values of these points are 129, 130.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_speed() {
|
||||
/*
|
||||
* At first, clear old points of speed, so give true to the second parameter.
|
||||
* It will not be filled in to the speed buffer.
|
||||
*/
|
||||
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint1.setSpeed(128.5f);
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
|
||||
assertEquals(0.0, point.getSpeed(), 0.01);
|
||||
|
||||
/*
|
||||
* Tests the logic when both metricUnits and reportSpeed are true.
|
||||
* This location will be filled into speed buffer.
|
||||
*/
|
||||
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
|
||||
|
||||
/*
|
||||
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
|
||||
*/
|
||||
trackPoint2.setTime(trackPoint1.getTime().plusMillis(222));
|
||||
trackPoint2.setSpeed(130f);
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to compute speed when use Imperial.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_speedImperial() {
|
||||
chartFragment.setMetricUnits(false);
|
||||
|
||||
// First data point is not added to the speed buffer
|
||||
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint1.setSpeed(100.0f);
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
|
||||
assertEquals(0.0, point.getSpeed(), 0.01);
|
||||
|
||||
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
|
||||
|
||||
/*
|
||||
* Add a time span here to make sure the second point and the speed is valid.
|
||||
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
|
||||
*/
|
||||
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
|
||||
trackPoint2.setSpeed(102f);
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get pace value when reportSpeed is false.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_pace_nonZeroSpeed() {
|
||||
chartFragment.setReportSpeed(false);
|
||||
|
||||
// First data point is not added to the speed buffer
|
||||
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint1.setSpeed(100.0f);
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
|
||||
assertEquals(0.0, point.getSpeed(), 0.01);
|
||||
|
||||
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
|
||||
|
||||
/*
|
||||
* Add a time span here to make sure the second point and the speed is valid.
|
||||
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
|
||||
*/
|
||||
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
|
||||
trackPoint2.setSpeed(102f);
|
||||
point = chartFragment.createPendingPoint(trackPoint2);
|
||||
assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the logic to get pace value when reportSpeed is false and average speed is zero.
|
||||
*/
|
||||
@Test
|
||||
public void testCreatePendingPoint_pace_zeroSpeed() {
|
||||
chartFragment.setReportSpeed(false);
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint.setSpeed(0f);
|
||||
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
|
||||
assertEquals(0.0, point.getPace(), 0.01);
|
||||
}
|
||||
}
|
||||
+160
-90
@@ -3,10 +3,13 @@ package de.dennisguse.opentracks.io.file.importer;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
@@ -14,14 +17,12 @@ import androidx.test.rule.ServiceTestRule;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
@@ -31,7 +32,10 @@ import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -39,9 +43,7 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -53,7 +55,7 @@ import static org.junit.Assert.assertTrue;
|
||||
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
|
||||
* <p>
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExportImportTest {
|
||||
|
||||
private static final String TAG = ExportImportTest.class.getSimpleName();
|
||||
@@ -71,7 +73,6 @@ public class ExportImportTest {
|
||||
}
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
@@ -79,6 +80,9 @@ public class ExportImportTest {
|
||||
private static final String TRACK_CATEGORY = "the category";
|
||||
private static final String TRACK_DESCRIPTION = "the description";
|
||||
|
||||
private File tmpFile;
|
||||
private Uri tmpFileUri;
|
||||
|
||||
private Track track;
|
||||
private List<Marker> markers = new ArrayList<>();
|
||||
private List<TrackPoint> trackPoints = new ArrayList<>();
|
||||
@@ -86,23 +90,49 @@ public class ExportImportTest {
|
||||
private Track.Id trackId;
|
||||
private Track.Id importTrackId;
|
||||
|
||||
private TrackImporter trackImporter;
|
||||
|
||||
@Before
|
||||
public void setUp() throws TimeoutException {
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)));
|
||||
public void fileSetup() throws IOException {
|
||||
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
|
||||
tmpFileUri = Uri.fromFile(tmpFile);
|
||||
|
||||
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||
}
|
||||
|
||||
@After
|
||||
public void FileTearDown() {
|
||||
tmpFile.deleteOnExit();
|
||||
tmpFileUri = null;
|
||||
}
|
||||
|
||||
public void setUp(boolean hasSensorDistance) throws TimeoutException {
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
|
||||
.getService();
|
||||
|
||||
|
||||
trackId = service.startNewTrack();
|
||||
//TODO Workaround as those managers overwrite input data; We need to refactor TrackRecordingService to make it actually testable
|
||||
service.setAltitudeSumManager(null);
|
||||
service.setRemoteSensorManager(null);
|
||||
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50, 5), 0);
|
||||
Distance sensorDistance = hasSensorDistance ? Distance.of(5) : null;
|
||||
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance), 0);
|
||||
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, 5), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, 5), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
|
||||
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
||||
service.pauseCurrentTrack();
|
||||
|
||||
service.resumeCurrentTrack();
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, 5), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50, 5), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, 5), 0);
|
||||
//TODO Workaround as those managers overwrite input data; We need to refactor TrackRecordingService to make it actually testable
|
||||
service.setAltitudeSumManager(null);
|
||||
service.setRemoteSensorManager(null);
|
||||
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
|
||||
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance), 0);
|
||||
service.endCurrentTrack();
|
||||
|
||||
track = contentProviderUtils.getTrack(trackId);
|
||||
@@ -126,17 +156,11 @@ public class ExportImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore("Not implemented")
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_only_track() {
|
||||
// TODO
|
||||
Log.e(TAG, "Test not implemented.");
|
||||
}
|
||||
public void kml_with_trackdetail() throws TimeoutException, IOException {
|
||||
setUp(false);
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_with_trackdetail() {
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
@@ -144,14 +168,13 @@ public class ExportImportTest {
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
trackExporter.writeTrack(track, outputStream);
|
||||
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
|
||||
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -167,7 +190,7 @@ public class ExportImportTest {
|
||||
assertTrackpoints(trackPoints, false, false, false, false, false, false);
|
||||
|
||||
// 3. trackstatistics
|
||||
assertTrackStatistics(false, false, false);
|
||||
assertTrackStatistics(false, false, true);
|
||||
|
||||
// 4. markers
|
||||
assertMarkers();
|
||||
@@ -175,7 +198,9 @@ public class ExportImportTest {
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_with_trackdetail_and_sensordata() {
|
||||
public void kml_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
|
||||
setUp(true);
|
||||
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
@@ -183,14 +208,52 @@ public class ExportImportTest {
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
trackExporter.writeTrack(track, outputStream);
|
||||
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
|
||||
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals(track.getCategory(), importedTrack.getCategory());
|
||||
assertEquals(track.getDescription(), importedTrack.getDescription());
|
||||
assertEquals(track.getName(), importedTrack.getName());
|
||||
assertEquals(track.getIcon(), importedTrack.getIcon());
|
||||
|
||||
// 2. trackpoints
|
||||
assertTrackpoints(trackPoints, true, true, true, true, true, true);
|
||||
|
||||
// 2. trackstatistics
|
||||
assertTrackStatistics(false, true, true);
|
||||
|
||||
// 4. markers
|
||||
assertMarkers();
|
||||
}
|
||||
|
||||
//TODO Does not test images
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
|
||||
setUp(true);
|
||||
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// 2. import
|
||||
KmzTrackImporter importer = new KmzTrackImporter(context, trackImporter);
|
||||
importTrackId = importer.importFile(tmpFileUri).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -213,22 +276,24 @@ public class ExportImportTest {
|
||||
|
||||
@LargeTest
|
||||
@Test(expected = ImportAlreadyExistsException.class)
|
||||
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() {
|
||||
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException, IOException {
|
||||
setUp(false);
|
||||
|
||||
// given
|
||||
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.import_prevent_reimport_key, true);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
|
||||
editor.commit();
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
trackExporter.writeTrack(track, outputStream);
|
||||
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
|
||||
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
@@ -237,7 +302,9 @@ public class ExportImportTest {
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void gpx() {
|
||||
public void gpx() throws TimeoutException, IOException {
|
||||
setUp(true);
|
||||
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
@@ -245,14 +312,13 @@ public class ExportImportTest {
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
trackExporter.writeTrack(track, outputStream);
|
||||
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
|
||||
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -283,22 +349,25 @@ public class ExportImportTest {
|
||||
|
||||
@LargeTest
|
||||
@Test(expected = ImportAlreadyExistsException.class)
|
||||
public void gpx_duplicate_trackUUID() {
|
||||
public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
|
||||
setUp(false);
|
||||
|
||||
// given
|
||||
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.import_prevent_reimport_key, true);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putBoolean(context.getString(R.string.import_prevent_reimport_key), true);
|
||||
editor.commit();
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
trackExporter.writeTrack(track, outputStream);
|
||||
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
|
||||
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -325,7 +394,7 @@ public class ExportImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss, boolean verifyDistance) {
|
||||
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyAltitudeGain, boolean verifyAltitudeLoss, boolean verifyDistance) {
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(trackPoints.size(), importedTrackPoints.size());
|
||||
|
||||
@@ -341,18 +410,18 @@ public class ExportImportTest {
|
||||
// TODO Not exported for GPX/KML
|
||||
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
|
||||
|
||||
assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
||||
assertEquals("" + i, trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
||||
if (trackPoint.hasLocation()) {
|
||||
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||
}
|
||||
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
|
||||
assertEquals("" + i, trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
|
||||
if (trackPoint.hasSpeed()) {
|
||||
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
|
||||
}
|
||||
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
|
||||
assertEquals("" + i, trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
|
||||
if (trackPoint.hasAltitude()) {
|
||||
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001);
|
||||
}
|
||||
|
||||
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
|
||||
@@ -361,27 +430,27 @@ public class ExportImportTest {
|
||||
}
|
||||
|
||||
if (verifyHeartrate) {
|
||||
assertEquals("" + trackPoint, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
||||
}
|
||||
if (verifyCadence) {
|
||||
assertEquals("" + trackPoint, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
|
||||
}
|
||||
if (verifyPower) {
|
||||
assertEquals("" + trackPoint, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
||||
}
|
||||
if (verifyElevationGain) {
|
||||
assertEquals(trackPoint.getElevationGain(), importedTrackPoint.getElevationGain(), 0.01);
|
||||
if (verifyAltitudeGain) {
|
||||
assertEquals(trackPoint.getAltitudeGain(), importedTrackPoint.getAltitudeGain(), 0.01);
|
||||
}
|
||||
if (verifyElevationLoss) {
|
||||
assertEquals(trackPoint.getElevationLoss(), importedTrackPoint.getElevationLoss(), 0.01);
|
||||
if (verifyAltitudeLoss) {
|
||||
assertEquals(trackPoint.getAltitudeLoss(), importedTrackPoint.getAltitudeLoss(), 0.01);
|
||||
}
|
||||
if (verifyDistance) {
|
||||
assertEquals(trackPoint.getSensorDistance(), importedTrackPoint.getSensorDistance(), 0.01);
|
||||
assertEquals(trackPoint.getSensorDistance(), importedTrackPoint.getSensorDistance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTrackStatistics(boolean isGpx, boolean verifyElevationGainAndLoss, boolean verifyDistance) {
|
||||
private void assertTrackStatistics(boolean isGpx, boolean verifyAltitudeGainAndLoss, boolean verifyDistance) {
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
|
||||
assertNotNull(importedTrack.getTrackStatistics());
|
||||
@@ -400,35 +469,36 @@ public class ExportImportTest {
|
||||
|
||||
// Distance
|
||||
if (verifyDistance) {
|
||||
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01);
|
||||
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
|
||||
}
|
||||
|
||||
// Speed
|
||||
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed(), 0.01);
|
||||
assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed(), 0.01);
|
||||
assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed(), 0.01);
|
||||
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
|
||||
assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
|
||||
assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
|
||||
}
|
||||
|
||||
// Elevation
|
||||
assertEquals(trackStatistics.getMinElevation(), importedTrackStatistics.getMinElevation(), 0.01);
|
||||
assertEquals(trackStatistics.getMaxElevation(), importedTrackStatistics.getMaxElevation(), 0.01);
|
||||
if (verifyElevationGainAndLoss) {
|
||||
assertEquals(trackStatistics.getTotalElevationGain(), importedTrackStatistics.getTotalElevationGain(), 0.01);
|
||||
assertEquals(trackStatistics.getTotalElevationLoss(), importedTrackStatistics.getTotalElevationLoss(), 0.01);
|
||||
// Altitude
|
||||
assertEquals(trackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
|
||||
assertEquals(trackStatistics.getMaxAltitude(), importedTrackStatistics.getMaxAltitude(), 0.01);
|
||||
if (verifyAltitudeGainAndLoss) {
|
||||
assertEquals(trackStatistics.getTotalAltitudeGain(), importedTrackStatistics.getTotalAltitudeGain(), 0.01);
|
||||
assertEquals(trackStatistics.getTotalAltitudeLoss(), importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
|
||||
} else {
|
||||
assertFalse(importedTrackStatistics.hasTotalElevationGain());
|
||||
assertFalse(importedTrackStatistics.hasTotalElevationLoss());
|
||||
assertFalse(importedTrackStatistics.hasTotalAltitudeGain());
|
||||
assertFalse(importedTrackStatistics.hasTotalAltitudeLoss());
|
||||
}
|
||||
}
|
||||
|
||||
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float elevationGain, float heartRate, float cyclingCadence, float power, float distance) {
|
||||
TrackPoint tp = new TrackPoint(latitude, longitude, (double) altitude, Instant.ofEpochMilli(time));
|
||||
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) {
|
||||
TrackPoint tp = new TrackPoint(latitude, longitude, Altitude.WGS84.of(altitude), Instant.ofEpochMilli(time));
|
||||
tp.setAccuracy(accuracy);
|
||||
tp.setSpeed(speed);
|
||||
tp.setSpeed(Speed.of(speed));
|
||||
tp.setHeartRate_bpm(heartRate);
|
||||
tp.setCyclingCadence_rpm(cyclingCadence);
|
||||
tp.setPower(power);
|
||||
tp.setElevationGain(elevationGain);
|
||||
tp.setAltitudeGain(altitudeGain);
|
||||
tp.setAltitudeLoss(altitudeGain); //TODO
|
||||
tp.setSensorDistance(distance);
|
||||
return tp;
|
||||
}
|
||||
|
||||
+16
-64
@@ -7,13 +7,16 @@ import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -28,13 +31,20 @@ import static org.junit.Assert.assertNotNull;
|
||||
* Test that legacy KML/GPX formats can still be imported.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class LegacyImportTest {
|
||||
public class GPXImportTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
private TrackImporter trackImporter;
|
||||
|
||||
private Track.Id importTrackId;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (importTrackId != null) {
|
||||
@@ -42,76 +52,19 @@ public class LegacyImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that data with statistics markers is imported and those ignored.
|
||||
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_with_statistics_marker() {
|
||||
// given
|
||||
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_statistics_marker);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("unknown", importedTrack.getCategory());
|
||||
assertEquals("Test Track", importedTrack.getDescription());
|
||||
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(6, importedTrackPoints.size());
|
||||
|
||||
// first 3 trackpoints
|
||||
assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
||||
assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
||||
assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
||||
|
||||
// created resume trackpoint with time of next valid trackpoint
|
||||
assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
||||
assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
||||
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
||||
}
|
||||
|
||||
/**
|
||||
* At least one valid location is required.
|
||||
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test(expected = ImportParserException.class)
|
||||
public void kml_without_locations() {
|
||||
// given
|
||||
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty);
|
||||
|
||||
// when
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that data that contains pause (lat=100, lng=0) and resume (lat=200, lng=0) locations are restored to a segment break.
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void gpx_with_pause_resume() {
|
||||
public void gpx_with_pause_resume() throws IOException {
|
||||
// given
|
||||
GpxFileTrackImporter trackImporter = new GpxFileTrackImporter(context);
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -137,7 +90,7 @@ public class LegacyImportTest {
|
||||
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:04.103Z", 14.003, 3.0, 10.0);
|
||||
}
|
||||
|
||||
private void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
|
||||
static void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
|
||||
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
|
||||
assertEquals(type, trackPoint.getType());
|
||||
|
||||
@@ -151,8 +104,7 @@ public class LegacyImportTest {
|
||||
if (altitude == null) {
|
||||
assertFalse(trackPoint.hasAltitude());
|
||||
} else {
|
||||
assertEquals(altitude, (Double) trackPoint.getAltitude());
|
||||
assertEquals(altitude, (Double) trackPoint.getAltitude().toM());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* Test that legacy KML/GPX formats can still be imported.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class KMLImportTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
private TrackImporter trackImporter;
|
||||
|
||||
private Track.Id importTrackId;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (importTrackId != null) {
|
||||
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KML does not specify the order of gx:coords and when; only count has to be the same.
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml22_order_location_and_when() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_order_location_and_when);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("", importedTrack.getCategory());
|
||||
assertEquals("", importedTrack.getDescription());
|
||||
assertEquals("", importedTrack.getName());
|
||||
assertEquals("", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(5, importedTrackPoints.size());
|
||||
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-05-29T18:06:21.767Z", null, null, null);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.042Z", 14.0, 3.0, 10.0);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.192Z", 14.001, 3.0, 10.0);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.318Z", 14.002, 3.0, 10.0);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.SEGMENT_END_MANUAL, "2021-05-29T18:06:22.512Z", null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinates / when might not be ordered by increasing time.
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml22_time_decreases() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_time_decreases);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("", importedTrack.getCategory());
|
||||
assertEquals("", importedTrack.getDescription());
|
||||
assertEquals("", importedTrack.getName());
|
||||
assertEquals("", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(2, importedTrackPoints.size());
|
||||
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:21.767Z", 14.0, 3.0, 10.0);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-05-29T18:06:22.042Z", null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that data with statistics markers is imported and those ignored.
|
||||
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml22_with_statistics_marker() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml22_statistics_marker);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("unknown", importedTrack.getCategory());
|
||||
assertEquals("Test Track", importedTrack.getDescription());
|
||||
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
assertEquals(6, importedTrackPoints.size());
|
||||
|
||||
// first 3 trackpoints
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
||||
|
||||
// created resume trackpoint with time of next valid trackpoint
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
||||
GPXImportTest.assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
||||
}
|
||||
|
||||
/**
|
||||
* At least one valid location is required.
|
||||
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
|
||||
*/
|
||||
@LargeTest
|
||||
@Test(expected = ImportParserException.class)
|
||||
public void kml_without_locations() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_without_locations);
|
||||
|
||||
// when
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test(expected = ImportParserException.class)
|
||||
public void kml_when_locations_different() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_when_locations_different);
|
||||
|
||||
// when
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -11,11 +11,12 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -41,8 +42,8 @@ public class TrackRecordingServiceNotificationManagerTest {
|
||||
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
|
||||
when(trackPointMock.hasAccuracy()).thenReturn(true);
|
||||
when(trackPointMock.getAccuracy()).thenReturn(999f);
|
||||
when(trackStatisticsMock.getTotalDistance()).thenReturn(0d);
|
||||
when(notificationCompatBuilder.setContentText(anyString())).thenReturn(notificationCompatBuilder);
|
||||
when(trackPointMock.getSpeed()).thenReturn(Speed.of(0));
|
||||
when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
|
||||
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean())).thenReturn(notificationCompatBuilder);
|
||||
|
||||
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
|
||||
|
||||
+50
-58
@@ -31,13 +31,14 @@ import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -83,6 +84,10 @@ public class TrackRecordingServiceTest {
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void finalTearDown() {
|
||||
if (Looper.myLooper() != null) Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
@@ -113,7 +118,8 @@ public class TrackRecordingServiceTest {
|
||||
@After
|
||||
public void tearDown() throws TimeoutException {
|
||||
// Reset service (if some previous test failed)
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
if (service.isRecording() || service.isPaused()) {
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
@@ -146,36 +152,38 @@ public class TrackRecordingServiceTest {
|
||||
// when
|
||||
Intent startIntent = createStartIntent(context);
|
||||
mServiceRule.startService(startIntent);
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent))
|
||||
.getService();
|
||||
|
||||
// then
|
||||
// Test if we start in no-recording mode by default.
|
||||
assertFalse(service.isRecording());
|
||||
assertNull(service.getRecordingTrackId());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_oldTracks() throws TimeoutException {
|
||||
// given
|
||||
createDummyTrack(trackId, false);
|
||||
createDummyTrack(trackId);
|
||||
|
||||
// when
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
|
||||
// then
|
||||
assertFalse(service.isRecording());
|
||||
assertNull(service.getRecordingTrackId());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_serviceRestart_whileRecording() throws TimeoutException {
|
||||
// given
|
||||
createDummyTrack(trackId, true);
|
||||
createDummyTrack(trackId);
|
||||
|
||||
//when
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
service.resumeTrack(trackId);
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
@@ -185,7 +193,8 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testRecording_start() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
|
||||
// when
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
@@ -201,7 +210,8 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testRecording_stop() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
// when
|
||||
@@ -219,7 +229,8 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testRecording_pauseAndResume() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
// when
|
||||
@@ -233,7 +244,6 @@ public class TrackRecordingServiceTest {
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertEquals(trackId, service.getRecordingTrackId());
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
|
||||
assertEquals(3, trackPoints.size());
|
||||
@@ -246,7 +256,8 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testRecording_resumeStoppedTrack() throws TimeoutException, InterruptedException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertTrue(service.isRecording());
|
||||
service.endCurrentTrack();
|
||||
@@ -259,7 +270,6 @@ public class TrackRecordingServiceTest {
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertEquals(trackId, service.getRecordingTrackId());
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
|
||||
assertEquals(4, trackPoints.size());
|
||||
@@ -273,7 +283,8 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testRecording_stopPausedTrack() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertTrue(service.isRecording());
|
||||
service.pauseCurrentTrack();
|
||||
@@ -285,7 +296,6 @@ public class TrackRecordingServiceTest {
|
||||
|
||||
// then
|
||||
assertFalse(service.isRecording());
|
||||
assertNull(service.getRecordingTrackId());
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
|
||||
assertEquals(2, trackPoints.size());
|
||||
@@ -293,48 +303,29 @@ public class TrackRecordingServiceTest {
|
||||
assertEquals(TrackPoint.Type.SEGMENT_END_MANUAL, trackPoints.get(1).getType());
|
||||
}
|
||||
|
||||
@Ignore("Sometimes fails on CI.")
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_orphanedRecordingTrack() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
|
||||
// when
|
||||
// Just set recording track to a bogus value.
|
||||
// Make sure that the service will not start recording and will clear the bogus track.
|
||||
PreferencesUtils.setLong(sharedPreferences, context, R.string.recording_track_id_key, 123L);
|
||||
|
||||
// then
|
||||
assertFalse(service.isRecording());
|
||||
assertNull(service.getRecordingTrackId());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testStartNewTrack_alreadyRecording() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
service.startNewTrack();
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertTrue(service.isRecording());
|
||||
|
||||
Track.Id trackId = service.getRecordingTrackId();
|
||||
|
||||
// when
|
||||
Track.Id newTrackId = service.startNewTrack();
|
||||
|
||||
// then
|
||||
assertNotNull(trackId);
|
||||
assertNull(newTrackId);
|
||||
|
||||
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(sharedPreferences, context));
|
||||
assertEquals(trackId, service.getRecordingTrackId());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testEndCurrentTrack_noRecording() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
// when
|
||||
@@ -342,15 +333,15 @@ public class TrackRecordingServiceTest {
|
||||
service.endCurrentTrack();
|
||||
|
||||
// then
|
||||
assertFalse(PreferencesUtils.isRecording(sharedPreferences, context));
|
||||
assertNull(service.getRecordingTrackId());
|
||||
assertFalse(service.isRecording());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testInsertWaypointMarker_noRecordingTrack() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
// when
|
||||
@@ -364,57 +355,58 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testInsertWaypointMarker_validWaypoint() throws TimeoutException, InterruptedException {
|
||||
// given
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
service.startNewTrack();
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
|
||||
.getService();
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertTrue(service.isRecording());
|
||||
newTrackPoint(service);
|
||||
Track.Id trackId = service.getRecordingTrackId();
|
||||
|
||||
// when
|
||||
Marker.Id markerId = service.insertMarker(null, null, null, null);
|
||||
|
||||
// then
|
||||
assertNotEquals(-1L, markerId);
|
||||
assertNotEquals(new Marker.Id(-1L), markerId);
|
||||
Marker wpt = contentProviderUtils.getMarker(markerId);
|
||||
assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
|
||||
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
|
||||
assertEquals(trackId, wpt.getTrackId());
|
||||
assertEquals(0.0, wpt.getLength(), 0.01);
|
||||
assertEquals(0.0, wpt.getLength().toM(), 0.01);
|
||||
assertNotNull(wpt.getLocation());
|
||||
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
|
||||
private void addTrack(Track track, boolean isRecording) {
|
||||
private void addTrack(Track track) {
|
||||
assertNotNull(track.getId());
|
||||
contentProviderUtils.insertTrack(track);
|
||||
assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
|
||||
PreferencesUtils.setLong(sharedPreferences, context, R.string.recording_track_id_key, isRecording ? track.getId().getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
|
||||
PreferencesUtils.setBoolean(sharedPreferences, context, R.string.recording_track_paused_key, !isRecording);
|
||||
}
|
||||
|
||||
// NOTE: Do not use to create a track that is currently recording.
|
||||
private void createDummyTrack(Track.Id id, boolean isRecording) {
|
||||
private void createDummyTrack(Track.Id id) {
|
||||
Track dummyTrack = new Track();
|
||||
dummyTrack.setId(id);
|
||||
dummyTrack.setName("Dummy Track");
|
||||
TrackStatistics trackStatistics = new TrackStatistics();
|
||||
Instant now = Instant.now();
|
||||
trackStatistics.setStartTime(now.minusSeconds(5L));
|
||||
trackStatistics.setStopTime(now.minusSeconds(1L));
|
||||
dummyTrack.setTrackStatistics(trackStatistics);
|
||||
addTrack(dummyTrack, isRecording);
|
||||
addTrack(dummyTrack);
|
||||
}
|
||||
|
||||
private static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
|
||||
private static void newTrackPoint(TrackRecordingService trackRecordingService) throws InterruptedException {
|
||||
newTrackPoint(trackRecordingService, 45.0f, 35f, 5, 10, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException {
|
||||
static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException {
|
||||
newTrackPoint(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a location and waits for 200ms.
|
||||
*/
|
||||
private static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException {
|
||||
private static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) {
|
||||
Location location = new Location("");
|
||||
location.setLongitude(longitude);
|
||||
location.setLatitude(latitude);
|
||||
|
||||
+10
-2
@@ -13,6 +13,7 @@ import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
@@ -44,6 +45,7 @@ import static org.junit.Assert.assertNotNull;
|
||||
* So, if it is executed on a real device, data might be lost.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
//TODO Implement as mock test; no need to store data in database
|
||||
public class TrackRecordingServiceTestLocation {
|
||||
|
||||
@Rule
|
||||
@@ -55,7 +57,7 @@ public class TrackRecordingServiceTestLocation {
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private TrackRecordingServiceInterface service;
|
||||
private TrackRecordingService service;
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
@@ -63,6 +65,11 @@ public class TrackRecordingServiceTestLocation {
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void finalTearDown() {
|
||||
if (Looper.myLooper() != null) Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws TimeoutException {
|
||||
// Set up the mock content resolver
|
||||
@@ -77,7 +84,8 @@ public class TrackRecordingServiceTestLocation {
|
||||
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
sharedPreferences.edit().clear().commit();
|
||||
|
||||
service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
|
||||
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
|
||||
.getService();
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
+38
-19
@@ -6,6 +6,7 @@ import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.MediumTest;
|
||||
@@ -35,7 +36,6 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
@@ -87,7 +87,8 @@ public class TrackRecordingServiceTestLooper {
|
||||
@After
|
||||
public void tearDown() throws TimeoutException {
|
||||
// Reset service (if some previous test failed)
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
|
||||
.getService();
|
||||
if (service.isRecording() || service.isPaused()) {
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
@@ -99,28 +100,36 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_minRequiredAccuracy() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_gps_accuracy_key, 500);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.recording_gps_accuracy_key), 500);
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_voiceFrequencyDefault() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default)));
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.voice_frequency_key), Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default)));
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_voiceFrequencyByDistance() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, -1);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.voice_frequency_key), -1);
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_voiceFrequencyByTime() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.voice_frequency_key, 1);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.voice_frequency_key), 1);
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@@ -128,29 +137,36 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.max_recording_distance_key, Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default)));
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.max_recording_distance_key), Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default)));
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_maxRecordingDistance() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.max_recording_distance_key, 50);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.max_recording_distance_key), 50);
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_minRecordingDistanceDefault() throws TimeoutException {
|
||||
int minRecordingIntervalDefault = Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_distance_interval_key, minRecordingIntervalDefault);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.recording_distance_interval_key), Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_minRecordingDistance() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_distance_interval_key, 2);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.recording_distance_interval_key), 2);
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@@ -171,22 +187,27 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_minRecordingIntervalDefault() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.min_recording_interval_key, Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.min_recording_interval_key), Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_minRecordingInterval() throws TimeoutException {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.min_recording_interval_key, 2);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.min_recording_interval_key), 2);
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_minRequiredAccuracyDefault() throws TimeoutException {
|
||||
int recordingGPSAccuracyDefault = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.recording_gps_accuracy_key, recordingGPSAccuracyDefault);
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
editor.putInt(context.getString(R.string.recording_gps_accuracy_key), Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default)));
|
||||
editor.commit();
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@@ -199,7 +220,8 @@ public class TrackRecordingServiceTestLooper {
|
||||
}
|
||||
|
||||
private void fullRecordingSession() throws TimeoutException {
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
|
||||
.getService();
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
// Start a track.
|
||||
@@ -209,8 +231,6 @@ public class TrackRecordingServiceTestLooper {
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
assertNotNull(track);
|
||||
assertEquals(trackId, track.getId());
|
||||
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(sharedPreferences, context));
|
||||
assertEquals(trackId, service.getRecordingTrackId());
|
||||
|
||||
// Insert a few points, markers and statistics.
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -234,7 +254,6 @@ public class TrackRecordingServiceTestLooper {
|
||||
// Stop the track. Validate if it has correct data.
|
||||
service.endCurrentTrack();
|
||||
assertFalse(service.isRecording());
|
||||
assertNull(service.getRecordingTrackId());
|
||||
track = contentProviderUtils.getTrack(trackId);
|
||||
assertNotNull(track);
|
||||
assertEquals(trackId, track.getId());
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package de.dennisguse.opentracks.services.sensors;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* NOTE: Test data is completely artificial.
|
||||
*/
|
||||
public class AltitudeSumManagerTest {
|
||||
|
||||
private final AltitudeSumManager subject = new AltitudeSumManager();
|
||||
|
||||
private static void addSensorValue(AltitudeSumManager altitudeSumManager, float[] values) {
|
||||
for (float f : values) {
|
||||
altitudeSumManager.onSensorValueChanged(f);
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
subject.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAltitudeGainLoss_downhill() {
|
||||
// given
|
||||
subject.setConnected(true);
|
||||
|
||||
// then
|
||||
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1015.14f, 1015.15f});
|
||||
|
||||
// then
|
||||
Assert.assertEquals(0f, subject.getAltitudeGain_m(), 0.01);
|
||||
Assert.assertEquals(48.0, subject.getAltitudeLoss_m(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sensorUnavailable() {
|
||||
// given
|
||||
subject.setConnected(false);
|
||||
|
||||
// then
|
||||
subject.onSensorValueChanged(999f);
|
||||
|
||||
// then
|
||||
Assert.assertNull(subject.getAltitudeGain_m());
|
||||
Assert.assertNull(subject.getAltitudeLoss_m());
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
package de.dennisguse.opentracks.services.sensors;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* NOTE: Test data is completely artificial.
|
||||
*/
|
||||
public class ElevationSumManagerTest {
|
||||
|
||||
private final ElevationSumManager elevationSumManager = new ElevationSumManager();
|
||||
|
||||
private static void addSensorValue(ElevationSumManager elevationSumManager, float[] values) {
|
||||
for (float f : values) {
|
||||
elevationSumManager.onSensorValueChanged(f);
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
elevationSumManager.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getElevationGainLoss_downhill() {
|
||||
// given
|
||||
elevationSumManager.setConnected(true);
|
||||
|
||||
// then
|
||||
addSensorValue(elevationSumManager, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1015.14f, 1015.15f});
|
||||
|
||||
// then
|
||||
Assert.assertEquals(0f, elevationSumManager.getElevationGain_m(), 0.01);
|
||||
Assert.assertEquals(48.0, elevationSumManager.getElevationLoss_m(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sensorUnavailable() {
|
||||
// given
|
||||
elevationSumManager.setConnected(false);
|
||||
|
||||
// then
|
||||
elevationSumManager.onSensorValueChanged(999f);
|
||||
|
||||
// then
|
||||
Assert.assertNull(elevationSumManager.getElevationGain_m());
|
||||
Assert.assertNull(elevationSumManager.getElevationLoss_m());
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010 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.services.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests for {@link AnnouncementPeriodicTaskFactory}.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AnnouncementPeriodicTaskFactoryTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void finalTearDown() {
|
||||
if (Looper.myLooper() != null) Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory();
|
||||
PeriodicTask task = factory.create(context);
|
||||
assertTrue(task instanceof AnnouncementPeriodicTask);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package de.dennisguse.opentracks.services.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class PeriodicTaskExecutorTest {
|
||||
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void finalTearDown() {
|
||||
if (Looper.myLooper() != null) Looper.myLooper().quit();
|
||||
}
|
||||
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void calculateNextTaskDistance() throws TimeoutException {
|
||||
// given
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
|
||||
.getService();
|
||||
|
||||
PeriodicTaskExecutor periodicTaskExecutor = new PeriodicTaskExecutor(service, new AnnouncementPeriodicTask.Factory());
|
||||
periodicTaskExecutor.setMetricUnits(true);
|
||||
periodicTaskExecutor.setTaskFrequency(-5);
|
||||
|
||||
// when
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
statistics.setTotalDistance(Distance.of(13000));
|
||||
assertEquals(Distance.of(15000), periodicTaskExecutor.calculateNextTaskDistance(statistics));
|
||||
|
||||
statistics.setTotalDistance(Distance.of(15100));
|
||||
assertEquals(Distance.of(20000), periodicTaskExecutor.calculateNextTaskDistance(statistics));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,14 +15,18 @@
|
||||
*/
|
||||
package de.dennisguse.opentracks.stats;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
@@ -32,7 +36,7 @@ import static org.junit.Assert.assertNull;
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackStatisticsTest {
|
||||
|
||||
private TrackStatistics statistics;
|
||||
@@ -56,13 +60,13 @@ public class TrackStatisticsTest {
|
||||
assertEquals(Duration.ofSeconds(0), statistics.getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(0), statistics.getTotalTime());
|
||||
|
||||
assertNull(statistics.getTotalElevationGain());
|
||||
assertNull(statistics.getTotalElevationLoss());
|
||||
assertEquals(Double.NEGATIVE_INFINITY, statistics.getMaxElevation(), 0.0);
|
||||
assertEquals(Double.POSITIVE_INFINITY, statistics.getMinElevation(), 0.0);
|
||||
assertEquals(0.0, statistics.getMaxSpeed(), 0.0);
|
||||
assertEquals(0.0, statistics.getAverageSpeed(), 0.0);
|
||||
assertEquals(0.0, statistics.getAverageMovingSpeed(), 0.0);
|
||||
assertNull(statistics.getTotalAltitudeGain());
|
||||
assertNull(statistics.getTotalAltitudeLoss());
|
||||
assertEquals(Double.NEGATIVE_INFINITY, statistics.getMaxAltitude(), 0.0);
|
||||
assertEquals(Double.POSITIVE_INFINITY, statistics.getMinAltitude(), 0.0);
|
||||
assertEquals(0.0, statistics.getMaxSpeed().toMPS(), 0.0);
|
||||
assertEquals(0.0, statistics.getAverageSpeed().toMPS(), 0.0);
|
||||
assertEquals(0.0, statistics.getAverageMovingSpeed().toMPS(), 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,16 +81,16 @@ public class TrackStatisticsTest {
|
||||
statistics2.setTotalTime(Duration.ofMillis(1000)); // Result: 1500+1000
|
||||
statistics.setMovingTime(Duration.ofMillis(700));
|
||||
statistics2.setMovingTime(Duration.ofMillis(600)); // Result: 700+600
|
||||
statistics.setTotalDistance(750.0);
|
||||
statistics2.setTotalDistance(350.0); // Result: 750+350
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
statistics2.setTotalElevationGain(850.0f); // Result: 850+50
|
||||
statistics.setMaxSpeed(60.0); // Resulting max speed
|
||||
statistics2.setMaxSpeed(30.0);
|
||||
statistics.setMaxElevation(1250.0);
|
||||
statistics.setMinElevation(1200.0); // Resulting min elevation
|
||||
statistics2.setMaxElevation(3575.0); // Resulting max elevation
|
||||
statistics2.setMinElevation(2800.0);
|
||||
statistics.setTotalDistance(Distance.of(750.0));
|
||||
statistics2.setTotalDistance(Distance.of(350.0)); // Result: 750+350
|
||||
statistics.setTotalAltitudeGain(50.0f);
|
||||
statistics2.setTotalAltitudeGain(850.0f); // Result: 850+50
|
||||
statistics.setMaxSpeed(Speed.of(60.0)); // Resulting max speed
|
||||
statistics2.setMaxSpeed(Speed.of(30.0));
|
||||
statistics.setMaxAltitude(1250.0);
|
||||
statistics.setMinAltitude(1200.0); // Resulting min altitude
|
||||
statistics2.setMaxAltitude(3575.0); // Resulting max altitude
|
||||
statistics2.setMinAltitude(2800.0);
|
||||
|
||||
// when
|
||||
statistics.merge(statistics2);
|
||||
@@ -96,24 +100,24 @@ public class TrackStatisticsTest {
|
||||
assertEquals(Instant.ofEpochMilli(4000), statistics.getStopTime());
|
||||
assertEquals(Duration.ofMillis(2500), statistics.getTotalTime());
|
||||
assertEquals(Duration.ofMillis(1300), statistics.getMovingTime());
|
||||
assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
|
||||
assertEquals(900.0, statistics.getTotalElevationGain(), 0.001);
|
||||
assertEquals(statistics.getTotalDistance() / statistics.getMovingTime().getSeconds(), statistics.getMaxSpeed(), 0.001);
|
||||
assertEquals(1200.0, statistics.getMinElevation(), 0.001);
|
||||
assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
|
||||
assertEquals(1100.0, statistics.getTotalDistance().toM(), 0.001);
|
||||
assertEquals(900.0, statistics.getTotalAltitudeGain(), 0.001);
|
||||
assertEquals(Speed.of(statistics.getTotalDistance(), statistics.getMovingTime()).toMPS(), statistics.getMaxSpeed().toMPS(), 0.001);
|
||||
assertEquals(1200.0, statistics.getMinAltitude(), 0.001);
|
||||
assertEquals(3575.0, statistics.getMaxAltitude(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAverageSpeed() {
|
||||
statistics.setTotalDistance(1000.0);
|
||||
statistics.setTotalDistance(Distance.of(1000.0));
|
||||
statistics.setTotalTime(Duration.ofMillis(50000));
|
||||
assertEquals(20.0, statistics.getAverageSpeed(), 0.001);
|
||||
assertEquals(20.0, statistics.getAverageSpeed().toMPS(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAverageMovingSpeed() {
|
||||
statistics.setTotalDistance(1000.0);
|
||||
statistics.setTotalDistance(Distance.of(1000.0));
|
||||
statistics.setMovingTime(Duration.ofMillis(20000));
|
||||
assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001);
|
||||
assertEquals(50.0, statistics.getAverageMovingSpeed().toMPS(), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
+66
-35
@@ -9,6 +9,9 @@ import org.junit.runner.RunWith;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -18,7 +21,7 @@ import static org.junit.Assert.assertEquals;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackStatisticsUpdaterTest {
|
||||
|
||||
private static final int GPS_DISTANCE = 50;
|
||||
private static final Distance GPS_DISTANCE = Distance.of(50);
|
||||
|
||||
@Test
|
||||
public void addTrackPoint_TestingTrack() {
|
||||
@@ -31,18 +34,18 @@ public class TrackStatisticsUpdaterTest {
|
||||
|
||||
// then
|
||||
TrackStatistics statistics = subject.getTrackStatistics();
|
||||
assertEquals(85.35, statistics.getTotalDistance(), 0.01);
|
||||
assertEquals(Duration.ofMillis(13999), statistics.getTotalTime());
|
||||
assertEquals(Duration.ofSeconds(6), statistics.getMovingTime());
|
||||
assertEquals(99.58, statistics.getTotalDistance().toM(), 0.01);
|
||||
assertEquals(Duration.ofSeconds(14), statistics.getTotalTime());
|
||||
assertEquals(Duration.ofSeconds(7), statistics.getMovingTime());
|
||||
|
||||
assertEquals(2.5, statistics.getMinElevation(), 0.01);
|
||||
assertEquals(27.5, statistics.getMaxElevation(), 0.01);
|
||||
assertEquals(27, statistics.getTotalElevationGain(), 0.01);
|
||||
assertEquals(27.0, statistics.getTotalElevationLoss(), 0.01);
|
||||
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
|
||||
assertEquals(27.5, statistics.getMaxAltitude(), 0.01);
|
||||
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
|
||||
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
|
||||
|
||||
assertEquals(14.226, statistics.getMaxSpeed(), 0.01);
|
||||
assertEquals(14.226, statistics.getAverageMovingSpeed(), 0.01);
|
||||
assertEquals(6.566, statistics.getAverageSpeed(), 0.01);
|
||||
assertEquals(14.226, statistics.getMaxSpeed().toMPS(), 0.01);
|
||||
assertEquals(14.226, statistics.getAverageMovingSpeed().toMPS(), 0.01);
|
||||
assertEquals(7.11, statistics.getAverageSpeed().toMPS(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,8 +54,8 @@ public class TrackStatisticsUpdaterTest {
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
|
||||
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
|
||||
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
|
||||
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(tp1, GPS_DISTANCE);
|
||||
@@ -60,7 +63,7 @@ public class TrackStatisticsUpdaterTest {
|
||||
subject.addTrackPoint(tp3, GPS_DISTANCE);
|
||||
|
||||
// then
|
||||
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01);
|
||||
assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -69,9 +72,9 @@ public class TrackStatisticsUpdaterTest {
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
|
||||
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
|
||||
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
|
||||
tp3.setSpeed(5f);
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
|
||||
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
|
||||
tp3.setSpeed(Speed.of(5f));
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(tp1, GPS_DISTANCE);
|
||||
@@ -79,7 +82,7 @@ public class TrackStatisticsUpdaterTest {
|
||||
subject.addTrackPoint(tp3, GPS_DISTANCE);
|
||||
|
||||
// then
|
||||
assertEquals(1.10, subject.getTrackStatistics().getTotalDistance(), 0.01);
|
||||
assertEquals(1.10, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,15 +91,15 @@ public class TrackStatisticsUpdaterTest {
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
|
||||
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
|
||||
tp2.setSpeed(5f);
|
||||
TrackPoint tp3 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(3000));
|
||||
tp2.setSpeed(5f);
|
||||
TrackPoint tp4 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(4000));
|
||||
tp2.setSpeed(5f);
|
||||
tp4.setSensorDistance(5f);
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
|
||||
tp2.setSpeed(Speed.of(5f));
|
||||
TrackPoint tp3 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
|
||||
tp2.setSpeed(Speed.of(5f));
|
||||
TrackPoint tp4 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
|
||||
tp2.setSpeed(Speed.of(5f));
|
||||
tp4.setSensorDistance(Distance.of(5f));
|
||||
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
|
||||
tp5.setSensorDistance(10f);
|
||||
tp5.setSensorDistance(Distance.of(10f));
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(tp1, GPS_DISTANCE);
|
||||
@@ -104,14 +107,14 @@ public class TrackStatisticsUpdaterTest {
|
||||
subject.addTrackPoint(tp3, GPS_DISTANCE);
|
||||
|
||||
// then
|
||||
assertEquals(110.57, subject.getTrackStatistics().getTotalDistance(), 0.01);
|
||||
assertEquals(110.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(tp4, GPS_DISTANCE);
|
||||
subject.addTrackPoint(tp5, GPS_DISTANCE);
|
||||
|
||||
// then
|
||||
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance(), 0.01);
|
||||
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -120,12 +123,12 @@ public class TrackStatisticsUpdaterTest {
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
|
||||
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
|
||||
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
|
||||
TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000));
|
||||
tp4.setSensorDistance(5f);
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
|
||||
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
|
||||
TrackPoint tp4 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
|
||||
tp4.setSensorDistance(Distance.of(5f));
|
||||
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
|
||||
tp5.setSensorDistance(10f);
|
||||
tp5.setSensorDistance(Distance.of(10f));
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(tp1, GPS_DISTANCE);
|
||||
@@ -133,14 +136,42 @@ public class TrackStatisticsUpdaterTest {
|
||||
subject.addTrackPoint(tp3, GPS_DISTANCE);
|
||||
|
||||
// then
|
||||
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01);
|
||||
assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(tp4, GPS_DISTANCE);
|
||||
subject.addTrackPoint(tp5, GPS_DISTANCE);
|
||||
|
||||
// then
|
||||
assertEquals(15, subject.getTrackStatistics().getTotalDistance(), 0.01);
|
||||
assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
|
||||
// given
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
|
||||
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
|
||||
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
|
||||
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
|
||||
tp3.setSensorDistance(Distance.of(5f));
|
||||
TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
|
||||
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(tp1, GPS_DISTANCE);
|
||||
subject.addTrackPoint(tp2, GPS_DISTANCE);
|
||||
subject.addTrackPoint(tp3, GPS_DISTANCE);
|
||||
|
||||
// then
|
||||
assertEquals(5, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
|
||||
// when
|
||||
subject.addTrackPoint(tp4, GPS_DISTANCE);
|
||||
subject.addTrackPoint(tp5, GPS_DISTANCE);
|
||||
|
||||
// then
|
||||
assertEquals(59.18, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||
}
|
||||
|
||||
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||
|
||||
@@ -23,15 +27,21 @@ import static org.junit.Assert.assertEquals;
|
||||
public class AnnouncementUtilsTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric() {
|
||||
public void getAnnouncement_metric_speed() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setTotalElevationGain(6000f);
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, null);
|
||||
@@ -41,22 +51,196 @@ public class AnnouncementUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_metric() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setTotalElevationGain(6000f);
|
||||
|
||||
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000);
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
public void getAnnouncement_withInterval_metric_speed() {
|
||||
// given
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0));
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 18.4 kilometers per hour Lap speed of 51.2 kilometers per hour", announcement);
|
||||
assertEquals(
|
||||
"OpenTracks total distance " +
|
||||
StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first +
|
||||
" kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
|
||||
StringUtils.getSpeedParts(context, stats.getAverageMovingSpeed(), true, true).first +
|
||||
" kilometers per hour Lap speed of " +
|
||||
StringUtils.getSpeedParts(context, lastInterval.getSpeed(), true, true).first +
|
||||
" kilometers per hour",
|
||||
announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_pace() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, false, null);
|
||||
|
||||
// then
|
||||
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 3 minutes 15 seconds per kilometer", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_metric_pace() {
|
||||
// given
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0));
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals(
|
||||
"OpenTracks total distance " +
|
||||
StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first +
|
||||
" kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
|
||||
buildAndGetTimeText(stats.getAverageMovingSpeed().toPace(true), true) +
|
||||
" per kilometer Lap time of " +
|
||||
buildAndGetTimeText(lastInterval.getSpeed().toPace(true), true) +
|
||||
" per kilometer",
|
||||
announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_imperial_speed() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, true, null);
|
||||
|
||||
// then
|
||||
assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 11.4 miles per hour", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_imperial_speed() {
|
||||
// given
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0));
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval);
|
||||
|
||||
// then
|
||||
assertEquals(
|
||||
"OpenTracks total distance " +
|
||||
StringUtils.getDistanceParts(context, stats.getTotalDistance(), false).first +
|
||||
" miles in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
|
||||
StringUtils.getSpeedParts(context, stats.getAverageMovingSpeed(), false, true).first +
|
||||
" miles per hour Lap speed of " +
|
||||
StringUtils.getSpeedParts(context, lastInterval.getSpeed(), false, true).first +
|
||||
" miles per hour",
|
||||
announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_imperial_pace() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, false, null);
|
||||
|
||||
// then
|
||||
assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_imperial_pace() {
|
||||
// given
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0));
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval);
|
||||
|
||||
// then
|
||||
//assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile Lap time of 1 minute 53 seconds per mile", announcement);
|
||||
assertEquals(
|
||||
"OpenTracks total distance " +
|
||||
StringUtils.getDistanceParts(context, stats.getTotalDistance(), false).first +
|
||||
" miles in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " +
|
||||
buildAndGetTimeText(stats.getAverageMovingSpeed().toPace(false), true) +
|
||||
" per mile Lap time of " +
|
||||
buildAndGetTimeText(lastInterval.getSpeed().toPace(false), true) +
|
||||
" per mile",
|
||||
announcement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an returns the text representing the duration's time.
|
||||
*
|
||||
* @param duration Duration object.
|
||||
* @param showFromMinutes show minutes tough it's 0.
|
||||
* @return text representing the duratin's time.
|
||||
*/
|
||||
private String buildAndGetTimeText(Duration duration, boolean showFromMinutes) {
|
||||
long hours = Math.abs(duration.getSeconds()) / 3600;
|
||||
long minutes = (Math.abs(duration.getSeconds()) % 3600) / 60;
|
||||
long seconds = Math.abs(duration.getSeconds()) % 60;
|
||||
String hUnit = hours > 1 || hours == 0 ? "hours" : "hour";
|
||||
String mUnit = minutes > 1 || minutes == 0 ? "minutes" : "minute";
|
||||
String sUnit = seconds > 1 || seconds == 0 ? "seconds" : "second";
|
||||
|
||||
String res = hours > 0 ? hours + " " + hUnit : "";
|
||||
if (hours > 0) {
|
||||
res += minutes > 0 || showFromMinutes ? " " + minutes + " " + mUnit : "";
|
||||
} else {
|
||||
res += minutes > 0 || showFromMinutes ? minutes + " " + mUnit : "";
|
||||
}
|
||||
if (hours > 0 || minutes > 0 || showFromMinutes) {
|
||||
res += " " + seconds + " " + sUnit;
|
||||
} else {
|
||||
res += seconds + " " + sUnit;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class EGM2008UtilsTest {
|
||||
|
||||
private static final double MAX_BILINEAR_ERROR = 0.478;
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void fileVerification() throws IOException {
|
||||
// given
|
||||
int expectedLength = 18671444;
|
||||
int expectedHeaderLength = 404;
|
||||
|
||||
String expectedHeader = "P5\n" +
|
||||
"# Geoid file in PGM format for the GeographicLib::Geoid class\n" +
|
||||
"# Description WGS84 EGM2008, 5-minute grid\n" +
|
||||
"# URL http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008\n" +
|
||||
"# DateTime 2009-08-29 18:45:00\n" +
|
||||
"# MaxBilinearError 0.478\n" +
|
||||
"# RMSBilinearError 0.012\n" +
|
||||
"# MaxCubicError 0.294\n" +
|
||||
"# RMSCubicError 0.005\n" +
|
||||
"# Offset -108\n" +
|
||||
"# Scale 0.003\n" +
|
||||
"# Origin 90N 0E\n" +
|
||||
"# AREA_OR_POINT Point\n" +
|
||||
"# Vertical_Datum WGS84\n" +
|
||||
"4320 2161\n" +
|
||||
"65535" +
|
||||
"\n";
|
||||
|
||||
// when
|
||||
try (InputStream inputStream = context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA)) {
|
||||
assertEquals(expectedLength, inputStream.available());
|
||||
|
||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII);
|
||||
char[] data = new char[expectedHeaderLength];
|
||||
int length = reader.read(data);
|
||||
|
||||
// then
|
||||
assertEquals(expectedHeaderLength, length);
|
||||
assertEquals(expectedHeader, new String(data));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Northpole() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(90);
|
||||
trackPoint.setLongitude(0.1);
|
||||
trackPoint.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
|
||||
// then
|
||||
assertEquals(-14.8980, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Southpole() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(-90);
|
||||
trackPoint.setLongitude(0);
|
||||
trackPoint.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
|
||||
// then
|
||||
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Southpole2() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(-90);
|
||||
trackPoint.setLongitude(180);
|
||||
trackPoint.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
|
||||
// then
|
||||
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Southpole3() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(-90);
|
||||
trackPoint.setLongitude(-180);
|
||||
trackPoint.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
|
||||
// then
|
||||
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_0() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(0);
|
||||
trackPoint.setLongitude(0);
|
||||
trackPoint.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
|
||||
// then
|
||||
assertEquals(-17.2260, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Berlin() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(52.530644);
|
||||
trackPoint.setLongitude(13.383068);
|
||||
trackPoint.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
|
||||
// then
|
||||
assertEquals(-39.4865, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Berlin_Caching() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint1.setLatitude(52.530644);
|
||||
trackPoint1.setLongitude(13.383068);
|
||||
trackPoint1.setAltitude(0);
|
||||
|
||||
TrackPoint trackPoint2 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint2.setLatitude(52.530000);
|
||||
trackPoint2.setLongitude(13.380000);
|
||||
trackPoint2.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint1.getLocation());
|
||||
|
||||
// then
|
||||
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_MaxUndulation() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(-8.417);
|
||||
trackPoint.setLongitude(147.367);
|
||||
trackPoint.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
|
||||
// then
|
||||
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getIndices() {
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(90);
|
||||
trackPoint.setLongitude(0);
|
||||
assertEquals(new EGM2008Utils.Indices(0, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
|
||||
|
||||
trackPoint.setLatitude(0);
|
||||
trackPoint.setLongitude(0);
|
||||
assertEquals(new EGM2008Utils.Indices(1080, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
|
||||
|
||||
trackPoint.setLatitude(-90);
|
||||
trackPoint.setLongitude(180);
|
||||
assertEquals(new EGM2008Utils.Indices(2160, 4320 / 2), EGM2008Utils.getIndices(trackPoint.getLocation()));
|
||||
|
||||
trackPoint.setLatitude(-90);
|
||||
trackPoint.setLongitude(-180);
|
||||
assertEquals(new EGM2008Utils.Indices(2160, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUndulationRaw_ok() throws IOException {
|
||||
try (DataInputStream dataInputStream = new DataInputStream(context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA))) {
|
||||
assertEquals(40966, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(0, 0)));
|
||||
assertEquals(41742, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(1081, 0)));
|
||||
assertEquals(25950, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(2160, 4319)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = EOFException.class)
|
||||
public void getUndulationRaw_error() throws IOException {
|
||||
try (DataInputStream dataInputStream = new DataInputStream(context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA))) {
|
||||
assertEquals(0, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(2161, 4320)), 0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,14 @@ 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
|
||||
|
||||
@@ -9,7 +9,7 @@ public class PressureSensorUtilsTest {
|
||||
|
||||
// real data at 500Hz stationary
|
||||
@Test
|
||||
public void elevationChanges_none() {
|
||||
public void altitudeChanges_none() {
|
||||
// given
|
||||
float[] sensorValues_hPa = new float[]{1015.6876f, 1015.699f, 1015.70905f, 1015.71075f, 1015.7224f, 1015.72f, 1015.7373f, 1015.7481f, 1015.74133f, 1015.73553f, 1015.7462f, 1015.74896f, 1015.752f, 1015.754f, 1015.7456f, 1015.73303f, 1015.7288f, 1015.7239f, 1015.7298f, 1015.7387f, 1015.72534f, 1015.72577f, 1015.72797f, 1015.71796f, 1015.7113f, 1015.7097f, 1015.7096f, 1015.7013f, 1015.7072f, 1015.71063f, 1015.7088f, 1015.7115f, 1015.713f, 1015.7121f, 1015.7062f, 1015.7062f, 1015.6994f, 1015.69617f, 1015.68945f, 1015.698f, 1015.6913f, 1015.69696f, 1015.69293f, 1015.6955f, 1015.6928f, 1015.6939f, 1015.68787f, 1015.6881f, 1015.68536f, 1015.68726f, 1015.6879f, 1015.6904f, 1015.6937f, 1015.69476f, 1015.7046f, 1015.7019f, 1015.7079f, 1015.71124f, 1015.71216f, 1015.71436f, 1015.7171f, 1015.7186f, 1015.7186f, 1015.7246f, 1015.72046f, 1015.7211f, 1015.72375f, 1015.7194f, 1015.72125f, 1015.7161f, 1015.71185f, 1015.7188f, 1015.721f, 1015.7252f, 1015.72266f, 1015.7236f, 1015.72614f, 1015.7266f, 1015.7344f, 1015.7376f, 1015.7337f, 1015.7253f, 1015.71875f, 1015.7219f, 1015.7219f, 1015.7317f, 1015.7284f, 1015.73444f, 1015.73584f, 1015.7369f, 1015.73254f, 1015.7369f, 1015.7336f, 1015.73254f, 1015.73676f, 1015.74176f, 1015.74506f, 1015.7478f, 1015.755f, 1015.7515f, 1015.7543f, 1015.7459f, 1015.7468f, 1015.7492f, 1015.74585f, 1015.74756f, 1015.74756f, 1015.75085f, 1015.754f, 1015.75336f, 1015.7568f, 1015.75165f, 1015.761f, 1015.7527f, 1015.75684f, 1015.7635f, 1015.7585f, 1015.7552f, 1015.7475f, 1015.74506f, 1015.7542f, 1015.75586f, 1015.74176f, 1015.74414f, 1015.7501f, 1015.751f, 1015.7478f, 1015.755f, 1015.75836f, 1015.7592f, 1015.75256f, 1015.7559f, 1015.76654f, 1015.76154f, 1015.766f, 1015.77014f, 1015.76904f, 1015.76013f, 1015.7674f, 1015.76514f, 1015.7674f, 1015.7635f, 1015.76404f, 1015.7607f, 1015.754f, 1015.7574f, 1015.7607f, 1015.7607f, 1015.7607f, 1015.754f, 1015.7567f, 1015.7616f, 1015.7643f, 1015.7666f, 1015.7609f, 1015.766f, 1015.76764f, 1015.755f, 1015.75757f, 1015.74927f, 1015.74927f, 1015.7426f, 1015.745f, 1015.745f, 1015.7409f, 1015.7376f, 1015.7326f, 1015.74164f, 1015.74664f, 1015.745f, 1015.7459f, 1015.7502f, 1015.7493f, 1015.7476f, 1015.7493f, 1015.751f, 1015.7601f, 1015.76434f, 1015.766f, 1015.756f, 1015.7617f, 1015.7623f, 1015.76404f, 1015.76733f, 1015.7607f, 1015.7623f, 1015.76337f, 1015.7584f, 1015.7573f, 1015.7502f, 1015.7534f, 1015.7534f, 1015.7601f, 1015.75684f, 1015.7518f, 1015.75574f, 1015.7524f, 1015.7507f, 1015.7484f, 1015.7502f, 1015.7534f, 1015.7623f, 1015.75903f, 1015.75574f, 1015.75574f, 1015.7573f, 1015.7524f, 1015.75574f, 1015.75574f, 1015.7607f, 1015.7573f, 1015.754f, 1015.74677f, 1015.754f, 1015.75574f, 1015.75903f, 1015.7573f, 1015.7551f, 1015.7573f, 1015.754f, 1015.7507f, 1015.7374f, 1015.739f, 1015.73566f, 1015.73566f, 1015.7367f, 1015.7417f, 1015.7417f, 1015.739f, 1015.74835f, 1015.74567f, 1015.7517f, 1015.74066f, 1015.7374f, 1015.739f, 1015.7417f, 1015.73065f, 1015.72894f, 1015.73505f, 1015.73334f, 1015.74005f, 1015.74005f, 1015.7367f, 1015.7434f, 1015.744f, 1015.7423f, 1015.7384f, 1015.739f, 1015.7374f, 1015.7374f, 1015.7374f, 1015.7341f, 1015.73505f, 1015.7341f, 1015.7267f, 1015.72833f, 1015.7234f, 1015.7167f, 1015.725f, 1015.7217f, 1015.7234f, 1015.73004f, 1015.7324f, 1015.7423f, 1015.74005f, 1015.73505f, 1015.73004f, 1015.739f, 1015.7417f, 1015.74567f, 1015.74066f, 1015.74146f, 1015.74146f, 1015.73816f, 1015.7274f, 1015.72906f, 1015.7274f, 1015.7341f, 1015.739f, 1015.744f, 1015.7417f, 1015.7374f, 1015.7324f, 1015.73816f, 1015.74146f, 1015.7374f, 1015.7341f, 1015.7365f, 1015.7365f, 1015.74146f, 1015.74414f, 1015.7475f, 1015.7448f, 1015.74146f, 1015.74585f, 1015.7475f, 1015.75146f, 1015.75146f, 1015.7591f, 1015.75476f, 1015.7614f, 1015.75476f, 1015.7492f, 1015.74817f, 1015.7498f, 1015.7515f, 1015.75586f, 1015.7509f, 1015.7448f, 1015.7525f, 1015.7581f, 1015.7592f};
|
||||
float firstSensorValue = sensorValues_hPa[0];
|
||||
@@ -20,7 +20,7 @@ public class PressureSensorUtilsTest {
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate a sudden drop of elevation.
|
||||
// Simulate a sudden drop of altitude.
|
||||
// test data
|
||||
@Test
|
||||
public void computeChanges_downhill() {
|
||||
@@ -28,22 +28,22 @@ public class PressureSensorUtilsTest {
|
||||
float firstSensorValue = sensorValues_hPa[0];
|
||||
|
||||
// when
|
||||
float elevationGain_m = 0;
|
||||
float elevationLoss_m = 0;
|
||||
float altitudeGain_m = 0;
|
||||
float altitudeLoss = 0;
|
||||
float lastUsedPressureValue_hPa = firstSensorValue;
|
||||
|
||||
for (float v : sensorValues_hPa) {
|
||||
PressureSensorUtils.ElevationChange elevationChange = PressureSensorUtils.computeChanges_m(lastUsedPressureValue_hPa, v);
|
||||
if (elevationChange != null) {
|
||||
elevationGain_m += elevationChange.getElevationGain_m();
|
||||
elevationLoss_m += elevationChange.getElevationLoss_m();
|
||||
lastUsedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa();
|
||||
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChanges_m(lastUsedPressureValue_hPa, v);
|
||||
if (altitudeChange != null) {
|
||||
altitudeGain_m += altitudeChange.getAltitudeGain_m();
|
||||
altitudeLoss += altitudeChange.getAltitudeLoss_m();
|
||||
lastUsedPressureValue_hPa = altitudeChange.getCurrentSensorValue_hPa();
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
assertEquals(0f, elevationGain_m, 0.01);
|
||||
assertEquals(15.0f, elevationLoss_m, 0.01);
|
||||
assertEquals(0f, altitudeGain_m, 0.01);
|
||||
assertEquals(15.0f, altitudeLoss, 0.01);
|
||||
assertEquals(1016.80f, lastUsedPressureValue_hPa, 0.01); //Expect exponential smoothing
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ import java.time.Duration;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -70,19 +73,18 @@ public class StringUtilsTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#formatDistance(android.content.Context, double,
|
||||
* boolean)}.
|
||||
* Tests {@link StringUtils#formatDistance(android.content.Context, Distance, boolean)}.
|
||||
*/
|
||||
@Test
|
||||
public void testFormatDistance() {
|
||||
// A large number in metric
|
||||
assertEquals("5.00 km", StringUtils.formatDistance(context, 5000, true));
|
||||
assertEquals("5.00 km", StringUtils.formatDistance(context, Distance.of(5000), true));
|
||||
// A large number in imperial
|
||||
assertEquals("3.11 mi", StringUtils.formatDistance(context, 5000, false));
|
||||
assertEquals("3.11 mi", StringUtils.formatDistance(context, Distance.of(5000), false));
|
||||
// A small number in metric
|
||||
assertEquals("100.00 m", StringUtils.formatDistance(context, 100, true));
|
||||
assertEquals("100.00 m", StringUtils.formatDistance(context, Distance.of(100), true));
|
||||
// A small number in imperial
|
||||
assertEquals("328.08 ft", StringUtils.formatDistance(context, 100, false));
|
||||
assertEquals("328.08 ft", StringUtils.formatDistance(context, Distance.of(100), false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,35 +153,35 @@ public class StringUtilsTest {
|
||||
assertEquals("10", StringUtils.formatDecimal(10.1, 0));
|
||||
assertEquals("-0", StringUtils.formatDecimal(-0.1, 0));
|
||||
|
||||
assertEquals("0", StringUtils.formatDecimal(0.0, 2));
|
||||
assertEquals("0.1", StringUtils.formatDecimal(0.1, 2));
|
||||
assertEquals("1.1", StringUtils.formatDecimal(1.1, 2));
|
||||
assertEquals("10", StringUtils.formatDecimal(10, 2));
|
||||
assertEquals("10.1", StringUtils.formatDecimal(10.1, 2));
|
||||
assertEquals("0.00", StringUtils.formatDecimal(0.0, 2));
|
||||
assertEquals("0.10", StringUtils.formatDecimal(0.1, 2));
|
||||
assertEquals("1.10", StringUtils.formatDecimal(1.1, 2));
|
||||
assertEquals("10.00", StringUtils.formatDecimal(10, 2));
|
||||
assertEquals("10.10", StringUtils.formatDecimal(10.1, 2));
|
||||
assertEquals("10.11", StringUtils.formatDecimal(10.111, 2));
|
||||
assertEquals("-0.1", StringUtils.formatDecimal(-0.1, 2));
|
||||
assertEquals("-0.10", StringUtils.formatDecimal(-0.1, 2));
|
||||
|
||||
assertEquals("1", StringUtils.formatDecimal(0.99, 1));
|
||||
assertEquals("1.0", StringUtils.formatDecimal(0.99, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts() {
|
||||
assertEquals("4:59", StringUtils.getSpeedParts(context, 3.34, true, false).first);
|
||||
assertEquals("5:00", StringUtils.getSpeedParts(context, 3.33, true, false).first);
|
||||
assertEquals("4:59", StringUtils.getSpeedParts(context, Speed.of(3.34), true, false).first);
|
||||
assertEquals("5:00", StringUtils.getSpeedParts(context, Speed.of(3.33), true, false).first);
|
||||
|
||||
assertEquals("11.9", StringUtils.getSpeedParts(context, 3.31, true, true).first);
|
||||
assertEquals("7.5", StringUtils.getSpeedParts(context, 3.34, false, true).first);
|
||||
assertEquals("11.9", StringUtils.getSpeedParts(context, Speed.of(3.31), true, true).first);
|
||||
assertEquals("7.5", StringUtils.getSpeedParts(context, Speed.of(3.34), false, true).first);
|
||||
|
||||
assertEquals("min/km", StringUtils.getSpeedParts(context, 0, true, false).second);
|
||||
assertEquals("min/mi", StringUtils.getSpeedParts(context, 0, false, false).second);
|
||||
assertEquals("min/km", StringUtils.getSpeedParts(context, Speed.zero(), true, false).second);
|
||||
assertEquals("min/mi", StringUtils.getSpeedParts(context, Speed.zero(), false, false).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatSpeed() {
|
||||
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, 3.34, true, false));
|
||||
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, 3.34, false, false));
|
||||
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, Speed.of(3.34), true, false));
|
||||
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, Speed.of(3.34), false, false));
|
||||
|
||||
assertEquals("12.02 km/h", StringUtils.formatSpeed(context, 3.34, true, true));
|
||||
assertEquals("7.47 mph", StringUtils.formatSpeed(context, 3.34, false, true));
|
||||
assertEquals("12.0 km/h", StringUtils.formatSpeed(context, Speed.of(3.34), true, true));
|
||||
assertEquals("7.5 mph", StringUtils.formatSpeed(context, Speed.of(3.34), false, true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class UintUtilsTest {
|
||||
|
||||
@Test
|
||||
public void diff() {
|
||||
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT16_MAX));
|
||||
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT16_MAX));
|
||||
assertEquals(3, UintUtils.diff(5, 2, UintUtils.UINT16_MAX));
|
||||
assertEquals(65534, UintUtils.diff(1, 2, UintUtils.UINT16_MAX));
|
||||
|
||||
assertEquals(65530, UintUtils.diff(UintUtils.UINT16_MAX, 5, UintUtils.UINT16_MAX));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realData() {
|
||||
assertEquals(1, UintUtils.diff(381616, 381615, UintUtils.UINT32_MAX));
|
||||
}
|
||||
}
|
||||
+40
-38
@@ -13,6 +13,8 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
@@ -25,17 +27,17 @@ public class AggregatedStatisticsTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
private static Track createTrack(Context context, long totalDistance, long totalTime, String category) {
|
||||
private static Track createTrack(Context context, Distance totalDistance, Duration totalTime, String category) {
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
statistics.setStartTime(Instant.ofEpochMilli(1000L)); // Resulting start time
|
||||
statistics.setStopTime(Instant.ofEpochMilli(1000L + totalTime));
|
||||
statistics.setTotalTime(Duration.ofMillis(totalTime));
|
||||
statistics.setMovingTime(Duration.ofMillis(totalTime));
|
||||
statistics.setStopTime(statistics.getStartTime().plus(totalTime));
|
||||
statistics.setTotalTime(totalTime);
|
||||
statistics.setMovingTime(totalTime);
|
||||
statistics.setTotalDistance(totalDistance);
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
statistics.setMaxSpeed(50.0); // Resulting max speed
|
||||
statistics.setMaxElevation(1250.0);
|
||||
statistics.setMinElevation(1200.0); // Resulting min elevation
|
||||
statistics.setTotalAltitudeGain(50.0f);
|
||||
statistics.setMaxSpeed(Speed.of(50.0)); // Resulting max speed
|
||||
statistics.setMaxAltitude(1250.0);
|
||||
statistics.setMinAltitude(1200.0); // Resulting min altitude
|
||||
|
||||
Track track = new Track();
|
||||
track.setIcon(TrackIconUtils.getIconValue(context, category));
|
||||
@@ -48,8 +50,8 @@ public class AggregatedStatisticsTest {
|
||||
public void testAggregate() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
Distance totalDistance = Distance.of(10000);
|
||||
Duration totalTime = Duration.ofMillis(2400000);
|
||||
String biking = context.getString(R.string.activity_type_biking);
|
||||
Track track = createTrack(context, totalDistance, totalTime, biking);
|
||||
|
||||
@@ -62,16 +64,16 @@ public class AggregatedStatisticsTest {
|
||||
assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
|
||||
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance());
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregate_mountainBiking() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
Distance totalDistance = Distance.of(10000);
|
||||
Duration totalTime = Duration.ofMillis(2400000);
|
||||
String mountainBiking = context.getString(R.string.activity_type_mountain_biking);
|
||||
Track track = createTrack(context, totalDistance, totalTime, mountainBiking);
|
||||
|
||||
@@ -86,8 +88,8 @@ public class AggregatedStatisticsTest {
|
||||
public void testAggregate_trailRunning() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
Distance totalDistance = Distance.of(10000);
|
||||
Duration totalTime = Duration.ofMillis(2400000);
|
||||
String trailRunning = context.getString(R.string.activity_type_trail_running);
|
||||
Track track = createTrack(context, totalDistance, totalTime, trailRunning);
|
||||
|
||||
@@ -102,8 +104,8 @@ public class AggregatedStatisticsTest {
|
||||
public void testAggregate_twoBikingTracks() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
Distance totalDistance = Distance.of(10000);
|
||||
Duration totalTime = Duration.ofMillis(2400000);
|
||||
String biking = context.getString(R.string.activity_type_biking);
|
||||
List<Track> tracks = List.of(createTrack(context, totalDistance, totalTime, biking), createTrack(context, totalDistance, totalTime, biking));
|
||||
|
||||
@@ -116,20 +118,20 @@ public class AggregatedStatisticsTest {
|
||||
assertEquals(2, aggregatedStatistics.get(biking).getCountTracks());
|
||||
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
|
||||
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregate_threeDifferentTracks() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
Distance totalDistance = Distance.of(10000);
|
||||
|
||||
String biking = context.getString(R.string.activity_type_biking);
|
||||
String running = context.getString(R.string.activity_type_running);
|
||||
String walking = context.getString(R.string.activity_type_walking);
|
||||
long totalTime = 2400000;
|
||||
Duration totalTime = Duration.ofMillis(2400000);
|
||||
List<Track> tracks = List.of(
|
||||
createTrack(context, totalDistance, totalTime, biking),
|
||||
createTrack(context, totalDistance, totalTime, running),
|
||||
@@ -150,20 +152,20 @@ public class AggregatedStatisticsTest {
|
||||
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance());
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance());
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance());
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +173,8 @@ public class AggregatedStatisticsTest {
|
||||
public void testAggregate_severalTracksWithSeveralActivities() {
|
||||
// given
|
||||
// 10km in 40 minutes.
|
||||
long totalDistance = 10000;
|
||||
long totalTime = 2400000;
|
||||
Distance totalDistance = Distance.of(10000);
|
||||
Duration totalTime = Duration.ofMillis(2400000);
|
||||
String biking = context.getString(R.string.activity_type_biking);
|
||||
String running = context.getString(R.string.activity_type_running);
|
||||
String walking = context.getString(R.string.activity_type_walking);
|
||||
@@ -213,29 +215,29 @@ public class AggregatedStatisticsTest {
|
||||
// Biking.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
|
||||
assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime * 5), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance.multipliedBy(5), statistics2.getTotalDistance());
|
||||
assertEquals(totalTime.multipliedBy(5), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Running.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
|
||||
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
|
||||
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Walking.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
|
||||
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
|
||||
assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Driving.
|
||||
{
|
||||
TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
|
||||
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime());
|
||||
assertEquals(totalDistance, statistics2.getTotalDistance());
|
||||
assertEquals(totalTime, statistics2.getMovingTime());
|
||||
}
|
||||
|
||||
// Check order
|
||||
|
||||
+104
-93
@@ -1,14 +1,23 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
@@ -18,121 +27,123 @@ import static org.junit.Assert.assertEquals;
|
||||
@RunWith(JUnit4.class)
|
||||
public class IntervalStatisticsTest {
|
||||
|
||||
private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
|
||||
private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
|
||||
|
||||
private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) {
|
||||
return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second;
|
||||
}
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
|
||||
@Before
|
||||
public void setUp() {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
}
|
||||
|
||||
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
|
||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
for (TrackPoint tp : trackPoints) {
|
||||
trackStatisticsUpdater.addTrackPoint(tp, 0);
|
||||
trackStatisticsUpdater.addTrackPoint(tp, Distance.of(0));
|
||||
}
|
||||
return trackStatisticsUpdater.getTrackStatistics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_1() {
|
||||
// With 50 points and interval distance of 1000m.
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_1() {
|
||||
// With 50 points and interval distance of 1000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(50);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 1000f;
|
||||
// given
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
}
|
||||
// when and then
|
||||
whenAndThen(50, distanceInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_2() {
|
||||
// With 200 points and interval distance of 1000m.
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_2() {
|
||||
// With 200 points and interval distance of 1000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(200);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 1000f;
|
||||
// given
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
}
|
||||
// when and then
|
||||
whenAndThen(200, distanceInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_3() {
|
||||
// With 200 points and interval distance of 3000m.
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_3() {
|
||||
// With 200 points and interval distance of 3000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(200);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 3000f;
|
||||
// given
|
||||
float distanceInterval = 3000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
}
|
||||
// when and then
|
||||
whenAndThen(3000, distanceInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_4() {
|
||||
// With 1000 points and interval distance of 3000m.
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_4() {
|
||||
// With 1000 points and interval distance of 3000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(1000);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 3000f;
|
||||
// given
|
||||
float distanceInterval = 3000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
}
|
||||
// when and then
|
||||
whenAndThen(1000, distanceInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_5() {
|
||||
// With 10000 points and interval distance of 1000m.
|
||||
/**
|
||||
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
|
||||
*/
|
||||
@Test
|
||||
public void testBuild_5() {
|
||||
// With 10000 points and interval distance of 1000m.
|
||||
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(10000);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 1000f;
|
||||
// given
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackPoints, trackStatistics, distanceInterval);
|
||||
}
|
||||
// when and then
|
||||
whenAndThen(10000, distanceInterval);
|
||||
}
|
||||
|
||||
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) {
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
|
||||
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
|
||||
double totalDistance = 0d;
|
||||
float totalTime = 0L;
|
||||
float totalGain = 0f;
|
||||
for (IntervalStatistics.Interval i : intervalList) {
|
||||
totalDistance += i.getDistance_m();
|
||||
totalTime += i.getDistance_m() / i.getSpeed_ms();
|
||||
totalGain += i.getGain_m();
|
||||
}
|
||||
private void whenAndThen(int numberOfPoints, float distanceInterval) {
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(distanceInterval), Distance.of(0));
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics trackStatistics = trackWithStats.second;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
}
|
||||
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
|
||||
Distance totalDistance = Distance.of(0);
|
||||
float totalTime = 0L;
|
||||
float totalGain = 0f;
|
||||
for (IntervalStatistics.Interval i : intervalList) {
|
||||
totalDistance = totalDistance.plus(i.getDistance());
|
||||
totalTime += i.getDistance().toM() / i.getSpeed().toMPS();
|
||||
totalGain += i.getGain_m();
|
||||
}
|
||||
|
||||
// then
|
||||
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01);
|
||||
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
|
||||
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
|
||||
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ELEVATION_GAIN, 0.1);
|
||||
for (int i = 0; i < intervalList.size() - 1; i++) {
|
||||
assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001);
|
||||
totalDistance -= intervalList.get(i).getDistance_m();
|
||||
}
|
||||
assertEquals(intervalList.get(intervalList.size() - 1).getDistance_m(), totalDistance, 0.01);
|
||||
}
|
||||
// then
|
||||
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
|
||||
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
|
||||
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
|
||||
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
|
||||
|
||||
for (int i = 0; i < intervalList.size() - 1; i++) {
|
||||
assertEquals(intervalList.get(i).getDistance().toM(), distanceInterval, 0.001);
|
||||
totalDistance = totalDistance.minus(intervalList.get(i).getDistance());
|
||||
}
|
||||
assertEquals(intervalList.get(intervalList.size() - 1).getDistance().toM(), totalDistance.toM(), 0.01);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2021-05-29T18:06:21.767Z</when>
|
||||
<gx:coord />
|
||||
|
||||
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||
<when>2021-05-29T18:06:22.042Z</when>
|
||||
|
||||
<when>2021-05-29T18:06:22.192Z</when>
|
||||
<when>2021-05-29T18:06:22.318Z</when>
|
||||
<when>2021-05-29T18:06:22.512Z</when>
|
||||
|
||||
<gx:coord>14.001 3.0 10.0</gx:coord>
|
||||
<gx:coord>14.002 3.0 10.0</gx:coord>
|
||||
<gx:coord />
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2021-05-29T18:06:22.042Z</when>
|
||||
<gx:coord />
|
||||
|
||||
<when>2021-05-29T18:06:21.767Z</when>
|
||||
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2021-05-29T18:06:21.767Z</when>
|
||||
<gx:coord />
|
||||
|
||||
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
<Placemark>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track />
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
|
||||
<Document>
|
||||
<open>1</open>
|
||||
<visibility>1</visibility>
|
||||
<name><![CDATA[2020-08-24 20:57]]></name>
|
||||
<atom:generator><![CDATA[OpenTracks]]></atom:generator>
|
||||
<Style id="track">
|
||||
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Style id="start"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="end"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="waypoint"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
|
||||
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="speed" type="float">
|
||||
<displayName><![CDATA[Speed (m/s)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="power" type="float">
|
||||
<displayName><![CDATA[Power (W)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="float">
|
||||
<displayName><![CDATA[Cadence (rpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="heart_rate" type="float">
|
||||
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-08-24 20:57]]></name>
|
||||
<description><![CDATA[]]></description>
|
||||
<icon><![CDATA[UNKNOWN]]></icon>
|
||||
<opentracks:trackid>9316e527-a5b8-47c5-bdbd-954b4be7d80e</opentracks:trackid>
|
||||
<styleUrl>#track</styleUrl>
|
||||
<ExtendedData>
|
||||
<Data name="type"><value><![CDATA[unknown]]></value></Data>
|
||||
</ExtendedData>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:hasFragileUserData="true"
|
||||
tools:replace="android:hasFragileUserData" />
|
||||
</manifest>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -51,18 +51,14 @@ limitations under the License.
|
||||
android:name=".Startup"
|
||||
android:allowBackup="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:hasFragileUserData="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/ThemeCustom">
|
||||
|
||||
<activity android:name=".AboutActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".TrackDeleteActivity"
|
||||
android:theme="@style/TrackDeleteActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".HelpActivity"
|
||||
android:label="@string/menu_help" />
|
||||
@@ -350,5 +346,18 @@ limitations under the License.
|
||||
android:name=".io.file.importer.ImportService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".services.TrackDeleteService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:icon="@drawable/ic_logo_color_24dp"
|
||||
android:label="@string/delete_service" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -32,9 +31,4 @@ public class AboutActivity extends AbstractActivity {
|
||||
viewBinding = AboutBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 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;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* An abstract class for the following common tasks across
|
||||
* {@link TrackListActivity}, {@link TrackRecordedActivity}, and {@link TrackRecordedActivity}:
|
||||
* <p>
|
||||
* - share track <br>
|
||||
* - delete tracks <br>
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
|
||||
|
||||
private static final String TAG = AbstractListActivity.class.getSimpleName();
|
||||
|
||||
protected static final int GPS_REQUEST_CODE = 6;
|
||||
private static final int DELETE_REQUEST_CODE = 3;
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == DELETE_REQUEST_CODE) {
|
||||
onTrackDeleted();
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete tracks.
|
||||
*
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
protected void deleteTracks(Track.Id... trackIds) {
|
||||
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfirmDeleteDone(Track.Id... trackIds) {
|
||||
boolean stopRecording = false;
|
||||
|
||||
Track.Id recordingTrackId = PreferencesUtils.getRecordingTrackId(PreferencesUtils.getSharedPreferences(this), this);
|
||||
for (Track.Id trackId : trackIds) {
|
||||
if (trackId.equals(recordingTrackId)) {
|
||||
stopRecording = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stopRecording) {
|
||||
getTrackRecordingServiceConnection().stopRecording(this, false);
|
||||
}
|
||||
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
|
||||
intent.putParcelableArrayListExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
|
||||
startActivityForResult(intent, DELETE_REQUEST_CODE);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track recording service connection.
|
||||
* For stopping the current recording if need to delete the current recording track.
|
||||
*/
|
||||
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
|
||||
|
||||
/**
|
||||
* Called after {@link TrackDeleteActivity} returns its result.
|
||||
*/
|
||||
abstract protected void onTrackDeleted();
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2012 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;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
|
||||
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||
import de.dennisguse.opentracks.services.TrackDeleteServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
|
||||
/**
|
||||
* An abstract class for the following common tasks across
|
||||
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
|
||||
* <p>
|
||||
* - delete tracks <br>
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
|
||||
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteServiceConnection.Listener {
|
||||
|
||||
private static final String TAG = AbstractTrackDeleteActivity.class.getSimpleName();
|
||||
|
||||
private TrackDeleteServiceConnection trackDeleteServiceConnection;
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
|
||||
trackDeleteServiceConnection.bind(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (trackDeleteServiceConnection != null) {
|
||||
trackDeleteServiceConnection.unbind(this);
|
||||
trackDeleteServiceConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete tracks.
|
||||
*
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
protected void deleteTracks(Track.Id... trackIds) {
|
||||
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfirmDeleteDone(Track.Id... trackIds) {
|
||||
boolean stopRecording = false;
|
||||
|
||||
onDeleteConfirmed();
|
||||
|
||||
for (Track.Id trackId : trackIds) {
|
||||
if (trackId.equals(getRecordingTrackId())) {
|
||||
stopRecording = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stopRecording) {
|
||||
getTrackRecordingServiceConnection().stopRecording(this);
|
||||
}
|
||||
|
||||
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
|
||||
trackDeleteServiceConnection.startAndBind(this, new ArrayList<>(Arrays.asList(trackIds)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track recording service connection.
|
||||
* For stopping the current recording if need to delete the current recording track.
|
||||
*/
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every time a track is deleted.
|
||||
*/
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
|
||||
trackDeleteServiceConnection.unbind(this);
|
||||
trackDeleteServiceConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void onDeleteConfirmed();
|
||||
|
||||
@Nullable
|
||||
protected Track.Id getRecordingTrackId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connected() {
|
||||
TrackDeleteService service = trackDeleteServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
service.getDeletingStatusObservable().observe(AbstractTrackDeleteActivity.this, status -> onTrackDeleteStatus(status));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright 2012 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;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.drawable.TransitionDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import de.dennisguse.opentracks.databinding.ControllerFragmentBinding;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Track controller for record, pause, resume, and stop.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
//TODO Move buttons into separate components
|
||||
public class ControllerFragment extends Fragment implements View.OnTouchListener {
|
||||
|
||||
private static final String TAG = ControllerFragment.class.getSimpleName();
|
||||
|
||||
private Handler handlerUpdateTotalTime;
|
||||
|
||||
private Runnable buttonDelay;
|
||||
private int buttonDelayDuration;
|
||||
|
||||
private ControllerFragmentBinding viewBinding;
|
||||
private TransitionDrawable transition;
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackRecordingService.RecordingStatus recordingStatus;
|
||||
private TrackRecordingService.RecordingData recordingData;
|
||||
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
Log.w(TAG, "could not get TrackRecordingService");
|
||||
return;
|
||||
}
|
||||
|
||||
service.getRecordingStatusObservable()
|
||||
.observe(ControllerFragment.this, status -> onRecordingStatusChanged(status));
|
||||
|
||||
service.getRecordingDataObservable()
|
||||
.observe(ControllerFragment.this, recordingData -> onTotalTimeChanged(recordingData));
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
buttonDelayDuration = getResources().getInteger(R.integer.buttonDelayMillis);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
handlerUpdateTotalTime = new Handler();
|
||||
recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
viewBinding = ControllerFragmentBinding.inflate(inflater, container, false);
|
||||
|
||||
viewBinding.controllerRecord.setOnTouchListener(this);
|
||||
viewBinding.controllerRecord.setOnClickListener((view) -> {
|
||||
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
getCallback().recordStart();
|
||||
});
|
||||
|
||||
viewBinding.controllerStop.setOnTouchListener(this);
|
||||
transition = (TransitionDrawable) viewBinding.controllerContainer.getBackground();
|
||||
|
||||
onRecordingStatusChanged(recordingStatus);
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
trackRecordingServiceConnection.unbind(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
viewBinding = null;
|
||||
transition = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
trackRecordingServiceConnection = null;
|
||||
handlerUpdateTotalTime = null;
|
||||
recordingStatus = null;
|
||||
}
|
||||
|
||||
//TODO Move into a separate object
|
||||
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
|
||||
@Override
|
||||
public boolean onTouch(final View view, MotionEvent event) {
|
||||
if (!isResumed()) {
|
||||
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
|
||||
return false;
|
||||
}
|
||||
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
|
||||
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
|
||||
buttonDelay = null;
|
||||
view.setPressed(false);
|
||||
|
||||
transition.resetTransition();
|
||||
|
||||
showStatusSetDefaultText();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Trigger button pressed animation
|
||||
view.setPressed(true);
|
||||
|
||||
//To pause a recording
|
||||
if (viewBinding.controllerRecord.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& recordingStatus.isRecordingAndNotPaused()) {
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
ActivityUtils.vibrate(getContext(), 150);
|
||||
viewBinding.controllerStatus.setText(R.string.hold_to_pause);
|
||||
|
||||
buttonDelay = () -> {
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
view.performClick();
|
||||
getCallback().recordPause();
|
||||
|
||||
transition.resetTransition();
|
||||
|
||||
ActivityUtils.vibrate(getContext(), 1000);
|
||||
};
|
||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
//To stop a recording
|
||||
if (viewBinding.controllerStop.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& recordingStatus.isRecording()) {
|
||||
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
ActivityUtils.vibrate(getContext(), 150);
|
||||
viewBinding.controllerStatus.setText(R.string.hold_to_stop);
|
||||
|
||||
buttonDelay = () -> {
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
getCallback().recordStop();
|
||||
ActivityUtils.vibrate(getContext(), 1000);
|
||||
|
||||
transition.resetTransition();
|
||||
};
|
||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
viewBinding.controllerContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
viewBinding.controllerContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void showStatusSetDefaultText() {
|
||||
viewBinding.controllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
|
||||
}
|
||||
|
||||
private Callback getCallback() {
|
||||
return (Callback) getActivity();
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void recordStart();
|
||||
|
||||
void recordPause();
|
||||
|
||||
void recordStop();
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
|
||||
viewBinding.controllerRecord.setImageResource(recordingStatus.isRecordingAndNotPaused() ? R.drawable.ic_button_pause : R.drawable.button_record);
|
||||
viewBinding.controllerRecord.setContentDescription(getContext().getString(recordingStatus.isRecordingAndNotPaused() ? R.string.image_pause : R.string.image_record));
|
||||
|
||||
viewBinding.controllerStop.setEnabled(recordingStatus.isRecording());
|
||||
|
||||
viewBinding.controllerStatus.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
|
||||
if (recordingStatus.isRecording()) {
|
||||
viewBinding.controllerStatus.setTextColor(getContext().getResources().getColor(recordingStatus.isPaused() ? android.R.color.white : R.color.recording_text));
|
||||
showStatusSetDefaultText();
|
||||
}
|
||||
|
||||
viewBinding.controllerTotalTime.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
private void onTotalTimeChanged(TrackRecordingService.RecordingData recordingData) {
|
||||
this.recordingData = recordingData;
|
||||
|
||||
viewBinding.controllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(this.recordingData.getTrackStatistics().getTotalTime()));
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,9 @@ package de.dennisguse.opentracks;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -42,9 +41,10 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.MarkerListBinding;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Activity to show a list of markers in a track.
|
||||
@@ -59,25 +59,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
|
||||
private Track.Id recordingTrackId;
|
||||
private boolean recordingTrackPaused;
|
||||
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, MarkerListActivity.this);
|
||||
}
|
||||
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, MarkerListActivity.this);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(MarkerListActivity.this::invalidateOptionsMenu);
|
||||
}
|
||||
}
|
||||
};
|
||||
private Track track;
|
||||
private MarkerResourceCursorAdapter resourceCursorAdapter;
|
||||
|
||||
@@ -85,6 +68,22 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback();
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
|
||||
private final Runnable bindCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
Log.w(TAG, "could not get TrackRecordingService");
|
||||
return;
|
||||
}
|
||||
|
||||
service.getRecordingStatusObservable()
|
||||
.observe(MarkerListActivity.this, status -> onRecordingStatusChanged(status));
|
||||
}
|
||||
};
|
||||
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
@Override
|
||||
@@ -112,10 +111,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
super.onCreate(savedInstanceState);
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
|
||||
contentProviderUtils = new ContentProviderUtils(this);
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
|
||||
track = trackId != null ? contentProviderUtils.getTrack(trackId) : null;
|
||||
|
||||
@@ -132,18 +129,20 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
viewBinding.markerList.setOnScrollListener(scrollVisibleViews);
|
||||
viewBinding.markerList.setAdapter(resourceCursorAdapter);
|
||||
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||
trackRecordingServiceConnection.startConnection(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
trackRecordingServiceConnection.bind(this);
|
||||
this.invalidateOptionsMenu();
|
||||
loadData(getIntent());
|
||||
}
|
||||
@@ -151,7 +150,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
trackRecordingServiceConnection.unbind(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,7 +160,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
viewBinding = null;
|
||||
resourceCursorAdapter = null;
|
||||
contentProviderUtils = null;
|
||||
sharedPreferences = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,14 +175,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.marker_list_search);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem, null);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingTrackId) && !recordingTrackPaused);
|
||||
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingStatus.getTrackId()) && !recordingStatus.isPaused());
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -263,15 +261,16 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
|
||||
if (loaderCallbacks.getSearchQuery() != null) {
|
||||
loaderCallbacks.setSearch(null);
|
||||
return;
|
||||
}
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@@ -296,6 +295,10 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
}
|
||||
|
||||
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private String searchQuery = null;
|
||||
|
||||
@@ -24,10 +24,10 @@ public class Startup extends Application {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
//Include version information into stack traces.
|
||||
// Include version information into stack traces.
|
||||
Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE);
|
||||
|
||||
//Set default values of preferences on first start.
|
||||
// Set default values of preferences on first start.
|
||||
SharedPreferences sharedPreferences = PreferencesUtils.resetPreferences(this, false);
|
||||
if (PreferencesUtils.getString(sharedPreferences, this, R.string.stats_units_key, "").equals("")) {
|
||||
String statsUnits = getString(Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial : R.string.stats_units_metric);
|
||||
@@ -36,13 +36,7 @@ public class Startup extends Application {
|
||||
|
||||
ActivityUtils.applyNightMode(sharedPreferences, this);
|
||||
|
||||
//TODO Workaround to reset recordingTrackId on app startup as the TrackRecordingService (likely) crashed.
|
||||
if (PreferencesUtils.isRecording(sharedPreferences, this)) {
|
||||
Log.e(TAG, "Reset recordingTrackId; likely the TrackRecordingService crashed.");
|
||||
PreferencesUtils.setLong(sharedPreferences, this, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
|
||||
}
|
||||
|
||||
//In debug builds: show thread and VM warnings.
|
||||
// In debug builds: show thread and VM warnings.
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Enabling strict mode");
|
||||
StrictMode.enableDefaults();
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 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;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.TransitionDrawable;
|
||||
import android.os.Handler;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
* Track controller for record, pause, resume, and stop.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class TrackController implements View.OnTouchListener {
|
||||
|
||||
private static final String TAG = TrackController.class.getSimpleName();
|
||||
|
||||
private final Activity activity;
|
||||
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private final Handler handlerUpdateTotalTime = new Handler();
|
||||
private final boolean alwaysShow;
|
||||
|
||||
private Runnable buttonDelay;
|
||||
private final int buttonDelayDuration;
|
||||
|
||||
private final TrackControllerBinding viewBinding;
|
||||
private final TransitionDrawable transition;
|
||||
|
||||
private boolean isRecording;
|
||||
private boolean isRecordingPaused;
|
||||
private Duration totalTime;
|
||||
private boolean isResumed = false;
|
||||
|
||||
private final Callback callback;
|
||||
|
||||
// A runnable to update the total time.
|
||||
private final Runnable updateTotalTimeRunnable = new Runnable() {
|
||||
public void run() {
|
||||
if (isResumed && isRecording && !isRecordingPaused) {
|
||||
updateTotalTime();
|
||||
setTotalTime();
|
||||
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, Callback callback) {
|
||||
this.activity = activity;
|
||||
this.viewBinding = viewBinding;
|
||||
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
|
||||
this.alwaysShow = alwaysShow;
|
||||
this.callback = callback;
|
||||
|
||||
buttonDelayDuration = activity.getResources().getInteger(R.integer.buttonDelayMillis);
|
||||
|
||||
viewBinding.trackControllerRecord.setOnTouchListener(this);
|
||||
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
|
||||
if (buttonDelay != null || (isRecording && !isRecordingPaused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback.recordStart();
|
||||
});
|
||||
|
||||
viewBinding.trackControllerStop.setOnTouchListener(this);
|
||||
transition = (TransitionDrawable) viewBinding.trackControllerContainer.getBackground();
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
|
||||
@Override
|
||||
public boolean onTouch(final View view, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
|
||||
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
|
||||
buttonDelay = null;
|
||||
view.setPressed(false);
|
||||
|
||||
transition.resetTransition();
|
||||
|
||||
showStatusSetDefaultText();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Trigger button pressed animation
|
||||
view.setPressed(true);
|
||||
|
||||
//To pause a recording
|
||||
if (viewBinding.trackControllerRecord.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& isRecording && !isRecordingPaused) {
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
ActivityUtils.vibrate(activity, 150);
|
||||
viewBinding.trackControllerStatus.setText(R.string.hold_to_pause);
|
||||
|
||||
buttonDelay = () -> {
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
view.performClick();
|
||||
callback.recordStart(); //TODO Should be recordPause();
|
||||
|
||||
transition.resetTransition();
|
||||
|
||||
ActivityUtils.vibrate(activity, 1000);
|
||||
};
|
||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
//To stop a recording
|
||||
if (viewBinding.trackControllerStop.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& isRecording) {
|
||||
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
ActivityUtils.vibrate(activity, 150);
|
||||
viewBinding.trackControllerStatus.setText(R.string.hold_to_stop);
|
||||
|
||||
buttonDelay = () -> {
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
callback.recordStop();
|
||||
ActivityUtils.vibrate(activity, 1000);
|
||||
|
||||
transition.resetTransition();
|
||||
};
|
||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void update(boolean recording, boolean paused) {
|
||||
if (!isResumed) {
|
||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||
return;
|
||||
}
|
||||
isRecording = recording;
|
||||
isRecordingPaused = paused;
|
||||
boolean visible = alwaysShow || isRecording;
|
||||
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (!visible) {
|
||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRecording && !isRecordingPaused) {
|
||||
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
|
||||
viewBinding.trackControllerRecord.setImageResource(isRecording && !isRecordingPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
|
||||
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isRecordingPaused ? R.string.image_pause : R.string.image_record));
|
||||
|
||||
viewBinding.trackControllerStop.setEnabled(isRecording);
|
||||
|
||||
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
if (isRecording) {
|
||||
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isRecordingPaused ? android.R.color.white : R.color.recording_text));
|
||||
showStatusSetDefaultText();
|
||||
}
|
||||
|
||||
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
|
||||
if (isRecording) {
|
||||
updateTotalTime();
|
||||
setTotalTime();
|
||||
}
|
||||
}
|
||||
|
||||
void onResume(boolean recording, boolean paused) {
|
||||
isResumed = true;
|
||||
update(recording, paused);
|
||||
}
|
||||
|
||||
void onPause() {
|
||||
isResumed = false;
|
||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
viewBinding.trackControllerContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void updateTotalTime() {
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (trackRecordingService != null) {
|
||||
totalTime = trackRecordingService.getTotalTime();
|
||||
}
|
||||
}
|
||||
|
||||
private void setTotalTime() {
|
||||
if (totalTime != null) {
|
||||
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
||||
}
|
||||
}
|
||||
|
||||
private void showStatusSetDefaultText() {
|
||||
viewBinding.trackControllerStatus.setText(isRecordingPaused ? R.string.generic_paused : R.string.generic_recording);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void recordStart();
|
||||
|
||||
void recordStop();
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 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;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.PowerManager;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.TrackDeleteBinding;
|
||||
import de.dennisguse.opentracks.util.SystemUtils;
|
||||
|
||||
/**
|
||||
* An activity for deleting tracks.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class TrackDeleteActivity extends AbstractActivity {
|
||||
|
||||
public static final String EXTRA_TRACK_IDS = "track_ids";
|
||||
|
||||
private List<Track.Id> trackIds;
|
||||
|
||||
private Thread deleteThread;
|
||||
|
||||
private TrackDeleteBinding viewBinding;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
Intent intent = getIntent();
|
||||
trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
|
||||
deleteThread = new Thread(() -> {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
|
||||
|
||||
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
|
||||
contentProviderUtils.deleteTracks(TrackDeleteActivity.this, trackIds);
|
||||
|
||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||
if (Thread.interrupted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
runOnUiThread(TrackDeleteActivity.this::onAsyncTaskCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
deleteThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
deleteThread.interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
viewBinding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
public void onAsyncTaskCompleted() {
|
||||
viewBinding.progressbar.setVisibility(View.INVISIBLE);
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
@@ -48,12 +49,13 @@ import androidx.loader.content.Loader;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.databinding.TrackListBinding;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
@@ -69,16 +71,16 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback {
|
||||
public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, ControllerFragment.Callback {
|
||||
|
||||
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||
|
||||
protected static final int GPS_REQUEST_CODE = 6;
|
||||
|
||||
// The following are set in onCreate
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackController trackController;
|
||||
private ResourceCursorAdapter resourceCursorAdapter;
|
||||
private GpsStatusValue gpsStatusValue;
|
||||
|
||||
private TrackListBinding viewBinding;
|
||||
|
||||
@@ -86,7 +88,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
|
||||
// Preferences
|
||||
private boolean metricUnits = true;
|
||||
private Track.Id recordingTrackId = null;
|
||||
|
||||
private GpsStatusValue gpsStatusValue = TrackRecordingService.STATUS_GPS_DEFAULT;
|
||||
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
@@ -105,29 +109,16 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
}
|
||||
};
|
||||
|
||||
private boolean recordingTrackPaused;
|
||||
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.stats_units_key, key)) {
|
||||
metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, TrackListActivity.this);
|
||||
}
|
||||
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, TrackListActivity.this);
|
||||
if (key != null && PreferencesUtils.isRecording(recordingTrackId)) {
|
||||
trackRecordingServiceConnection.startAndBind(TrackListActivity.this);
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, TrackListActivity.this);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(() -> {
|
||||
TrackListActivity.this.invalidateOptionsMenu();
|
||||
loaderCallbacks.restart();
|
||||
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
|
||||
trackController.update(isRecording, recordingTrackPaused);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -141,33 +132,27 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
|
||||
|
||||
// After binding changes (e.g., becomes available), update the total time in trackController.
|
||||
runOnUiThread(() -> trackController.update(isRecording, recordingTrackPaused));
|
||||
|
||||
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
Log.d(TAG, "service not available to start gps or a new recording");
|
||||
gpsStatusValue = GpsStatusValue.GPS_NONE;
|
||||
Log.e(TAG, "service not available to start gps or a new recording");
|
||||
return;
|
||||
}
|
||||
|
||||
service.getRecordingStatusObservable()
|
||||
.observe(TrackListActivity.this, status -> onRecordingStatusChanged(status));
|
||||
|
||||
// Get GPS status and listen GPS status changes.
|
||||
gpsStatusValue = service.getGpsStatus();
|
||||
updateGpsMenuItem(true, isRecording);
|
||||
service.addListener(newStatus -> {
|
||||
gpsStatusValue = newStatus;
|
||||
updateGpsMenuItem(true, isRecording);
|
||||
});
|
||||
service.getGpsStatusObservable()
|
||||
.observe(TrackListActivity.this, status -> onGpsStatusChanged(status));
|
||||
|
||||
if (isGpsStarted()) {
|
||||
updateGpsMenuItem(true, recordingStatus.isRecording());
|
||||
|
||||
if (service.getGpsStatusObservable().getValue().isGpsStarted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
service.startGps();
|
||||
gpsStatusValue = GpsStatusValue.GPS_ENABLED;
|
||||
updateGpsMenuItem(true, isRecording);
|
||||
//TODO Not cool to do this in a callback that might be called more than once!
|
||||
service.tryStartGps();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,25 +164,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
super.onCreate(savedInstanceState);
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
gpsStatusValue = GpsStatusValue.GPS_NONE;
|
||||
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, this);
|
||||
|
||||
|
||||
// Show trackController when search dialog is dismissed
|
||||
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
searchManager.setOnDismissListener(() -> trackController.show());
|
||||
}
|
||||
|
||||
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
|
||||
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
|
||||
if (trackId == recordingTrackId.getId()) {
|
||||
if (recordingStatus.isRecording() && trackId == recordingStatus.getTrackId().getId()) {
|
||||
// Is recording -> open record activity.
|
||||
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
|
||||
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId));
|
||||
@@ -227,18 +200,18 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
|
||||
|
||||
Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
|
||||
boolean isRecording = trackId.equals(recordingTrackId);
|
||||
boolean isRecording = trackId.equals(recordingStatus.getTrackId());
|
||||
String icon = cursor.getString(iconIndex);
|
||||
int iconId = TrackIconUtils.getIconDrawable(icon);
|
||||
String name = cursor.getString(nameIndex);
|
||||
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
|
||||
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
|
||||
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, Distance.of(cursor.getDouble(totalDistanceIndex)), metricUnits);
|
||||
int markerCount = cursor.getInt(markerCountIndex);
|
||||
long startTime = cursor.getLong(startTimeIndex);
|
||||
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
|
||||
String description = cursor.getString(descriptionIndex);
|
||||
|
||||
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingTrackPaused,
|
||||
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingStatus.isPaused(),
|
||||
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
|
||||
startTime, true, category, description, false);
|
||||
}
|
||||
@@ -266,14 +239,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
trackRecordingServiceConnection.startConnection(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
// Update UI
|
||||
trackController.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
@@ -281,7 +246,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
// Update UI
|
||||
this.invalidateOptionsMenu();
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
|
||||
trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -314,7 +278,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
getMenuInflater().inflate(R.menu.track_list, menu);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.track_list_search);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
|
||||
SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||
searchView.setOnCloseListener(() -> {
|
||||
searchView.clearFocus();
|
||||
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
return true;
|
||||
});
|
||||
|
||||
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
@@ -322,9 +292,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
boolean isGpsStarted = isGpsStarted();
|
||||
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
|
||||
updateMenuItems(isGpsStarted, isRecording);
|
||||
updateGpsMenuItem(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
|
||||
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
searchView.setQuery("", false);
|
||||
@@ -340,13 +308,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
} else {
|
||||
// Invoke trackRecordingService
|
||||
if (!isGpsStarted()) {
|
||||
if (!gpsStatusValue.isGpsStarted()) {
|
||||
trackRecordingServiceConnection.startAndBind(this);
|
||||
bindChangedCallback.run();
|
||||
} else {
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (trackRecordingService != null) {
|
||||
trackRecordingService.stopGps();
|
||||
trackRecordingService.stopGpsAndShutdown();
|
||||
}
|
||||
trackRecordingServiceConnection.unbindAndStop(this);
|
||||
}
|
||||
@@ -373,6 +341,12 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_list_search) {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
searchView.setIconified(false);
|
||||
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@@ -384,17 +358,23 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overridePendingTransition(int enterAnim, int exitAnim) {
|
||||
//Disable animations as it is weird going into searchMode; looks okay for SplashScreen.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
|
||||
if (loaderCallbacks.getSearchQuery() != null) {
|
||||
loaderCallbacks.setSearch(null);
|
||||
return;
|
||||
}
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@@ -414,25 +394,20 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
loaderCallbacks.setSearch(searchQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSearchRequested() {
|
||||
// Hide trackController when search dialog is shown
|
||||
trackController.hide();
|
||||
return super.onSearchRequested();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackDeleted() {
|
||||
protected void onDeleteConfirmed() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
private boolean isGpsStarted() {
|
||||
return gpsStatusValue != GpsStatusValue.GPS_NONE && gpsStatusValue != GpsStatusValue.GPS_DISABLED;
|
||||
@Nullable
|
||||
@Override
|
||||
protected Track.Id getRecordingTrackId() {
|
||||
return recordingStatus.getTrackId();
|
||||
}
|
||||
|
||||
private void requestGPSPermissions() {
|
||||
@@ -441,22 +416,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the menu items with not fixed icon for gps option.
|
||||
*
|
||||
* @param isGpsStarted true if gps is started
|
||||
* @param isRecording true if recording
|
||||
*/
|
||||
private void updateMenuItems(boolean isGpsStarted, boolean isRecording) {
|
||||
updateGpsMenuItem(isGpsStarted, isRecording);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the menu items with the icon specified.
|
||||
*
|
||||
* @param isGpsStarted true if gps is started
|
||||
* @param isRecording true if recording
|
||||
*/
|
||||
//TODO Check if if can be avoided to call this outside of onGpsStatusChanged()
|
||||
private void updateGpsMenuItem(boolean isGpsStarted, boolean isRecording) {
|
||||
if (startGpsMenuItem != null) {
|
||||
startGpsMenuItem.setVisible(!isRecording);
|
||||
@@ -495,7 +461,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (itemId == R.id.list_context_menu_edit) {
|
||||
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
|
||||
@@ -507,6 +472,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
deleteTracks(trackIds);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.list_context_menu_select_all) {
|
||||
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
|
||||
viewBinding.trackList.setItemChecked(i, true);
|
||||
@@ -572,27 +538,36 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
|
||||
@Override
|
||||
public void recordStart() {
|
||||
if (!PreferencesUtils.isRecording(recordingTrackId)) {
|
||||
if (recordingStatus.getTrackId() == null) {
|
||||
// Not recording -> Recording
|
||||
updateMenuItems(false, true);
|
||||
updateGpsMenuItem(false, true);
|
||||
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
|
||||
startActivity(newIntent);
|
||||
} else if (recordingTrackPaused) {
|
||||
} else if (recordingStatus.isPaused()) {
|
||||
// Paused -> Resume
|
||||
updateMenuItems(false, true);
|
||||
updateGpsMenuItem(false, true);
|
||||
trackRecordingServiceConnection.resumeTrack();
|
||||
trackController.update(true, false);
|
||||
} else {
|
||||
// Recording -> Paused
|
||||
updateMenuItems(false, true);
|
||||
trackRecordingServiceConnection.pauseTrack();
|
||||
trackController.update(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordPause() {
|
||||
updateGpsMenuItem(false, true);
|
||||
trackRecordingServiceConnection.pauseTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordStop() {
|
||||
updateMenuItems(false, false);
|
||||
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
|
||||
updateGpsMenuItem(false, false);
|
||||
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
|
||||
}
|
||||
|
||||
public void onGpsStatusChanged(GpsStatusValue newStatus) {
|
||||
gpsStatusValue = newStatus;
|
||||
updateGpsMenuItem(true, recordingStatus.isRecording());
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
@@ -24,6 +25,7 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
@@ -32,7 +34,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
|
||||
@@ -40,6 +41,8 @@ import de.dennisguse.opentracks.fragments.ChartFragment;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
||||
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
@@ -52,30 +55,60 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
|
||||
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
|
||||
public class TrackRecordedActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
|
||||
|
||||
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
|
||||
|
||||
public static final String VIEW_TRACK_ICON = "track_icon";
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
|
||||
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
||||
|
||||
// The following are set in onCreate.
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
private TrackRecordedBinding viewBinding;
|
||||
|
||||
private Track.Id trackId;
|
||||
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
|
||||
private final Runnable bindCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
Log.w(TAG, "could not get TrackRecordingService");
|
||||
return;
|
||||
}
|
||||
|
||||
service.getRecordingStatusObservable()
|
||||
.observe(TrackRecordedActivity.this, status -> onRecordingStatusChanged(status));
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(TrackRecordedActivity.this, R.string.recording_distance_interval_key, key)) {
|
||||
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordedActivity.this));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
contentProviderUtils = new ContentProviderUtils(this);
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
|
||||
handleIntent(getIntent());
|
||||
|
||||
trackDataHub = new TrackDataHub(this);
|
||||
@@ -88,12 +121,18 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
|
||||
|
||||
postponeEnterTransition();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||
|
||||
trackDataHub.start();
|
||||
}
|
||||
|
||||
@@ -107,12 +146,16 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
if (trackId != null) {
|
||||
trackDataHub.loadTrack(trackId);
|
||||
}
|
||||
|
||||
trackRecordingServiceConnection.bind(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
trackRecordingServiceConnection.unbind(this);
|
||||
trackDataHub.stop();
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,7 +186,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
menu.findItem(R.id.track_detail_resume_track).setVisible(!PreferencesUtils.isRecording(PreferencesUtils.getSharedPreferences(this), this));
|
||||
menu.findItem(R.id.track_detail_resume_track).setVisible(!recordingStatus.isRecording());
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
setTitle(track != null ? track.getName() : "");
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
@@ -198,14 +241,22 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
// Not needed.
|
||||
return null;
|
||||
protected Track.Id getRecordingTrackId() {
|
||||
return recordingStatus.getTrackId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackDeleted() {
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||
super.onTrackDeleteStatus(deleteStatus);
|
||||
if (deleteStatus.isDeleted(trackId)) {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleteConfirmed() {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
|
||||
@@ -219,18 +270,6 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
|
||||
|
||||
Marker.Id markerId = intent.getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if (markerId != null) {
|
||||
// Use the trackId from the marker
|
||||
Marker marker = contentProviderUtils.getMarker(markerId);
|
||||
if (marker == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
trackId = marker.getTrackId();
|
||||
}
|
||||
|
||||
if (trackId == null) {
|
||||
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
|
||||
finish();
|
||||
@@ -250,7 +289,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
case 0:
|
||||
return StatisticsRecordedFragment.newInstance(trackId);
|
||||
case 1:
|
||||
return IntervalsFragment.newInstance();
|
||||
return IntervalsFragment.newInstance(trackId, true);
|
||||
case 2:
|
||||
return ChartFragment.newInstance(false);
|
||||
case 3:
|
||||
@@ -281,8 +320,12 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
}
|
||||
}
|
||||
|
||||
public void startPostponedEnterTransitionWith(View viewIcon, View viewName) {
|
||||
public void startPostponedEnterTransitionWith(View viewIcon) {
|
||||
ViewCompat.setTransitionName(viewIcon, TrackRecordedActivity.VIEW_TRACK_ICON);
|
||||
startPostponedEnterTransition();
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ import de.dennisguse.opentracks.fragments.ChartFragment;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
@@ -43,7 +43,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
||||
*/
|
||||
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
|
||||
//This mode of communication should be removed anyhow.
|
||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
|
||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, ControllerFragment.Callback {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
|
||||
@@ -56,29 +56,28 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackDataHub trackDataHub;
|
||||
private TrackController trackController;
|
||||
|
||||
private TrackRecordingBinding viewBinding;
|
||||
|
||||
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
|
||||
@Deprecated
|
||||
private Track.Id trackId;
|
||||
|
||||
// Preferences
|
||||
private boolean recordingTrackPaused;
|
||||
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// After binding changes (is available), update the total time in trackController.
|
||||
runOnUiThread(() -> trackController.update(true, recordingTrackPaused));
|
||||
|
||||
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
Log.d(TAG, "could not get TrackRecordingService");
|
||||
Log.w(TAG, "could not get TrackRecordingService");
|
||||
return;
|
||||
}
|
||||
|
||||
service.getRecordingStatusObservable()
|
||||
.observe(TrackRecordingActivity.this, status -> onRecordingStatusChanged(status));
|
||||
|
||||
if (!service.isRecording()) {
|
||||
// Starts or resumes a track.
|
||||
if (trackId == null) {
|
||||
// trackId isn't initialized -> leads a new recording.
|
||||
trackId = service.startNewTrack();
|
||||
@@ -89,8 +88,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
|
||||
// A recording track is on.
|
||||
trackDataHub.loadTrack(trackId);
|
||||
trackController.update(true, false);
|
||||
trackController.onResume(true, recordingTrackPaused);
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -98,29 +96,21 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, TrackRecordingActivity.this);
|
||||
setLockscreenPolicy();
|
||||
setScreenOnPolicy();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) {
|
||||
setLockscreenPolicy();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_keep_screen_on_while_recording_key, key)) {
|
||||
setScreenOnPolicy();
|
||||
}
|
||||
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_fullscreen_while_recording_key, key)) {
|
||||
setFullscreenPolicy();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_distance_interval_key, key)) {
|
||||
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordingActivity.this));
|
||||
}
|
||||
if (key == null) return;
|
||||
|
||||
runOnUiThread(() -> {
|
||||
TrackRecordingActivity.this.invalidateOptionsMenu();
|
||||
trackController.update(true, recordingTrackPaused);
|
||||
});
|
||||
runOnUiThread(TrackRecordingActivity.this::invalidateOptionsMenu);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,8 +135,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
}
|
||||
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
trackDataHub = new TrackDataHub(this);
|
||||
|
||||
@@ -157,8 +145,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
if (savedInstanceState != null) {
|
||||
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
|
||||
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -217,11 +203,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
super.onResume();
|
||||
|
||||
// Update UI
|
||||
this.invalidateOptionsMenu();
|
||||
invalidateOptionsMenu();
|
||||
|
||||
if (trackId != null) {
|
||||
//TODO Pass recordingStatus directly to them
|
||||
trackDataHub.loadTrack(trackId);
|
||||
trackController.onResume(true, recordingTrackPaused);
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -240,12 +227,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
outState.putParcelable(EXTRA_TRACK_ID, trackId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
trackController.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
@@ -272,7 +253,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
updateMenuItems(recordingTrackPaused);
|
||||
updateMenuItems(recordingStatus.isPaused());
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -345,22 +326,19 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
|
||||
@Override
|
||||
public void recordStart() {
|
||||
if (recordingTrackPaused) {
|
||||
// Paused -> Resume
|
||||
updateMenuItems(false);
|
||||
trackRecordingServiceConnection.resumeTrack();
|
||||
trackController.update(true, false);
|
||||
} else {
|
||||
// Recording -> Paused
|
||||
updateMenuItems(true);
|
||||
trackRecordingServiceConnection.pauseTrack();
|
||||
trackController.update(true, true);
|
||||
}
|
||||
updateMenuItems(false);
|
||||
trackRecordingServiceConnection.resumeTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordPause() {
|
||||
updateMenuItems(true);
|
||||
trackRecordingServiceConnection.pauseTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordStop() {
|
||||
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this, true);
|
||||
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
|
||||
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
|
||||
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
@@ -382,7 +360,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
case 0:
|
||||
return StatisticsRecordingFragment.newInstance();
|
||||
case 1:
|
||||
return IntervalsFragment.IntervalsRecordingFragment.newInstance();
|
||||
return IntervalsFragment.newInstance(trackId, false);
|
||||
case 2:
|
||||
return ChartFragment.newInstance(false);
|
||||
case 3:
|
||||
@@ -412,4 +390,13 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
|
||||
setLockscreenPolicy();
|
||||
setScreenOnPolicy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class ExecutorListViewService<T> {
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
Future future = executorService.submit(runnable);
|
||||
Future<?> future = executorService.submit(runnable);
|
||||
try {
|
||||
future.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
|
||||
@@ -4,73 +4,83 @@ import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||
|
||||
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> {
|
||||
public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private List<IntervalStatistics.Interval> intervalList;
|
||||
private final Context context;
|
||||
private final StackMode stackMode;
|
||||
private final boolean metricUnits;
|
||||
private final String category;
|
||||
private boolean metricUnits;
|
||||
private boolean isReportSpeed;
|
||||
|
||||
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, String category, StackMode stackMode) {
|
||||
super(context, R.layout.interval_stats_list_item, intervalList);
|
||||
metricUnits = PreferencesUtils.isMetricUnits(PreferencesUtils.getSharedPreferences(context), context);
|
||||
this.category = category;
|
||||
public IntervalStatisticsAdapter(Context context, StackMode stackMode, boolean metricUnits, boolean isReportSpeed) {
|
||||
this.metricUnits = metricUnits;
|
||||
this.context = context;
|
||||
this.stackMode = stackMode;
|
||||
this.isReportSpeed = isReportSpeed;
|
||||
}
|
||||
|
||||
//TODO Check preference handling! Should not be accessed in getView()
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View intervalView, @NonNull ViewGroup parent) {
|
||||
int actualPosition = stackMode == StackMode.STACK_FROM_TOP ? position : getCount() - 1 - position;
|
||||
IntervalStatistics.Interval interval = getItem(actualPosition);
|
||||
ViewHolder viewHolder;
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
|
||||
return new IntervalStatisticsAdapter.ViewHolder(view);
|
||||
}
|
||||
|
||||
if (intervalView == null) {
|
||||
viewHolder = new ViewHolder();
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
int actualPosition = stackMode == StackMode.STACK_FROM_TOP ? position : getItemCount() - 1 - position;
|
||||
int nextPosition = actualPosition + 1;
|
||||
boolean isLast = actualPosition == getItemCount() - 1;
|
||||
IntervalStatisticsAdapter.ViewHolder viewHolder = (IntervalStatisticsAdapter.ViewHolder) holder;
|
||||
IntervalStatistics.Interval interval = intervalList.get(actualPosition);
|
||||
viewHolder.itemView.setTag(actualPosition);
|
||||
|
||||
intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
|
||||
|
||||
viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance);
|
||||
viewHolder.rate = intervalView.findViewById(R.id.interval_item_rate);
|
||||
viewHolder.gain = intervalView.findViewById(R.id.interval_item_gain);
|
||||
viewHolder.loss = intervalView.findViewById(R.id.interval_item_loss);
|
||||
|
||||
intervalView.setTag(viewHolder);
|
||||
Distance sumDistance;
|
||||
if (isLast && actualPosition > 0) {
|
||||
sumDistance = intervalList.get(actualPosition - 1).getDistance()
|
||||
.multipliedBy(actualPosition)
|
||||
.plus(interval.getDistance());
|
||||
} else {
|
||||
viewHolder = (ViewHolder) intervalView.getTag();
|
||||
sumDistance = interval.getDistance().multipliedBy(nextPosition);
|
||||
}
|
||||
viewHolder.distance.setText(StringUtils.formatDistance(context, sumDistance, metricUnits));
|
||||
|
||||
viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed(), metricUnits, isReportSpeed));
|
||||
|
||||
viewHolder.gain.setText(StringUtils.formatDistance(context, Distance.of(interval.getGain_m()), metricUnits));
|
||||
viewHolder.loss.setText(StringUtils.formatDistance(context, Distance.of(interval.getLoss_m()), metricUnits));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (intervalList == null) {
|
||||
return 0;
|
||||
}
|
||||
return intervalList.size();
|
||||
}
|
||||
|
||||
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) {
|
||||
this.metricUnits = metricUnits;
|
||||
this.isReportSpeed = isReportSpeed;
|
||||
intervalList = data;
|
||||
|
||||
if (data != null) {
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
float sumDistance_m;
|
||||
if (actualPosition + 1 == getCount() && actualPosition > 0) {
|
||||
sumDistance_m = actualPosition * getItem(actualPosition - 1).getDistance_m() + interval.getDistance_m();
|
||||
} else {
|
||||
sumDistance_m = (actualPosition + 1) * interval.getDistance_m();
|
||||
}
|
||||
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits));
|
||||
|
||||
if (PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category)) {
|
||||
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, true));
|
||||
} else {
|
||||
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, false));
|
||||
}
|
||||
|
||||
viewHolder.gain.setText(StringUtils.formatDistance(getContext(), interval.getGain_m(), metricUnits));
|
||||
viewHolder.loss.setText(StringUtils.formatDistance(getContext(), interval.getLoss_m(), metricUnits));
|
||||
|
||||
return intervalView;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,10 +91,18 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
|
||||
STACK_FROM_TOP
|
||||
}
|
||||
|
||||
private static class ViewHolder {
|
||||
private TextView distance;
|
||||
private TextView rate;
|
||||
private TextView gain;
|
||||
private TextView loss;
|
||||
private static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView distance;
|
||||
TextView rate;
|
||||
TextView gain;
|
||||
TextView loss;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
distance = itemView.findViewById(R.id.interval_item_distance);
|
||||
rate = itemView.findViewById(R.id.interval_item_rate);
|
||||
gain = itemView.findViewById(R.id.interval_item_gain);
|
||||
loss = itemView.findViewById(R.id.interval_item_loss);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
|
||||
private final Activity activity;
|
||||
|
||||
//TODO Should be Marker.Id
|
||||
private final ExecutorListViewService<Long> executorService = new ExecutorListViewService<Long>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
|
||||
private final ExecutorListViewService<Long> executorService = new ExecutorListViewService<>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
|
||||
|
||||
private boolean scroll = false;
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ package de.dennisguse.opentracks.chart;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
public class ChartPoint {
|
||||
@@ -13,51 +14,38 @@ public class ChartPoint {
|
||||
private double timeOrDistance;
|
||||
|
||||
//Y-axis
|
||||
private double elevation;
|
||||
private double speed;
|
||||
private double pace;
|
||||
private double heartRate = Double.NaN;
|
||||
private double cadence = Double.NaN;
|
||||
private double power = Double.NaN;
|
||||
private final double altitude;
|
||||
private Double speed;
|
||||
private Double pace;
|
||||
private Double heartRate;
|
||||
private Double cadence;
|
||||
private Double power;
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
ChartPoint(double elevation) {
|
||||
this.elevation = elevation;
|
||||
ChartPoint(double altitude) {
|
||||
this.altitude = altitude;
|
||||
}
|
||||
|
||||
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, TrackPoint trackPoint, boolean chartByDistance, boolean metricUnits) {
|
||||
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
|
||||
|
||||
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, double smoothedAltitude_m, boolean chartByDistance, boolean metricUnits) {
|
||||
if (chartByDistance) {
|
||||
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
timeOrDistance = distance;
|
||||
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
|
||||
} else {
|
||||
timeOrDistance = trackStatistics.getTotalTime().toMillis();
|
||||
}
|
||||
|
||||
elevation = trackStatisticsUpdater.getSmoothedElevation();
|
||||
if (!metricUnits) {
|
||||
elevation *= UnitConversions.M_TO_FT;
|
||||
}
|
||||
altitude = Distance.of(smoothedAltitude_m).toM_FT(metricUnits);
|
||||
|
||||
speed = trackStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MPS_TO_KMH;
|
||||
if (!metricUnits) {
|
||||
speed *= UnitConversions.KM_TO_MI;
|
||||
speed = smoothedSpeed.to(metricUnits);
|
||||
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
|
||||
if (trackPoint.hasHeartRate()) {
|
||||
heartRate = (double) trackPoint.getHeartRate_bpm();
|
||||
}
|
||||
pace = speed == 0 ? 0.0 : 60.0 / speed;
|
||||
if (trackPoint != null) {
|
||||
if (trackPoint.hasHeartRate()) {
|
||||
heartRate = trackPoint.getHeartRate_bpm();
|
||||
}
|
||||
if (trackPoint.hasCyclingCadence()) {
|
||||
cadence = trackPoint.getCyclingCadence_rpm();
|
||||
}
|
||||
if (trackPoint.hasPower()) {
|
||||
power = trackPoint.getPower();
|
||||
}
|
||||
if (trackPoint.hasCyclingCadence()) {
|
||||
cadence = (double) trackPoint.getCyclingCadence_rpm();
|
||||
}
|
||||
if (trackPoint.hasPower()) {
|
||||
power = (double) trackPoint.getPower();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,39 +53,32 @@ public class ChartPoint {
|
||||
return timeOrDistance;
|
||||
}
|
||||
|
||||
public double getElevation() {
|
||||
return elevation;
|
||||
public Double getAltitude() {
|
||||
return altitude;
|
||||
}
|
||||
|
||||
public double getSpeed() {
|
||||
public Double getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public double getPace() {
|
||||
public Double getPace() {
|
||||
return pace;
|
||||
}
|
||||
|
||||
public boolean hasHeartRate() {
|
||||
return Double.isNaN(heartRate);
|
||||
}
|
||||
|
||||
public double getHeartRate() {
|
||||
public Double getHeartRate() {
|
||||
return heartRate;
|
||||
}
|
||||
|
||||
public boolean hasCadence() {
|
||||
return Double.isNaN(cadence);
|
||||
}
|
||||
|
||||
public double getCadence() {
|
||||
public Double getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public boolean hasPower() {
|
||||
return Double.isNaN(power);
|
||||
}
|
||||
|
||||
public double getPower() {
|
||||
public Double getPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChartPoint{" + "timeOrDistance=" + timeOrDistance + '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,10 +132,10 @@ abstract class ChartValueSeries {
|
||||
}
|
||||
}
|
||||
|
||||
abstract double extractDataFromChartPoint(@NonNull ChartPoint chartPoint);
|
||||
abstract Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint);
|
||||
|
||||
boolean isChartPointValid(@NonNull ChartPoint chartPoint) {
|
||||
return !Double.isNaN(extractDataFromChartPoint(chartPoint));
|
||||
return extractDataFromChartPoint(chartPoint) != null;
|
||||
}
|
||||
|
||||
protected abstract boolean drawIfChartPointHasNoData();
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.graphics.Paint.Style;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
@@ -40,6 +41,7 @@ import androidx.core.view.GestureDetectorCompat;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.MarkerDetailActivity;
|
||||
@@ -50,7 +52,6 @@ import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.MarkerUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.ThemeUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
* Visualization of the chart.
|
||||
@@ -114,7 +115,7 @@ public class ChartView extends View {
|
||||
private int effectiveWidth = 0;
|
||||
private int effectiveHeight = 0;
|
||||
|
||||
private final boolean chartByDistance;
|
||||
private boolean chartByDistance = false;
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
private boolean showPointer = false;
|
||||
@@ -189,9 +190,9 @@ public class ChartView extends View {
|
||||
}
|
||||
});
|
||||
|
||||
public ChartView(Context context, final boolean chartByDistance) {
|
||||
super(context);
|
||||
this.chartByDistance = chartByDistance;
|
||||
public ChartView(Context context, AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
|
||||
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
|
||||
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
|
||||
|
||||
@@ -199,15 +200,15 @@ public class ChartView extends View {
|
||||
Integer.MIN_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
|
||||
R.string.description_elevation_metric,
|
||||
R.string.description_elevation_imperial,
|
||||
R.color.chart_elevation_fill,
|
||||
R.color.chart_elevation_border,
|
||||
R.string.description_altitude_metric,
|
||||
R.string.description_altitude_imperial,
|
||||
R.color.chart_altitude_fill,
|
||||
R.color.chart_altitude_border,
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getElevation();
|
||||
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getAltitude();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -227,7 +228,7 @@ public class ChartView extends View {
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getSpeed();
|
||||
}
|
||||
|
||||
@@ -249,7 +250,7 @@ public class ChartView extends View {
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getPace();
|
||||
}
|
||||
|
||||
@@ -271,7 +272,7 @@ public class ChartView extends View {
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getHeartRate();
|
||||
}
|
||||
|
||||
@@ -292,7 +293,7 @@ public class ChartView extends View {
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getCadence();
|
||||
}
|
||||
|
||||
@@ -312,7 +313,7 @@ public class ChartView extends View {
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getPower();
|
||||
}
|
||||
|
||||
@@ -366,6 +367,10 @@ public class ChartView extends View {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setChartByDistance(boolean chartByDistance) {
|
||||
this.chartByDistance = chartByDistance;
|
||||
}
|
||||
|
||||
public boolean getMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
@@ -424,7 +429,7 @@ public class ChartView extends View {
|
||||
}
|
||||
}
|
||||
updateDimensions();
|
||||
updatePaths();
|
||||
updateSeries();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,7 +476,7 @@ public class ChartView extends View {
|
||||
private void zoomIn() {
|
||||
if (canZoomIn()) {
|
||||
zoomLevel++;
|
||||
updatePaths();
|
||||
updateSeries();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@@ -486,7 +491,7 @@ public class ChartView extends View {
|
||||
scrollX = maxWidth;
|
||||
scrollTo(scrollX, 0);
|
||||
}
|
||||
updatePaths();
|
||||
updateSeries();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@@ -853,75 +858,49 @@ public class ChartView extends View {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates paths.
|
||||
* The path needs to be updated any time after the data or the dimensions change.
|
||||
*/
|
||||
private void updatePaths() {
|
||||
private void updateSeries() {
|
||||
synchronized (chartPoints) {
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
chartValueSeries.getPath().reset();
|
||||
}
|
||||
drawPaths();
|
||||
closePaths();
|
||||
seriesList.stream().forEach(this::updateSerie);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPaths() {
|
||||
boolean[] hasMoved = new boolean[seriesList.size()];
|
||||
private void updateSerie(ChartValueSeries series) {
|
||||
final int yCorner = topBorder + effectiveHeight;
|
||||
final Path path = series.getPath();
|
||||
|
||||
for (ChartPoint dataPoint : chartPoints) {
|
||||
for (int i = 0; i < seriesList.size(); i++) {
|
||||
ChartValueSeries chartValueSeries = seriesList.get(i);
|
||||
boolean drawFirstPoint = false;
|
||||
path.rewind();
|
||||
|
||||
if (!chartValueSeries.isChartPointValid(dataPoint)) {
|
||||
continue;
|
||||
}
|
||||
Iterator<ChartPoint> iterator = chartPoints.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ChartPoint point = iterator.next();
|
||||
if (!series.isChartPointValid(point)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double value = chartValueSeries.extractDataFromChartPoint(dataPoint);
|
||||
Path path = chartValueSeries.getPath();
|
||||
int x = getX(dataPoint.getTimeOrDistance());
|
||||
int y = getY(chartValueSeries, value);
|
||||
if (!hasMoved[i]) {
|
||||
hasMoved[i] = true;
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
double value = series.extractDataFromChartPoint(point);
|
||||
int x = getX(point.getTimeOrDistance());
|
||||
int y = getY(series, value);
|
||||
|
||||
// start from lower left corner
|
||||
if (!drawFirstPoint) {
|
||||
path.moveTo(x, yCorner);
|
||||
drawFirstPoint = true;
|
||||
}
|
||||
|
||||
// draw graph
|
||||
path.lineTo(x, y);
|
||||
|
||||
// last point: move to lower right
|
||||
if (!iterator.hasNext()) {
|
||||
path.lineTo(x, yCorner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void closePaths() {
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
int first = getFirstPopulatedChartDataIndex(chartValueSeries);
|
||||
|
||||
if (first != -1) {
|
||||
int xCorner = getX(chartPoints.get(first).getTimeOrDistance());
|
||||
int yCorner = topBorder + effectiveHeight;
|
||||
Path path = chartValueSeries.getPath();
|
||||
// Bottom right corner
|
||||
path.lineTo(getX(chartPoints.get(chartPoints.size() - 1).getTimeOrDistance()), yCorner);
|
||||
// Bottom left corner
|
||||
path.lineTo(xCorner, yCorner);
|
||||
// Top right corner
|
||||
double value = chartValueSeries.extractDataFromChartPoint(chartPoints.get(first));
|
||||
path.lineTo(xCorner, getY(chartValueSeries, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of the first data point containing data for a series.
|
||||
*
|
||||
* @return -1 if no data point contains data for the series.
|
||||
*/
|
||||
private int getFirstPopulatedChartDataIndex(ChartValueSeries chartValueSeries) {
|
||||
for (int i = 0; i < chartPoints.size(); i++) {
|
||||
if (chartValueSeries.isChartPointValid(chartPoints.get(i))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
// back to lower left corner
|
||||
path.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -973,7 +952,7 @@ public class ChartView extends View {
|
||||
width = newWidth;
|
||||
height = newHeight;
|
||||
updateEffectiveDimensions();
|
||||
updatePaths();
|
||||
updateSeries();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,10 +984,9 @@ public class ChartView extends View {
|
||||
|
||||
private double getMarkerXValue(Marker marker) {
|
||||
if (chartByDistance) {
|
||||
double length_km = marker.getLength() * UnitConversions.M_TO_KM;
|
||||
return metricUnits ? length_km : length_km * UnitConversions.KM_TO_MI;
|
||||
return marker.getLength().toKM_Miles(metricUnits);
|
||||
} else {
|
||||
return marker.getDuration();
|
||||
return marker.getDuration().toMillis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.annotation.VisibleForTesting;
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
@@ -127,24 +129,24 @@ public class DescriptionGenerator {
|
||||
// Fastest pace
|
||||
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
|
||||
|
||||
// Max elevation
|
||||
if (stats.hasElevationMax()) {
|
||||
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
|
||||
// Max altitude
|
||||
if (stats.hasAltitudeMax()) {
|
||||
writeAltitude(stats.getMaxAltitude(), builder, R.string.description_max_altitude, lineBreak);
|
||||
}
|
||||
|
||||
// Min elevation
|
||||
if (stats.hasElevationMin()) {
|
||||
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
|
||||
// Min altitude
|
||||
if (stats.hasAltitudeMin()) {
|
||||
writeAltitude(stats.getMinAltitude(), builder, R.string.description_min_altitude, lineBreak);
|
||||
}
|
||||
|
||||
// Elevation gain
|
||||
if (stats.hasTotalElevationGain()) {
|
||||
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
|
||||
// Altitude gain
|
||||
if (stats.hasTotalAltitudeGain()) {
|
||||
writeAltitude(stats.getTotalAltitudeGain(), builder, R.string.description_altitude_gain, lineBreak);
|
||||
}
|
||||
|
||||
// Elevation loss
|
||||
if (stats.hasTotalElevationLoss()) {
|
||||
writeElevation(stats.getTotalElevationLoss(), builder, R.string.description_elevation_loss, lineBreak);
|
||||
// Altitude loss
|
||||
if (stats.hasTotalAltitudeLoss()) {
|
||||
writeAltitude(stats.getTotalAltitudeLoss(), builder, R.string.description_altitude_loss, lineBreak);
|
||||
}
|
||||
|
||||
// Recorded time
|
||||
@@ -163,9 +165,9 @@ public class DescriptionGenerator {
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
|
||||
double distanceInKm = distance * UnitConversions.M_TO_KM;
|
||||
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
|
||||
void writeDistance(Distance distance, StringBuilder builder, int resId, String lineBreak) {
|
||||
double distanceInKm = distance.toKM();
|
||||
double distanceInMi = distance.toMI();
|
||||
builder.append(context.getString(resId, distanceInKm, distanceInMi));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
@@ -193,23 +195,18 @@ public class DescriptionGenerator {
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeSpeed(double speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
double speedInKmHr = speed * UnitConversions.MPS_TO_KMH;
|
||||
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
|
||||
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
|
||||
void writeSpeed(Speed speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
builder.append(context.getString(resId, speed.toKMH(), speed.toMPH()));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes pace.
|
||||
*
|
||||
* @param speed speed in meters per second
|
||||
* @param builder StringBuilder to append pace
|
||||
* @param resId resource id of pace string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
|
||||
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
|
||||
|
||||
@@ -221,18 +218,16 @@ public class DescriptionGenerator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes elevation.
|
||||
*
|
||||
* @param elevation elevation in meters
|
||||
* @param builder StringBuilder to append elevation
|
||||
* @param resId resource id of elevation string
|
||||
* @param lineBreak line break string
|
||||
* @param altitude_m altitude_m in meters
|
||||
* @param builder StringBuilder to append
|
||||
* @param resId resource id of altitude string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeElevation(double elevation, StringBuilder builder, int resId, String lineBreak) {
|
||||
long elevationInM = Math.round(elevation);
|
||||
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
|
||||
builder.append(context.getString(resId, elevationInM, elevationInFt));
|
||||
void writeAltitude(double altitude_m, StringBuilder builder, int resId, String lineBreak) {
|
||||
long altitudeInM = Math.round(altitude_m);
|
||||
long altitudeInFt = Math.round(altitude_m * UnitConversions.M_TO_FT);
|
||||
builder.append(context.getString(resId, altitudeInM, altitudeInFt));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.content;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.os.Handler;
|
||||
@@ -28,10 +27,13 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
@@ -40,7 +42,9 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.EGM2008Utils;
|
||||
|
||||
/**
|
||||
* Track data hub.
|
||||
@@ -50,7 +54,8 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
//TODO register contentobserver only for exact URL (incl. trackId) to not filter here.
|
||||
public class TrackDataHub {
|
||||
|
||||
/**
|
||||
* Target number of track points displayed by the diagrams (recommended).
|
||||
@@ -67,25 +72,27 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
private static final String TAG = TrackDataHub.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final TrackDataManager trackDataManager;
|
||||
private final Set<TrackDataListener> listeners;
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final int targetNumPoints;
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
private EGM2008Utils.EGM2008Correction egm2008Correction;
|
||||
|
||||
private boolean started;
|
||||
//TODO Check if this is needed.
|
||||
private HandlerThread handlerThread;
|
||||
private Handler handler;
|
||||
|
||||
// Preference values
|
||||
private Track.Id selectedTrackId;
|
||||
private Track.Id recordingTrackId;
|
||||
private boolean recordingTrackPaused;
|
||||
|
||||
private Distance recordingDistanceInterval;
|
||||
|
||||
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
|
||||
// Track points sampling state
|
||||
private int numLoadedPoints;
|
||||
private TrackPoint.Id firstSeenTrackPointId;
|
||||
private TrackPoint.Id lastSeenTrackPointId;
|
||||
private TrackStatisticsUpdater trackStatisticsUpdater;
|
||||
|
||||
// Registered listeners
|
||||
private ContentObserver tracksTableObserver;
|
||||
@@ -93,24 +100,23 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
private ContentObserver trackPointsTableObserver;
|
||||
|
||||
public TrackDataHub(Context context) {
|
||||
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
|
||||
this(context, new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
||||
private TrackDataHub(Context context, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
||||
this.context = context;
|
||||
this.trackDataManager = trackDataManager;
|
||||
this.listeners = new HashSet<>();
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.targetNumPoints = targetNumPoints;
|
||||
resetSamplingState();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (started) {
|
||||
if (isStarted()) {
|
||||
Log.i(TAG, "TrackDataHub already started, ignoring start.");
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
@@ -120,7 +126,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
tracksTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
|
||||
notifyTracksTableUpdate(listeners);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
|
||||
@@ -128,7 +134,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
markersTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
|
||||
notifyMarkersTableUpdate(listeners);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(MarkerColumns.CONTENT_URI, false, markersTableObserver);
|
||||
@@ -136,34 +142,18 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
trackPointsTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
|
||||
notifyTrackPointsTableUpdate(true, listeners);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
|
||||
|
||||
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
onSharedPreferenceChanged(sharedPreferences, null);
|
||||
|
||||
handler.post(() -> {
|
||||
if (started) {
|
||||
loadDataForAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!started) {
|
||||
if (!isStarted()) {
|
||||
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
|
||||
return;
|
||||
}
|
||||
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
sharedPreferences = null;
|
||||
|
||||
started = false;
|
||||
|
||||
//Unregister listeners
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
contentResolver.unregisterContentObserver(tracksTableObserver);
|
||||
@@ -175,6 +165,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
handlerThread = null;
|
||||
}
|
||||
handler = null;
|
||||
trackStatisticsUpdater = null;
|
||||
}
|
||||
|
||||
public void loadTrack(final @NonNull Track.Id trackId) {
|
||||
@@ -193,10 +184,10 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
*
|
||||
* @param trackDataListener the track data listener
|
||||
*/
|
||||
public void registerTrackDataListener(final TrackDataListener trackDataListener, final boolean tracksTable, final boolean markersTable, final boolean trackPointsTable_SampleIn, final boolean trackPointsTable_SampleOut) {
|
||||
public void registerTrackDataListener(final TrackDataListener trackDataListener) {
|
||||
handler.post(() -> {
|
||||
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, markersTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
|
||||
if (started) {
|
||||
listeners.add(trackDataListener);
|
||||
if (isStarted()) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
});
|
||||
@@ -208,33 +199,21 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
* @param trackDataListener the track data listener
|
||||
*/
|
||||
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
|
||||
handler.post(() -> trackDataManager.unregisterTrackDataListener(trackDataListener));
|
||||
handler.post(() -> listeners.remove(trackDataListener));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the selected track is recording.
|
||||
*/
|
||||
public boolean isSelectedTrackRecording() {
|
||||
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId) && PreferencesUtils.isRecording(recordingTrackId);
|
||||
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the selected track is paused.
|
||||
*/
|
||||
public boolean isSelectedTrackPaused() {
|
||||
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId) && recordingTrackPaused;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
|
||||
handler.post(() -> {
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(sharedPreferences, context);
|
||||
}
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(sharedPreferences, context);
|
||||
}
|
||||
});
|
||||
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId()) && recordingStatus.isPaused();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,17 +221,17 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
*/
|
||||
private void loadDataForAll() {
|
||||
resetSamplingState();
|
||||
if (!trackDataManager.hasListeners()) {
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
|
||||
notifyTracksTableUpdate(listeners);
|
||||
|
||||
for (TrackDataListener listener : trackDataManager.getListenerTrackPoints_SampledIn()) {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
|
||||
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
|
||||
notifyTrackPointsTableUpdate(true, listeners);
|
||||
notifyMarkersTableUpdate(listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,25 +242,19 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
private void loadDataForListener(TrackDataListener trackDataListener) {
|
||||
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
|
||||
|
||||
if (trackDataManager.listensForTracks(trackDataListener)) {
|
||||
notifyTracksTableUpdate(trackDataListeners);
|
||||
}
|
||||
//Track
|
||||
notifyTracksTableUpdate(trackDataListeners);
|
||||
|
||||
boolean hasSampledIn = trackDataManager.listensForTrackPoints_SampledIn(trackDataListener);
|
||||
boolean hasSampledOut = trackDataManager.listensForTrackPoints_SampledOut(trackDataListener);
|
||||
if (hasSampledIn || hasSampledOut) {
|
||||
trackDataListener.clearTrackPoints();
|
||||
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
|
||||
if (isOnlyListener) {
|
||||
resetSamplingState();
|
||||
}
|
||||
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.emptySet();
|
||||
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
|
||||
//TrackPoints
|
||||
trackDataListener.clearTrackPoints();
|
||||
boolean isOnlyListener = listeners.size() == 1;
|
||||
if (isOnlyListener) {
|
||||
resetSamplingState();
|
||||
}
|
||||
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners);
|
||||
|
||||
if (trackDataManager.listensForMarkers(trackDataListener)) {
|
||||
notifyMarkersTableUpdate(trackDataListeners);
|
||||
}
|
||||
//Markers
|
||||
notifyMarkersTableUpdate(trackDataListeners);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,11 +307,9 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
* Notifies track points table update; to be run in the {@link #handler} thread.
|
||||
*
|
||||
* @param updateSamplingState true to update the sampling state
|
||||
* @param sampledInListeners the sampled-in listeners
|
||||
* @param sampledOutListeners the sampled-out listeners
|
||||
*/
|
||||
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
|
||||
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
|
||||
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> listeners) {
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,7 +317,7 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
// Reload and resample the track at a lower frequency.
|
||||
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
|
||||
resetSamplingState();
|
||||
for (TrackDataListener listener : sampledInListeners) {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
}
|
||||
@@ -374,6 +345,13 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
|
||||
|
||||
while (trackPointIterator.hasNext()) {
|
||||
if (!isStarted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
//Prevents a NPE if stop() is happening while notifyTrackPointsTableUpdate()
|
||||
TrackStatisticsUpdater currentUpdater = trackStatisticsUpdater;
|
||||
|
||||
trackPoint = trackPointIterator.next();
|
||||
TrackPoint.Id trackPointId = trackPoint.getId();
|
||||
|
||||
@@ -382,6 +360,8 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
break;
|
||||
}
|
||||
|
||||
correctAltitude(trackPoint);
|
||||
|
||||
if (localFirstSeenTrackPointId == null) {
|
||||
localFirstSeenTrackPointId = trackPointId;
|
||||
}
|
||||
@@ -391,15 +371,16 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
|
||||
}
|
||||
|
||||
currentUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
|
||||
// Also include the last point if the selected track is not recording.
|
||||
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
||||
for (TrackDataListener trackDataListener : sampledInListeners) {
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint);
|
||||
for (TrackDataListener trackDataListener : listeners) {
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
|
||||
}
|
||||
} else {
|
||||
for (TrackDataListener trackDataListener : sampledOutListeners) {
|
||||
trackDataListener.onSampledOutTrackPoint(trackPoint);
|
||||
for (TrackDataListener trackDataListener : listeners) {
|
||||
trackDataListener.onSampledOutTrackPoint(trackPoint, currentUpdater.getTrackStatistics());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,11 +398,25 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
lastSeenTrackPointId = localLastSeenTrackPointIdId;
|
||||
}
|
||||
|
||||
if (trackPoint != null) {
|
||||
for (TrackDataListener listener : sampledInListeners) {
|
||||
listener.onNewTrackPointsDone(trackPoint);
|
||||
listeners.stream().forEach(TrackDataListener::onNewTrackPointsDone);
|
||||
}
|
||||
|
||||
private void correctAltitude(TrackPoint trackPoint) {
|
||||
if (!trackPoint.hasLocation() || !trackPoint.hasAltitude()) {
|
||||
Log.d(TAG, "No altitude correction necessary.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (egm2008Correction == null || !egm2008Correction.canCorrect(trackPoint.getLocation())) {
|
||||
try {
|
||||
egm2008Correction = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Could not load altitude correction for " + trackPoint, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
trackPoint.setAltitude(Altitude.EGM2008.of(egm2008Correction.correctAltitude(trackPoint.getLocation())));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,5 +426,18 @@ public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeL
|
||||
numLoadedPoints = 0;
|
||||
firstSeenTrackPointId = null;
|
||||
lastSeenTrackPointId = null;
|
||||
trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
}
|
||||
|
||||
private boolean isStarted() {
|
||||
return handlerThread != null;
|
||||
}
|
||||
|
||||
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
|
||||
this.recordingStatus = recordingStatus;
|
||||
}
|
||||
|
||||
public void setRecordingDistanceInterval(Distance recordingDistanceInterval) {
|
||||
this.recordingDistanceInterval = recordingDistanceInterval;
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,10 @@ package de.dennisguse.opentracks.content;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
/**
|
||||
* Listener for track data changes.
|
||||
@@ -47,36 +49,41 @@ public interface TrackDataListener {
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
*/
|
||||
void onSampledInTrackPoint(@NonNull TrackPoint trackPoint);
|
||||
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a sampled out track point is read.
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
*/
|
||||
void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint);
|
||||
default void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when finish sending new track points.
|
||||
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint)} and {@link #onSampledOutTrackPoint(TrackPoint)}.
|
||||
*/
|
||||
void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint);
|
||||
default void onNewTrackPointsDone() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to clear previously sent markers.
|
||||
*/
|
||||
void clearMarkers();
|
||||
default void clearMarkers() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new marker is read.
|
||||
*
|
||||
* @param marker the marker
|
||||
*/
|
||||
void onNewMarker(Marker marker);
|
||||
default void onNewMarker(@NonNull Marker marker) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when finish sending new markers.
|
||||
* This gets called after every batch of calls to {@link #clearMarkers()} and {@link #onNewMarker(Marker)}.
|
||||
*/
|
||||
void onNewMarkersDone();
|
||||
default void onNewMarkersDone() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011 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.content;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Manages register/unregister {@link TrackDataListener} and keeping the state for each registered listener.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
class TrackDataManager {
|
||||
|
||||
private static final String TAG = TrackDataManager.class.getSimpleName();
|
||||
|
||||
private final Set<TrackDataListener> dataListenerTracks = new HashSet<>();
|
||||
private final Set<TrackDataListener> dataListenerMarkers = new HashSet<>();
|
||||
private final Set<TrackDataListener> dataListenerTrackPoints_SampledIn = new HashSet<>();
|
||||
private final Set<TrackDataListener> dataListenerTrackPoints_SampledOut = new HashSet<>();
|
||||
|
||||
void registerTrackDataListener(final TrackDataListener trackDataListener, boolean tracksTable, boolean markersTable, boolean trackPointsTable_SampleIn, boolean trackPointsTable_SampleOut) {
|
||||
if (tracksTable) dataListenerTracks.add(trackDataListener);
|
||||
if (markersTable) dataListenerMarkers.add(trackDataListener);
|
||||
if (trackPointsTable_SampleIn) dataListenerTrackPoints_SampledIn.add(trackDataListener);
|
||||
if (trackPointsTable_SampleOut) dataListenerTrackPoints_SampledOut.add(trackDataListener);
|
||||
}
|
||||
|
||||
void unregisterTrackDataListener(TrackDataListener trackDataListener) {
|
||||
dataListenerTracks.add(trackDataListener);
|
||||
dataListenerMarkers.add(trackDataListener);
|
||||
dataListenerTrackPoints_SampledIn.add(trackDataListener);
|
||||
dataListenerTrackPoints_SampledOut.add(trackDataListener);
|
||||
}
|
||||
|
||||
boolean hasListeners() {
|
||||
return dataListenerTracks.size() + dataListenerMarkers.size() + dataListenerTrackPoints_SampledIn.size() + dataListenerTrackPoints_SampledOut.size() > 0;
|
||||
}
|
||||
|
||||
int getNumberOfListeners() {
|
||||
Set<TrackDataListener> listener = new HashSet<>();
|
||||
listener.addAll(dataListenerTracks);
|
||||
listener.addAll(dataListenerMarkers);
|
||||
listener.addAll(dataListenerTrackPoints_SampledIn);
|
||||
listener.addAll(dataListenerTrackPoints_SampledOut);
|
||||
|
||||
return listener.size();
|
||||
}
|
||||
|
||||
boolean listensForTracks(TrackDataListener listener) {
|
||||
return dataListenerTracks.contains(listener);
|
||||
}
|
||||
|
||||
boolean listensForMarkers(TrackDataListener listener) {
|
||||
return dataListenerMarkers.contains(listener);
|
||||
}
|
||||
|
||||
boolean listensForTrackPoints_SampledIn(TrackDataListener listener) {
|
||||
return dataListenerTrackPoints_SampledIn.contains(listener);
|
||||
}
|
||||
|
||||
boolean listensForTrackPoints_SampledOut(TrackDataListener listener) {
|
||||
return dataListenerTrackPoints_SampledOut.contains(listener);
|
||||
}
|
||||
|
||||
Set<TrackDataListener> getListenerTracks() {
|
||||
return dataListenerTracks;
|
||||
}
|
||||
|
||||
Set<TrackDataListener> getListenerMarkers() {
|
||||
return dataListenerMarkers;
|
||||
}
|
||||
|
||||
Set<TrackDataListener> getListenerTrackPoints_SampledIn() {
|
||||
return dataListenerTrackPoints_SampledIn;
|
||||
}
|
||||
|
||||
Set<TrackDataListener> getListenerTrackPoints_SampledOut() {
|
||||
return dataListenerTrackPoints_SampledOut;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
public abstract class Altitude {
|
||||
|
||||
private final double altitude_m;
|
||||
|
||||
private Altitude(double altitude_m) {
|
||||
this.altitude_m = altitude_m;
|
||||
}
|
||||
|
||||
public double toM() {
|
||||
return altitude_m;
|
||||
}
|
||||
|
||||
public static class WGS84 extends Altitude {
|
||||
|
||||
private WGS84(double altitude_m) {
|
||||
super(altitude_m);
|
||||
}
|
||||
|
||||
|
||||
public static Altitude of(double altitude_m) {
|
||||
return new WGS84(altitude_m);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EGM2008 extends Altitude {
|
||||
|
||||
private EGM2008(double altitude_m) {
|
||||
super(altitude_m);
|
||||
}
|
||||
|
||||
|
||||
public static Altitude of(double altitude_m) {
|
||||
return new EGM2008(altitude_m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
public class Distance {
|
||||
|
||||
public static Distance of(double distance_m) {
|
||||
return new Distance(distance_m);
|
||||
}
|
||||
|
||||
public static Distance of(String distance_m) {
|
||||
return of(Float.parseFloat(distance_m));
|
||||
}
|
||||
|
||||
public static Distance ofMile(double distance_mile) {
|
||||
return of(distance_mile * UnitConversions.MI_TO_M);
|
||||
}
|
||||
|
||||
public static Distance ofKilometer(double distance_km) {
|
||||
return of(distance_km * UnitConversions.KM_TO_M);
|
||||
}
|
||||
|
||||
public static Distance ofMM(double distance_mm) {
|
||||
return of(distance_mm * UnitConversions.MM_TO_M);
|
||||
}
|
||||
|
||||
public static Distance one(boolean metricUnit) {
|
||||
if (metricUnit) {
|
||||
return Distance.ofKilometer(1);
|
||||
} else {
|
||||
return Distance.ofMile(1);
|
||||
}
|
||||
}
|
||||
|
||||
public static Distance invalid() {
|
||||
return of(Double.NaN);
|
||||
}
|
||||
|
||||
private final double distance_m;
|
||||
|
||||
private Distance(double distance_m) {
|
||||
this.distance_m = distance_m;
|
||||
}
|
||||
|
||||
public Distance plus(@NonNull Distance distance) {
|
||||
return new Distance(distance_m + distance.distance_m);
|
||||
}
|
||||
|
||||
public Distance minus(@NonNull Distance distance) {
|
||||
return new Distance(distance_m - distance.distance_m);
|
||||
}
|
||||
|
||||
public Distance multipliedBy(double factor) {
|
||||
return new Distance(factor * distance_m);
|
||||
}
|
||||
|
||||
public Distance dividedBy(double divisor) {
|
||||
return multipliedBy(1 / divisor);
|
||||
}
|
||||
|
||||
public double dividedBy(@NonNull Distance divisor) {
|
||||
return distance_m / divisor.distance_m;
|
||||
}
|
||||
|
||||
public boolean isZero() {
|
||||
return distance_m == 0;
|
||||
}
|
||||
|
||||
public boolean isInvalid() {
|
||||
return Double.isNaN(distance_m) || Double.isInfinite(distance_m);
|
||||
}
|
||||
|
||||
public boolean lessThan(@NonNull Distance distance) {
|
||||
return !greaterThan(distance);
|
||||
}
|
||||
|
||||
public boolean greaterThan(@NonNull Distance distance) {
|
||||
return distance_m > distance.distance_m;
|
||||
}
|
||||
|
||||
public boolean greaterOrEqualThan(@NonNull Distance distance) {
|
||||
return distance_m >= distance.distance_m;
|
||||
}
|
||||
|
||||
public double toM() {
|
||||
return distance_m;
|
||||
}
|
||||
|
||||
public double toKM() {
|
||||
return distance_m * UnitConversions.M_TO_KM;
|
||||
}
|
||||
|
||||
public double toFT() {
|
||||
return distance_m * UnitConversions.M_TO_FT;
|
||||
}
|
||||
|
||||
public double toMI() {
|
||||
return toKM() * UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
public double toKM_Miles(boolean metricUnit) {
|
||||
return metricUnit ? toKM() : toMI();
|
||||
}
|
||||
|
||||
public double toM_FT(boolean metricUnit) {
|
||||
return metricUnit ? toM() : toFT();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Distance distance = (Distance) o;
|
||||
return Double.compare(distance.distance_m, distance_m) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(distance_m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Distance{" +
|
||||
"distance_m=" + distance_m +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -24,15 +24,19 @@ import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
/**
|
||||
* NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO All data should be final; no default values.
|
||||
public final class Marker {
|
||||
|
||||
private Id id;
|
||||
@@ -40,18 +44,18 @@ public final class Marker {
|
||||
private String description = "";
|
||||
private String category = "";
|
||||
private String icon = "";
|
||||
private final Track.Id trackId;
|
||||
private Track.Id trackId;
|
||||
|
||||
private final Instant time;
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Float accuracy;
|
||||
private Double altitude_m;
|
||||
private Altitude altitude;
|
||||
private Float bearing;
|
||||
|
||||
//TODO It is the distance from the track starting point; rename to something more meaningful
|
||||
private double length = 0.0;
|
||||
private long duration = 0; //TODO Duration
|
||||
private Distance length;
|
||||
private Duration duration;
|
||||
|
||||
@Deprecated //TODO Make an URI instead of String
|
||||
private String photoUrl = "";
|
||||
@@ -61,6 +65,7 @@ public final class Marker {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@Deprecated //TODO Marker cannot be created without length AND duration!
|
||||
public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) {
|
||||
this.trackId = trackId;
|
||||
|
||||
@@ -69,24 +74,33 @@ public final class Marker {
|
||||
if (!trackPoint.hasLocation())
|
||||
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
||||
|
||||
this.latitude = trackPoint.getLatitude();
|
||||
this.longitude = trackPoint.getLongitude();
|
||||
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
|
||||
if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude();
|
||||
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
|
||||
setTrackPoint(trackPoint);
|
||||
|
||||
this.length = Distance.of(0); //TODO Not cool!
|
||||
this.duration = Duration.ofMillis(0); //TODO Not cool!
|
||||
}
|
||||
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull TrackPoint trackPoint, String photoUrl) {
|
||||
@Deprecated
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
|
||||
this(trackId, trackPoint);
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.category = category;
|
||||
this.icon = icon;
|
||||
this.length = length;
|
||||
this.duration = duration;
|
||||
this.length = statistics.getTotalDistance();
|
||||
this.duration = statistics.getTotalTime();
|
||||
this.photoUrl = photoUrl;
|
||||
}
|
||||
|
||||
//TODO Is somehow part of the initialization process. Can we at least limit visibility?
|
||||
public void setTrackPoint(TrackPoint trackPoint) {
|
||||
this.latitude = trackPoint.getLatitude();
|
||||
this.longitude = trackPoint.getLongitude();
|
||||
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
|
||||
if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude();
|
||||
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
|
||||
}
|
||||
|
||||
/**
|
||||
* May be null if the it was not loaded from the database.
|
||||
*/
|
||||
@@ -135,16 +149,19 @@ public final class Marker {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Track.Id getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setTrackId(@NonNull Track.Id trackId) {
|
||||
this.trackId = trackId;
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return latitude != null || longitude != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Location getLocation() {
|
||||
Location location = new Location("");
|
||||
location.setTime(time.toEpochMilli());
|
||||
@@ -159,7 +176,7 @@ public final class Marker {
|
||||
location.setAccuracy(accuracy);
|
||||
}
|
||||
if (hasAltitude()) {
|
||||
location.setAltitude(altitude_m);
|
||||
location.setAltitude(altitude.toM());
|
||||
}
|
||||
|
||||
return location;
|
||||
@@ -194,15 +211,15 @@ public final class Marker {
|
||||
}
|
||||
|
||||
public boolean hasAltitude() {
|
||||
return altitude_m != null;
|
||||
return altitude != null;
|
||||
}
|
||||
|
||||
public Double getAltitude() {
|
||||
return altitude_m;
|
||||
public Altitude getAltitude() {
|
||||
return altitude;
|
||||
}
|
||||
|
||||
public void setAltitude(double altitude_m) {
|
||||
this.altitude_m = altitude_m;
|
||||
public void setAltitude(Altitude altitude) {
|
||||
this.altitude = altitude;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
@@ -217,19 +234,19 @@ public final class Marker {
|
||||
this.bearing = bearing;
|
||||
}
|
||||
|
||||
public double getLength() {
|
||||
public Distance getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(double length) {
|
||||
public void setLength(Distance length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
public Duration getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(long duration) {
|
||||
public void setDuration(@NonNull Duration duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public interface MarkerColumns extends BaseColumns {
|
||||
String LONGITUDE = "longitude"; // longitude
|
||||
String LATITUDE = "latitude"; // latitude
|
||||
String TIME = "time"; // time
|
||||
String ALTITUDE = "elevation"; // altitude
|
||||
String ALTITUDE = "elevation"; // altitude //TODO RENAME column
|
||||
String ACCURACY = "accuracy"; // accuracy
|
||||
String BEARING = "bearing"; // bearing
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
public class Speed {
|
||||
|
||||
public static Speed of(Distance distance, Duration duration) {
|
||||
if (duration.isZero()) {
|
||||
return zero();
|
||||
}
|
||||
|
||||
return new Speed(distance.toM() / (duration.toMillis() * UnitConversions.MS_TO_S));
|
||||
}
|
||||
|
||||
public static Speed of(double speed_mps) {
|
||||
return new Speed(speed_mps);
|
||||
}
|
||||
|
||||
public static Speed of(String speed_mps) {
|
||||
return of(Float.parseFloat(speed_mps));
|
||||
}
|
||||
|
||||
public static Speed invalid() {
|
||||
return of(Double.NaN);
|
||||
}
|
||||
|
||||
public static Speed zero() {
|
||||
return of(0.0);
|
||||
}
|
||||
|
||||
public static Speed max(Speed speed1, Speed speed2) {
|
||||
if (speed1.greaterThan(speed2)) {
|
||||
return speed1;
|
||||
}
|
||||
|
||||
return speed2;
|
||||
}
|
||||
|
||||
public static Speed absDiff(Speed speed1, Speed speed2) {
|
||||
//TODO Why Math.abs? Seems to be a leftover.
|
||||
return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps));
|
||||
}
|
||||
|
||||
|
||||
// Anything faster than that (in meters per second) will be considered moving.
|
||||
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
|
||||
|
||||
private final double speed_mps;
|
||||
|
||||
private Speed(double speed_mps) {
|
||||
this.speed_mps = speed_mps;
|
||||
}
|
||||
|
||||
public Speed mul(double factor) {
|
||||
return new Speed(factor * speed_mps);
|
||||
}
|
||||
|
||||
public boolean isZero() {
|
||||
return speed_mps == 0;
|
||||
}
|
||||
|
||||
public boolean isInvalid() {
|
||||
return Double.isNaN(speed_mps) || Double.isInfinite(speed_mps);
|
||||
}
|
||||
|
||||
public boolean isMoving() {
|
||||
return !isInvalid() && speed_mps >= MAX_NO_MOVEMENT_SPEED;
|
||||
}
|
||||
|
||||
public boolean lessThan(Speed speed) {
|
||||
return !greaterThan(speed);
|
||||
}
|
||||
|
||||
public boolean greaterThan(Speed speed) {
|
||||
return speed_mps > speed.speed_mps;
|
||||
}
|
||||
|
||||
public boolean greaterOrEqualThan(Speed speed) {
|
||||
return speed_mps >= speed.speed_mps;
|
||||
}
|
||||
|
||||
public double toMPS() {
|
||||
return speed_mps;
|
||||
}
|
||||
|
||||
public double toKMH() {
|
||||
return speed_mps * UnitConversions.MPS_TO_KMH;
|
||||
}
|
||||
|
||||
public double toMPH() {
|
||||
return toKMH() * UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
public Duration toPace(boolean metricUnit) {
|
||||
if (isZero()) {
|
||||
return Duration.ofSeconds(0);
|
||||
}
|
||||
|
||||
double distance = speed_mps * (metricUnit ? UnitConversions.M_TO_KM : UnitConversions.M_TO_MI);
|
||||
return Duration.ofSeconds(Math.round(1 / distance));
|
||||
}
|
||||
|
||||
public double to(boolean metricUnit) {
|
||||
return to(metricUnit ? Unit.KMH : Unit.MPH);
|
||||
}
|
||||
|
||||
public double to(Unit unit) {
|
||||
switch (unit) {
|
||||
case KMH:
|
||||
return toKMH();
|
||||
case MPH:
|
||||
return toMPH();
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
public enum Unit {
|
||||
KMH,
|
||||
MPH,
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Speed speed = (Speed) o;
|
||||
return Double.compare(speed.speed_mps, speed_mps) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(speed_mps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Speed{" +
|
||||
"speed_mps=" + speed_mps +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO Do not default initialize attributes; might be confusing for debugging
|
||||
public class Track {
|
||||
|
||||
private Track.Id id;
|
||||
@@ -46,9 +47,6 @@ public class Track {
|
||||
|
||||
private TrackStatistics trackStatistics = new TrackStatistics();
|
||||
|
||||
public Track() {
|
||||
}
|
||||
|
||||
/**
|
||||
* May be null if the track was not loaded from the database.
|
||||
*/
|
||||
@@ -101,11 +99,12 @@ public class Track {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public TrackStatistics getTrackStatistics() {
|
||||
return trackStatistics;
|
||||
}
|
||||
|
||||
public void setTrackStatistics(TrackStatistics trackStatistics) {
|
||||
public void setTrackStatistics(@NonNull TrackStatistics trackStatistics) {
|
||||
this.trackStatistics = trackStatistics;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -45,19 +46,18 @@ public class TrackPoint {
|
||||
|
||||
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
|
||||
|
||||
// Anything faster than that (in meters per second) will be considered moving.
|
||||
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
|
||||
|
||||
private TrackPoint.Id id;
|
||||
|
||||
@NonNull
|
||||
private Instant time;
|
||||
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Float accuracy;
|
||||
private Double altitude_m;
|
||||
private Float speed_mps;
|
||||
private Altitude altitude;
|
||||
private Speed speed;
|
||||
private Float bearing;
|
||||
private Float sensorDistance_m;
|
||||
private Distance sensorDistance;
|
||||
|
||||
public enum Type {
|
||||
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
|
||||
@@ -93,23 +93,12 @@ public class TrackPoint {
|
||||
private Float heartRate_bpm = null;
|
||||
private Float cyclingCadence_rpm = null;
|
||||
private Float power = null;
|
||||
private Float elevationGain = null;
|
||||
private Float elevationLoss = null;
|
||||
private Float altitudeGain_m = null;
|
||||
private Float altitudeLoss_m = null;
|
||||
|
||||
public TrackPoint(@NonNull Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Location location) {
|
||||
this(Type.TRACKPOINT);
|
||||
|
||||
this.latitude = location.getLatitude();
|
||||
this.longitude = location.getLongitude();
|
||||
this.altitude_m = location.getAltitude();
|
||||
this.speed_mps = location.getSpeed();
|
||||
this.accuracy = location.getAccuracy();
|
||||
|
||||
setTime(Instant.now());
|
||||
this.time = Instant.now(); //TODO This may result in #800
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Type type, Instant time) {
|
||||
@@ -117,11 +106,29 @@ public class TrackPoint {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public TrackPoint(double latitude, double longitude, Double altitude, Instant time) {
|
||||
public TrackPoint(@NonNull Location location) {
|
||||
this(Type.TRACKPOINT, location, Instant.now()); //TODO This may result in #800
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Type type, @NonNull Location location, @NonNull Instant time) {
|
||||
this(type);
|
||||
|
||||
this.latitude = location.getLatitude();
|
||||
this.longitude = location.getLongitude();
|
||||
this.altitude = Altitude.WGS84.of(location.getAltitude());
|
||||
this.speed = Speed.of(location.getSpeed());
|
||||
this.accuracy = location.getAccuracy();
|
||||
|
||||
//TODO Should we copy the bearing?
|
||||
|
||||
setTime(time);
|
||||
}
|
||||
|
||||
public TrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
|
||||
this(Type.TRACKPOINT);
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.altitude_m = altitude;
|
||||
this.altitude = altitude;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@@ -200,6 +207,7 @@ public class TrackPoint {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
//TODO Better return null, if no location is present aka latitude == null etc.
|
||||
@NonNull
|
||||
public Location getLocation() {
|
||||
Location location = new Location("");
|
||||
@@ -215,34 +223,34 @@ public class TrackPoint {
|
||||
location.setAccuracy(accuracy);
|
||||
}
|
||||
if (hasAltitude()) {
|
||||
location.setAltitude(altitude_m);
|
||||
location.setAltitude(altitude.toM());
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
public boolean hasElevationGain() {
|
||||
return elevationGain != null;
|
||||
public boolean hasAltitudeGain() {
|
||||
return altitudeGain_m != null;
|
||||
}
|
||||
|
||||
public float getElevationGain() {
|
||||
return elevationGain;
|
||||
public float getAltitudeGain() {
|
||||
return altitudeGain_m;
|
||||
}
|
||||
|
||||
public void setElevationGain(Float elevationGain) {
|
||||
this.elevationGain = elevationGain;
|
||||
public void setAltitudeGain(Float altitudeGain_m) {
|
||||
this.altitudeGain_m = altitudeGain_m;
|
||||
}
|
||||
|
||||
public boolean hasElevationLoss() {
|
||||
return elevationLoss != null;
|
||||
public boolean hasAltitudeLoss() {
|
||||
return altitudeLoss_m != null;
|
||||
}
|
||||
|
||||
public float getElevationLoss() {
|
||||
return elevationLoss;
|
||||
public float getAltitudeLoss() {
|
||||
return altitudeLoss_m;
|
||||
}
|
||||
|
||||
public void setElevationLoss(Float elevationLoss) {
|
||||
this.elevationLoss = elevationLoss;
|
||||
public void setAltitudeLoss(Float altitudeLoss_m) {
|
||||
this.altitudeLoss_m = altitudeLoss_m;
|
||||
}
|
||||
|
||||
public Instant getTime() {
|
||||
@@ -260,31 +268,36 @@ public class TrackPoint {
|
||||
|
||||
|
||||
public boolean hasAltitude() {
|
||||
return altitude_m != null;
|
||||
return altitude != null;
|
||||
}
|
||||
|
||||
public double getAltitude() {
|
||||
return altitude_m;
|
||||
public Altitude getAltitude() {
|
||||
return altitude;
|
||||
}
|
||||
|
||||
public void setAltitude(double altitude) {
|
||||
this.altitude_m = altitude;
|
||||
@VisibleForTesting
|
||||
public void setAltitude(double altitude_m) {
|
||||
this.altitude = Altitude.WGS84.of(altitude_m);
|
||||
}
|
||||
|
||||
public void setAltitude(Altitude altitude) {
|
||||
this.altitude = altitude;
|
||||
}
|
||||
|
||||
public boolean hasSpeed() {
|
||||
return speed_mps != null;
|
||||
return speed != null;
|
||||
}
|
||||
|
||||
public float getSpeed() {
|
||||
return speed_mps;
|
||||
public Speed getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public void setSpeed(Float speed) {
|
||||
this.speed_mps = speed;
|
||||
public void setSpeed(Speed speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public boolean isMoving() {
|
||||
return hasSpeed() && getSpeed() >= MAX_NO_MOVEMENT_SPEED;
|
||||
return hasSpeed() && getSpeed().isMoving();
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
@@ -311,37 +324,43 @@ public class TrackPoint {
|
||||
this.accuracy = horizontalAccuracy;
|
||||
}
|
||||
|
||||
public float distanceToPrevious(@NonNull TrackPoint previous) {
|
||||
@Nullable
|
||||
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
|
||||
if (hasSensorDistance()) {
|
||||
return getSensorDistance();
|
||||
}
|
||||
if (!(hasLocation() && previous.hasLocation())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getLocation().distanceTo(previous.getLocation());
|
||||
return Distance.of(getLocation().distanceTo(previous.getLocation()));
|
||||
}
|
||||
|
||||
public boolean fulfillsAccuracy(int poorAccuracy) {
|
||||
return hasAccuracy() && accuracy < poorAccuracy;
|
||||
}
|
||||
|
||||
//TODO Bearing requires a location; what do we do if we don't have any?
|
||||
public float bearingTo(@NonNull TrackPoint dest) {
|
||||
return getLocation().bearingTo(dest.getLocation());
|
||||
}
|
||||
|
||||
//TODO Bearing requires a location; what do we do if we don't have any?
|
||||
public float bearingTo(@NonNull Location dest) {
|
||||
return getLocation().bearingTo(dest);
|
||||
}
|
||||
|
||||
// Sensor data
|
||||
public boolean hasSensorDistance() {
|
||||
return sensorDistance_m != null;
|
||||
return sensorDistance != null;
|
||||
}
|
||||
|
||||
public Float getSensorDistance() {
|
||||
return sensorDistance_m;
|
||||
public Distance getSensorDistance() {
|
||||
return sensorDistance;
|
||||
}
|
||||
|
||||
public void setSensorDistance(Float distance_m) {
|
||||
this.sensorDistance_m = distance_m;
|
||||
public void setSensorDistance(Distance distance_m) {
|
||||
this.sensorDistance = distance_m;
|
||||
}
|
||||
|
||||
public boolean hasSensorData() {
|
||||
|
||||
@@ -52,8 +52,8 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
String SENSOR_CADENCE = "sensor_cadence";
|
||||
String SENSOR_DISTANCE = "sensor_distance"; //DISTANCE from previous TrackPoint
|
||||
String SENSOR_POWER = "sensor_power";
|
||||
String ELEVATION_GAIN = "elevation_gain";
|
||||
String ELEVATION_LOSS = "elevation_loss";
|
||||
String ALTITUDE_GAIN = "elevation_gain";
|
||||
String ALTITUDE_LOSS = "elevation_loss";
|
||||
|
||||
// Alias for sensor statistics
|
||||
String ALIAS_AVG_HR = "avg_hr";
|
||||
@@ -75,8 +75,8 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
+ SENSOR_HEARTRATE + " FLOAT, "
|
||||
+ SENSOR_CADENCE + " FLOAT, "
|
||||
+ SENSOR_POWER + " FLOAT, "
|
||||
+ ELEVATION_GAIN + " FLOAT, "
|
||||
+ ELEVATION_LOSS + " FLOAT, "
|
||||
+ ALTITUDE_GAIN + " FLOAT, "
|
||||
+ ALTITUDE_LOSS + " FLOAT, "
|
||||
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1)), "
|
||||
+ SENSOR_DISTANCE + " FLOAT, "
|
||||
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
|
||||
|
||||
@@ -52,10 +52,10 @@ public interface TracksColumns extends BaseColumns {
|
||||
String AVGSPEED = "avgspeed"; // average speed
|
||||
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed
|
||||
String MAXSPEED = "maxspeed"; // maximum speed
|
||||
String MINELEVATION = "minelevation"; // minimum elevation
|
||||
String MAXELEVATION = "maxelevation"; // maximum elevation
|
||||
String ELEVATIONGAIN = "elevationgain"; // elevation gain
|
||||
String ELEVATIONLOSS = "elevationloss"; // elevation loss
|
||||
String MIN_ALTITUDE = "minelevation"; // minimum altitude //TODO RENAME column
|
||||
String MAX_ALTITUDE = "maxelevation"; // maximum altitude //TODO RENAME column
|
||||
String ALTITUDE_GAIN = "elevationgain"; // altitude gain //TODO RENAME column
|
||||
String ALTITUDE_LOSS = "elevationloss"; // altitude loss //TODO RENAME column
|
||||
String ICON = "icon"; // track activity type icon
|
||||
|
||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
@@ -72,12 +72,12 @@ public interface TracksColumns extends BaseColumns {
|
||||
+ AVGSPEED + " FLOAT, "
|
||||
+ AVGMOVINGSPEED + " FLOAT, "
|
||||
+ MAXSPEED + " FLOAT, "
|
||||
+ MINELEVATION + " FLOAT, "
|
||||
+ MAXELEVATION + " FLOAT, "
|
||||
+ ELEVATIONGAIN + " FLOAT, "
|
||||
+ MIN_ALTITUDE + " FLOAT, "
|
||||
+ MAX_ALTITUDE + " FLOAT, "
|
||||
+ ALTITUDE_GAIN + " FLOAT, "
|
||||
+ ICON + " TEXT, "
|
||||
+ UUID + " BLOB, "
|
||||
+ ELEVATIONLOSS + " FLOAT)";
|
||||
+ ALTITUDE_LOSS + " FLOAT)";
|
||||
|
||||
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user