Compare commits

..

66 Commits

Author SHA1 Message Date
Dennis Guse 9dac5d3e72 Release: v3.24.0 2022-01-09 18:39:08 +01:00
Dennis Guse cd7bf36aa9 TrackRecordingService: add integration tests. 2022-01-09 18:28:35 +01:00
Dennis Guse ab07d3b130 TrackRecordingService: re-worked TrackPoint recording tests for state changes. 2022-01-09 18:28:35 +01:00
Dennis Guse 8b5320e40e TrackRecordingService: cleanup tests. 2022-01-09 18:28:35 +01:00
Dennis Guse ef15632d7b TrackRecordingService: refactored and unified tests. 2022-01-09 18:28:35 +01:00
Dennis Guse f5ce76955b TrackRecordingService: add tests for state changes. 2022-01-09 18:28:35 +01:00
mondstern 3701b61852 Translated using Weblate (Spanish)
Currently translated at 91.0% (71 of 78 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/es/
2022-01-08 19:19:57 +01:00
mondstern 3b8a243f55 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2022-01-08 19:19:57 +01:00
mondstern e19f55a618 Translated using Weblate (Ukrainian)
Currently translated at 97.4% (464 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2022-01-08 19:19:57 +01:00
mondstern 9e6a787ed9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2022-01-08 19:19:57 +01:00
mondstern 9626c5dd0b Translated using Weblate (Polish)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2022-01-08 19:19:57 +01:00
FTno 1e42ef5e6c Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-01-08 19:19:57 +01:00
mondstern e5792336a8 Translated using Weblate (Italian)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2022-01-08 19:19:57 +01:00
Xosé M 9d4ec8ba2a Translated using Weblate (Galician)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2022-01-08 19:19:56 +01:00
mondstern e928adf3b5 Translated using Weblate (French)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-01-08 19:19:56 +01:00
mondstern f061243a7d Translated using Weblate (Estonian)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2022-01-08 19:19:56 +01:00
mondstern 1d623a076f Translated using Weblate (Spanish)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2022-01-08 19:19:56 +01:00
mondstern 13ed210933 Translated using Weblate (Czech)
Currently translated at 100.0% (476 of 476 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2022-01-08 19:19:56 +01:00
Román Ginés eba4918b01 StringUtilsTest.testFormatDateTime removed. Fixes #1082. 2022-01-08 19:19:45 +01:00
Dennis Guse 9bde8944b9 Workaround: Clock.systemUTC() seems to be not monotonic.
Introduced in 25e590da02
2022-01-06 22:12:53 +01:00
Dennis Guse 1163f74b98 SENSORPOINT and TRACKPOINT require a distance to be stored.
TrackRecording and TrackStatisticsUpdater.

Part of #500
Fixes #1074.
2022-01-06 22:12:53 +01:00
Dennis Guse c4b8082efc Bluetooth sensors: event-driven TrackPoint creation.
Part of #500.
2022-01-06 22:12:53 +01:00
Dennis Guse 1fb8fc6a09 Bugfix: GpxTrackImporter needs to reset data when it encounters a new TrackPoint; affected only sensor data. 2022-01-06 22:12:53 +01:00
Dennis Guse dbc29a88ce Cleanup: removed useless tests. 2022-01-06 22:12:53 +01:00
mondstern 05af52a21b Translated using Weblate (Ukrainian)
Currently translated at 97.4% (460 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2022-01-03 16:27:56 +01:00
mondstern 1a5de37f8a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2022-01-03 16:27:56 +01:00
mondstern 015ddabf3a Translated using Weblate (French)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-01-03 16:27:56 +01:00
mondstern 12a008a99a Translated using Weblate (Vietnamese)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2022-01-03 16:27:56 +01:00
mondstern 99ccf65cc2 Translated using Weblate (Polish)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2022-01-03 16:27:56 +01:00
FTno a159d4c375 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-01-03 16:27:55 +01:00
mondstern 01ac31716a Translated using Weblate (Italian)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2022-01-03 16:27:55 +01:00
mondstern cfb2d0c2d3 Translated using Weblate (Estonian)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2022-01-03 16:27:55 +01:00
mondstern c05fa638a0 Translated using Weblate (Czech)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2022-01-03 16:27:55 +01:00
mondstern 519f2b016e Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-01-03 16:27:55 +01:00
Román Ginés 5cd6992c90 Proper handling of timezone data on UI. Fixes #301.
Details:
- Date and time handled by OffsetDateTime Java class.
- Track default name with offset (for date local and ISO 8601 options).
- TrackListActivity: shows relative today's date (like Today, Yesterday, 22 Dec, 22 Dec 2021, etc).
- StatisticsRecordedFragment: always shows the original date and time. For example, if you record an activity at 20h then it always will show 20h regardless the zone are where you are.
- Added new tests.
2022-01-03 16:27:40 +01:00
Peter Storch 1631836327 Try to takkle down export dir issues 2022-01-02 21:19:43 +01:00
Dennis Guse 07074ad260 TrackStatistics and ChartView: handle altitude and speed as optional with null as default value.
Then only the relevant part of the y-axis is shown.

Fixes #795.
2022-01-02 18:17:20 +01:00
Dennis Guse 8c20d5583a Charts: elevation is now optional.
Part of #500.
2022-01-02 18:17:20 +01:00
Xosé M 39bce2a487 Translated using Weblate (Galician)
Currently translated at 100.0% (472 of 472 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-12-30 12:16:45 +01:00
Dennis Guse a221194ab1 Translated using Weblate (German)
Currently translated at 89.5% (422 of 471 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-12-30 12:16:45 +01:00
Dennis Guse 55d3502ff9 Cleanup using AndroidStudio analyze. 2021-12-30 12:16:31 +01:00
Dennis Guse 0cd22a8ba0 README: add nightly repo. 2021-12-29 10:33:08 +01:00
Dennis Guse aff4bce3fe Cleanup: tests also should close cursors. 2021-12-28 23:17:52 +01:00
Dennis Guse bb290e2ee4 Settings: add option to disable filter of BLE sensors.
Fixes #1000.
2021-12-28 22:56:29 +01:00
Dennis Guse 6d04659370 Timezone support: store timezone offset per Track.
When a recording is started the offset of the phone is used.
Timezone is exported to KML and GPX.

Fixes #301.
2021-12-28 15:42:38 +01:00
Dennis Guse 7193171a09 Merge pull request #1033 from OpenTracksApp/indoor#500
Storing data from sensors without GPS
2021-12-28 14:24:57 +01:00
Dennis Guse 7d0d87e962 Tests: ServiceTestRule does not (always) restart the service.
Thus, resetting it's status is not happening and then leading to failures in subsequent tests.

Fixes #1058.
2021-12-28 13:05:24 +01:00
Dennis Guse d6cc7a0321 TrackPointCreator: only try fill TrackPoint with sensor data if started. 2021-12-28 13:05:24 +01:00
Dennis Guse c7bbaddc06 Cleanup: fix various issues in tests. 2021-12-28 13:05:24 +01:00
Dennis Guse 9af213fd12 Bugfix: selected interval may be null. 2021-12-28 13:05:24 +01:00
Dennis Guse 77f56a5376 Test: disable automatic creation of TrackPoints for tests.
Part of #500.
2021-12-28 13:05:24 +01:00
Dennis Guse ea5ac186d0 README: add sensor-only feature.
Part of #500.
2021-12-28 13:05:24 +01:00
Dennis Guse 84c51ba8bb GPX: export cumulative sensor data from SENSORPOINTs.
Part of #500.
2021-12-28 13:05:24 +01:00
Dennis Guse 89f9f25c55 GPX: export track statistics using TrackStatsExtensionv1.
Fixes #652.
2021-12-28 13:05:24 +01:00
Dennis Guse f8cc257895 KMZ/KML: Export/Import SENSORPOINTs.
Part of #500.
2021-12-28 13:05:23 +01:00
Dennis Guse ba66c45005 Recording data from BLE sensors alone (need to provide distance).
GPS is optional in this case, but no coordinates will be available.

Naive implementation tries to store data every second using the UI timer.

Part of #500.
2021-12-28 13:05:23 +01:00
Dennis Guse 24b638aa30 Add TrackPoint type for BLE sensor only data (i.e., no GPS).
Part of #500.
2021-12-28 13:05:23 +01:00
Dennis Guse 45125bcda3 Refactor: rename LocationHandler to GPSHandler. 2021-12-28 13:05:23 +01:00
Dennis Guse 3359a30907 TrackRecordingService: first process the TrackPoint, then update the notification. 2021-12-28 13:05:23 +01:00
dependabot[bot] 6f822afc71 Bump reactivecircus/android-emulator-runner from 2.21.0 to 2.22.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.21.0 to 2.22.0.
- [Release notes](https://github.com/reactivecircus/android-emulator-runner/releases)
- [Changelog](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/CHANGELOG.md)
- [Commits](https://github.com/reactivecircus/android-emulator-runner/compare/v2.21.0...v2.22.0)

---
updated-dependencies:
- dependency-name: reactivecircus/android-emulator-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-27 06:44:32 +01:00
Dennis Guse 230e97147c Update bug report template. 2021-12-25 19:54:59 +01:00
Román Ginés b4ce99440a AggregatedStatisticsActivity: added filter option (by category, start and/or end date).
Fixes #1039.
2021-12-24 11:50:32 +01:00
Dennis Guse 4d333bbafa Merge pull request #1056 from OpenTracksApp/whaoo#1050 2021-12-22 22:29:09 +01:00
Dennis Guse 0ee1c77680 Sensors: added Wahoo Blue SC. 2021-12-22 22:27:25 +01:00
Dennis Guse 6fe6cee475 BLE: CSC remove workarounds as those are not necessary.
The implementation of OpenTracks for the CSC parsing was broken and fixed in 35de5b6b26

Fixes #1046.
2021-12-21 22:00:38 +01:00
Dennis Guse 52228d44a9 Cleanup. 2021-12-21 21:55:41 +01:00
113 changed files with 3791 additions and 2106 deletions
+2 -1
View File
@@ -23,4 +23,5 @@ If applicable:
**Technical information**
- Device: [e.g. Nexus 7 (2013)]
- OS: [e.g. LineageOS 15.1]
- Version [e.g. v3.2.4]
- OpenTracks version: [e.g. v3.2.4]
- OpenTracks commit id (only for nightly builds): [e.g. 4b333bb]
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
java-version: '11'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2.21.0
uses: reactivecircus/android-emulator-runner@v2.22.0
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
+9 -1
View File
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="src/main/res/menu/aggregated_statistics.xml" value="0.10208333333333333" />
<entry key="src/main/res/xml/settings_sensors.xml" value="0.2101851851851852" />
</map>
</option>
</component>
<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">
<option name="id" value="Android" />
</component>
</project>
</project>
+14 -3
View File
@@ -6,7 +6,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<table>
<tr>
<th>Free</th>
<th>Free (F-Droid)</th>
<th>Free (Nightly for F-Droid)</th>
<th>Donations</th>
<th>Translations</th>
</tr>
@@ -16,6 +17,11 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="60" align="middle">
</a>
</td>
<td align="center">
<a href="https://fdroid.storchp.de/fdroid/repo?fingerprint=99985A7E73DCB0B16C9BDDCE7A0B4996F88068AE7C771ED53E217E69CD1FF196">
<img alt="Nightly builds (for F-Droid client)" src="https://opentracksapp.com/static/img/fdroid.storchp.de.png" height="90" align="middle">
</a>
</td>
<td align="center">
<a href="https://liberapay.com/OpenTracks/donate">
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
@@ -31,14 +37,16 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<td align="center">
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.svg" align="middle" >
</td>
<td></td>
<td align="center">
<a href="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc">
<img alt="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc" src="promotion/donations-bitcoin.png" height="90" align="middle">
</a>
</td align="center">
</td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td align="center">
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
@@ -48,6 +56,7 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td align="center">
<a href="https://www.amazon.de/dp/B09K2KNQ7X/">
@@ -78,7 +87,7 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* __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)
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
* __Altitude:__
* gain/loss via barometric sensor (if present)
@@ -89,6 +98,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* cycling: cadence
* cycling: power meter
* running: speed and cadence
* support for BLE sensor training only (i.e., without GPS) for indoor training
An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md)
### Gadgetbridge integration
+6 -8
View File
@@ -18,23 +18,21 @@
Please note that the according to the specification these sensors _may_ provide speed as well as cadence.
However, often only one value is provided.
### Speed
### Speed & Cadence
* Wahoo Blue SC
### Speed only
* Garmin Speed Sensor 2
* Has updatable firmware that requires an account for garmin.com
* This sensor reports speed data as cadence.
A workaround is in place.
* Wahoo Speed (Model: WFRPMSPD)
* CooSpo cadence and speed sensor (model: BK467)
### Cadence
### Cadence only
* Garmin Cadence Sensor 2
* Wahoo Cadence (Model: WFPODCAD2)
This sensor reports cadence data as speed.
A workaround is in place.
## 0x1818: Power Measurement
+2 -2
View File
@@ -63,8 +63,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4580
versionName "v3.23.0"
versionCode 4646
versionName "v3.24.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
+62
View File
@@ -0,0 +1,62 @@
<?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"
viewBox="0 0 24 24"
id="vector"
version="1.1"
sodipodi:docname="ic_clear_filter.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
<metadata
id="metadata8">
<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></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1011"
id="namedview4"
showgrid="false"
inkscape:zoom="16.541667"
inkscape:cx="12.392292"
inkscape:cy="24.710689"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="vector" />
<path
id="path"
style="fill:#707070;fill-opacity:1"
d="M 5.0390625 4 C 4.2090625 4 3.74 4.949375 4.25 5.609375 C 6.0862696 7.9637998 8.9006606 11.587089 9.4765625 12.328125 L 17.804688 4 L 5.0390625 4 z M 14 13.958984 L 10 17.958984 L 10 19 C 10 19.55 10.45 20 11 20 L 13 20 C 13.55 20 14 19.55 14 19 L 14 13.958984 z " />
<rect
style="fill:#707070;fill-opacity:1;stroke:none;stroke-width:1.118;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833"
width="21.37405"
height="1.7606405"
x="-10.108474"
y="16.886091"
ry="0.88032025"
transform="rotate(-45)" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,17 @@
v3.24.0: OpenTracks
Changes:
- GPS is optional (also for indoor use): if using a BLE sensors that provide distance are used
- Settings: option to disable BLE filtering of devices
- Charts: altitude starts at minimal value instead of 0m.
- UI supports showing stored timezone information
Bugfix:
- Re-added workaround for crash due to clock traveling back in time (workaround was removed in v3.23.0)
- Removed unecessary workaround for BLE cycling sensor (RTFM)
- GPX importer did not handle missing data properly
Developer:
- BLE sensors report data event-driven than pull-based
- Reworked and added integration tests for recording
@@ -0,0 +1,8 @@
v3.19.1: OpenTracks
Cambios:
- Traducciones actualizadas.
Corrección de errores:
- Corregido el error de exportar todas las pistas.
- Corregido el error de reanudar la pista.
@@ -0,0 +1,7 @@
v3.20.0: OpenTracks
Cambios:
- Durante la grabación: la interfaz de usuario de las estadísticas es ahora configurable
Corrección de errores:
- Se ignoraba el cambio de nombre de pista/categoría durante la grabación (introducido en la v3.19.0)
@@ -0,0 +1,6 @@
v3.20.1: OpenTracks
Cambios:
- Los anuncios ahora se pueden utilizar tanto por distancia como por tiempo
- Los anuncios emiten un pitido si no se ha instalado un software de texto a voz
- Se ha eliminado la frecuencia de localización GPS adaptativa
@@ -0,0 +1,10 @@
v3.23.0: OpenTracks
Cambios:
- Añadido perfiles UI
- Estadísticas agregadas para las pistas seleccionadas
- Reimplementación de TrackPoints inactivos/no inactivos ya que el tiempo de movimiento dejó de funcionar (introducido en la v3.22.0)
Corrección de errores:
- Sensores BLE: borrado de datos al desconectarse
- La exportación automática necesita persistir el permiso
@@ -0,0 +1,81 @@
package de.dennisguse.opentracks;
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.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import android.util.Pair;
import android.view.View;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.runner.AndroidJUnit4;
import org.hamcrest.core.IsInstanceOf;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoAggregatedFilterTest {
private final String CATEGORY = "category";
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@Before
public void setUp() {
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 20);
pair.first.setCategory(CATEGORY);
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), pair.first, pair.second);
}
@Test
public void espressoAggregatedFilterTest() {
// open AggregatedStatisticsActivity through toolbar's menu item
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.track_list_aggregated_stats))));
onView(withId(R.id.track_list_aggregated_stats)).perform(click());
// open FilterDialogFragment through toolbar's menu item
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
onView(withId(R.id.aggregated_statistics_filter)).perform(click());
// check there's a checkbox with CATEGORY text
ViewInteraction checkBox = onView(
allOf(withId(R.id.filter_dialog_check_button), withText(CATEGORY),
withParent(allOf(withId(R.id.filter_items),
withParent(IsInstanceOf.<View>instanceOf(android.view.ViewGroup.class)))),
isDisplayed()));
checkBox.check(matches(isDisplayed()));
// check there's an edit text for "from date"
onView(withId(R.id.filter_date_edit_text_from)).check(matches(isDisplayed()));
// check there's an edit text for "to date"
onView(withId(R.id.filter_date_edit_text_to)).check(matches(isDisplayed()));
}
}
@@ -1,6 +1,19 @@
package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.CoreMatchers.is;
import android.content.Context;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.NoMatchingViewException;
@@ -14,20 +27,6 @@ import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import android.content.Context;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
@@ -35,7 +34,9 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hamcrest.CoreMatchers.is;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@LargeTest
@RunWith(AndroidJUnit4.class)
@@ -89,7 +90,7 @@ public class EspressoCustomLayoutTest {
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
return new TypeSafeMatcher<>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
@@ -1,6 +1,19 @@
package de.dennisguse.opentracks;
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;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.content.Context;
@@ -35,19 +48,6 @@ 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 {
@@ -236,7 +236,6 @@ public class EspressoDeleteTrackTest {
@Override
public void describeTo(Description description) {
}
}));
@@ -1,9 +1,5 @@
package de.dennisguse.opentracks;
import android.content.Context;
import androidx.annotation.NonNull;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
@@ -11,11 +7,9 @@ import org.junit.runners.model.Statement;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class PreferenceMetricUnitRule implements TestRule {
private final Context context;
private final boolean metricUnits;
public PreferenceMetricUnitRule(@NonNull Context context, boolean metricUnits) {
this.context = context;
public PreferenceMetricUnitRule(boolean metricUnits) {
this.metricUnits = metricUnits;
}
@@ -1,5 +1,8 @@
package de.dennisguse.opentracks.chart;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
@@ -7,15 +10,13 @@ import org.junit.runner.RunWith;
import java.time.Duration;
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.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 {
@@ -26,7 +27,7 @@ public class ChartPointTest {
statistics.setTotalTime(Duration.ofSeconds(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), 0, false, false);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, false);
// then
assertEquals(1000000, (long) point.getTimeOrDistance());
@@ -39,7 +40,7 @@ public class ChartPointTest {
statistics.setTotalDistance(Distance.of(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), 0, true, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, true);
// then
assertEquals(1, (long) point.getTimeOrDistance());
@@ -51,7 +52,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true);
// then
assertEquals(50, point.getAltitude(), 0.01);
@@ -65,7 +66,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true);
// then
assertNull(point.getHeartRate());
@@ -84,7 +85,7 @@ public class ChartPointTest {
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), 50, false, true);
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, true);
// then
assertEquals(100.0, point.getHeartRate(), 0.01);
@@ -27,6 +27,8 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
@@ -91,7 +93,7 @@ public class DescriptionGeneratorTest {
+ "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@@ -127,7 +129,7 @@ public class DescriptionGeneratorTest {
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@@ -0,0 +1,205 @@
package de.dennisguse.opentracks.content.data;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import de.dennisguse.opentracks.content.provider.SelectionData;
@RunWith(AndroidJUnit4.class)
public class TrackSelectionTest extends TestCase {
@Test
public void testFilterBuildSelection_empty() {
// given
TrackSelection filter = new TrackSelection();
// when
SelectionData selection = filter.buildSelection();
// Then
assertNull(selection.getSelection());
assertNull(selection.getSelectionArgs());
}
@Test
public void testFilterBuildSelection_onlyOneTrackId() {
// given
Track.Id trackId = new Track.Id(1);
TrackSelection filter = new TrackSelection().addTrackId(trackId);
// when
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.getSelection(), "_id IN (?)");
assertEquals(selection.getSelectionArgs().length, 1);
assertEquals(selection.getSelectionArgs()[0], "1");
}
@Test
public void testFilterBuildSelection_severalTracksId() {
// given
Track.Id trackId1 = new Track.Id(1);
Track.Id trackId2 = new Track.Id(2);
Track.Id trackId3 = new Track.Id(3);
TrackSelection filter = new TrackSelection()
.addTrackId(trackId1)
.addTrackId(trackId2)
.addTrackId(trackId3);
// when
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.getSelection(), "_id IN (?,?,?)");
assertEquals(selection.getSelectionArgs().length, 3);
assertEquals(selection.getSelectionArgs()[0], "1");
assertEquals(selection.getSelectionArgs()[1], "2");
assertEquals(selection.getSelectionArgs()[2], "3");
}
@Test
public void testFilterBuildSelection_onlyOneCategory() {
// given
TrackSelection filter = new TrackSelection().addCategory("running");
// when
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.getSelection(), "category IN (?)");
assertEquals(selection.getSelectionArgs().length, 1);
assertEquals(selection.getSelectionArgs()[0], "running");
}
@Test
public void testFilterBuildSelection_severalCategories() {
// given
TrackSelection filter = new TrackSelection()
.addCategory("running")
.addCategory("road biking")
.addCategory("mountain biking")
.addCategory("trail walking");
// when
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.getSelection(), "category IN (?,?,?,?)");
assertEquals(selection.getSelectionArgs().length, 4);
assertEquals(selection.getSelectionArgs()[0], "running");
assertEquals(selection.getSelectionArgs()[1], "road biking");
assertEquals(selection.getSelectionArgs()[2], "mountain biking");
assertEquals(selection.getSelectionArgs()[3], "trail walking");
}
@Test
public void testFilterBuildSelection_onlyDateRange() {
// given
Instant instant = Instant.now();
long oneDay = 24 * 60 * 60 * 1000;
TrackSelection filterWrong1 = new TrackSelection().addDateRange(instant, null);
TrackSelection filterWrong2 = new TrackSelection().addDateRange(null, instant);
TrackSelection filterOk = new TrackSelection().addDateRange(instant, instant.plusMillis(oneDay));
// when
SelectionData selectionWrong1 = filterWrong1.buildSelection();
SelectionData selectionWrong2 = filterWrong2.buildSelection();
SelectionData selectionOk = filterOk.buildSelection();
// Then
assertNull(selectionWrong1.getSelection());
assertNull(selectionWrong1.getSelectionArgs());
assertNull(selectionWrong2.getSelection());
assertNull(selectionWrong2.getSelectionArgs());
assertEquals(selectionOk.getSelection(), "starttime BETWEEN ? AND ?");
assertEquals(selectionOk.getSelectionArgs().length, 2);
assertEquals(selectionOk.getSelectionArgs()[0], Long.toString(instant.toEpochMilli()));
assertEquals(selectionOk.getSelectionArgs()[1], Long.toString(instant.toEpochMilli() + oneDay));
}
@Test
public void testFilterBuildSelection_tracksId_and_categories() {
// given
Track.Id trackId1 = new Track.Id(1);
Track.Id trackId2 = new Track.Id(2);
Track.Id trackId3 = new Track.Id(3);
TrackSelection filter = new TrackSelection()
.addTrackId(trackId1)
.addTrackId(trackId2)
.addTrackId(trackId3)
.addCategory("running")
.addCategory("road biking");
// when
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.getSelection(), "_id IN (?,?,?) AND category IN (?,?)");
assertEquals(selection.getSelectionArgs().length, 5);
assertEquals(selection.getSelectionArgs()[0], "1");
assertEquals(selection.getSelectionArgs()[1], "2");
assertEquals(selection.getSelectionArgs()[2], "3");
assertEquals(selection.getSelectionArgs()[3], "running");
assertEquals(selection.getSelectionArgs()[4], "road biking");
}
@Test
public void testFilterBuildSelection_tracksId_and_dateRange() {
// given
Instant instant = Instant.now();
long oneDay = 24 * 60 * 60 * 1000;
Track.Id trackId1 = new Track.Id(1);
Track.Id trackId2 = new Track.Id(2);
Track.Id trackId3 = new Track.Id(3);
TrackSelection filter = new TrackSelection()
.addTrackId(trackId1)
.addTrackId(trackId2)
.addTrackId(trackId3)
.addDateRange(instant, instant.plusMillis(oneDay));
// when
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.getSelection(), "_id IN (?,?,?) AND starttime BETWEEN ? AND ?");
assertEquals(selection.getSelectionArgs().length, 5);
assertEquals(selection.getSelectionArgs()[0], "1");
assertEquals(selection.getSelectionArgs()[1], "2");
assertEquals(selection.getSelectionArgs()[2], "3");
assertEquals(selection.getSelectionArgs()[3], Long.toString(instant.toEpochMilli()));
assertEquals(selection.getSelectionArgs()[4], Long.toString(instant.toEpochMilli() + oneDay));
}
@Test
public void testFilterBuildSelection_categories_and_dateRange() {
// given
Instant instant = Instant.now();
long oneDay = 24 * 60 * 60 * 1000;
TrackSelection filter = new TrackSelection()
.addCategory("running")
.addCategory("road biking")
.addDateRange(instant, instant.plusMillis(oneDay));
// when
SelectionData selection = filter.buildSelection();
// Then
assertEquals(selection.getSelection(), "category IN (?,?) AND starttime BETWEEN ? AND ?");
assertEquals(selection.getSelectionArgs().length, 4);
assertEquals(selection.getSelectionArgs()[0], "running");
assertEquals(selection.getSelectionArgs()[1], "road biking");
assertEquals(selection.getSelectionArgs()[2], Long.toString(instant.toEpochMilli()));
assertEquals(selection.getSelectionArgs()[3], Long.toString(instant.toEpochMilli() + oneDay));
}
}
@@ -15,6 +15,13 @@
*/
package de.dennisguse.opentracks.content.provider;
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;
import static org.mockito.Mockito.when;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -57,13 +64,6 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.UUIDUtils;
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;
import static org.mockito.Mockito.when;
/**
* A unit test for {@link ContentProviderUtils}.
*
@@ -192,6 +192,19 @@ public class CustomContentProviderUtilsTest {
assertEquals(name, track.getName());
}
private void assertCount(int trackCount, int trackPointCount, int markerCount) {
ContentResolver contentResolver = context.getContentResolver();
try (Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID)) {
assertEquals(trackCount, tracksCursor.getCount());
}
try (Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID)) {
assertEquals(trackPointCount, tracksPointsCursor.getCount());
}
try (Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID)) {
assertEquals(markerCount, markerCursor.getCount());
}
}
/**
* Tests the method {@link ContentProviderUtils#deleteAllTracks(Context)}
*/
@@ -204,22 +217,13 @@ public class CustomContentProviderUtilsTest {
Marker marker = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
contentProviderUtils.insertMarker(marker);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(1, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(10, tracksPointsCursor.getCount());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
// Delete all.
assertCount(1, 10, 1);
// when
contentProviderUtils.deleteAllTracks(context);
// Check whether all have been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(0, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(0, tracksPointsCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, markerCursor.getCount());
// then
assertCount(0, 0, 0);
}
/**
@@ -235,28 +239,18 @@ public class CustomContentProviderUtilsTest {
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint);
contentProviderUtils.insertMarker(marker);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(1, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(10, tracksPointsCursor.getCount());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
// Check marker has photo and it's in the external storage.
assertCount(1, 10, 1);
assertTrue(marker.hasPhoto());
File dir = FileUtils.getPhotoDir(context, trackId);
assertTrue(dir.isDirectory());
assertEquals(1, dir.list().length);
assertTrue(dir.exists());
// Delete all.
// when
contentProviderUtils.deleteAllTracks(context);
// Check whether all have been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(0, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(0, tracksPointsCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, markerCursor.getCount());
// then
assertCount(0, 0, 0);
assertFalse(dir.exists());
}
@@ -279,22 +273,13 @@ public class CustomContentProviderUtilsTest {
Marker marker = new Marker(trackId1, contentProviderUtils.getLastValidTrackPoint(trackId2));
contentProviderUtils.insertMarker(marker);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(30, tracksPointsCursor.getCount());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
// Delete one track.
assertCount(3, 30, 1);
// when
contentProviderUtils.deleteTrack(context, trackId1);
// Check whether all data of a track has been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(2, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(20, tracksPointsCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, markerCursor.getCount());
// then
assertCount(2, 20, 0);
}
/**
@@ -324,13 +309,7 @@ public class CustomContentProviderUtilsTest {
File dir2 = FileUtils.getPhotoDir(context, trackId2);
// Check.
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(30, tracksPointsCursor.getCount());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(2, markerCursor.getCount());
assertCount(3, 30, 2);
assertTrue(marker1.hasPhoto());
assertTrue(dir1.isDirectory());
assertEquals(1, dir1.list().length);
@@ -338,15 +317,12 @@ public class CustomContentProviderUtilsTest {
assertTrue(dir2.isDirectory());
assertEquals(1, dir2.list().length);
assertTrue(dir2.exists());
// Delete one track.
// when
contentProviderUtils.deleteTrack(context, trackId1);
// Check whether all data of a track has been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(2, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(20, tracksPointsCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
// then
assertCount(2, 20, 1);
assertFalse(dir1.exists());
assertTrue(dir2.exists());
}
@@ -1,5 +1,7 @@
package de.dennisguse.opentracks.io.file.exporter;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
@@ -10,12 +12,11 @@ import org.junit.runners.JUnit4;
import java.io.ByteArrayOutputStream;
import java.time.Instant;
import java.time.ZoneOffset;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import static org.junit.Assert.assertEquals;
@RunWith(JUnit4.class)
public class KmlTrackExporterTest {
@@ -27,6 +28,8 @@ public class KmlTrackExporterTest {
@Test
public void writeCloseSegment_only_write_sensordata_if_present() {
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
"<gx:coord/>\n" +
"<when>1970-01-01T01:00:00+01:00</when>\n" +
"<gx:coord/>\n" +
"<ExtendedData>\n" +
"<SchemaData schemaUrl=\"#schema\">\n" +
@@ -41,7 +44,8 @@ public class KmlTrackExporterTest {
KMLTrackExporter kmlTrackWriter = (KMLTrackExporter) TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
kmlTrackWriter.prepare(outputStream);
kmlTrackWriter.writeTrackPoint(trackPoint);
kmlTrackWriter.writeTrackPoint(ZoneOffset.UTC, trackPoint);
kmlTrackWriter.writeTrackPoint(ZoneOffset.ofTotalSeconds(3600), trackPoint);
// when
kmlTrackWriter.writeCloseSegment();
@@ -30,15 +30,12 @@ import org.mockito.Mockito;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
@@ -55,6 +52,7 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
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.TrackRecordingServiceTestUtils;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
@@ -98,55 +96,68 @@ public class ExportImportTest {
private TrackImporter trackImporter;
@Before
public void fileSetup() throws IOException {
public void fileSetup() throws IOException, TimeoutException {
TrackRecordingServiceTestUtils.resetService(mServiceRule, context);
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
tmpFileUri = Uri.fromFile(tmpFile);
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
TrackRecordingServiceTestUtils.resetService(mServiceRule, context);
}
@After
public void FileTearDown() throws TimeoutException {
public void tearDown() throws TimeoutException {
tmpFile.deleteOnExit();
tmpFileUri = null;
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getTrackPointCreator().setClock(Clock.systemUTC());
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
TrackRecordingServiceTestUtils.resetService(mServiceRule, context);
}
public void setUp(boolean hasSensorDistance) throws TimeoutException {
public void setUp() throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
trackPointCreator.setClock("2020-02-02T02:02:02Z");
trackId = service.startNewTrack();
Distance sensorDistance = hasSensorDistance ? Distance.of(10) : null; // recording distance interval
Distance sensorDistance = Distance.of(10); // recording distance interval
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:03Z"), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance);
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 15, 10, 1);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:04Z"), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:05Z"), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
// A sensor-only TrackPoint
trackPointCreator.setClock("2020-02-02T02:02:04Z");
mockAltitudeChange(trackPointCreator, 1);
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:05Z");
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 15, 10, 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:06Z"), ZoneId.of("CET")));
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context));
trackPointCreator.setClock("2020-02-02T02:02:06Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
service.pauseCurrentTrack();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:20Z"), ZoneId.of("CET")));
trackPointCreator.setClock("2020-02-02T02:02:20Z");
service.resumeCurrentTrack();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:21Z"), ZoneId.of("CET")));
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:21Z"), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 15, 10, 0);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:22Z"), 3, 16, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:23Z"), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, sensorDistance);
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 15, 10, 0);
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:24Z"), ZoneId.of("CET")));
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context));
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:02:24Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId);
@@ -162,21 +173,11 @@ public class ExportImportTest {
assertEquals(2, markers.size());
}
@After
public void tearDown() {
if (trackId != null) {
contentProviderUtils.deleteTrack(context, trackId);
}
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
}
//TODO Does not test images
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp(true);
setUp();
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -204,32 +205,51 @@ public class ExportImportTest {
// 2. trackpoints
TrackPointAssert a = new TrackPointAssert()
.noAccuracy();
a.assertEquals(trackPoints, TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId));
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
a.assertEquals(trackPoints, actual);
// 3. trackstatistics
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:24Z"), importedTrackStatistics.getStopTime());
assertEquals(track.getTrackStatistics().getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(8), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getMovingTime());
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(7), importedTrackStatistics.getTotalTime());
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(Distance.of(60), importedTrackStatistics.getTotalDistance());
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(Speed.of(15), importedTrackStatistics.getMaxSpeed());
assertEquals(Speed.of(7.5), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(15), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(17.59, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
// Altitude
assertEquals(originalTrackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(originalTrackStatistics.getMaxAltitude(), importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(10, importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(originalTrackStatistics.getTotalAltitudeGain(), importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(originalTrackStatistics.getTotalAltitudeLoss(), importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
// 4. markers
assertMarkers();
@@ -238,7 +258,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false);
setUp();
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
@@ -264,7 +284,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void gpx() throws TimeoutException, IOException {
setUp(true);
setUp();
// given
Track track = contentProviderUtils.getTrack(trackId);
@@ -295,39 +315,78 @@ public class ExportImportTest {
// 2. trackpoints
// The GPX exporter does not support exporting TrackPoints without lat/lng.
// Therefore, the track segmentation is changes.
List<TrackPoint> trackPointsWithCoordinates = trackPoints.stream().filter(it -> TrackPoint.Type.SEGMENT_START_AUTOMATIC.equals(it.getType()) || TrackPoint.Type.TRACKPOINT.equals(it.getType())).collect(Collectors.toList());
trackPointsWithCoordinates.get(0).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackPointsWithCoordinates.get(3).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
TrackPointAssert a = new TrackPointAssert()
.setDelta(0.05)
.noAccuracy(); // speed is not fully
a.assertEquals(trackPointsWithCoordinates, TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId));
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:03Z"))
.setLatitude(3)
.setLongitude(14)
.setAltitude(10)
.setSpeed(Speed.of(15))
.setAltitudeLoss(1f)
.setAltitudeGain(1f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
.setLatitude(3)
.setLongitude(14.001)
.setAltitude(10)
.setSpeed(Speed.of(5))
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setSensorDistance(Distance.of(12))
.setHeartRate_bpm(66f)
.setPower(50f)
.setCadence_rpm(3f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z"))
.setLatitude(3)
.setLongitude(14.002)
.setAltitude(10)
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setSpeed(Speed.of(15)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:22Z"))
.setLatitude(3)
.setLongitude(16)
.setAltitude(10)
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setSpeed(Speed.of(15)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:23Z"))
.setLatitude(3)
.setLongitude(16.001)
.setAltitude(10)
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setSpeed(Speed.of(15))
), actual);
// 3. trackstatistics
TrackStatistics trackStatistics = track.getTrackStatistics();
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(Distance.of(60), importedTrackStatistics.getTotalDistance());
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(Speed.of(15), importedTrackStatistics.getMaxSpeed());
assertEquals(Speed.of(15), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(15), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(41.05, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(41.05, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
assertEquals(10, importedTrackStatistics.getMaxAltitude(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(1, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(2, importedTrackStatistics.getTotalAltitudeLoss(), 0.01);
// 4. markers
assertMarkers();
@@ -336,7 +395,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false);
setUp();
// given
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
@@ -380,23 +439,8 @@ public class ExportImportTest {
}
}
private void sendLocation(TrackPointCreator trackPointCreator, Instant time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
location.setAltitude(altitude);
trackPointCreator.setAltitudeSumManager(new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(altitudeGain);
trackPoint.setAltitudeLoss(altitudeGain);
}
});
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, Float heartRate, Float cyclingCadence, Float power) {
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator) {
@Override
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
SensorDataSet sensorDataSet = new SensorDataSet();
@@ -408,21 +452,40 @@ public class ExportImportTest {
Mockito.when(cadence.getValue()).thenReturn(cyclingCadence);
sensorDataSet.set(cadence);
SensorDataCycling.DistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCycling.DistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCycling.DistanceSpeed distanceSpeed = Mockito.mock(SensorDataCycling.DistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed);
if (distance != null && speed != null) {
SensorDataCycling.DistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCycling.DistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCycling.DistanceSpeed distanceSpeed = Mockito.mock(SensorDataCycling.DistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed);
}
sensorDataSet.fillTrackPoint(trackPoint);
return sensorDataSet;
}
});
trackPointCreator.onChange(null);
}
trackPointCreator.setClock(Clock.fixed(time, ZoneId.of("CET")));
trackPointCreator.getLocationHandler().onLocationChanged(location);
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
AltitudeSumManager altitudeSumManager = trackPointCreator.getAltitudeSumManager();
altitudeSumManager.setAltitudeGain_m(altitudeGain);
altitudeSumManager.setAltitudeLoss_m(altitudeGain);
}
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
location.setAltitude(altitude);
mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(time);
trackPointCreator.getGpsHandler().onLocationChanged(location);
}
}
@@ -8,7 +8,6 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
public class TrackPointAssert {
private boolean assertTime = true;
private boolean assertAccuracy = true;
private double delta = 0.001;
@@ -17,10 +16,7 @@ public class TrackPointAssert {
}
public void assertEquals(TrackPoint expected, TrackPoint actual) {
Assert.assertNotNull(actual.getTime());
if (assertTime) {
Assert.assertEquals(expected.getTime(), actual.getTime());
}
Assert.assertEquals(expected.getTime(), actual.getTime());
Assert.assertEquals(expected.getType(), actual.getType());
@@ -100,11 +96,6 @@ public class TrackPointAssert {
}
}
public TrackPointAssert ignoreTime() {
this.assertTime = false;
return this;
}
public TrackPointAssert noAccuracy() {
this.assertAccuracy = false;
return this;
@@ -1,464 +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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
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;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
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;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests for the track recording service.
*
* @author Bartlomiej Niechwiej
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTest {
@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();
private ContentProviderUtils contentProviderUtils;
private final Track.Id trackId = new Track.Id(Math.abs(new Random().nextLong()));
static Intent createStartIntent(Context context) {
return new Intent(context, TrackRecordingService.class);
}
private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(0f);
trackPoint.setAltitudeLoss(0f);
}
};
@Before
public void setUp() {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
PreferencesUtils.clear();
// Ensure that the database is empty before every test
contentProviderUtils.deleteAllTracks(context);
}
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
service.getTrackPointCreator().setClock(Clock.systemUTC());
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@SmallTest
@Test
public void testStartable() throws TimeoutException {
mServiceRule.startService(createStartIntent(context));
assertNotNull(mServiceRule.bindService(createStartIntent(context)));
}
@MediumTest
@Test
public void testBindable() throws TimeoutException {
IBinder service = mServiceRule.bindService(createStartIntent(context));
assertNotNull(service);
}
@MediumTest
@Test
public void testRecording_noTracks() throws TimeoutException {
// given
List<Track> tracks = contentProviderUtils.getTracks();
assertTrue(tracks.isEmpty());
// when
Intent startIntent = createStartIntent(context);
mServiceRule.startService(startIntent);
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent))
.getService();
// then
// Test if we start in no-recording mode by default.
assertFalse(service.isRecording());
}
@MediumTest
@Test
public void testRecording_oldTracks() throws TimeoutException {
// given
createDummyTrack(trackId);
// when
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
// then
assertFalse(service.isRecording());
}
@MediumTest
@Test
public void testRecording_serviceRestart_whileRecording() throws TimeoutException {
// given
createDummyTrack(trackId);
//when
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
service.resumeTrack(trackId);
// then
assertTrue(service.isRecording());
}
@MediumTest
@Test
public void testRecording_stop() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.endCurrentTrack();
// then
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:03Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@Test
public void testRecording_pauseAndResume() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.pauseCurrentTrack();
// then
assertEquals(2, contentProviderUtils.getTrackPointCursor(trackId, null).getCount());
//when
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.resumeTrack(trackId);
// then
assertTrue(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:03Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:04Z"))
), trackPoints);
}
@MediumTest
@Test
public void testRecording_resumeStoppedTrack() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.endCurrentTrack();
// when
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.resumeTrack(trackId);
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z")), Distance.of(50));
// then
assertTrue(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:03Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:04Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@Test
public void testRecording_stopPausedTrack() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:03Z"), ZoneId.of("CET")));
service.pauseCurrentTrack();
// when
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:04Z"), ZoneId.of("CET")));
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:03Z"))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
// when
Track.Id newTrackId = service.startNewTrack();
// then
assertNotNull(trackId);
assertNull(newTrackId);
}
@MediumTest
@Test
public void testEndCurrentTrack_noRecording() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
assertFalse(service.isRecording());
// when
// Ending the current track when there is no recording should not result in any error.
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
}
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
assertFalse(service.isRecording());
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
// then
assertNull(markerId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(createStartIntent(context)))
.getService();
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.stopGPS();
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
trackPointCreator.onNewTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:03Z"))
.setLatitude(10)
.setLongitude(10)
, Distance.of(50));
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
// then
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().toM(), 0.01);
assertNotNull(wpt.getLocation());
service.endCurrentTrack();
}
private void addTrack(Track track) {
assertNotNull(track.getId());
contentProviderUtils.insertTrack(track);
assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
}
// NOTE: Do not use to create a track that is currently recording.
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);
}
static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed) {
newTrackPoint(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis());
}
static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(time))
.setLongitude(longitude)
.setLatitude(latitude)
.setHorizontalAccuracy(Distance.of(accuracy))
.setSpeed(Speed.of(speed))
.setBearing(3.0f);
trackRecordingService.getTrackPointCreator().onNewTrackPoint(trackPoint, Distance.of(50));
}
}
@@ -1,539 +0,0 @@
package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertFalse;
import android.content.ContentProvider;
import android.content.Context;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
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;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
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.CustomContentProvider;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Tests insert location.
*/
@RunWith(AndroidJUnit4.class)
//TODO Implement as mock test; no need to store data in database
public class TrackRecordingServiceTestLocation {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private TrackRecordingService service;
private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(0f);
trackPoint.setAltitudeLoss(0f);
}
};
@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() throws TimeoutException {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
tearDown();
// Let's use default values.
PreferencesUtils.clear();
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
service.getTrackPointCreator().stopGPS();
}
@After
public void tearDown() {
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_movingAccurate() {
// given
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0001, 35.0, 2, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 3, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0003, 35.0, 4, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0005, 35.0, 6, 15);
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(3))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0002)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(4))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0003)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0004)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_slowMovingAccurate() {
// given
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000001, 35.0, 2, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 3, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 4, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000004, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000005, 35.0, 6, 15);
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0005)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_idle() {
// given
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 6, 0);
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_idle_withMovement() {
// given
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
service.getTrackPointCreator().stopGPS();
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0); // will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0); // will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 6, 15);
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_idle_withSensorData() {
// given
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
@Override
public boolean isEnabled() {
return true;
}
@Override
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataHeartRate("sensorName", "sensorAddress", 5f));
sensorDataSet.fillTrackPoint(trackPoint);
return sensorDataSet;
}
});
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 6, 0);
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f)
), trackPoints);
}
@Deprecated // Will be superseded when fixing #500
@MediumTest
@Test
public void testOnLocationChangedAsync_idle_withSensorDistance() {
BluetoothRemoteSensorManager remoteSensorManager = new BluetoothRemoteSensorManager(context) {
@Override
public boolean isEnabled() {
return true;
}
};
AltitudeSumManager altitudeSumManager = new AltitudeSumManager();
// given
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setRemoteSensorManager(remoteSensorManager);
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
altitudeSumManager.stop(service);
altitudeSumManager.setConnected(true);
// when
altitudeSumManager.addAltitudeGain_m(6f);
altitudeSumManager.addAltitudeLoss_m(6f);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(0)));
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(2)));
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(12)));
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 15);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(13)));
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 15);
altitudeSumManager.addAltitudeGain_m(6f);
altitudeSumManager.addAltitudeLoss_m(6f);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(14)));
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 15);
altitudeSumManager.addAltitudeGain_m(6f);
altitudeSumManager.addAltitudeLoss_m(6f);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(16)));
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(5))
.setAltitudeGain(6f)
.setAltitudeLoss(6f)
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(4))
.setSpeed(Speed.of(5))
.setAltitudeGain(6f)
.setAltitudeLoss(6f)
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setSensorDistance(Distance.of(11))
.setSpeed(Speed.of(5))
.setAltitudeGain(6f)
.setAltitudeLoss(6f)
.setSensorDistance(Distance.of(2))
), trackPoints);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_segment() {
// given
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.1, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.1, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.2, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.2, 35.0, 5, 0);
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert()
.ignoreTime();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, null),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, null)
.setLatitude(45.1)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.1)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(3))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, null)
.setLatitude(45.2)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(4))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.2)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
}
@@ -1,233 +0,0 @@
package de.dennisguse.opentracks.services;
import android.content.ContentProvider;
import android.content.Context;
import android.content.SharedPreferences;
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;
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;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
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.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Tests for the track recording service, which require a {@link Looper}.
*
* @author Bartlomiej Niechwiej
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestLooper {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@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() {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
PreferencesUtils.clear();
// Ensure that the database is empty before every test
contentProviderUtils.deleteAllTracks(context);
}
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracy() throws TimeoutException {
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_maxRecordingDistanceDefault() throws TimeoutException {
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 {
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 {
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 {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putInt(context.getString(R.string.recording_distance_interval_key), 2);
editor.commit();
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDefault() throws TimeoutException {
PreferencesUtils.setString(R.string.stats_units_key, context.getString(R.string.stats_units_default));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDisabled() throws TimeoutException {
PreferencesUtils.setString(R.string.stats_units_key, context.getString(R.string.stats_units_imperial));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingIntervalDefault() throws TimeoutException {
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 {
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 {
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();
}
@MediumTest
@Test
public void testIntegration_completeRecordingSession() throws TimeoutException {
List<Track> tracks = contentProviderUtils.getTracks();
assertTrue(tracks.isEmpty());
fullRecordingSession();
}
private void fullRecordingSession() throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
assertFalse(service.isRecording());
// Start a track.
Track.Id trackId = service.startNewTrack();
assertNotNull(trackId);
assertTrue(service.isRecording());
Track track = contentProviderUtils.getTrack(trackId);
assertNotNull(track);
assertEquals(trackId, track.getId());
// Insert a few points, markers and statistics.
for (int i = 0; i < 30; i++) {
//TODO Should send locations to LocationHandler instead of TrackPoints to TrackPointCreator?
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, service.getTrackPointCreator().createNow())
.setLongitude(35.0f + i / 10.0f)
.setLatitude(45.0f - i / 5.0f)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(10))
.setBearing(3.0f);
Distance prefAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
service.getTrackPointCreator().onNewTrackPoint(trackPoint, prefAccuracy);
if (i % 7 == 0) {
service.insertMarker(null, null, null, null);
}
}
// Stop the track. Validate if it has correct data.
service.endCurrentTrack();
assertFalse(service.isRecording());
track = contentProviderUtils.getTrack(trackId);
assertNotNull(track);
assertEquals(trackId, track.getId());
TrackStatistics trackStatistics = track.getTrackStatistics();
assertNotNull(trackStatistics);
assertTrue(trackStatistics.getStartTime().isAfter(Instant.ofEpochMilli(0)));
assertTrue(trackStatistics.getStopTime().isAfter(trackStatistics.getStartTime()));
}
}
@@ -0,0 +1,158 @@
/*
* 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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
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.filters.MediumTest;
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;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestMarker {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private TrackRecordingService service;
@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 TrackRecordingService startService() throws TimeoutException {
Intent startIntent = new Intent(context, TrackRecordingService.class);
return ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent))
.getService();
}
@Before
public void setUp() throws TimeoutException {
contentProviderUtils = new ContentProviderUtils(context);
service = startService();
tearDown();
}
@After
public void tearDown() throws TimeoutException {
TrackRecordingServiceTestUtils.resetService(mServiceRule, context);
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@MediumTest
@Test
public void notRecording_testInsertMarker() {
// given
assertFalse(service.isRecording());
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
// then
assertNull(markerId);
}
@MediumTest
@Test
public void recording_noGPSfix_cannotCreateMarker() {
// given
service.startNewTrack();
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
// then
assertNull(markerId);
}
@MediumTest
@Test
public void recording_GPSfix_createsMarker() {
// given
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.stopGPS();
trackPointCreator.setClock("2020-02-02T02:02:02Z");
Track.Id trackId = service.startNewTrack();
service.stopUpdateRecordingData();
assertTrue(service.isRecording());
trackPointCreator.onNewTrackPoint(
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:03Z"))
.setLatitude(10)
.setLongitude(10)
);
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
// then
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().toM(), 0.01);
assertNotNull(wpt.getLocation());
service.endCurrentTrack();
}
}
@@ -0,0 +1,826 @@
package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
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;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
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.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.sensor.SensorDataRunning;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests insert location.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestRecording {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private TrackRecordingService service;
private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(0f);
trackPoint.setAltitudeLoss(0f);
}
};
@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 TrackRecordingService startService() throws TimeoutException {
Intent startIntent = new Intent(context, TrackRecordingService.class);
return ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent))
.getService();
}
@Before
public void setUp() throws TimeoutException {
contentProviderUtils = new ContentProviderUtils(context);
tearDown();
PreferencesUtils.setString(R.string.recording_distance_interval_key, R.string.recording_distance_interval_default);
PreferencesUtils.setString(R.string.idle_speed_key, R.string.idle_speed_default);
service = startService();
service.getTrackPointCreator().stopGPS();
}
@After
public void tearDown() throws TimeoutException {
TrackRecordingServiceTestUtils.resetService(mServiceRule, context);
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@MediumTest
@Test
public void recording_startStop() {
// given
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
// when
String startTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// then
assertEquals(new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
// when
String stopTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 1, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_startPauseResume() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String pauseTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(pauseTime);
service.pauseCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, pauseTime, 0, 1, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
//when
String resumeTime = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(resumeTime);
service.resumeTrack(trackId);
// then
assertEquals(new TrackStatistics(startTime, resumeTime, 0, 1, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_startPauseStop() {
// given
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
String starTime = "2020-02-02T02:02:02Z";
trackPointCreator.setClock(starTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
String pauseTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(pauseTime);
service.pauseCurrentTrack();
// when
trackPointCreator.setClock("2020-02-02T02:02:04Z");
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(starTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(pauseTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@Test
public void testRecording_startStopResumeStop() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
String stopTime = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// when
String resumeTime = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(resumeTime);
service.resumeTrack(trackId);
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(resumeTime))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_above() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps1, 0, 1, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0001, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps2, 11.113178253173828f, 4, 3, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps3 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0002, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps3, 22.226356506347656, 6, 5, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 22.226356506347656, 10, 5, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps2))
.setLatitude(45.0001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45.0001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_above_speed_0() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 0);
// then
TrackStatistics gps1statistics = new TrackStatistics(startTime, gps1, 0, 1, 0, 0, 0f, 0f);
assertEquals(gps1statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0001, 35.0, 1, 0);
// then
assertEquals(gps1statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps3 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0002, 35.0, 1, 0);
// then
assertEquals(gps1statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 10, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45.0001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_below() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
TrackStatistics gps1Statistics = new TrackStatistics(startTime, gps1, 0, 1, 0, 0, 0f, 0f);
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.00001, 35.0, 1, 15);
// then
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps3 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.00002, 35.0, 1, 15);
// then
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 2.222635507583618, 10, 5, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45.00002)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_idle() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 0);
// then
TrackStatistics gps1Statistics = new TrackStatistics(startTime, gps1, 0, 1, 0, 0, 0f, 0f);
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 0);
// then
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps3 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0, 35.0, 1, 0);
// then
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 10, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45.00002)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_idle_movement() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps1, 0, 1, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 0);
// then
final TrackStatistics gps2statistics = new TrackStatistics(startTime, gps2, 0, 4, 0, 0, 0f, 0f);
assertEquals(gps2statistics
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps3 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0, 35.0, 1, 0);
// then
assertEquals(gps2statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps4 = "2020-02-02T02:02:10Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps4, 45.0, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps4, 0, 8, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 10, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps2))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps4))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_ignore_inaccurate() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 100, 15);
// then
TrackStatistics startStatistics = new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null);
assertEquals(startStatistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.1, 35.0, 100, 15);
// then
assertEquals(startStatistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 10, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_segment() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
// when
String gps1 = "2020-02-02T02:02:03Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps1, 0, 1, 0, 0, 0f, 0f), contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String gps2 = "2020-02-02T02:02:06Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.1, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps2, 0, 1, 0, 0, 0f, 0f), contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
String stopTime = "2020-02-02T02:02:12Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 7, 0, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse(gps2))
.setLatitude(45.1)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
/**
* Make sure that GPS-based TrackPoints are stored, if the distance to the previous GPS-based TrackPoint is greater than recordingDistanceInterval.
*/
@MediumTest
@Test
public void testRecording_gpsAndSensor_gpsIdleMoving_sensorMoving() {
// TODO Check TrackStatistics
AltitudeSumManager altitudeSumManager = new AltitudeSumManager();
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager();
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(0))); //Should be ignored
// when
String sensor2 = "2020-02-02T02:02:04Z";
trackPointCreator.setClock(sensor2);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(2))); //TODO Should be ignored; distance should be added to the next TrackPoint
// when
String gps1 = "2020-02-02T02:02:05Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// when
String sensor3 = "2020-02-02T02:02:06Z";
trackPointCreator.setClock(sensor3);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(12)));
// when
String sensor4 = "2020-02-02T02:02:07Z";
trackPointCreator.setClock(sensor4);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(14))); //Should be ignored
// when
String gps2 = "2020-02-02T02:02:08Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 4, 15); //Should be ignored
// when
String sensor5 = "2020-02-02T02:02:10Z";
trackPointCreator.setClock(sensor5);
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(16)));
// when
String gps3 = "2020-02-02T02:02:12Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.001, 35.0, 1, 15);
// when
String gps4 = "2020-02-02T02:02:14Z";
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps4, 45.001, 35.0, 1, 15);
// when
String stopTime = "2020-02-02T02:02:16Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor2)) // TODO Should be ignored; is stored as it assumed to be first in current segment.
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(0)),
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)),
/*
//TODO BUG Should be stored, but sensorDistance is used instead of distance to previous TRACKPOINT.
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
.setLatitude(45.001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps4))
.setLatitude(45.001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(2)),
*/
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps4))
.setLatitude(45.001)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(1))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(4)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setSensorDistance(Distance.of(11))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(0))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
}
@@ -0,0 +1,282 @@
/*
* 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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
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.filters.MediumTest;
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.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
/**
* Testing the states of TrackRecordingService.
* As states are checked against MutableLiveData that are updated via `postValue()` (is asynchronous to be inform other Threads), Thread.sleep() is required.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestStateMachine {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private TrackRecordingService service;
@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 TrackRecordingService startService() throws TimeoutException {
Intent startIntent = new Intent(context, TrackRecordingService.class);
return ((TrackRecordingService.Binder) mServiceRule.bindService(startIntent))
.getService();
}
@Before
public void setUp() throws TimeoutException {
contentProviderUtils = new ContentProviderUtils(context);
service = startService();
tearDown();
}
@After
public void tearDown() throws TimeoutException {
TrackRecordingServiceTestUtils.resetService(mServiceRule, context);
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@MediumTest
@Test
public void initialState() {
// given
List<Track> tracks = contentProviderUtils.getTracks();
assertTrue(tracks.isEmpty());
// when
// noop
// then
assertFalse(service.isRecording());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
}
@MediumTest
@Test
public void gps_startStop() throws InterruptedException {
// given
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when
service.tryStartGps();
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
// when
service.stopGps(true);
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
// assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); TODO BUG: GPS can only be stopped if a service is recording.
}
@MediumTest
@Test
public void recording_startPauseStopResume_no_data() throws InterruptedException {
// given
assertFalse(service.isRecording());
// when
Track.Id trackId = service.startNewTrack();
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertEquals(new TrackRecordingService.RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
// when
service.pauseCurrentTrack();
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertTrue(service.isPaused());
assertEquals(new TrackRecordingService.RecordingStatus(trackId, true), service.getRecordingStatusObservable().getValue());
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); //TODO The GPS is kept in pause, right? Might be a bug.
// when
service.resumeCurrentTrack();
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertFalse(service.isPaused());
assertEquals(new TrackRecordingService.RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
// when
service.endCurrentTrack();
Thread.sleep(1000);
// then
assertFalse(service.isRecording());
assertFalse(service.isPaused());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when
service.resumeTrack(trackId);
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertFalse(service.isPaused());
assertEquals(new TrackRecordingService.RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
}
@MediumTest
@Test
public void invalidStateChanges() throws InterruptedException {
// given
assertFalse(service.isRecording());
// when
service.pauseCurrentTrack();
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when
service.resumeCurrentTrack();
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when
service.endCurrentTrack();
Thread.sleep(1000);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
}
@Ignore("TODO Bug: GPS can be stopped although the current track is recording")
@MediumTest
@Test
public void recording_stopGPS_noop() throws InterruptedException {
// given
Track.Id trackId = service.startNewTrack();
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
// when
service.stopGps(false); //TODO Should be ignored as service is recording
Thread.sleep(1000);
// then
assertTrue(service.isRecording());
assertFalse(service.isPaused());
assertEquals(new TrackRecordingService.RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
}
@MediumTest
@Test
public void recording_startRecording_alreadyRecording() throws InterruptedException {
// given
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
// when
Track.Id newTrackId = service.startNewTrack();
Thread.sleep(1000);
// then
assertNotNull(trackId);
assertNull(newTrackId);
}
}
@@ -1,198 +0,0 @@
package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
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.CustomContentProvider;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests resulting TrackStatistics.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestStatistics {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private TrackRecordingService service;
private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(0f);
trackPoint.setAltitudeLoss(0f);
}
};
@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() throws TimeoutException {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
tearDown();
// Let's use default values.
PreferencesUtils.clear();
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
service.getTrackPointCreator().stopGPS();
}
@After
public void tearDown() throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getTrackPointCreator().setClock(Clock.systemUTC());
}
/**
* Moving time should increase if the previous and current TrackPoint have speed > threshold by the timeDiff(previousTrackPoint, currentTrackPoint).
*/
@MediumTest
@Test
public void movingtime_with_pauses() {
Assume.assumeTrue(
"Test fails on API23; reproducible on CI and some machines.",
Build.VERSION.SDK_INT > 23
);
// given
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()));
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
Function<Integer, Void> assertMovingTime = expected -> {
Duration actual = contentProviderUtils.getTrack(trackId).getTrackStatistics().getMovingTime();
assertEquals(Duration.ofSeconds(expected), actual);
return null;
};
Function<Integer, Void> assertTotalTime = expected -> {
Duration actual = contentProviderUtils.getTrack(trackId).getTrackStatistics().getTotalTime();
assertEquals(Duration.ofSeconds(expected), actual);
return null;
};
// when / then
int movingtime_s = 0;
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15, 5 * 60000);
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0001, 35.0, 2, 15, 6 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 2, 15, (long) (6.5 * 60000));
TrackRecordingServiceTest.newTrackPoint(service, 45.0003, 35.0, 2, 15, 7 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 15, 8 * 60000);
movingtime_s += 3 * 60;
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 0, 9 * 60000); //will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 0, 10 * 60000); //will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 0, 11 * 60000);
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 15, 13 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 15, (long) (13.5 * 60000)); //will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0005, 35.0, 2, 15, 14 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0006, 35.0, 2, 15, 15 * 60000);
movingtime_s += 2 * 60;
assertMovingTime.apply(movingtime_s);
assertTotalTime.apply(15 * 60);
TrackRecordingServiceTest.newTrackPoint(service, 45.0006, 35.0, 2, 0, 16 * 60000); //will be ignored
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0015, 35.0, 2, 0, 17 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0016, 35.0, 2, 15, 18 * 60000);
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0016, 35.0, 2, 0, 19 * 60000); //TODO we could ignore this TrackPoint
assertMovingTime.apply(movingtime_s);
assertTotalTime.apply(19 * 60);
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochSecond(40 * 60), ZoneId.systemDefault()));
assertMovingTime.apply(movingtime_s);
service.pauseCurrentTrack();
assertTotalTime.apply(40 * 60);
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochSecond(41 * 60), ZoneId.systemDefault()));
service.resumeCurrentTrack();
TrackRecordingServiceTest.newTrackPoint(service, 45.0016, 35.0, 2, 15, 42 * 60000);
assertMovingTime.apply(movingtime_s);
assertTotalTime.apply(41 * 60);
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochSecond(50 * 60), ZoneId.systemDefault()));
service.endCurrentTrack();
assertMovingTime.apply(movingtime_s);
assertTotalTime.apply(49 * 60);
// then
assertFalse(service.isRecording());
TrackStatistics trackStatistics = contentProviderUtils.getTrack(trackId).getTrackStatistics();
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(20, trackPoints.size());
assertEquals(Duration.ofMinutes(49), trackStatistics.getTotalTime());
}
}
@@ -0,0 +1,47 @@
package de.dennisguse.opentracks.services;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import androidx.test.rule.ServiceTestRule;
import java.time.Clock;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class TrackRecordingServiceTestUtils {
//TODO Workaround as service is not stopped on API23; thus sharedpreferences are not reset between tests.
//TODO Anyhow, the service should re-create all it's resources if a recording starts and makes sure that there is no leftovers from previous recordings.
@Deprecated
public static void resetService(ServiceTestRule mServiceRule, Context context) throws TimeoutException {
// Let's use default values.
PreferencesUtils.clear();
// Reset service (if some previous test failed)
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context, service.getTrackPointCreator()));
service.getTrackPointCreator().setClock(Clock.systemUTC());
service.endCurrentTrack();
service.sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
Location location = new Location("mock");
location.setTime(1L); // Should be ignored anyhow.
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
trackPointCreator.setClock(time);
trackPointCreator.getGpsHandler().onLocationChanged(location);
}
}
@@ -26,7 +26,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(MockitoJUnitRunner.class)
public class LocationHandlerTest {
public class GPSHandlerTest {
private final Context context = ApplicationProvider.getApplicationContext();
@@ -34,7 +34,7 @@ public class LocationHandlerTest {
private TrackPointCreator trackPointCreator;
@InjectMocks
private LocationHandler locationHandler;
private GPSHandler locationHandler;
@BeforeClass
public static void preSetUp() {
@@ -62,7 +62,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis()));
// then
verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class));
}
/**
@@ -78,7 +78,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis()));
// then
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class));
}
/**
@@ -94,7 +94,7 @@ public class LocationHandlerTest {
// then
// no newTrackPoint called
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class));
}
@Test
@@ -107,7 +107,7 @@ public class LocationHandlerTest {
locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
// then
verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class), any(Distance.class));
verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class));
}
/**
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.services.handlers;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.content.SharedPreferences;
import org.junit.After;
import org.junit.Before;
@@ -26,10 +25,7 @@ public class TrackPointCreatorTest {
private TrackPointCreator.Callback server;
@Mock
private LocationHandler locationHandler;
@Mock
private SharedPreferences sharedPreferences;
private GPSHandler locationHandler;
private TrackPointCreator subject;
@@ -52,7 +48,7 @@ public class TrackPointCreatorTest {
Distance horizontalAccuracyThreshold = Distance.of(50);
// when
subject.onNewTrackPoint(trackPoint, horizontalAccuracyThreshold);
subject.onNewTrackPoint(trackPoint);
// then
Thread.sleep(10); // Wait for executor service
@@ -16,6 +16,9 @@
package de.dennisguse.opentracks.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
@@ -24,16 +27,27 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.TextStyle;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.stream.Collectors;
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;
/**
* Tests for {@link StringUtils}.
*
@@ -141,7 +155,7 @@ public class StringUtilsTest {
// This comparision tends to be flaky (difference of 1ms)
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toEpochMilli()) <= 1);
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toInstant().toEpochMilli()) <= 1);
}
@Test
@@ -184,4 +198,43 @@ public class StringUtilsTest {
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));
}
@Test
public void testFormatDateTodayRelative() {
// given
ArrayList<String> shortDays = Arrays.stream(DayOfWeek.values()).map(d -> d.getDisplayName(TextStyle.FULL, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
ArrayList<String> shortMonths = Arrays.stream(Month.values()).map(m -> m.getDisplayName(TextStyle.SHORT, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
LocalDate today = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toLocalDate();
LocalDate yesterday = today.minus(1, ChronoUnit.DAYS);
LocalDate dayName = today.minus(2, ChronoUnit.DAYS);
LocalDate thisYear = today.minus(15, ChronoUnit.DAYS);
LocalDate aYearAgo = today.minus(400, ChronoUnit.DAYS);
int offsetFromLocale = OffsetDateTime.now().getOffset().getTotalSeconds() - 28800; // -08:00
OffsetDateTime todayRecordedOdt = OffsetDateTime.of(LocalDateTime.of(today.getYear(), today.getMonth(), today.getDayOfMonth(), 20, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
OffsetDateTime yesterdayRecordedOdt = OffsetDateTime.of(LocalDateTime.of(yesterday.getYear(), yesterday.getMonth(), yesterday.getDayOfMonth(), 1, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
OffsetDateTime dayNameRecordedOdt = OffsetDateTime.of(LocalDateTime.of(dayName.getYear(), dayName.getMonth(), dayName.getDayOfMonth(), 7, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
OffsetDateTime thisYearRecordedOdt = OffsetDateTime.of(LocalDateTime.of(thisYear.getYear(), thisYear.getMonth(), thisYear.getDayOfMonth(), 12, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
OffsetDateTime aYearAgoRecordedOdt = OffsetDateTime.of(LocalDateTime.of(aYearAgo.getYear(), aYearAgo.getMonth(), aYearAgo.getDayOfMonth(), 17, 0, 0), ZoneOffset.ofTotalSeconds(offsetFromLocale));
// when
String formatToday = StringUtils.formatDateTodayRelative(context, todayRecordedOdt);
String formatYesterday = StringUtils.formatDateTodayRelative(context, yesterdayRecordedOdt);
String formatDayName = StringUtils.formatDateTodayRelative(context, dayNameRecordedOdt);
String formatThisYear = StringUtils.formatDateTodayRelative(context, thisYearRecordedOdt);
String formatAYearAgo = StringUtils.formatDateTodayRelative(context, aYearAgoRecordedOdt);
// then
assertEquals("Today", formatToday);
assertEquals("Yesterday", formatYesterday);
assertTrue(shortDays.contains(formatDayName)); // Something like Friday
if (today.getYear() != thisYear.getYear()) {
assertTrue(shortMonths.stream().anyMatch(fty -> formatThisYear.matches("\\d+ " + fty + " \\d{4}"))); // Something like 14 Dec 2021
} else {
assertTrue(shortMonths.stream().anyMatch(fty -> formatThisYear.matches("\\d+ " + fty))); // Something like 14 Dec
}
assertTrue(shortMonths.stream().anyMatch(fty -> formatAYearAgo.matches("\\d+ " + fty + " \\d{4}"))); // Something like 14 Dec 2021
}
}
@@ -16,6 +16,8 @@
package de.dennisguse.opentracks.util;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
@@ -24,16 +26,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Locale;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import static org.junit.Assert.assertEquals;
/**
* Tests {@link TrackNameUtils}.
*
@@ -43,7 +42,7 @@ import static org.junit.Assert.assertEquals;
public class TrackNameUtilsTest {
private static final Track.Id TRACK_ID = new Track.Id(1L);
private static final Instant START_TIME = Instant.ofEpochMilli(1288213406000L);
private static final OffsetDateTime START_TIME = OffsetDateTime.parse("2022-01-02T10:15:30+01:00");
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
@@ -53,7 +52,7 @@ public class TrackNameUtilsTest {
@Test
public void testTrackName_date_local() {
PreferencesUtils.setString(R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value));
assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
assertEquals(StringUtils.formatDateTimeWithOffset(START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**
@@ -62,8 +61,7 @@ public class TrackNameUtilsTest {
@Test
public void testTrackName_date_iso_8601() {
PreferencesUtils.setString(R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US);
assertEquals(simpleDateFormat.format(START_TIME.toEpochMilli()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
assertEquals(StringUtils.formatDateTimeIso8601(START_TIME.toInstant(), START_TIME.getOffset()), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**
@@ -1,35 +1,60 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackSelection;
import de.dennisguse.opentracks.databinding.AggregatedStatsBinding;
import de.dennisguse.opentracks.fragments.FilterDialogFragment;
import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel;
public class AggregatedStatisticsActivity extends AbstractActivity {
public class AggregatedStatisticsActivity extends AbstractActivity implements FilterDialogFragment.FilterDialogListener {
public static final String EXTRA_TRACK_IDS = "track_ids";
static final String STATE_ARE_FILTERS_APPLIED = "areFiltersApplied";
private AggregatedStatsBinding viewBinding;
private AggregatedStatisticsAdapter adapter;
private AggregatedStatisticsModel viewModel;
private final TrackSelection selection = new TrackSelection();
private boolean areFiltersApplied;
private MenuItem filterItem;
private MenuItem clearFilterItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding.aggregatedStatsList.setEmptyView(viewBinding.aggregatedStatsEmptyView);
List<Track.Id> trackIds = getIntent().getParcelableArrayListExtra(EXTRA_TRACK_IDS);
areFiltersApplied = savedInstanceState != null && savedInstanceState.getBoolean(STATE_ARE_FILTERS_APPLIED);
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
viewModel.getAggregatedStats(trackIds).observe(this, aggregatedStatistics -> {
List<Track.Id> trackIds = getIntent().getParcelableArrayListExtra(EXTRA_TRACK_IDS);
if (trackIds != null && !trackIds.isEmpty()) {
trackIds.stream().forEach(selection::addTrackId);
}
viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
viewModel.getAggregatedStats(selection).observe(this, aggregatedStatistics -> {
if ((aggregatedStatistics == null || aggregatedStatistics.getCount() == 0) && !selection.isEmpty()) {
viewBinding.aggregatedStatsEmptyView.setText(getString(R.string.aggregated_stats_filter_no_results));
}
if (aggregatedStatistics != null) {
adapter = new AggregatedStatisticsAdapter(this, aggregatedStatistics);
viewBinding.aggregatedStatsList.setAdapter(adapter);
@@ -38,9 +63,58 @@ public class AggregatedStatisticsActivity extends AbstractActivity {
});
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_ARE_FILTERS_APPLIED, areFiltersApplied);
}
@Override
protected View getRootView() {
viewBinding = AggregatedStatsBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.aggregated_statistics, menu);
clearFilterItem = menu.findItem(R.id.aggregated_statistics_clear_filter);
filterItem = menu.findItem(R.id.aggregated_statistics_filter);
setMenuVisibility(areFiltersApplied);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.aggregated_statistics_filter) {
ArrayList<FilterDialogFragment.FilterItem> filterItems = new ArrayList<>();
adapter.getCategories().stream().forEach(category -> filterItems.add(new FilterDialogFragment.FilterItem(category, category, true)));
FilterDialogFragment.showDialog(getSupportFragmentManager(), filterItems);
return true;
}
if (item.getItemId() == R.id.aggregated_statistics_clear_filter) {
setMenuVisibility(false);
viewModel.clearSelection();
return true;
}
return super.onOptionsItemSelected(item);
}
private void setMenuVisibility(boolean areFiltersApplied) {
this.areFiltersApplied = areFiltersApplied;
if (clearFilterItem != null && filterItem != null) {
clearFilterItem.setVisible(this.areFiltersApplied);
filterItem.setVisible(!this.areFiltersApplied);
}
}
@Override
public void onFilterDone(ArrayList<FilterDialogFragment.FilterItem> filterItems, LocalDateTime from, LocalDateTime to) {
setMenuVisibility(true);
selection.addDateRange(from.atZone(ZoneId.systemDefault()).toInstant(), to.atZone(ZoneId.systemDefault()).toInstant());
filterItems.stream().filter(fi -> fi.isChecked).forEach(fi -> selection.addCategory(fi.value));
viewModel.updateSelection(selection);
}
}
@@ -326,7 +326,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{MarkerColumns._ID,
MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY,
MarkerColumns.TIME, MarkerColumns.PHOTOURL};
MarkerColumns.TIME, MarkerColumns.PHOTOURL, MarkerColumns.TRACKID};
if (searchQuery == null) {
if (track != null) {
@@ -49,10 +49,11 @@ import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Track;
@@ -192,6 +193,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int startTimeOffsetIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET);
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
@@ -205,12 +207,14 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, Distance.of(cursor.getDouble(totalDistanceIndex)), metricUnits);
int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex);
int startTimeOffset = cursor.getInt(startTimeOffsetIndex);
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingStatus.isPaused(),
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
startTime, true, category, description, false);
OffsetDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneOffset.ofTotalSeconds(startTimeOffset)),
category, description, false);
}
};
viewBinding.trackList.setAdapter(resourceCursorAdapter);
@@ -517,7 +521,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME, TracksColumns.STARTTIME_OFFSET,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT};
final String sortOrder = TracksColumns.STARTTIME + " DESC";
@@ -9,6 +9,9 @@ import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
@@ -17,7 +20,7 @@ import de.dennisguse.opentracks.viewmodels.AggregatedStatistics;
public class AggregatedStatisticsAdapter extends BaseAdapter {
private final AggregatedStatistics aggregatedStatistics;
private AggregatedStatistics aggregatedStatistics;
private final Context context;
public AggregatedStatisticsAdapter(Context context, AggregatedStatistics aggregatedStatistics) {
@@ -66,6 +69,20 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
return convertView;
}
public AggregatedStatistics swapData(AggregatedStatistics aggregatedStatistics) {
this.aggregatedStatistics = aggregatedStatistics;
this.notifyDataSetChanged();
return aggregatedStatistics;
}
public List<String> getCategories() {
List<String> categories = new ArrayList<>();
for (int i = 0; i < aggregatedStatistics.getCount(); i++) {
categories.add(aggregatedStatistics.getItem(i).getCategory());
}
return categories;
}
private class ViewHolder {
private final ImageView sportIcon;
private final TextView typeLabel;
@@ -19,9 +19,15 @@ import androidx.cursoradapter.widget.ResourceCursorAdapter;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
@@ -66,6 +72,7 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
int categoryIndex = cursor.getColumnIndex(MarkerColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(MarkerColumns.DESCRIPTION);
int photoUrlIndex = cursor.getColumnIndex(MarkerColumns.PHOTOURL);
int trackIdIndex = cursor.getColumnIndex(MarkerColumns.TRACKID);
long id = cursor.getLong(idIndex);
int iconId = MarkerUtils.ICON_ID;
@@ -74,6 +81,7 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
String category = cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
long trackId = cursor.getLong(trackIdIndex);
view.setTag(String.valueOf(id));
@@ -96,7 +104,10 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
}
}
ListItemUtils.setListItem(activity, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, hasPhoto);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
Track track = contentProviderUtils.getTrack(new Track.Id(trackId));
ListItemUtils.setListItem(activity, view, false, true, iconId, R.string.image_marker, name, null, null, 0,
OffsetDateTime.ofInstant(Instant.ofEpochMilli(time), track.getZoneOffset()), category, description, hasPhoto);
}
public void clear() {
@@ -20,7 +20,7 @@ public class SettingsCustomLayoutListAdapter extends RecyclerView.Adapter<Recycl
private List<Layout> layoutList;
private final Context context;
private SettingsCustomLayoutProfileClickListener itemClickListener;
private final SettingsCustomLayoutProfileClickListener itemClickListener;
public SettingsCustomLayoutListAdapter(Context context, SettingsCustomLayoutProfileClickListener itemClickListener) {
this.context = context;
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
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.TrackPoint;
@@ -14,7 +15,7 @@ public class ChartPoint {
private double timeOrDistance;
//Y-axis
private final double altitude;
private Double altitude;
private Double speed;
private Double pace;
private Double heartRate;
@@ -27,17 +28,21 @@ public class ChartPoint {
this.altitude = altitude;
}
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, double smoothedAltitude_m, boolean chartByDistance, boolean metricUnits) {
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, boolean metricUnits) {
if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
altitude = Distance.of(smoothedAltitude_m).toM_FT(metricUnits);
if (smoothedAltitude != null) {
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(metricUnits);
}
speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
if (smoothedSpeed != null) {
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();
}
@@ -41,7 +41,6 @@ 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;
@@ -213,7 +212,7 @@ public class ChartView extends View {
@Override
protected boolean drawIfChartPointHasNoData() {
return true;
return false;
}
});
@@ -873,9 +872,9 @@ public class ChartView extends View {
boolean drawFirstPoint = false;
path.rewind();
Iterator<ChartPoint> iterator = chartPoints.iterator();
while (iterator.hasNext()) {
ChartPoint point = iterator.next();
Integer finalX = null;
for (ChartPoint point : chartPoints) {
if (!series.isChartPointValid(point)) {
continue;
}
@@ -893,10 +892,12 @@ public class ChartView extends View {
// draw graph
path.lineTo(x, y);
// last point: move to lower right
if (!iterator.hasNext()) {
path.lineTo(x, yCorner);
}
finalX = x;
}
// last point: move to lower right
if (finalX != null) {
path.lineTo(finalX, yCorner);
}
// back to lower left corner
@@ -22,6 +22,8 @@ import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
@@ -150,7 +152,7 @@ public class DescriptionGenerator {
}
// Recorded time
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(OffsetDateTime.ofInstant(stats.getStartTime(), ZoneId.systemDefault()))));
builder.append(lineBreak);
return builder.toString();
@@ -17,7 +17,9 @@
package de.dennisguse.opentracks.content;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
@@ -49,7 +51,7 @@ public interface TrackDataListener {
*
* @param trackPoint the trackPoint
*/
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, @Nullable Altitude smoothedAltitude_m) {
}
/**
@@ -14,6 +14,8 @@ public abstract class Altitude {
return altitude_m;
}
public abstract Altitude replace(double altitude_m);
public abstract int getLabelId();
public static class WGS84 extends Altitude {
@@ -30,6 +32,11 @@ public abstract class Altitude {
public static Altitude of(double altitude_m) {
return new WGS84(altitude_m);
}
@Override
public Altitude replace(double altitude_m) {
return new WGS84(altitude_m);
}
}
public static class EGM2008 extends Altitude {
@@ -46,6 +53,18 @@ public abstract class Altitude {
public static Altitude of(double altitude_m) {
return new EGM2008(altitude_m);
}
@Override
public Altitude replace(double altitude_m) {
return new EGM2008(altitude_m);
}
}
@Override
public String toString() {
return "Altitude{" +
"altitude_m=" + altitude_m + this.getClass().getSimpleName() +
'}';
}
}
@@ -34,7 +34,7 @@ public class DataField implements Parcelable {
isWide = in.readByte() != 0;
}
public static final Creator<DataField> CREATOR = new Creator<DataField>() {
public static final Creator<DataField> CREATOR = new Creator<>() {
@Override
public DataField createFromParcel(Parcel in) {
return new DataField(in);
@@ -58,7 +58,7 @@ public class Layout implements Parcelable {
in.readList(dataFields, DataField.class.getClassLoader());
}
public static final Creator<Layout> CREATOR = new Creator<Layout>() {
public static final Creator<Layout> CREATOR = new Creator<>() {
@Override
public Layout createFromParcel(Parcel in) {
return new Layout(in);
@@ -21,7 +21,10 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Objects;
import java.util.UUID;
@@ -45,8 +48,19 @@ public class Track {
private String icon = "";
private final ZoneOffset zoneOffset;
private TrackStatistics trackStatistics = new TrackStatistics();
@VisibleForTesting
public Track() {
this.zoneOffset = ZoneOffset.UTC;
}
public Track(@NonNull ZoneOffset zoneOffset) {
this.zoneOffset = zoneOffset;
}
/**
* May be null if the track was not loaded from the database.
*/
@@ -99,6 +113,14 @@ public class Track {
this.icon = icon;
}
public ZoneOffset getZoneOffset() {
return zoneOffset;
}
public OffsetDateTime getStartTime() {
return trackStatistics.getStartTime().atOffset(zoneOffset);
}
@NonNull
public TrackStatistics getTrackStatistics() {
return trackStatistics;
@@ -63,7 +63,8 @@ public class TrackPoint {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
TRACKPOINT(0), //Just GPS data.
TRACKPOINT(0), //Just GPS data and may contain BLE sensor data
SENSORPOINT(2), //Just BLE sensor data; required to have speed and sensorDistance
SEGMENT_END_MANUAL(1); //End of a segment
@@ -108,13 +109,7 @@ public class TrackPoint {
public TrackPoint(@NonNull Type type, @NonNull Location location, @NonNull Instant time) {
this(type, time);
this.latitude = location.getLatitude();
this.longitude = location.getLongitude();
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null;
//TODO Should we copy the bearing?
setLocation(location);
}
@VisibleForTesting
@@ -151,6 +146,10 @@ public class TrackPoint {
return type == Type.SEGMENT_END_MANUAL;
}
public boolean wasCreatedManually() {
return hasLocation() || hasSpeed();
}
/**
* May be null if the track was not loaded from the database.
*/
@@ -210,6 +209,17 @@ public class TrackPoint {
return location;
}
public TrackPoint setLocation(@NonNull Location location) {
this.latitude = location.getLatitude();
this.longitude = location.getLongitude();
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null;
//TODO Should we copy the bearing?
return this;
}
public boolean hasAltitudeGain() {
return altitudeGain_m != null;
}
@@ -309,13 +319,13 @@ public class TrackPoint {
return this;
}
@Nullable
public Distance distanceToPrevious(TrackPoint previous) {
@NonNull
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
if (previous == null || !(hasLocation() && previous.hasLocation())) {
return null;
if (!hasLocation() || hasLocation() != previous.hasLocation()) {
throw new RuntimeException("Cannot compute distance.");
}
return Distance.of(getLocation().distanceTo(previous.getLocation()));
@@ -405,21 +415,25 @@ public class TrackPoint {
return this;
}
@NonNull
@Override
public String toString() {
String result = "time=" + getTime() + " (type=" + getType() + ")";
if (hasLocation()) {
result += ": lat=" + getLatitude() + " lng=" + getLongitude();
}
if (hasHorizontalAccuracy()) {
result += " acc=" + getHorizontalAccuracy();
}
if (hasSensorDistance()) {
result += " distance=" + getSensorDistance();
}
return result;
return "TrackPoint{" +
"id=" + id +
", time=" + time +
", latitude=" + latitude +
", longitude=" + longitude +
", horizontalAccuracy=" + horizontalAccuracy +
", altitude=" + altitude +
", speed=" + speed +
", bearing=" + bearing +
", sensorDistance=" + sensorDistance +
", type=" + type +
", heartRate_bpm=" + heartRate_bpm +
", cadence_rpm=" + cadence_rpm +
", power=" + power +
", altitudeGain_m=" + altitudeGain_m +
", altitudeLoss_m=" + altitudeLoss_m +
'}';
}
public static class Id {
@@ -77,7 +77,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ SENSOR_POWER + " FLOAT, "
+ ALTITUDE_GAIN + " FLOAT, "
+ ALTITUDE_LOSS + " FLOAT, "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1)), "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1, 2)), "
+ SENSOR_DISTANCE + " FLOAT, "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
+ ")";
@@ -0,0 +1,77 @@
package de.dennisguse.opentracks.content.data;
import android.text.TextUtils;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.SelectionData;
public class TrackSelection implements ContentProviderUtils.ContentProviderSelectionInterface {
private final List<Track.Id> trackIds = new ArrayList<>();
private final List<String> categories = new ArrayList<>();
private Instant from;
private Instant to;
public TrackSelection addDateRange(Instant from, Instant to) {
this.from = from;
this.to = to;
return this;
}
public TrackSelection addTrackId(Track.Id trackId) {
if (!this.trackIds.contains(trackId)) {
this.trackIds.add(trackId);
}
return this;
}
public TrackSelection addCategory(String category) {
if (!this.categories.contains(category)) {
this.categories.add(category);
}
return this;
}
public boolean isEmpty() {
return trackIds.isEmpty() && categories.isEmpty() && from == null && to == null;
}
@Override
public SelectionData buildSelection() {
String selection = "";
String[] selectionArgs;
ArrayList<String> fromToArgs = new ArrayList<>();
// Builds selection.
if (!trackIds.isEmpty()) {
selection = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
}
if (!categories.isEmpty()) {
selection += selection.isEmpty() ? "" : " AND ";
selection += String.format(TracksColumns.CATEGORY + " IN (%s)", TextUtils.join(",", Collections.nCopies(categories.size(), "?")));
}
if (from != null && to != null) {
selection += selection.isEmpty() ? "" : " AND ";
selection += TracksColumns.STARTTIME + " BETWEEN ? AND ?";
fromToArgs.add(Long.toString(from.toEpochMilli()));
fromToArgs.add(Long.toString(to.toEpochMilli()));
}
if (selection.isEmpty()) {
return new SelectionData();
}
// Builds selection arguments.
ArrayList<String> args = trackIds.stream().map(id -> Long.toString(id.getId())).collect(Collectors.toCollection(ArrayList::new));
args.addAll(categories);
args.addAll(fromToArgs);
selectionArgs = args.stream().toArray(String[]::new);
return new SelectionData(selection, selectionArgs);
}
}
@@ -41,6 +41,7 @@ public interface TracksColumns extends BaseColumns {
String DESCRIPTION = "description"; // track description
String CATEGORY = "category"; // track activity type
String STARTTIME = "starttime"; // track start time
String STARTTIME_OFFSET = "starttime_offset"; // in plus/minus in seconds
String STOPTIME = "stoptime"; // track stop time
String MARKER_COUNT = "markerCount"; // the numbers of markers (virtual column)
@Deprecated
@@ -77,7 +78,8 @@ public interface TracksColumns extends BaseColumns {
+ ALTITUDE_GAIN + " FLOAT, "
+ ICON + " TEXT, "
+ UUID + " BLOB, "
+ ALTITUDE_LOSS + " FLOAT)";
+ ALTITUDE_LOSS + " FLOAT, "
+ STARTTIME_OFFSET + " INTEGER)";
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
@@ -32,6 +32,7 @@ import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -73,6 +74,10 @@ public class ContentProviderUtils {
private final ContentResolver contentResolver;
public interface ContentProviderSelectionInterface {
SelectionData buildSelection();
}
public ContentProviderUtils(Context context) {
contentResolver = context.getContentResolver();
}
@@ -94,6 +99,7 @@ public class ContentProviderUtils {
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int startTimeOffsetIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET);
int stopTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME);
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
@@ -105,7 +111,7 @@ public class ContentProviderUtils {
int altitudeLossIndex = cursor.getColumnIndexOrThrow(TracksColumns.ALTITUDE_LOSS);
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
Track track = new Track();
Track track = new Track(ZoneOffset.ofTotalSeconds(cursor.getInt(startTimeOffsetIndex)));
TrackStatistics trackStatistics = track.getTrackStatistics();
if (!cursor.isNull(idIndex)) {
track.setId(new Track.Id(cursor.getLong(idIndex)));
@@ -122,6 +128,7 @@ public class ContentProviderUtils {
if (!cursor.isNull(categoryIndex)) {
track.setCategory(cursor.getString(categoryIndex));
}
if (!cursor.isNull(startTimeIndex)) {
trackStatistics.setStartTime(Instant.ofEpochMilli(cursor.getLong(startTimeIndex)));
}
@@ -205,15 +212,10 @@ public class ContentProviderUtils {
return tracks;
}
public List<Track> getTracks(List<Track.Id> trackIds) {
if (trackIds == null || trackIds.isEmpty()) {
return getTracks();
}
String selection = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
String[] selectionArgs = trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new);
public List<Track> getTracks(ContentProviderSelectionInterface selection) {
SelectionData selectionData = selection.buildSelection();
ArrayList<Track> tracks = new ArrayList<>();
try (Cursor cursor = getTrackCursor(selection, selectionArgs, TracksColumns._ID)) {
try (Cursor cursor = getTrackCursor(selectionData.getSelection(), selectionData.getSelectionArgs(), TracksColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
tracks.ensureCapacity(cursor.getCount());
do {
@@ -289,6 +291,7 @@ public class ContentProviderUtils {
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.CATEGORY, track.getCategory());
values.put(TracksColumns.STARTTIME_OFFSET, track.getZoneOffset().getTotalSeconds());
if (trackStatistics.getStartTime() != null) {
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime().toEpochMilli());
}
@@ -9,6 +9,9 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.zone.ZoneRules;
import java.util.UUID;
import de.dennisguse.opentracks.Startup;
@@ -26,7 +29,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 31;
private static final int DATABASE_VERSION = 33;
public CustomSQLiteOpenHelper(Context context) {
this(context, ((Startup) context.getApplicationContext()).getDatabaseName());
@@ -83,6 +86,12 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 31:
upgradeFrom30to31(db);
break;
case 32:
upgradeFrom31to32(db);
break;
case 33:
upgradeFrom32to33(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -119,6 +128,12 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 30:
downgradeFrom31to30(db);
break;
case 31:
downgradeFrom32to31(db);
break;
case 32:
downgradeFrom33to32(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
@@ -422,6 +437,83 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
/**
* Add TrackPoint type SENSORPOINT (2).
*/
private void upgradeFrom31to32(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1, 2)), sensor_distance FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom32to31(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1)), sensor_distance FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
/**
* Add timezone to Track.
*/
private void upgradeFrom32to33(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE tracks ADD COLUMN starttime_offset INTEGER");
ZoneRules zoneRules = ZoneOffset.systemDefault().getRules();
try (Cursor cursor = db.query("tracks", new String[]{"_id", "starttime"}, null, null, null, null, null)) {
if (cursor.moveToFirst()) {
int trackIdIndex = cursor.getColumnIndexOrThrow("_id");
int startTimeIndex = cursor.getColumnIndexOrThrow("starttime");
do {
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
long startTime = cursor.getLong(startTimeIndex);
ContentValues cv = new ContentValues();
cv.put("starttime_offset", zoneRules.getOffset(Instant.ofEpochMilli(startTime)).getTotalSeconds());
db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId.getId())});
} while (cursor.moveToNext());
}
}
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom33to32(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("DROP INDEX tracks_uuid_index");
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, icon TEXT, uuid BLOB, elevationloss FLOAT)");
db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, icon, uuid, elevationloss FROM tracks_old");
db.execSQL("DROP TABLE tracks_old");
db.execSQL("CREATE UNIQUE INDEX tracks_uuid_index ON tracks(uuid)");
db.setTransactionSuccessful();
db.endTransaction();
}
@@ -0,0 +1,24 @@
package de.dennisguse.opentracks.content.provider;
public class SelectionData {
private final String selection;
private final String[] selectionArgs;
public SelectionData() {
selection = null;
selectionArgs = null;
}
public SelectionData(String selection, String[] selectionArgs) {
this.selection = selection;
this.selectionArgs = selectionArgs;
}
public String getSelection() {
return selection;
}
public String[] getSelectionArgs() {
return selectionArgs;
}
}
@@ -29,7 +29,6 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
/**
* A content provider that mimics the behavior of {@link androidx.core.content.FileProvider}, which shares virtual (non-existing) KML-files.
@@ -35,14 +35,15 @@ import de.dennisguse.opentracks.TrackActivityDataHubInterface;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Altitude;
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.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* A fragment to display track chart to the user.
@@ -189,9 +190,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
if (isResumed()) {
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, viewBinding.chartView.getMetricUnits());
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getMetricUnits());
pendingPoints.add(point);
}
}
@@ -0,0 +1,186 @@
package de.dennisguse.opentracks.fragments;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import android.widget.DatePicker;
import android.widget.GridLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.textfield.TextInputEditText;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.StringUtils;
public class FilterDialogFragment extends DialogFragment {
private static final String TAG = FilterDialogFragment.class.getSimpleName();
public static final String KEY_FILTER_ITEMS = "filterItems";
private FilterDialogListener filterDialogListener;
private ArrayList<FilterItem> filterItems = new ArrayList<>();
public static void showDialog(FragmentManager fragmentManager) {
FilterDialogFragment filterDialogFragment = new FilterDialogFragment();
filterDialogFragment.show(fragmentManager, TAG);
}
public static void showDialog(FragmentManager fragmentManager, ArrayList<FilterItem> items) {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(KEY_FILTER_ITEMS, items);
FilterDialogFragment filterDialogFragment = new FilterDialogFragment();
filterDialogFragment.setArguments(bundle);
filterDialogFragment.show(fragmentManager, TAG);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
filterItems = getArguments().getParcelableArrayList(KEY_FILTER_ITEMS);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getString(R.string.generic_filter));
LayoutInflater inflater = LayoutInflater.from(getActivity());
View layout = inflater.inflate(R.layout.fragment_filter_dialog, null, false);
GridLayout itemsLayout = layout.findViewById(R.id.filter_items);
builder.setView(layout);
for (FilterItem item : filterItems) {
View view = inflater.inflate(R.layout.fragment_filter_dialog_item, null);
CheckBox checkBox = view.findViewById(R.id.filter_dialog_check_button);
checkBox.setText(item.value);
checkBox.setChecked(item.isChecked);
checkBox.setTag(item.id);
checkBox.setOnClickListener(v -> item.isChecked = !item.isChecked);
itemsLayout.addView(view);
}
DatePicker datePickerFrom = layout.findViewById(R.id.filter_date_picker_from);
DatePicker datePickerTo = layout.findViewById(R.id.filter_date_picker_to);
TextInputEditText dateFrom = layout.findViewById(R.id.filter_date_edit_text_from);
TextInputEditText dateTo = layout.findViewById(R.id.filter_date_edit_text_to);
LocalDateTime firstDayThisWeek = LocalDate.now().with(WeekFields.of(Locale.getDefault()).getFirstDayOfWeek()).atStartOfDay();
dateFrom.setText(StringUtils.formatLocalDateTime(firstDayThisWeek));
datePickerFrom.init(firstDayThisWeek.getYear(), firstDayThisWeek.getMonthValue() - 1, firstDayThisWeek.getDayOfMonth(), (view, year, monthOfYear, dayOfMonth) -> {
LocalDateTime localDateTime = LocalDateTime.of(year, monthOfYear + 1, dayOfMonth, 0, 0, 0);
dateFrom.setText(StringUtils.formatLocalDateTime(localDateTime));
datePickerFrom.setVisibility(View.GONE);
datePickerTo.setMinDate(localDateTime.toInstant(ZoneOffset.ofTotalSeconds(0)).toEpochMilli());
if (localDateTime.isAfter(LocalDateTime.of(datePickerTo.getYear(), datePickerTo.getMonth() + 1, datePickerTo.getDayOfMonth(), 23, 59, 59))) {
datePickerTo.updateDate(year, monthOfYear, dayOfMonth);
}
});
LocalDateTime lastDayThisWeek = firstDayThisWeek.plusDays(6).withHour(23).withMinute(59).withSecond(59);
dateTo.setText(StringUtils.formatLocalDateTime(lastDayThisWeek));
datePickerTo.init(lastDayThisWeek.getYear(), lastDayThisWeek.getMonthValue() - 1, lastDayThisWeek.getDayOfMonth(), (view, year, monthOfYear, dayOfMonth) -> {
LocalDateTime localDateTime = LocalDateTime.of(year, monthOfYear + 1, dayOfMonth, 23, 59, 59);
dateTo.setText(StringUtils.formatLocalDateTime(localDateTime));
datePickerTo.setVisibility(View.GONE);
});
dateFrom.setOnClickListener(v -> {
datePickerFrom.setVisibility(View.VISIBLE);
datePickerTo.setVisibility(View.GONE);
});
dateTo.setOnClickListener(v -> {
datePickerFrom.setVisibility(View.GONE);
datePickerTo.setVisibility(View.VISIBLE);
});
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> filterDialogListener.onFilterDone(
filterItems,
LocalDateTime.of(datePickerFrom.getYear(), datePickerFrom.getMonth() + 1, datePickerFrom.getDayOfMonth(), 0, 0, 0),
LocalDateTime.of(datePickerTo.getYear(), datePickerTo.getMonth() + 1, datePickerTo.getDayOfMonth(), 23, 59, 59)
));
builder.setNegativeButton(android.R.string.cancel, null);
return builder.create();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
filterDialogListener = (FilterDialogListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + FilterDialogListener.class.getSimpleName());
}
}
public interface FilterDialogListener {
void onFilterDone(ArrayList<FilterItem> filters, LocalDateTime from, LocalDateTime to);
}
public static class FilterItem implements Parcelable {
public final String id;
public final String value;
public boolean isChecked;
public FilterItem(String id, String value) {
this.id = id;
this.value = value;
this.isChecked = true;
}
public FilterItem(String id, String value, boolean isChecked) {
this.id = id;
this.value = value;
this.isChecked = isChecked;
}
protected FilterItem(Parcel in) {
id = in.readString();
value = in.readString();
isChecked = in.readByte() != 0;
}
public static final Creator<FilterItem> CREATOR = new Creator<>() {
@Override
public FilterItem createFromParcel(Parcel in) {
return new FilterItem(in);
}
@Override
public FilterItem[] newArray(int size) {
return new FilterItem[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(value);
dest.writeByte((byte) (isChecked ? 1 : 0));
}
}
}
@@ -182,7 +182,9 @@ public class IntervalsFragment extends Fragment {
}
private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) {
boolean update = metricUnits != this.metricUnits || !selectedInterval.sameMultiplier(this.selectedInterval);
boolean update = metricUnits != this.metricUnits
|| selectedInterval == null
|| !selectedInterval.sameMultiplier(this.selectedInterval);
this.metricUnits = metricUnits;
this.selectedInterval = selectedInterval;
@@ -179,7 +179,7 @@ public class StatisticsRecordedFragment extends Fragment {
private void loadTrackDescription(@NonNull Track track) {
viewBinding.statsNameValue.setText(track.getName());
viewBinding.statsDescriptionValue.setText(track.getDescription());
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTime(getContext(), track.getTrackStatistics().getStartTime()));
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTime(track.getStartTime()));
}
private void updateUI() {
@@ -24,13 +24,20 @@ import androidx.annotation.NonNull;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.time.ZoneOffset;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -122,6 +129,8 @@ public class GPXTrackExporter implements TrackExporter {
boolean wroteTrack = false;
boolean wroteSegment = false;
LinkedList<TrackPoint> sensorPoints = new LinkedList<>();
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) throw new InterruptedException();
@@ -149,7 +158,12 @@ public class GPXTrackExporter implements TrackExporter {
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
wroteSegment = true;
writeTrackPoint(trackPoint);
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
sensorPoints.clear();
break;
case SENSORPOINT:
sensorPoints.add(trackPoint);
break;
case TRACKPOINT:
if (!wroteSegment) {
@@ -157,11 +171,21 @@ public class GPXTrackExporter implements TrackExporter {
writeOpenSegment();
wroteSegment = true;
}
writeTrackPoint(trackPoint);
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
sensorPoints.clear();
break;
default:
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
}
if (!sensorPoints.isEmpty()) {
//TODO We might miss to export data; this happens if there are SENSORPOINTs after the final TRACKPOINT of a track.
//For segments the data is added to the next segment.
Log.d(TAG, "SENSORPOINTs after final TRACKPOINT; this data is not exported.");
}
if (wroteSegment) {
// Should not be necessary as tracks should end with SEGMENT_END_MANUAL.
// Anyhow, make sure that the last segment is closed.
@@ -200,12 +224,14 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
printWriter.println("xmlns:gpxtrkx=\"http://www.garmin.com/xmlschemas/TrackStatsExtension/v1\"");
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
printWriter.println("xsi:schemaLocation=" +
"\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd"
+ " http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd"
+ " http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd"
+ " http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
}
}
@@ -224,7 +250,7 @@ public class GPXTrackExporter implements TrackExporter {
throw new InterruptedException();
}
Marker marker = contentProviderUtils.createMarker(cursor);
writeMarker(marker);
writeMarker(track.getZoneOffset(), marker);
cursor.moveToNext();
}
@@ -232,13 +258,13 @@ public class GPXTrackExporter implements TrackExporter {
}
}
public void writeMarker(Marker marker) {
public void writeMarker(ZoneOffset zoneOffset, Marker marker) {
if (printWriter != null) {
printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">");
if (marker.hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(marker.getAltitude().toM()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime()) + "</time>");
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime(), zoneOffset) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(marker.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(marker.getCategory()) + "</type>");
@@ -256,6 +282,18 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<extensions>");
printWriter.println("<topografix:color>c0c0c0</topografix:color>");
printWriter.println("<opentracks:trackid>" + track.getUuid() + "</opentracks:trackid>");
TrackStatistics trackStatistics = track.getTrackStatistics();
printWriter.println("<gpxtrkx:TrackStatsExtension>");
printWriter.println("<gpxtrkx:Distance>" + trackStatistics.getTotalDistance().toM() + "</gpxtrkx:Distance>");
printWriter.println("<gpxtrkx:TimerTime>" + trackStatistics.getTotalTime().getSeconds() + "</gpxtrkx:TimerTime>");
printWriter.println("<gpxtrkx:MovingTime>" + trackStatistics.getMovingTime().getSeconds() + "</gpxtrkx:MovingTime>");
printWriter.println("<gpxtrkx:StoppedTime>" + trackStatistics.getStoppedTime().getSeconds() + "</gpxtrkx:StoppedTime>");
printWriter.println("<gpxtrkx:MaxSpeed>" + trackStatistics.getMaxSpeed().toMPS() + "</gpxtrkx:MaxSpeed>");
printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.getTotalAltitudeGain() + "</gpxtrkx:Ascent>");
printWriter.println("<gpxtrkx:Descent>" + trackStatistics.getTotalAltitudeLoss() + "</gpxtrkx:Descent>");
printWriter.println("</gpxtrkx:TrackStatsExtension>");
printWriter.println("</extensions>");
}
}
@@ -274,7 +312,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("</trkseg>");
}
public void writeTrackPoint(TrackPoint trackPoint) {
public void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
@@ -283,46 +321,61 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude().toM()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime(), zoneOffset) + "</time>");
if (trackPoint.hasSpeed() || trackPoint.hasHeartRate() || trackPoint.hasCadence() || trackPoint.hasAltitudeGain() || trackPoint.hasAltitudeLoss()) {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
{
String trackPointExtensionContent = "";
if (trackPoint.hasSpeed()) {
printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>");
trackPointExtensionContent += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>\n";
}
if (trackPoint.hasHeartRate()) {
printWriter.println("<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate_bpm()) + "</gpxtpx:hr>");
trackPointExtensionContent += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate_bpm()) + "</gpxtpx:hr>\n";
}
if (trackPoint.hasCadence()) {
printWriter.println("<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence_rpm()) + "</gpxtpx:cad>");
trackPointExtensionContent += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence_rpm()) + "</gpxtpx:cad>\n";
}
if (trackPoint.hasPower()) {
printWriter.println("<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower()) + "</pwr:PowerInWatts>");
trackPointExtensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower()) + "</pwr:PowerInWatts>\n";
}
if (trackPoint.hasAltitudeGain()) {
printWriter.println("<opentracks:gain>" + ALTITUDE_FORMAT.format(trackPoint.getAltitudeGain()) + "</opentracks:gain>");
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGain() ? (double) tp.getAltitudeGain() : null);
if (cumulativeGain != null) {
trackPointExtensionContent += ("<opentracks:gain>" + ALTITUDE_FORMAT.format(cumulativeGain) + "</opentracks:gain>\n");
}
if (trackPoint.hasAltitudeLoss()) {
printWriter.println("<opentracks:loss>" + ALTITUDE_FORMAT.format(trackPoint.getAltitudeLoss()) + "</opentracks:loss>");
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeLoss() ? (double) tp.getAltitudeLoss() : null);
if (cumulativeLoss != null) {
trackPointExtensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
}
if (trackPoint.hasSensorDistance()) {
printWriter.println("<opentracks:distance>" + DISTANCE_FORMAT.format(trackPoint.getSensorDistance().toM()) + "</opentracks:distance>");
Double cumulativeDistance = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null);
if (cumulativeDistance != null) {
trackPointExtensionContent += ("<opentracks:distance>" + ALTITUDE_FORMAT.format(cumulativeDistance) + "</opentracks:distance>\n");
}
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
if (!trackPointExtensionContent.isEmpty()) {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
printWriter.print(trackPointExtensionContent);
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
}
}
printWriter.println("</trkpt>");
}
}
private Double cumulateSensorData(TrackPoint trackPoint, List<TrackPoint> sensorPoints, Function<TrackPoint, Double> map) {
return Stream.concat(sensorPoints.stream(), Stream.of(trackPoint))
.map(map)
.filter(Objects::nonNull)
.reduce(Double::sum)
.orElse(null);
}
private String formatLocation(double latitude, double longitude) {
return "lat=\"" + COORDINATE_FORMAT.format(latitude) + "\" lon=\"" + COORDINATE_FORMAT.format(longitude) + "\"";
}
@@ -27,8 +27,8 @@ import androidx.annotation.VisibleForTesting;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -110,10 +110,7 @@ public class KMLTrackExporter implements TrackExporter {
if (hasMultipleTracks) {
writeMultiTrackBegin();
}
//TODO Why use startTime of first track for the others?
Instant startTime = tracks[0].getTrackStatistics().getStartTime();
for (Track track : tracks) {
Duration offset = Duration.between(track.getTrackStatistics().getStartTime(), startTime);
writeLocations(track);
}
if (hasMultipleTracks) {
@@ -142,7 +139,7 @@ public class KMLTrackExporter implements TrackExporter {
hasMarkers = true;
}
Marker marker = contentProviderUtils.createMarker(cursor);
writeMarker(marker);
writeMarker(marker, track.getZoneOffset());
cursor.moveToNext();
}
@@ -172,23 +169,26 @@ public class KMLTrackExporter implements TrackExporter {
case SEGMENT_START_AUTOMATIC:
if (wroteSegment) writeCloseSegment();
writeOpenSegment();
writeTrackPoint(trackPoint);
writeTrackPoint(track.getZoneOffset(), trackPoint);
wroteSegment = true;
break;
case SEGMENT_END_MANUAL:
if (!wroteSegment) writeOpenSegment();
writeTrackPoint(trackPoint);
writeTrackPoint(track.getZoneOffset(), trackPoint);
writeCloseSegment();
wroteSegment = false;
break;
case SENSORPOINT:
case TRACKPOINT:
if (!wroteSegment) {
// Might happen for older data (pre v3.15.0)
writeOpenSegment();
wroteSegment = true;
}
writeTrackPoint(trackPoint);
writeTrackPoint(track.getZoneOffset(), trackPoint);
break;
default:
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
}
@@ -265,14 +265,14 @@ public class KMLTrackExporter implements TrackExporter {
}
}
private void writeMarker(Marker marker) {
private void writeMarker(Marker marker, ZoneOffset zoneOffset) {
if (printWriter != null) {
boolean existsPhoto = FileUtils.buildInternalPhotoFile(context, marker.getTrackId(), marker.getPhotoURI()) != null;
if (marker.hasPhoto() && exportPhotos && existsPhoto) {
float heading = getHeading(marker.getTrackId(), marker.getLocation());
writePhotoOverlay(marker, heading);
writePhotoOverlay(marker, heading, zoneOffset);
} else {
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation());
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation(), zoneOffset);
}
}
}
@@ -369,12 +369,12 @@ public class KMLTrackExporter implements TrackExporter {
}
@VisibleForTesting
void writeTrackPoint(TrackPoint trackPoint) {
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
if (printWriter != null) {
printWriter.println("<when>" + getTime(trackPoint.getLocation()) + "</when>");
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>");
if (trackPoint.hasLocation()) {
printWriter.println("<gx:coord>" + (trackPoint.hasLocation() ? getCoordinates(trackPoint.getLocation(), " ") : "") + "</gx:coord>");
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
} else {
printWriter.println("<gx:coord/>");
}
@@ -417,12 +417,12 @@ public class KMLTrackExporter implements TrackExporter {
* @param description the description
* @param location the location
*/
private void writePlacemark(String name, String category, String description, Location location) {
private void writePlacemark(String name, String category, String description, Location location, ZoneOffset zoneOffset) {
if (location != null) {
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>" + getTime(location) + "</when></TimeStamp>");
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, location) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + KMLTrackExporter.MARKER_STYLE + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
@@ -432,7 +432,7 @@ public class KMLTrackExporter implements TrackExporter {
}
}
private void writePhotoOverlay(Marker marker, float heading) {
private void writePhotoOverlay(Marker marker, float heading, ZoneOffset zoneOffset) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(marker.getDescription()) + "</description>");
@@ -443,7 +443,7 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>" + getTime(marker.getLocation()) + "</when></TimeStamp>");
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getLocation()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + MARKER_STYLE + "</styleUrl>");
writeCategory(marker.getCategory());
@@ -469,8 +469,8 @@ public class KMLTrackExporter implements TrackExporter {
*
* @param location the location
*/
private String getTime(Location location) {
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime()));
private String getTime(ZoneOffset zoneOffset, Location location) {
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime()), zoneOffset);
}
/**
@@ -45,7 +45,7 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
protected void onActivityResultCustom(int requestCode, int resultCode, @Nullable Intent resultData) {
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
if (resultCode == Activity.RESULT_OK && resultData != null) {
Uri directoryUri = resultData.getData();
int takeFlags = resultData.getFlags();
@@ -107,16 +107,21 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
@Override
protected void onActivityResultCustom(int requestCode, int resultCode, @Nullable Intent resultData) {
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
DocumentFile oldDirectoryUri = PreferencesUtils.getDefaultExportDirectoryUri(this);
switch (resultCode) {
case RESULT_OK:
IntentUtils.releaseDirectoryAccessPermission(getApplicationContext(), PreferencesUtils.getDefaultExportDirectoryUri(this));
if (resultData != null) {
Uri newDirectoryUri = resultData.getData();
if (oldDirectoryUri != null && !newDirectoryUri.equals(oldDirectoryUri.getUri())) {
IntentUtils.releaseDirectoryAccessPermission(getApplicationContext(), oldDirectoryUri);
}
Uri directoryUri = resultData.getData();
PreferencesUtils.setDefaultExportDirectoryUri(directoryUri);
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), directoryUri, resultData.getFlags());
PreferencesUtils.setDefaultExportDirectoryUri(newDirectoryUri);
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), newDirectoryUri, resultData.getFlags());
}
break;
case RESULT_CANCELED:
IntentUtils.releaseDirectoryAccessPermission(getApplicationContext(), PreferencesUtils.getDefaultExportDirectoryUri(this));
IntentUtils.releaseDirectoryAccessPermission(getApplicationContext(), oldDirectoryUri);
PreferencesUtils.setDefaultExportDirectoryUri(null);
break;
}
@@ -23,7 +23,8 @@ import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.helpers.DefaultHandler;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -86,6 +87,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private final Context context;
private ZoneOffset zoneOffset;
// Belongs to the current track
private final ArrayList<Marker> markers = new ArrayList<>();
@@ -108,7 +111,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
protected String uuid;
protected String gain;
protected String loss;
protected String distance;
protected String sensorDistance;
private final LinkedList<TrackPoint> currentSegment = new LinkedList<>();
@@ -157,7 +160,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
onMarkerEnd();
break;
case TAG_TRACK:
trackImporter.setTrack(context, name, uuid, description, category, null);
trackImporter.setTrack(context, name, uuid, description, category, null, zoneOffset);
zoneOffset = null;
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentEnd();
@@ -233,7 +237,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
break;
case TAG_EXTENSION_DISTANCE:
if (content != null) {
distance = content.trim();
sensorDistance = content.trim();
}
break;
}
@@ -243,7 +247,7 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private void onTrackSegmentEnd() {
if (currentSegment.isEmpty()) {
Log.w(TAG, "No locations in current segment.");
Log.w(TAG, "No TrackPoints in current segment.");
return;
}
@@ -256,14 +260,17 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
private TrackPoint createTrackPoint() throws ParsingException {
Instant parsedTime = null;
OffsetDateTime parsedTime;
try {
parsedTime = StringUtils.parseTime(time);
if (zoneOffset == null) {
zoneOffset = parsedTime.getOffset();
}
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, parsedTime);
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, parsedTime.toInstant());
if (latitude == null || longitude == null) {
return trackPoint;
}
@@ -328,11 +335,11 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
}
}
if (distance != null) {
if (sensorDistance != null) {
try {
trackPoint.setSensorDistance(Distance.of(distance));
trackPoint.setSensorDistance(Distance.of(sensorDistance));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", sensorDistance)), e);
}
}
@@ -345,9 +352,14 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
altitude = null;
time = null;
speed = null;
power = null;
gain = null;
loss = null;
sensorDistance = null;
power = null;
heartrate = null;
cadence = null;
}
private void onMarkerStart(Attributes attributes) {
@@ -26,6 +26,8 @@ import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -74,6 +76,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
private final Context context;
// Belongs to the current track
private ZoneOffset zoneOffset;
private final ArrayList<Instant> whenList = new ArrayList<>();
private final ArrayList<Location> locationList = new ArrayList<>();
@@ -157,7 +161,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
onMarkerLocationEnd();
break;
case TAG_GX_MULTI_TRACK:
trackImporter.setTrack(context, name, uuid, description, category, icon);
trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset);
zoneOffset = null;
break;
case TAG_GX_TRACK:
onTrackSegmentEnd();
@@ -195,7 +200,15 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
break;
case TAG_WHEN:
if (content != null) {
whenList.add(StringUtils.parseTime(content.trim()));
try {
OffsetDateTime time = StringUtils.parseTime(content.trim());
if (zoneOffset == null) {
zoneOffset = time.getOffset();
}
whenList.add(time.toInstant());
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", content.trim())), e);
}
}
break;
@@ -293,17 +306,10 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
Instant time = whenList.get(i);
Location location = locationList.get(i);
TrackPoint trackPoint;
if (i == 0) {
if (location == null) {
trackPoint = TrackPoint.createSegmentStartManualWithTime(time);
} else {
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, location, time);
}
} else if (i == locationList.size() - 1 && location == null) {
trackPoint = TrackPoint.createSegmentEndWithTime(time);
} else {
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, location, time);
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SENSORPOINT, time);
if (location != null) {
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
trackPoint.setLocation(location);
}
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
@@ -328,6 +334,21 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
}
// Update TrackPoint type for START / STOP.
TrackPoint.Type type = trackPoint.getType();
if (i == 0) {
//first
if (!trackPoint.wasCreatedManually()) {
type = TrackPoint.Type.SEGMENT_START_MANUAL;
} else {
type = TrackPoint.Type.SEGMENT_START_AUTOMATIC;
}
} else if (i == locationList.size() - 1 && !trackPoint.wasCreatedManually()) {
//last
type = TrackPoint.Type.SEGMENT_END_MANUAL;
}
trackPoint.setType(type);
trackImporter.addTrackPoint(trackPoint);
}
}
@@ -4,6 +4,10 @@ import androidx.annotation.NonNull;
public class ParsingException extends RuntimeException {
protected ParsingException(@NonNull String message) {
super(message);
}
protected ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@@ -5,10 +5,12 @@ import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
@@ -89,8 +91,8 @@ public class TrackImporter {
this.markers.addAll(markers);
}
void setTrack(Context context, String name, String uuid, String description, String category, String icon) {
track = new Track();
void setTrack(Context context, String name, String uuid, String description, String category, String icon, @Nullable ZoneOffset zoneOffset) {
track = new Track(zoneOffset != null ? zoneOffset : ZoneOffset.UTC);
track.setName(name != null ? name : "");
try {
@@ -196,8 +198,8 @@ public class TrackImporter {
TrackPoint previous = trackPoints.get(i - 1);
TrackPoint current = trackPoints.get(i);
Distance distanceToPrevious = current.distanceToPrevious(previous);
if (distanceToPrevious != null) {
if (current.hasSensorDistance() || (previous.hasLocation() && current.hasLocation())) {
Distance distanceToPrevious = current.distanceToPrevious(previous);
if (!current.hasSpeed()) {
Duration timeDifference = Duration.between(previous.getTime(), current.getTime());
current.setSpeed(Speed.of(distanceToPrevious, timeDifference));
@@ -9,6 +9,9 @@ import android.util.Pair;
import androidx.annotation.NonNull;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
@@ -36,18 +39,18 @@ class TrackRecordingManager {
private Track.Id trackId;
private TrackStatisticsUpdater trackStatisticsUpdater;
private boolean currentSegmentHasTrackPoint;
private TrackPoint lastTrackPoint;
private TrackPoint lastStoredTrackPoint;
private TrackPoint lastStoredTrackPointWithLocation;
TrackRecordingManager(Context context) {
this.context = context;
contentProviderUtils = new ContentProviderUtils(context);
}
Track.Id start(TrackPoint segmentStartTrackPoint) {
Track.Id start(TrackPoint segmentStartTrackPoint, ZoneOffset zoneOffset) {
// Create new track
Track track = new Track();
Track track = new Track(zoneOffset);
trackId = contentProviderUtils.insertTrack(track);
track.setId(trackId);
@@ -55,17 +58,14 @@ class TrackRecordingManager {
insertTrackPoint(trackId, segmentStartTrackPoint);
//TODO Pass TrackPoint
track.setName(TrackNameUtils.getTrackName(context, trackId, segmentStartTrackPoint.getTime()));
String category = PreferencesUtils.getDefaultActivity();
track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(context, category));
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
//TODO Pass TrackPoint
track.setName(TrackNameUtils.getTrackName(context, trackId, track.getStartTime()));
contentProviderUtils.updateTrack(track);
currentSegmentHasTrackPoint = false;
return trackId;
}
@@ -80,15 +80,18 @@ class TrackRecordingManager {
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(trackId, segmentStartTrackPoint);
currentSegmentHasTrackPoint = false;
lastTrackPoint = null;
lastStoredTrackPoint = null; }
lastStoredTrackPoint = null;
lastStoredTrackPointWithLocation = null;
}
void pause(TrackPointCreator trackPointCreator) {
insertTrackPoint(trackId, trackPointCreator.createSegmentEnd());
currentSegmentHasTrackPoint = false;
lastTrackPoint = null;
lastStoredTrackPoint = null;
lastStoredTrackPointWithLocation = null;
}
void end(TrackPointCreator trackPointCreator) {
@@ -97,9 +100,10 @@ class TrackRecordingManager {
trackId = null;
trackStatisticsUpdater = null;
lastTrackPoint = null;
lastStoredTrackPoint = null;
currentSegmentHasTrackPoint = false;
lastStoredTrackPointWithLocation = null;
}
Pair<Track, Pair<TrackPoint, SensorDataSet>> get(TrackPointCreator trackPointCreator) {
@@ -131,7 +135,7 @@ class TrackRecordingManager {
name = context.getString(R.string.marker_name_format, nextMarkerNumber + 1);
}
if (lastStoredTrackPoint == null) {
if (lastStoredTrackPointWithLocation == null) {
Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
return null;
}
@@ -142,36 +146,51 @@ class TrackRecordingManager {
photoUrl = photoUrl != null ? photoUrl : "";
// Insert marker
Marker marker = new Marker(name, description, category, icon, trackId, getTrackStatistics(), lastStoredTrackPoint, photoUrl);
Marker marker = new Marker(name, description, category, icon, trackId, getTrackStatistics(), lastStoredTrackPointWithLocation, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri));
}
boolean onNewTrackPoint(TrackPoint trackPoint) {
/**
* @return TrackPoint was stored?
*/
boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
//Storing trackPoint
// Always insert the first segment location
if (!currentSegmentHasTrackPoint) {
if (lastStoredTrackPoint == null) {
insertTrackPoint(trackId, trackPoint);
currentSegmentHasTrackPoint = true;
return true;
}
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastStoredTrackPoint);
if (distanceToLastTrackLocation != null) {
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(trackId, trackPoint);
return true;
}
if (distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval) && trackPoint.isMoving()) {
insertTrackPoint(trackId, trackPoint);
return true;
}
if (trackPoint.hasLocation() && lastStoredTrackPointWithLocation == null) {
insertTrackPoint(trackId, trackPoint);
return true;
}
if (lastStoredTrackPoint != null && trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) {
if (!trackPoint.hasLocation() && !trackPoint.hasSensorDistance()) {
Log.d(TAG, "Ignoring TrackPoint as it has no distance.");
return false;
}
TrackPoint distanceTo = lastStoredTrackPoint;
if (trackPoint.hasLocation() && !lastStoredTrackPoint.hasLocation()) {
distanceTo = lastStoredTrackPointWithLocation;
}
Distance distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(distanceTo);
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(trackId, trackPoint);
return true;
}
if (distanceToLastStoredTrackPoint.greaterOrEqualThan(recordingDistanceInterval)
&& trackPoint.isMoving()) {
insertTrackPoint(trackId, trackPoint);
return true;
}
if (trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) {
// Moving from non-moving to moving or vice versa; required to compute moving time correctly.
insertTrackPoint(trackId, trackPoint);
return true;
@@ -179,6 +198,7 @@ class TrackRecordingManager {
Log.d(TAG, "Not recording TrackPoint");
lastTrackPoint = trackPoint;
return false;
}
@@ -186,6 +206,7 @@ class TrackRecordingManager {
return trackStatisticsUpdater.getTrackStatistics();
}
//TODO Should be only be called from onNewTrackPoint().
private void insertTrackPoint(@NonNull Track.Id trackId, @NonNull TrackPoint trackPoint) {
if (lastTrackPoint != null) {
if (lastStoredTrackPoint != null && lastTrackPoint.getTime().equals(lastStoredTrackPoint.getTime())) {
@@ -209,6 +230,9 @@ class TrackRecordingManager {
contentProviderUtils.updateTrackStatistics(trackId, trackStatisticsUpdater.getTrackStatistics());
lastStoredTrackPoint = trackPoint;
if (trackPoint.hasLocation()) {
lastStoredTrackPointWithLocation = lastStoredTrackPoint;
}
} catch (SQLiteException e) {
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.services;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
@@ -35,6 +34,8 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.time.Duration;
import java.time.ZoneOffset;
import java.util.Objects;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
@@ -50,11 +51,11 @@ import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.tasks.VoiceAnnouncementManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
/**
@@ -73,6 +74,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
private final Binder binder = new Binder();
// The following variables are setFrequency in onCreate:
private VoiceAnnouncementManager voiceAnnouncementManager;
private TrackRecordingServiceNotificationManager notificationManager;
@@ -86,6 +89,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
@Override
public void run() {
updateRecordingDataWhileRecording();
Handler localHandler = TrackRecordingService.this.handler;
if (localHandler == null) {
// when this happens, no recording is running and we should not send any notifications.
@@ -96,7 +100,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
};
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Deprecated
//TODO Workaround as service is not stopped on API23; thus sharedpreferences are not reset between tests.
@VisibleForTesting
final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
@@ -115,11 +122,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
};
// The following variables are setFrequency when recording:
// The following variables are set when recording:
private WakeLock wakeLock;
private final Binder binder = new Binder();
private TrackPointCreator trackPointCreator; //TODO Move to TrackRecordingManager?
private RecordingStatus recordingStatus;
@@ -148,27 +153,20 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public Binder onBind(Intent intent) {
return binder;
}
@Override
public void onDestroy() {
handler.removeCallbacksAndMessages(null); //Some tests do not finish the recording completely
handler = null;
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackPointCreator.stop();
trackPointCreator = null;
trackRecordingManager = null;
// Reverse order from onCreate
showNotification(false); //TODO Why?
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
notificationManager = null;
try {
voiceAnnouncementManager.shutdown();
@@ -179,6 +177,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
// This should be the next to last operation
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
updateRecordingStatus(STATUS_DEFAULT);
recordingStatusObservable = null;
gpsStatusObservable = null;
recordingDataObservable = null;
@@ -186,6 +185,16 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public Binder onBind(Intent intent) {
return binder;
}
public boolean isRecording() {
return recordingStatus.isRecording();
}
@@ -214,7 +223,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
// Set recording status
Track.Id trackId = trackRecordingManager.start(trackPointCreator.createSegmentStartManual());
TrackPoint segmentStartManual = trackPointCreator.createSegmentStartManual();
ZoneOffset zoneOffset = ZoneOffset.systemDefault().getRules().getOffset(segmentStartManual.getTime());
Track.Id trackId = trackRecordingManager.start(segmentStartManual, zoneOffset);
updateRecordingStatus(RecordingStatus.record(trackId));
startRecording();
@@ -289,9 +300,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
trackRecordingManager.end(trackPointCreator);
}
ExportUtils.postWorkoutExport(this, trackId, new ExportServiceResultReceiver(new Handler(), this));
endRecording(true);
ExportUtils.postWorkoutExport(this, trackId, new ExportServiceResultReceiver(new Handler(), this));
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
@@ -317,7 +328,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
* @param trackStopped true if track is stopped, false if track is paused
*/
private void endRecording(boolean trackStopped) {
handler.removeCallbacks(updateRecordingData);
stopUpdateRecordingData();
if (!trackStopped) {
updateRecordingDataWhileRecording();
} else {
@@ -360,8 +371,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return false;
}
boolean stored = trackRecordingManager.onNewTrackPoint(trackPoint);
notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy);
return trackRecordingManager.onNewTrackPoint(trackPoint);
return stored;
}
@Override
@@ -419,6 +431,12 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return trackPointCreator;
}
@Deprecated
@VisibleForTesting
public TrackRecordingManager getTrackRecordingManager() {
return trackRecordingManager;
}
public LiveData<GpsStatusValue> getGpsStatusObservable() {
return gpsStatusObservable;
}
@@ -456,6 +474,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second));
}
@VisibleForTesting
public void stopUpdateRecordingData() {
handler.removeCallbacks(updateRecordingData);
}
public LiveData<RecordingStatus> getRecordingStatusObservable() {
return recordingStatusObservable;
}
@@ -493,7 +516,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private final Track.Id trackId;
private final boolean paused;
private RecordingStatus(Track.Id trackId, boolean paused) {
@VisibleForTesting
RecordingStatus(Track.Id trackId, boolean paused) {
this.trackId = trackId;
this.paused = paused;
}
@@ -538,6 +562,19 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
", paused=" + paused +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RecordingStatus that = (RecordingStatus) o;
return paused == that.paused && Objects.equals(trackId, that.trackId);
}
@Override
public int hashCode() {
return Objects.hash(trackId, paused);
}
}
public static class RecordingData {
@@ -584,5 +621,27 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RecordingData that = (RecordingData) o;
return Objects.equals(track, that.track) && Objects.equals(latestTrackPoint, that.latestTrackPoint) && Objects.equals(sensorDataSet, that.sensorDataSet);
}
@Override
public int hashCode() {
return Objects.hash(track, latestTrackPoint, sensorDataSet);
}
@Override
public String toString() {
return "RecordingData{" +
"track=" + track +
", latestTrackPoint=" + latestTrackPoint +
", sensorDataSet=" + sensorDataSet +
'}';
}
}
}
@@ -15,13 +15,13 @@ import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.LocationUtils;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class LocationHandler implements LocationListener, GpsStatus.GpsStatusListener {
public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener {
private final String TAG = LocationHandler.class.getSimpleName();
private final String TAG = GPSHandler.class.getSimpleName();
private LocationManager locationManager;
private final TrackPointCreator trackPointCreator;
@@ -30,7 +30,7 @@ public class LocationHandler implements LocationListener, GpsStatus.GpsStatusLis
private Distance thresholdHorizontalAccuracy;
private TrackPoint lastTrackPoint;
public LocationHandler(TrackPointCreator trackPointCreator) {
public GPSHandler(TrackPointCreator trackPointCreator) {
this.trackPointCreator = trackPointCreator;
}
@@ -121,7 +121,7 @@ public class LocationHandler implements LocationListener, GpsStatus.GpsStatusLis
}
lastTrackPoint = trackPoint;
trackPointCreator.onNewTrackPoint(trackPoint, thresholdHorizontalAccuracy);
trackPointCreator.onNewTrackPoint(trackPoint);
}
@Override
@@ -158,6 +158,10 @@ public class LocationHandler implements LocationListener, GpsStatus.GpsStatusLis
return lastTrackPoint;
}
Distance getThresholdHorizontalAccuracy() {
return thresholdHorizontalAccuracy;
}
/**
* Called from {@link GpsStatus} to inform that GPS status has changed from prevStatus to currentStatus.
*
@@ -10,6 +10,7 @@ import androidx.annotation.VisibleForTesting;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -20,7 +21,7 @@ import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
/**
* Creates TrackPoints while recording by fusing data from different sensors (e.g., GNSS, barometer, BLE sensors).
*/
public class TrackPointCreator {
public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDataSetChangeObserver {
private static final String TAG = TrackPointCreator.class.getSimpleName();
@@ -31,38 +32,42 @@ public class TrackPointCreator {
@NonNull
private Clock clock = Clock.systemUTC();
private final LocationHandler locationHandler;
private final GPSHandler gpsHandler;
private BluetoothRemoteSensorManager remoteSensorManager;
private AltitudeSumManager altitudeSumManager;
public TrackPointCreator(Callback service) {
this.service = service;
this.locationHandler = new LocationHandler(this);
this.gpsHandler = new GPSHandler(this);
}
@VisibleForTesting
TrackPointCreator(LocationHandler locationHandler, Callback service) {
TrackPointCreator(GPSHandler gpsHandler, Callback service) {
this.service = service;
this.locationHandler = locationHandler;
this.gpsHandler = gpsHandler;
}
public void start(@NonNull Context context) {
this.context = context;
locationHandler.onStart(context);
remoteSensorManager = new BluetoothRemoteSensorManager(context);
remoteSensorManager.start();
gpsHandler.onStart(context);
remoteSensorManager = new BluetoothRemoteSensorManager(context, this);
altitudeSumManager = new AltitudeSumManager();
remoteSensorManager.start();
altitudeSumManager.start(context);
}
private boolean isStarted() {
return context != null;
}
@Deprecated
//There should be a cooler way to do this; we want to send fake locations without getting affected by real GPS data.
@VisibleForTesting
public void stopGPS() {
locationHandler.onStop();
gpsHandler.onStop();
}
public void resetSensorData() {
@@ -75,6 +80,10 @@ public class TrackPointCreator {
}
private SensorDataSet fill(TrackPoint trackPoint) {
if (!isStarted()) {
Log.w(TAG, "Not started, should not be called.");
return null;
}
SensorDataSet sensorDataSet = remoteSensorManager.fill(trackPoint);
altitudeSumManager.fill(trackPoint);
@@ -82,7 +91,7 @@ public class TrackPointCreator {
}
public void stop() {
locationHandler.onStop();
gpsHandler.onStop();
if (remoteSensorManager != null) {
remoteSensorManager.stop();
@@ -98,13 +107,23 @@ public class TrackPointCreator {
}
public void onSharedPreferenceChanged(String key) {
locationHandler.onSharedPreferenceChanged(key);
gpsHandler.onSharedPreferenceChanged(key);
}
public void onNewTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) {
/**
* Got a new TrackPoint from Bluetooth only; contains no GPS location.
*
* @param sensorDataSet
*/
@Override
public void onChange(SensorDataSet sensorDataSet) {
onNewTrackPoint(new TrackPoint(TrackPoint.Type.SENSORPOINT, createNow()));
}
public synchronized void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
fill(trackPoint);
boolean stored = service.newTrackPoint(trackPoint, thresholdHorizontalAccuracy);
boolean stored = service.newTrackPoint(trackPoint, gpsHandler.getThresholdHorizontalAccuracy());
if (stored) {
resetSensorData();
}
@@ -123,7 +142,7 @@ public class TrackPointCreator {
public Pair<TrackPoint, SensorDataSet> createCurrentTrackPoint(@Nullable TrackPoint lastValidTrackPoint) {
TrackPoint currentTrackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, createNow());
TrackPoint lastTrackPoint = locationHandler.getLastTrackPoint();
TrackPoint lastTrackPoint = gpsHandler.getLastTrackPoint();
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
currentTrackPoint.setSpeed(lastTrackPoint.getSpeed());
@@ -148,26 +167,42 @@ public class TrackPointCreator {
return Instant.now(clock);
}
@VisibleForTesting
public AltitudeSumManager getAltitudeSumManager() {
return altitudeSumManager;
}
@Deprecated
@VisibleForTesting
public void setAltitudeSumManager(AltitudeSumManager altitudeSumManager) {
this.altitudeSumManager = altitudeSumManager;
}
@Deprecated
@VisibleForTesting
public BluetoothRemoteSensorManager getRemoteSensorManager() {
return remoteSensorManager;
}
@Deprecated
@VisibleForTesting
public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
this.remoteSensorManager = remoteSensorManager;
}
@VisibleForTesting
public void setClock(@NonNull String time) {
this.clock = Clock.fixed(Instant.parse(time), ZoneId.of("CET"));
}
@VisibleForTesting
public void setClock(@NonNull Clock clock) {
this.clock = clock;
}
@VisibleForTesting
public LocationHandler getLocationHandler() {
return locationHandler;
public GPSHandler getGpsHandler() {
return gpsHandler;
}
void sendGpsStatus(GpsStatusValue gpsStatusValue) {
@@ -65,29 +65,38 @@ public class AltitudeSumManager implements SensorEventListener {
trackPoint.setAltitudeLoss(altitudeLoss_m);
}
public @Nullable
Float getAltitudeGain_m() {
@Nullable
public Float getAltitudeGain_m() {
return isConnected ? altitudeGain_m : null;
}
@VisibleForTesting
public void setAltitudeGain_m(float altitudeGain_m) {
this.altitudeGain_m = altitudeGain_m;
}
@VisibleForTesting
public void addAltitudeGain_m(float altitudeGain_m) {
this.altitudeGain_m = this.altitudeGain_m == null ? 0f : this.altitudeGain_m;
this.altitudeGain_m += altitudeGain_m ;
this.altitudeGain_m += altitudeGain_m;
}
@VisibleForTesting
public void addAltitudeLoss_m(Float altitudeLoss_m) {
this.altitudeLoss_m = this.altitudeLoss_m == null ? 0f : this.altitudeLoss_m;
this.altitudeLoss_m += altitudeLoss_m ;
this.altitudeLoss_m += altitudeLoss_m;
}
public @Nullable
Float getAltitudeLoss_m() {
@Nullable
public Float getAltitudeLoss_m() {
return isConnected ? altitudeLoss_m : null;
}
@VisibleForTesting
public void setAltitudeLoss_m(float altitudeLoss_m) {
this.altitudeLoss_m = altitudeLoss_m;
}
public void reset() {
Log.d(TAG, "Reset");
altitudeGain_m = null;
@@ -203,11 +203,6 @@ public abstract class BluetoothConnectionManager<DataType> {
return cadenceAndSpeed.getCadence();
}
//Workaround for Wahoo CADENCE: this sensor reports speed (instead of cadence)
if (cadenceAndSpeed.getDistanceSpeed() != null) {
return new SensorDataCycling.Cadence(cadenceAndSpeed.getDistanceSpeed());
}
return null;
}
}
@@ -226,15 +221,14 @@ public abstract class BluetoothConnectionManager<DataType> {
@Override
protected SensorDataCycling.DistanceSpeed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed != null) {
// Workaround for Garmin Speed Sensor 2: provides cadence instead of speed
SensorDataCycling.DistanceSpeed result = cadenceAndSpeed.getDistanceSpeed();
if (result == null && cadenceAndSpeed.getCadence() != null) {
result = new SensorDataCycling.DistanceSpeed(cadenceAndSpeed.getSensorAddress(), cadenceAndSpeed.getSensorName(), (int) cadenceAndSpeed.getCadence().getCrankRevolutionsCount(), cadenceAndSpeed.getCadence().getCrankRevolutionsTime());
}
return result;
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getDistanceSpeed() != null) {
return cadenceAndSpeed.getDistanceSpeed();
}
return null;
}
}
@@ -33,8 +33,8 @@ import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.BluetoothUtils;
/**
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
@@ -71,6 +71,8 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private final SensorDataSet sensorDataSet = new SensorDataSet();
private final SensorDataSetChangeObserver observer;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
@@ -110,8 +112,9 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
}
};
public BluetoothRemoteSensorManager(Context context) {
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull SensorDataSetChangeObserver observer) {
this.context = context;
this.observer = observer;
bluetoothAdapter = BluetoothUtils.getAdapter(context);
}
@@ -208,10 +211,15 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
}
sensorDataSet.set(sensorData);
observer.onChange(new SensorDataSet(sensorDataSet));
}
@Override
public void onDisconnecting(SensorData<?> sensorData) {
sensorDataSet.remove(sensorData);
}
public interface SensorDataSetChangeObserver {
void onChange(SensorDataSet sensorDataSet);
}
}
@@ -45,7 +45,7 @@ public class BluetoothLeAdapter extends BaseAdapter {
Device device = devices.get(position);
TextView textView = currentView.findViewById(android.R.id.text1);
textView.setText(device.getName());
textView.setText(device.getNameOrAddress());
return currentView;
}
@@ -81,8 +81,8 @@ public class BluetoothLeAdapter extends BaseAdapter {
this.address = address;
}
public String getName() {
return name;
public String getNameOrAddress() {
return name != null ? name : getAddress();
}
public void setName(String name) {
@@ -93,9 +93,6 @@ public class BluetoothLeAdapter extends BaseAdapter {
return address;
}
/**
* Check if the address is identical.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -66,7 +66,11 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
instantExportDirectoryPreference.setSummaryProvider(preference -> {
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(getContext());
//Use same value for not set as Androidx ListPreference and EditTextPreference
return directory != null ? directory.getName() : getString(R.string.not_set);
if (directory == null) {
return getString(R.string.not_set);
}
return directory.getUri().toString() + (directory.canWrite() ? "" : getString(R.string.export_dir_not_writable));
});
}
}
@@ -168,6 +168,11 @@ public class PreferencesUtils {
editor.apply();
}
@VisibleForTesting
public static void setString(int keyId, int valueId) {
setString(keyId, resources.getString(valueId));
}
static void setInt(int keyId, int value) {
Editor editor = sharedPreferences.edit();
editor.putInt(getKey(keyId), value);
@@ -232,6 +237,11 @@ public class PreferencesUtils {
return getString(R.string.settings_sensor_bluetooth_running_speed_and_cadence_key, getBluetoothSensorAddressNone());
}
public static boolean getBluetoothFilterEnabled() {
final boolean DEFAULT = resources.getBoolean(R.bool.settings_sensor_bluetooth_service_filter_enabled_default);
return getBoolean(R.string.settings_sensor_bluetooth_service_filter_enabled_key, DEFAULT);
}
public static boolean shouldShowStatsOnLockscreen() {
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = resources.getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
return getBoolean(R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
@@ -526,12 +536,12 @@ public class PreferencesUtils {
}
public static DocumentFile getDefaultExportDirectoryUri(Context context) {
String singleExportDirectorySettingsKey = getString(R.string.settings_default_export_directory_key, null);
if (singleExportDirectorySettingsKey == null) {
String singleExportDirectory = getString(R.string.settings_default_export_directory_key, null);
if (singleExportDirectory == null) {
return null;
}
try {
return DocumentFile.fromTreeUri(context, Uri.parse(singleExportDirectorySettingsKey));
return DocumentFile.fromTreeUri(context, Uri.parse(singleExportDirectory));
} catch (Exception e) {
Log.w(TAG, "Could not decode default export directory: " + e.getMessage());
}
@@ -34,8 +34,8 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.BluetoothLeAdapter;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.BluetoothUtils;
/**
* Preference to select a discoverable Bluetooth LE device.
@@ -109,17 +109,14 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
private final ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
Log.d(TAG, "Found device " + result.getDevice().getName() + " " + result);
listAdapter.add(result.getDevice());
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult result : results) {
Log.d(TAG, "Found device " + result.getDevice().getName() + " " + result);
listAdapter.add(result.getDevice());
onScanResult(-1, result);
}
}
@@ -213,7 +210,12 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
selectedEntryIndex = 1;
}
List<ScanFilter> scanFilter = serviceUUIDs.stream().map(it -> new ScanFilter.Builder().setServiceUuid(it).build()).collect(Collectors.toList());
List<ScanFilter> scanFilter = null;
if (PreferencesUtils.getBluetoothFilterEnabled()) {
scanFilter = serviceUUIDs.stream()
.map(it -> new ScanFilter.Builder().setServiceUuid(it).build())
.collect(Collectors.toList());
}
ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder();
scanSettingsBuilder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
@@ -0,0 +1,32 @@
package de.dennisguse.opentracks.stats;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.content.data.Altitude;
public class AltitudeRingBuffer extends RingBuffer<Altitude> {
private Altitude firstAltitude;
AltitudeRingBuffer(int size) {
super(size);
}
AltitudeRingBuffer(RingBuffer<Altitude> toCopy) {
super(toCopy);
}
@Nullable
@Override
protected Number from(Altitude object) {
if (firstAltitude == null) {
firstAltitude = object;
}
return object.toM();
}
@Override
protected Altitude to(double object) {
return firstAltitude.replace(object);
}
}
@@ -17,8 +17,9 @@
package de.dennisguse.opentracks.stats;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.ArrayList;
/**
* This class maintains a ring buffer of doubles.
@@ -27,10 +28,9 @@ import java.util.Arrays;
*
* @author Sandor Dornbush
*/
class DoubleRingBuffer {
abstract class RingBuffer<T> {
// The sliding buffer of doubles.
private final double[] buffer;
private final ArrayList<T> buffer;
// The location that the next write will occur at.
private int index;
@@ -43,16 +43,17 @@ class DoubleRingBuffer {
*
* @param size the size
*/
DoubleRingBuffer(int size) {
RingBuffer(int size) {
if (size < 1) {
throw new IllegalArgumentException("The buffer size must be greater than 1.");
}
buffer = new double[size];
buffer = new ArrayList<T>(size);
reset();
}
DoubleRingBuffer(DoubleRingBuffer toCopy) {
this.buffer = Arrays.copyOf(toCopy.buffer, toCopy.buffer.length);
RingBuffer(RingBuffer<T> toCopy) {
this.buffer = new ArrayList<>(toCopy.buffer);
this.index = toCopy.index;
this.isFull = toCopy.isFull;
}
@@ -75,31 +76,48 @@ class DoubleRingBuffer {
/**
* Gets the average of the buffer.
*/
public double getAverage() {
int numberOfEntries = isFull ? buffer.length : index;
public T getAverage() {
int numberOfEntries = isFull ? buffer.size() : index;
if (numberOfEntries == 0) {
return 0;
return null;
}
double sum = 0;
Double sum = null;
int numberOfUsedEntries = 0;
for (int i = 0; i < numberOfEntries; i++) {
sum += buffer[i];
Number value = from(buffer.get(i));
if (value != null) {
if (sum == null) {
sum = 0.0;
}
sum += value.doubleValue();
numberOfUsedEntries++;
}
}
if (sum == null) {
return null;
} else {
return to(sum / numberOfUsedEntries);
}
return sum / numberOfEntries;
}
@Nullable
protected abstract Number from(T object);
protected abstract T to(double object);
/**
* Adds a double to the buffer.
* If the buffer is full the oldest element is overwritten.
*
* @param value the double to add
*/
public void setNext(double value) {
if (index == buffer.length) {
public void setNext(T value) {
if (index == buffer.size()) {
index = 0;
}
buffer[index] = value;
buffer.add(index, value);
index++;
if (index == buffer.length) {
if (index == buffer.size()) {
isFull = true;
}
}
@@ -110,9 +128,9 @@ class DoubleRingBuffer {
StringBuilder builder = new StringBuilder("Full: ");
builder.append(isFull);
builder.append("\n");
for (int i = 0; i < buffer.length; i++) {
for (int i = 0; i < buffer.size(); i++) {
builder.append((i == index) ? "<<" : "[");
builder.append(buffer[i]);
builder.append(buffer.get(i));
builder.append((i == index) ? ">> " : "] ");
}
return builder.toString();
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.stats;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.content.data.Speed;
public class SpeedRingBuffer extends RingBuffer<Speed> {
SpeedRingBuffer(int size) {
super(size);
}
SpeedRingBuffer(RingBuffer<Speed> toCopy) {
super(toCopy);
}
@Nullable
@Override
protected Number from(Speed object) {
return object.toMPS();
}
@Override
protected Speed to(double object) {
return Speed.of(object);
}
}
@@ -23,8 +23,10 @@ import androidx.annotation.VisibleForTesting;
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.TrackPoint;
/**
* Statistical data about a {@link de.dennisguse.opentracks.content.data.Track}.
@@ -75,6 +77,18 @@ public class TrackStatistics {
totalAltitudeLoss_m = other.totalAltitudeLoss_m;
}
@VisibleForTesting
public TrackStatistics(String startTime, String stopTime, double totalDistance_m, int totalTime_s, int movingTime_s, float maxSpeed_mps, Float totalAltitudeGain_m, Float totalAltitudeLoss_m) {
this.startTime = Instant.parse(startTime);
this.stopTime = Instant.parse(stopTime);
this.totalDistance = Distance.of(totalDistance_m);
this.totalTime = Duration.ofSeconds(totalTime_s);
this.movingTime = Duration.ofSeconds(movingTime_s);
this.maxSpeed = Speed.of(maxSpeed_mps);
this.totalAltitudeGain_m = totalAltitudeGain_m;
this.totalAltitudeLoss_m = totalAltitudeLoss_m;
}
/**
* Combines these statistics with those from another object.
* This assumes that the time periods covered by each do not intersect.
@@ -160,6 +174,7 @@ public class TrackStatistics {
public void setStopTime(Instant stopTime) {
if (stopTime.isBefore(startTime)) {
// Time must be monotonically increasing, but we might have events at the same point in time (BLE and GPS)
throw new RuntimeException("stopTime cannot be less than startTime: " + startTime + " " + stopTime);
}
this.stopTime = stopTime;
@@ -198,11 +213,24 @@ public class TrackStatistics {
this.movingTime = movingTime;
}
public void addMovingTime(TrackPoint trackPoint, TrackPoint lastTrackPoint) {
addMovingTime(Duration.between(lastTrackPoint.getTime(), trackPoint.getTime()));
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void addMovingTime(Duration time) {
if (time.isNegative()) {
// Needs implementation of a monotonic Clock #800
// throw new RuntimeException("Moving time cannot be negative");
return;
}
movingTime = movingTime.plus(time);
}
public Duration getStoppedTime() {
return totalTime.minus(movingTime);
}
/**
* Gets the average speed.
* This calculation only takes into account the displacement until the last point that was accounted for in statistics.
@@ -254,8 +282,10 @@ public class TrackStatistics {
altitudeExtremities.setMax(altitude_m);
}
public void updateAltitudeExtremities(double altitude_m) {
altitudeExtremities.update(altitude_m);
public void updateAltitudeExtremities(Altitude altitude) {
if (altitude != null) {
altitudeExtremities.update(altitude.toM());
}
}
public boolean hasTotalAltitudeGain() {
@@ -300,6 +330,14 @@ public class TrackStatistics {
totalAltitudeLoss_m += loss_m;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TrackStatistics)) return false;
return toString().equals(o.toString());
}
@NonNull
@Override
public String toString() {
@@ -24,6 +24,7 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.util.List;
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.TrackPoint;
@@ -61,8 +62,8 @@ public class TrackStatisticsUpdater {
private final TrackStatistics trackStatistics;
private final DoubleRingBuffer altitudeBuffer_m;
private final DoubleRingBuffer speedBuffer_mps;
private final AltitudeRingBuffer altitudeBuffer;
private final SpeedRingBuffer speedBuffer;
// The current segment's statistics
private final TrackStatistics currentSegment;
@@ -82,16 +83,16 @@ public class TrackStatisticsUpdater {
this.trackStatistics = trackStatistics;
this.currentSegment = new TrackStatistics();
altitudeBuffer_m = new DoubleRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
speedBuffer_mps = new DoubleRingBuffer(SPEED_SMOOTHING_FACTOR);
altitudeBuffer = new AltitudeRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
speedBuffer = new SpeedRingBuffer(SPEED_SMOOTHING_FACTOR);
}
public TrackStatisticsUpdater(TrackStatisticsUpdater toCopy) {
this.currentSegment = new TrackStatistics(toCopy.currentSegment);
this.trackStatistics = new TrackStatistics(toCopy.trackStatistics);
this.altitudeBuffer_m = new DoubleRingBuffer(toCopy.altitudeBuffer_m);
this.speedBuffer_mps = new DoubleRingBuffer(toCopy.speedBuffer_mps);
this.altitudeBuffer = new AltitudeRingBuffer(toCopy.altitudeBuffer);
this.speedBuffer = new SpeedRingBuffer(toCopy.speedBuffer);
this.lastTrackPoint = toCopy.lastTrackPoint;
}
@@ -136,35 +137,35 @@ public class TrackStatisticsUpdater {
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
updateAbsoluteAltitude(trackPoint.getAltitude().toM());
// Update altitude using the smoothed average
altitudeBuffer.setNext(trackPoint.getAltitude());
Altitude newAverage = altitudeBuffer.getAverage();
currentSegment.updateAltitudeExtremities(newAverage);
}
// Update total distance
if (trackPoint.hasSensorDistance()) {
// Sensor-based distance/speed
currentSegment.addTotalDistance(trackPoint.getSensorDistance());
} else if (lastTrackPoint != null && trackPoint.isMoving()) {
} else if (lastTrackPoint != null
&& lastTrackPoint.hasLocation()
&& trackPoint.hasLocation() && trackPoint.isMoving()) {
// GPS-based distance/speed
// Assumption: we ignore TrackPoints that are not moving as those are likely imprecise GPS measurements
Distance movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
if (movingDistance != null) {
currentSegment.addTotalDistance(movingDistance);
}
currentSegment.addTotalDistance(movingDistance);
}
// Update moving time
if (trackPoint.isMoving() && lastTrackPoint != null && lastTrackPoint.isMoving()) {
Duration movingTime = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
if (movingTime.isNegative()) {
throw new RuntimeException("Moving time cannot be negative");
}
currentSegment.addMovingTime(movingTime);
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
// Update max speed
updateSpeed(trackPoint, lastTrackPoint);
} else {
speedBuffer_mps.reset();
speedBuffer.reset();
}
@@ -183,20 +184,20 @@ public class TrackStatisticsUpdater {
currentSegment.reset(trackPoint.getTime());
lastTrackPoint = null;
altitudeBuffer_m.reset();
speedBuffer_mps.reset();
altitudeBuffer.reset();
speedBuffer.reset();
}
/**
* Gets the smoothed altitude over several readings.
* The altitude readings is noisy so the smoothed altitude is better than the raw altitude for many tasks.
*/
public double getSmoothedAltitude() {
return altitudeBuffer_m.getAverage();
public Altitude getSmoothedAltitude() {
return altitudeBuffer.getAverage();
}
public Speed getSmoothedSpeed() {
return Speed.of(speedBuffer_mps.getAverage());
return speedBuffer.getAverage();
}
/**
@@ -205,10 +206,10 @@ public class TrackStatisticsUpdater {
@VisibleForTesting
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
if (!trackPoint.isMoving()) {
speedBuffer_mps.reset();
speedBuffer.reset();
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_mps.setNext(trackPoint.getSpeed().toMPS());
Speed average = Speed.of(speedBuffer_mps.getAverage());
speedBuffer.setNext(trackPoint.getSpeed());
Speed average = speedBuffer.getAverage();
if (average.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(average);
}
@@ -217,14 +218,6 @@ public class TrackStatisticsUpdater {
}
}
private void updateAbsoluteAltitude(double altitude) {
// Update altitude using the smoothed average
altitudeBuffer_m.setNext(altitude);
double newAverage = altitudeBuffer_m.getAverage();
currentSegment.updateAltitudeExtremities(newAverage);
}
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
// There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last.
if (trackPoint.getSpeed().isZero()) {
@@ -242,8 +235,8 @@ public class TrackStatisticsUpdater {
}
// Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration.
if (speedBuffer_mps.isFull()) {
Speed average = Speed.of(speedBuffer_mps.getAverage());
if (speedBuffer.isFull()) {
Speed average = speedBuffer.getAverage();
Speed speedDifference = Speed.absDiff(average, trackPoint.getSpeed());
return trackPoint.getSpeed().lessThan(average.mul(10)) && speedDifference.lessThan(maxAcceleration);
@@ -2,8 +2,6 @@ package de.dennisguse.opentracks.util;
import androidx.annotation.Nullable;
import com.google.android.material.navigation.NavigationBarView;
import java.util.Arrays;
import java.util.List;
@@ -6,6 +6,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.util.Log;
import android.widget.Toast;
import androidx.documentfile.provider.DocumentFile;
@@ -17,6 +18,7 @@ import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.ExportService;
@@ -33,6 +35,11 @@ public class ExportUtils {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(context);
if (directory == null || !directory.canWrite()) {
Toast.makeText(context, R.string.export_cannot_write_to_dir, Toast.LENGTH_LONG).show();
return;
}
ExportService.enqueue(context, resultReceiver, trackId, trackFileFormat, directory.getUri());
}
}
@@ -217,13 +217,10 @@ public class IntentUtils {
return;
}
final Uri documentUri = documentFile.getUri();
final List<UriPermission> persistedUriPermissions = context.getContentResolver().getPersistedUriPermissions();
for (final UriPermission permission : persistedUriPermissions) {
final Uri uri = permission.getUri();
if (uri.equals(documentUri)) {
context.getContentResolver().releasePersistableUriPermission(uri, 0);
}
}
context.getContentResolver().getPersistedUriPermissions().stream()
.map(UriPermission::getUri)
.filter(documentUri::equals)
.forEach(u -> context.getContentResolver().releasePersistableUriPermission(u, 0));
}
}
@@ -17,8 +17,6 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.text.format.DateUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
@@ -26,6 +24,9 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import de.dennisguse.opentracks.R;
/**
@@ -51,13 +52,12 @@ public class ListItemUtils {
* @param totalTime the total time value
* @param totalDistance the total distance value
* @param markerCount the marker count
* @param startTime the start time value
* @param useRelativeTime true to display relative time if appropriate
* @param offsetDateTime the start time with offset
* @param category the category value
* @param description the description value
* @param hasPhoto true if this list item has photo
*/
public static void setListItem(Context context, View view, boolean isRecording, boolean isPaused, int iconId, int iconContentDescriptionId, String name, String totalTime, String totalDistance, int markerCount, long startTime, boolean useRelativeTime, String category, String description, boolean hasPhoto) {
public static void setListItem(Context context, View view, boolean isRecording, boolean isPaused, int iconId, int iconContentDescriptionId, String name, String totalTime, String totalDistance, int markerCount, OffsetDateTime offsetDateTime, String category, String description, boolean hasPhoto) {
// Set icon
if (isRecording) {
iconId = isPaused ? R.drawable.ic_track_paused : R.drawable.ic_track_recording;
@@ -104,12 +104,16 @@ public class ListItemUtils {
setTextView(context, markerCountTextView, markerCountValue, hasPhoto);
// Set date/time
String[] dateTime = getDateTime(isRecording, context, startTime, useRelativeTime);
TextView dateTextView = view.findViewById(R.id.list_item_date);
setTextView(context, dateTextView, dateTime[0], hasPhoto);
TextView timeTextView = view.findViewById(R.id.list_item_time);
setTextView(context, timeTextView, dateTime[1], hasPhoto);
String dateValue = null;
String timeValue = null;
if (!isRecording) {
dateValue = StringUtils.formatDateTodayRelative(context, offsetDateTime);
timeValue = offsetDateTime.format(DateTimeFormatter.ofPattern("hh:mm"));
}
setTextView(context, dateTextView, dateValue, hasPhoto);
setTextView(context, timeTextView, timeValue, hasPhoto);
// Set category and description
TextView categoryDescriptionTextView = view.findViewById(R.id.list_item_category_description);
@@ -133,27 +137,6 @@ public class ListItemUtils {
params.gravity = timeDistanceTextView.getVisibility() == View.GONE && markerCountIcon.getVisibility() == View.GONE ? Gravity.TOP : Gravity.CENTER_VERTICAL;
}
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
/**
* Gets a string for share owner, total time, and total distance.
*
@@ -177,30 +160,6 @@ public class ListItemUtils {
return builder.toString();
}
/**
* Gets the date and time as an array of two strings.
*
* @param isRecording true if recording
* @param context the context
* @param time the start time
*/
private static String[] getDateTime(boolean isRecording, Context context, long time, boolean useRelativeTime) {
if (isRecording || time == 0L) {
return new String[]{null, null};
}
boolean isToday = DateUtils.isToday(time);
int timeFlags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL;
if (isToday && useRelativeTime) {
return new String[]{DateUtils.getRelativeTimeSpanString(time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE).toString(), null};
} else if (isToday) {
return new String[]{DateUtils.formatDateTime(context, time, timeFlags), null};
}
return new String[]{DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_ALL), DateUtils.formatDateTime(context, time, timeFlags)};
}
/**
* Sets a text view.
*
@@ -15,6 +15,8 @@
*/
package de.dennisguse.opentracks.util;
import static java.time.temporal.ChronoUnit.DAYS;
import android.content.Context;
import android.location.Location;
import android.text.TextUtils;
@@ -28,10 +30,14 @@ import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
import de.dennisguse.opentracks.R;
@@ -48,24 +54,60 @@ public class StringUtils {
private static final String TAG = StringUtils.class.getSimpleName();
private static final String COORDINATE_DEGREE = "\u00B0";
private StringUtils() {
}
/**
* Formats the date and time based on user's phone date/time preferences.
* Formats the date and time of the OffsetDateTime (using default Locale format)
*/
public static String formatDateTime(Context context, Instant time) {
return DateUtils.formatDateTime(context, time.toEpochMilli(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE)
+ " " + DateUtils.formatDateTime(context, time.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME);
public static String formatDateTime(OffsetDateTime odt) {
return odt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
}
/**
* Formats the time using the ISO 8601 date time format with fractional seconds in UTC time zone.
* Formats the date and time with the offset (using default Locale format).
*/
public static String formatDateTimeIso8601(@NonNull Instant time) {
return time.toString();
public static String formatDateTimeWithOffset(OffsetDateTime odt) {
return odt.toZonedDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL));
}
public static String formatLocalDateTime(LocalDateTime localDateTime) {
return localDateTime.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL));
}
/**
* Formats the date relative to today date.
*/
public static String formatDateTodayRelative(Context context, OffsetDateTime odt) {
LocalDate today = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toLocalDate();
LocalDate ld = odt.toLocalDate();
long daysBetween = DAYS.between(ld, today);
if (daysBetween == 0) {
// Today
return context.getString(R.string.generic_today);
} else if (daysBetween == 1) {
// Yesterday
return context.getString(R.string.generic_yesterday);
} else if (daysBetween < 7) {
// Name of the week day
return ld.format(DateTimeFormatter.ofPattern("EEEE"));
} else if (today.getYear() == ld.getYear()) {
// Short date without year
return ld.format(DateTimeFormatter.ofPattern("d MMM"));
} else {
// Short date with year
return ld.format(DateTimeFormatter.ofPattern("d MMM y"));
}
}
/**
* Formats the time using the ISO 8601 date time format with fractional seconds.
*/
public static String formatDateTimeIso8601(@NonNull Instant time, ZoneOffset zoneOffset) {
return time
.atOffset(zoneOffset)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}
/**
@@ -278,14 +320,14 @@ public class StringUtils {
*
* @param xmlDateTime the XML date time string
*/
public static Instant parseTime(String xmlDateTime) {
public static OffsetDateTime parseTime(String xmlDateTime) {
try {
TemporalAccessor t = DateTimeFormatter.ISO_DATE_TIME.parseBest(xmlDateTime, ZonedDateTime::from, LocalDateTime::from);
if (t instanceof LocalDateTime) {
Log.w(TAG, "Date does not contain timezone information: using UTC.");
t = ((LocalDateTime) t).atZone(ZoneOffset.UTC);
}
return Instant.from(t);
return OffsetDateTime.from(t);
} catch (Exception e) {
Log.e(TAG, "Invalid XML dateTime value");
throw e;
@@ -18,11 +18,7 @@ package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Locale;
import java.time.OffsetDateTime;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
@@ -35,21 +31,17 @@ import de.dennisguse.opentracks.settings.PreferencesUtils;
*/
public class TrackNameUtils {
//TODO Could be available in java.time?
@VisibleForTesting
static final String ISO_8601_FORMAT = "yyyy-MM-dd HH:mm";
private TrackNameUtils() {
}
//TODO Should not access sharedPreferences; trackName should be an ENUM.
public static String getTrackName(Context context, Track.Id trackId, Instant startTime) {
public static String getTrackName(Context context, Track.Id trackId, OffsetDateTime startTime) {
String trackName = PreferencesUtils.getString(R.string.track_name_key, context.getString(R.string.track_name_default));
if (trackName.equals(context.getString(R.string.settings_recording_track_name_date_local_value))) {
return StringUtils.formatDateTime(context, startTime);
return StringUtils.formatDateTimeWithOffset(startTime);
} else if (trackName.equals(context.getString(R.string.settings_recording_track_name_date_iso_8601_value))) {
return new SimpleDateFormat(ISO_8601_FORMAT, Locale.US).format(startTime.toEpochMilli());
return StringUtils.formatDateTimeIso8601(startTime.toInstant(), startTime.getOffset());
} else {
return context.getString(R.string.track_name_format, trackId.getId());
}
@@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackSelection;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
public class AggregatedStatisticsModel extends AndroidViewModel {
@@ -21,18 +22,26 @@ public class AggregatedStatisticsModel extends AndroidViewModel {
super(application);
}
public LiveData<AggregatedStatistics> getAggregatedStats(@Nullable List<Track.Id> trackIds) {
public LiveData<AggregatedStatistics> getAggregatedStats(@Nullable TrackSelection selection) {
if (aggregatedStats == null) {
aggregatedStats = new MutableLiveData<>();
loadAggregatedStats(trackIds);
loadAggregatedStats(selection);
}
return aggregatedStats;
}
private void loadAggregatedStats(@Nullable List<Track.Id> trackIds) {
public void updateSelection(TrackSelection selection) {
loadAggregatedStats(selection);
}
public void clearSelection() {
loadAggregatedStats(new TrackSelection());
}
private void loadAggregatedStats(TrackSelection selection) {
new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication().getApplicationContext());
List<Track> tracks = contentProviderUtils.getTracks(trackIds);
List<Track> tracks = selection != null ? contentProviderUtils.getTracks(selection) : contentProviderUtils.getTracks();
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,11L7,11v2h2v-2zM13,11h-2v2h2v-2zM17,11h-2v2h2v-2zM19,4h-1L18,2h-2v2L8,4L8,2L6,2v2L5,4c-1.11,0 -1.99,0.9 -1.99,2L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM19,20L5,20L5,9h14v11z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
</vector>
@@ -0,0 +1,14 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:name="path"
android:pathData="M 5.039 4 C 4.209 4 3.74 4.949 4.25 5.609 C 6.086 7.964 8.901 11.587 9.477 12.328 L 17.805 4 L 5.039 4 Z M 14 13.959 L 10 17.959 L 10 19 C 10 19.55 10.45 20 11 20 L 13 20 C 13.55 20 14 19.55 14 19 L 14 13.959 Z M 5.415 18.466 L 19.284 4.597 C 19.449 4.432 19.673 4.339 19.906 4.339 C 20.14 4.339 20.364 4.432 20.529 4.597 L 20.529 4.597 C 20.694 4.762 20.787 4.986 20.787 5.219 C 20.787 5.453 20.694 5.677 20.529 5.842 L 6.66 19.711 C 6.495 19.876 6.271 19.968 6.037 19.968 C 5.804 19.968 5.58 19.876 5.415 19.711 C 5.306 19.601 5.227 19.465 5.187 19.316 C 5.147 19.167 5.147 19.009 5.187 18.86 C 5.227 18.711 5.306 18.575 5.415 18.466"
android:fillColor="@android:color/white"
android:strokeWidth="1"/>
</vector>
+1
View File
@@ -16,6 +16,7 @@
style="@style/TextLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:gravity="center_horizontal|center_vertical"
android:text="@string/aggregated_stats_empty_message" />
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<ScrollView style="@style/ScrollView.WithBottomButtons">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<GridLayout
android:id="@+id/filter_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:orientation="horizontal"
android:padding="8dp"
android:alignmentMode="alignBounds"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/filter_date_from" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/filter_date_from"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/generic_from"
android:imeOptions="actionNext"
android:importantForAutofill="no"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="@id/filter_items"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/filter_date_picker_from">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/filter_date_edit_text_from"
android:drawableStart="@drawable/ic_baseline_date_range_24"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="true"
android:focusable="false"
android:focusableInTouchMode="false"
android:inputType="date"
android:selectAllOnFocus="true" />
</com.google.android.material.textfield.TextInputLayout>
<DatePicker
android:id="@+id/filter_date_picker_from"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
android:paddingLeft="8dp"
android:paddingRight="8dp"
app:layout_constraintTop_toBottomOf="@id/filter_date_from"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/filter_date_to" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/filter_date_to"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/generic_to"
android:imeOptions="actionNext"
android:importantForAutofill="no"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="@id/filter_date_picker_from"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/filter_date_picker_to">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/filter_date_edit_text_to"
android:drawableStart="@drawable/ic_baseline_date_range_24"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="true"
android:focusable="false"
android:focusableInTouchMode="false"
android:inputType="date"
android:selectAllOnFocus="true" />
</com.google.android.material.textfield.TextInputLayout>
<DatePicker
android:id="@+id/filter_date_picker_to"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
android:paddingLeft="8dp"
android:paddingRight="8dp"
app:layout_constraintTop_toBottomOf="@id/filter_date_to"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</LinearLayout>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/filter_dialog_check_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
tools:text="Check Button" />
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/aggregated_statistics_clear_filter"
android:icon="@drawable/ic_clear_filter_24dp"
app:showAsAction="always"
android:title="@string/menu_filter" />
<item
android:id="@+id/aggregated_statistics_filter"
android:icon="@drawable/ic_baseline_filter_alt_24"
app:showAsAction="always"
android:title="@string/menu_filter" />
</menu>
+11 -1
View File
@@ -563,7 +563,7 @@ limitations under the License.
<string name="settings_sensors_title">Bluetooth senzory</string>
<string name="settings_gps_summary">Časový interval, Interval vzdálenosti, Maximální vzdálenost, Přesnost</string>
<string name="settings_ui_summary">Vzhled, Rozložení záznamu, Téma</string>
<string name="settings_open_tracks_title">Open Tracks</string>
<string name="settings_open_tracks_title">Otevřené stopy</string>
<string name="settings_open_tracks_summary">Vše o projektu OpenTracks</string>
<string name="settings_defaults_title">Výchozí jednotky a aktivity</string>
<string name="settings_ui_title">Uživatelské rozhraní</string>
@@ -577,4 +577,14 @@ limitations under the License.
<string name="custom_layout_list_edit_already_exists">Název rozvržení již existuje</string>
<string name="custom_layout_list_layout_removed">Rozložení bylo odstraněno</string>
<string name="stats_custom_layout_default_layout">Výchozí rozložení</string>
<string name="generic_filter">Filtr</string>
<string name="generic_from">Z</string>
<string name="generic_to">Na</string>
<string name="menu_filter">Filtr</string>
<string name="settings_sensor_bluetooth_service_filter_enabled">Zobrazit pouze zařízení Bluetooth, která oznamují požadované služby</string>
<string name="aggregated_stats_filter_no_results">Pro zadaný filtr neexistují žádné aktivity</string>
<string name="generic_yesterday">Včera</string>
<string name="export_cannot_write_to_dir">Nelze zapisovat do adresáře pro export.</string>
<string name="export_dir_not_writable">" (nelze zapisovat!)"</string>
<string name="generic_today">Dnes</string>
</resources>
+24 -3
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
--><resources>
<string name="about_preference_title">Über OpenTracks</string>
<string name="about_description"><i>OpenTracks</i> ist eine Sport-Tracking-Anwendung, die Ihre Privatsphäre vollständig respektiert.</string>
<string name="about_url">Projekt-Website: <a href="%1$s">%1$s</a></string>
@@ -513,4 +513,25 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
<string name="delete_service">Löschdienst</string>
<string name="settings_sensor_cycling_cadence">Radfahren: Trittfrequenz</string>
<string name="settings_sensor_cycling_distance_speed">Radfahren: Entfernung und Geschwindigkeit</string>
<string name="generic_filter">Filter</string>
<string name="generic_from">Von</string>
<string name="generic_to">Bis</string>
<string name="menu_add_custom_layout">Neues Layout</string>
<string name="menu_filter">Filter</string>
<string name="settings_units_category">Einheiten</string>
<string name="settings_reset_summary">Einstellungen zurücksetzen</string>
<string name="settings_recording_customize_layout_title">Layout anpassen</string>
<string name="settings_recording_customize_layout_select_columns">Felder pro Zeile</string>
<string name="settings_announcements">Sprachansagen</string>
<string name="custom_layout_list_title">Layouts</string>
<string name="description_speed_source_gps">GPS</string>
<string name="menu_select_layout">Wähle Layout</string>
<string name="generic_choose_an_option">Wähle eine Option</string>
<string name="settings_recording_idle_speed_title">Inaktivitätsgeschwindigkeit</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_import_export_title">Import/Export</string>
<string name="settings_sensors_title">Bluetooth Sensoren</string>
<string name="settings_sensors_summary">Fahrradsensoren, Radumfang und Lauf-Sensoren</string>
<string name="settings_announcements_title">Sprachansagen</string>
<string name="generic_undo">Rückgängig</string>
</resources>

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